mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-09-11 10:55:27 +03:00
Move geom to its own repo
This commit is contained in:
parent
a3f31bb470
commit
4c69f7ed4c
23
Cargo.lock
generated
23
Cargo.lock
generated
@ -1902,9 +1902,9 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "geom"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/a-b-street/geom#34194e73db3fd6343ad223e8a4b1075201094f1a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
"earcutr",
|
||||
"fs-err",
|
||||
"geo",
|
||||
@ -1913,8 +1913,6 @@ dependencies = [
|
||||
"instant",
|
||||
"ordered-float",
|
||||
"polylabel",
|
||||
"rand",
|
||||
"rand_xorshift",
|
||||
"rstar",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -3362,10 +3360,22 @@ version = "6.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
|
||||
|
||||
[[package]]
|
||||
name = "osm2lanes"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/a-b-street/osm2streets#086a347e6cad7c556d7f7d5bf57d4b270c8a030f"
|
||||
dependencies = [
|
||||
"abstutil",
|
||||
"anyhow",
|
||||
"enumset",
|
||||
"geom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "osm2streets"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/a-b-street/osm2streets#c098792f790ce9cefce074f9e4a9c8b1f27900bc"
|
||||
source = "git+https://github.com/a-b-street/osm2streets#086a347e6cad7c556d7f7d5bf57d4b270c8a030f"
|
||||
dependencies = [
|
||||
"abstutil",
|
||||
"anyhow",
|
||||
@ -3373,8 +3383,9 @@ dependencies = [
|
||||
"geo",
|
||||
"geojson",
|
||||
"geom",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.11.0",
|
||||
"log",
|
||||
"osm2lanes",
|
||||
"petgraph",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -4555,7 +4566,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
[[package]]
|
||||
name = "streets_reader"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/a-b-street/osm2streets#c098792f790ce9cefce074f9e4a9c8b1f27900bc"
|
||||
source = "git+https://github.com/a-b-street/osm2streets#086a347e6cad7c556d7f7d5bf57d4b270c8a030f"
|
||||
dependencies = [
|
||||
"abstutil",
|
||||
"anyhow",
|
||||
|
@ -10,7 +10,6 @@ members = [
|
||||
"cli",
|
||||
"collisions",
|
||||
"convert_osm",
|
||||
"geom",
|
||||
"headless",
|
||||
"importer",
|
||||
"kml",
|
||||
@ -49,6 +48,7 @@ futures = { version = "0.3.27"}
|
||||
futures-channel = { version = "0.3.29"}
|
||||
geo = "0.26.0"
|
||||
geojson = { version = "0.24.1", features = ["geo-types"] }
|
||||
geom = { git = "https://github.com/a-b-street/geom" }
|
||||
getrandom = "0.2.11"
|
||||
instant = "0.1.7"
|
||||
log = "0.4.20"
|
||||
@ -65,7 +65,6 @@ web-sys = "0.3.65"
|
||||
# due to the 2 core dependency crates listed below. This patch is required to
|
||||
# avoid Cargo from getting confused.
|
||||
[patch."https://github.com/a-b-street/abstreet/"]
|
||||
geom = { path = "geom" }
|
||||
abstutil = { path = "abstutil" }
|
||||
|
||||
[patch.crates-io]
|
||||
|
@ -16,7 +16,7 @@ abstio = { path = "../../abstio" }
|
||||
abstutil = { path = "../../abstutil" }
|
||||
contour = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
log = { workspace = true }
|
||||
map_gui = { path = "../../map_gui" }
|
||||
|
@ -29,7 +29,7 @@ fs-err = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
instant = { workspace = true }
|
||||
kml = { path = "../../kml" }
|
||||
|
@ -22,7 +22,7 @@ flate2 = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
home = { version = "0.5.5", optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
|
@ -15,7 +15,7 @@ wasm = ["getrandom/js", "wasm-bindgen", "widgetry/wasm-backend"]
|
||||
abstio = { path = "../../abstio" }
|
||||
abstutil = { path = "../../abstutil" }
|
||||
fs-err = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
log = { workspace = true }
|
||||
raw_map = { path = "../../raw_map" }
|
||||
|
@ -14,7 +14,7 @@ wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"]
|
||||
[dependencies]
|
||||
abstio = { path = "../../abstio" }
|
||||
abstutil = { path = "../../abstutil" }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
map_gui = { path = "../../map_gui" }
|
||||
map_model = { path = "../../map_model" }
|
||||
|
@ -12,7 +12,7 @@ abstio = { path = "../../abstio" }
|
||||
abstutil = { path = "../../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
map_gui = { path = "../../map_gui" }
|
||||
map_model = { path = "../../map_model" }
|
||||
|
@ -15,7 +15,7 @@ wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"]
|
||||
abstio = { path = "../../abstio" }
|
||||
abstutil = { path = "../../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
geom = { path = "../../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
kml = { path = "../../kml" }
|
||||
log = { workspace = true }
|
||||
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
abstutil = { path = "../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
serde = { workspace = true }
|
||||
|
@ -11,7 +11,7 @@ convert_osm = { path = "../convert_osm" }
|
||||
csv = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
importer = { path = "../importer" }
|
||||
log = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
|
@ -5,7 +5,7 @@ authors = ["Dustin Carlino <dabreegster@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
kml = { path = "../kml" }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
@ -10,7 +10,7 @@ abstutil = { path = "../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
kml = { path = "../kml" }
|
||||
log = { workspace = true }
|
||||
osm2streets = { git = "https://github.com/a-b-street/osm2streets" }
|
||||
|
@ -1,24 +0,0 @@
|
||||
[package]
|
||||
name = "geom"
|
||||
version = "0.1.0"
|
||||
authors = ["Dustin Carlino <dabreegster@gmail.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
earcutr = "0.4.3"
|
||||
fs-err = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
histogram = "0.6.9"
|
||||
instant = { workspace = true }
|
||||
ordered-float = { version = "3.7.0", features=["serde"] }
|
||||
polylabel = "2.5.0"
|
||||
rstar = "0.11.0"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
bincode = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_xorshift = { workspace = true }
|
@ -1,17 +0,0 @@
|
||||
# geom
|
||||
|
||||
This crate contains primitive types used by A/B Street. It's unclear if other
|
||||
apps will have any use for this crate. In some cases, `geom` just wraps much
|
||||
more polished APIs, like `rust-geo`. In others, it has its own geometric
|
||||
algorithms, but they likely have many bugs and make use-case-driven assumptions.
|
||||
So, be warned if you use this.
|
||||
|
||||
## Contents
|
||||
|
||||
Many of the types are geometric: `Pt2D`, `Ring`, `Distance`, `Line`,
|
||||
`InfiniteLine`, `FindClosest`, `Circle`, `Angle`, `LonLat`, `Bounds`,
|
||||
`GPSBounds`, `PolyLine`, `Polygon`, `Triangle`.
|
||||
|
||||
Some involve time: `Time`, `Duration`, `Speed`.
|
||||
|
||||
And there's also a `Percent` wrapper and a `Histogram`.
|
@ -1,137 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// An angle, stored in radians.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
pub struct Angle(f64);
|
||||
|
||||
impl Angle {
|
||||
pub const ZERO: Angle = Angle(0.0);
|
||||
|
||||
/// Create an angle in radians.
|
||||
// TODO Normalize here, and be careful about % vs euclid_rem
|
||||
pub fn new_rads(rads: f64) -> Angle {
|
||||
// Retain more precision for angles...
|
||||
Angle((rads * 10_000_000.0).round() / 10_000_000.0)
|
||||
}
|
||||
|
||||
/// Create an angle in degrees.
|
||||
pub fn degrees(degs: f64) -> Angle {
|
||||
Angle::new_rads(degs.to_radians())
|
||||
}
|
||||
|
||||
/// Invert the direction of this angle.
|
||||
pub fn opposite(self) -> Angle {
|
||||
Angle::new_rads(self.0 + std::f64::consts::PI)
|
||||
}
|
||||
|
||||
pub(crate) fn invert_y(self) -> Angle {
|
||||
Angle::new_rads(2.0 * std::f64::consts::PI - self.0)
|
||||
}
|
||||
|
||||
/// Rotates this angle by some degrees.
|
||||
pub fn rotate_degs(self, degrees: f64) -> Angle {
|
||||
Angle::new_rads(self.0 + degrees.to_radians())
|
||||
}
|
||||
|
||||
/// Returns [0, 2pi)
|
||||
pub fn normalized_radians(self) -> f64 {
|
||||
if self.0 < 0.0 {
|
||||
// TODO Be more careful about how we store the angle, to make sure this works
|
||||
self.0 + (2.0 * std::f64::consts::PI)
|
||||
} else {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [0, 360)
|
||||
pub fn normalized_degrees(self) -> f64 {
|
||||
self.normalized_radians().to_degrees()
|
||||
}
|
||||
|
||||
/// Returns [-180, 180]
|
||||
pub fn simple_shortest_rotation_towards(self, other: Angle) -> f64 {
|
||||
// https://math.stackexchange.com/questions/110080/shortest-way-to-achieve-target-angle
|
||||
((self.normalized_degrees() - other.normalized_degrees() + 540.0) % 360.0) - 180.0
|
||||
}
|
||||
|
||||
/// Logically this returns [-180, 180], but keep in mind when we print this angle, it'll
|
||||
/// normalize to be [0, 360].
|
||||
pub fn shortest_rotation_towards(self, other: Angle) -> Angle {
|
||||
Angle::degrees(self.simple_shortest_rotation_towards(other))
|
||||
}
|
||||
|
||||
/// True if this angle is within some degrees of another, accounting for rotation
|
||||
pub fn approx_eq(self, other: Angle, within_degrees: f64) -> bool {
|
||||
self.simple_shortest_rotation_towards(other).abs() < within_degrees
|
||||
}
|
||||
|
||||
/// True if this angle is within some degrees of another, accounting for rotation and allowing
|
||||
/// two angles that point in opposite directions
|
||||
pub fn approx_parallel(self, other: Angle, within_degrees: f64) -> bool {
|
||||
self.approx_eq(other, within_degrees) || self.opposite().approx_eq(other, within_degrees)
|
||||
}
|
||||
|
||||
/// I don't know how to describe what this does. Use for rotating labels in map-space and making
|
||||
/// sure the text is never upside-down.
|
||||
pub fn reorient(self) -> Angle {
|
||||
let theta = self.normalized_degrees().rem_euclid(360.0);
|
||||
let mut result = self;
|
||||
if theta > 90.0 {
|
||||
result = result.opposite();
|
||||
}
|
||||
if theta > 270.0 {
|
||||
result = result.opposite();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Calculates the average of some angles.
|
||||
pub fn average(input: Vec<Angle>) -> Angle {
|
||||
// Thanks https://rosettacode.org/wiki/Averages/Mean_angle
|
||||
let num = input.len() as f64;
|
||||
let mut cos_sum = 0.0;
|
||||
let mut sin_sum = 0.0;
|
||||
for x in input {
|
||||
cos_sum += x.0.cos();
|
||||
sin_sum += x.0.sin();
|
||||
}
|
||||
Angle::new_rads((sin_sum / num).atan2(cos_sum / num))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Angle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Angle({} degrees)", self.normalized_degrees())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Angle {
|
||||
type Output = Angle;
|
||||
|
||||
fn add(self, other: Angle) -> Angle {
|
||||
Angle::new_rads(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Neg for Angle {
|
||||
type Output = Angle;
|
||||
|
||||
fn neg(self) -> Angle {
|
||||
Angle::new_rads(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_average() {
|
||||
assert_eq!(
|
||||
Angle::degrees(30.0),
|
||||
Angle::average(vec![Angle::degrees(30.0)])
|
||||
);
|
||||
}
|
||||
}
|
@ -1,333 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use rstar::primitives::{GeomWithData, Rectangle};
|
||||
use rstar::{RTree, SelectionFunction, AABB};
|
||||
|
||||
use crate::{Circle, Distance, LonLat, Polygon, Pt2D, Ring};
|
||||
|
||||
/// Represents a rectangular boundary of `Pt2D` points.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Bounds {
|
||||
pub min_x: f64,
|
||||
pub min_y: f64,
|
||||
pub max_x: f64,
|
||||
pub max_y: f64,
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
/// A boundary including no points.
|
||||
pub fn new() -> Bounds {
|
||||
Bounds {
|
||||
min_x: f64::MAX,
|
||||
min_y: f64::MAX,
|
||||
max_x: f64::MIN,
|
||||
max_y: f64::MIN,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Bounds {
|
||||
min_x: 0.0,
|
||||
min_y: 0.0,
|
||||
max_x: 0.0,
|
||||
max_y: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a boundary covering some points.
|
||||
pub fn from(pts: &[Pt2D]) -> Bounds {
|
||||
let mut b = Bounds::new();
|
||||
for pt in pts {
|
||||
b.update(*pt);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Create a boundary covering some polygons.
|
||||
pub fn from_polygons(polygons: &[Polygon]) -> Bounds {
|
||||
let mut b = Bounds::new();
|
||||
for poly in polygons {
|
||||
for ring in poly.get_rings() {
|
||||
for pt in ring.points() {
|
||||
b.update(*pt);
|
||||
}
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Update the boundary to include this point.
|
||||
pub fn update(&mut self, pt: Pt2D) {
|
||||
self.min_x = self.min_x.min(pt.x());
|
||||
self.max_x = self.max_x.max(pt.x());
|
||||
self.min_y = self.min_y.min(pt.y());
|
||||
self.max_y = self.max_y.max(pt.y());
|
||||
}
|
||||
|
||||
/// Unions two boundaries.
|
||||
pub fn union(&mut self, other: Bounds) {
|
||||
self.update(Pt2D::new(other.min_x, other.min_y));
|
||||
self.update(Pt2D::new(other.max_x, other.max_y));
|
||||
}
|
||||
|
||||
/// Expand the existing boundary by some distance evenly on all sides.
|
||||
pub fn add_buffer(&mut self, sides: Distance) {
|
||||
self.min_x -= sides.inner_meters();
|
||||
self.max_x += sides.inner_meters();
|
||||
self.min_y -= sides.inner_meters();
|
||||
self.max_y += sides.inner_meters();
|
||||
}
|
||||
|
||||
/// Transform the boundary by scaling its corners.
|
||||
pub fn scale(mut self, factor: f64) -> Self {
|
||||
self.min_x *= factor;
|
||||
self.min_y *= factor;
|
||||
self.max_x *= factor;
|
||||
self.max_y *= factor;
|
||||
self
|
||||
}
|
||||
|
||||
/// True if the point is within the boundary.
|
||||
pub fn contains(&self, pt: Pt2D) -> bool {
|
||||
pt.x() >= self.min_x && pt.x() <= self.max_x && pt.y() >= self.min_y && pt.y() <= self.max_y
|
||||
}
|
||||
|
||||
/// Converts the boundary to the format used by `rstar`.
|
||||
pub fn as_bbox<T>(&self, data: T) -> GeomWithData<Rectangle<[f64; 2]>, T> {
|
||||
GeomWithData::new(
|
||||
Rectangle::from_corners([self.min_x, self.min_y], [self.max_x, self.max_y]),
|
||||
data,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a rectangle covering this boundary.
|
||||
pub fn get_rectangle(&self) -> Polygon {
|
||||
Ring::must_new(vec![
|
||||
Pt2D::new(self.min_x, self.min_y),
|
||||
Pt2D::new(self.max_x, self.min_y),
|
||||
Pt2D::new(self.max_x, self.max_y),
|
||||
Pt2D::new(self.min_x, self.max_y),
|
||||
Pt2D::new(self.min_x, self.min_y),
|
||||
])
|
||||
.into_polygon()
|
||||
}
|
||||
|
||||
/// Creates a circle centered in the middle of this boundary. Always uses the half width as a
|
||||
/// radius, so if the width and height don't match, this is pretty meaningless.
|
||||
pub fn to_circle(&self) -> Circle {
|
||||
Circle::new(self.center(), Distance::meters(self.width() / 2.0))
|
||||
}
|
||||
|
||||
/// The width of this boundary.
|
||||
// TODO Really should be Distance
|
||||
pub fn width(&self) -> f64 {
|
||||
self.max_x - self.min_x
|
||||
}
|
||||
|
||||
/// The height of this boundary.
|
||||
pub fn height(&self) -> f64 {
|
||||
self.max_y - self.min_y
|
||||
}
|
||||
|
||||
/// The center point of this boundary.
|
||||
pub fn center(&self) -> Pt2D {
|
||||
Pt2D::new(
|
||||
self.min_x + self.width() / 2.0,
|
||||
self.min_y + self.height() / 2.0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a rectangular boundary of `LonLat` points. After building one of these, `LonLat`s
|
||||
/// can be transformed into `Pt2D`s, treating the top-left of the boundary as (0, 0), and growing
|
||||
/// to the right and down (screen-drawing order, not Cartesian) in meters.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GPSBounds {
|
||||
pub min_lon: f64,
|
||||
pub min_lat: f64,
|
||||
pub max_lon: f64,
|
||||
pub max_lat: f64,
|
||||
}
|
||||
|
||||
impl GPSBounds {
|
||||
/// A boundary including no points.
|
||||
pub fn new() -> GPSBounds {
|
||||
GPSBounds {
|
||||
min_lon: f64::MAX,
|
||||
min_lat: f64::MAX,
|
||||
max_lon: f64::MIN,
|
||||
max_lat: f64::MIN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a boundary covering some points.
|
||||
pub fn from(pts: Vec<LonLat>) -> GPSBounds {
|
||||
let mut b = GPSBounds::new();
|
||||
for pt in pts {
|
||||
b.update(pt);
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
/// Update the boundary to include this point.
|
||||
pub fn update(&mut self, pt: LonLat) {
|
||||
self.min_lon = self.min_lon.min(pt.x());
|
||||
self.max_lon = self.max_lon.max(pt.x());
|
||||
self.min_lat = self.min_lat.min(pt.y());
|
||||
self.max_lat = self.max_lat.max(pt.y());
|
||||
}
|
||||
|
||||
/// True if the point is within the boundary.
|
||||
pub fn contains(&self, pt: LonLat) -> bool {
|
||||
pt.x() >= self.min_lon
|
||||
&& pt.x() <= self.max_lon
|
||||
&& pt.y() >= self.min_lat
|
||||
&& pt.y() <= self.max_lat
|
||||
}
|
||||
|
||||
/// The bottom-right corner of the boundary, in map-space.
|
||||
// TODO cache this
|
||||
pub fn get_max_world_pt(&self) -> Pt2D {
|
||||
let width = LonLat::new(self.min_lon, self.min_lat)
|
||||
.gps_dist(LonLat::new(self.max_lon, self.min_lat));
|
||||
let height = LonLat::new(self.min_lon, self.min_lat)
|
||||
.gps_dist(LonLat::new(self.min_lon, self.max_lat));
|
||||
Pt2D::new(width.inner_meters(), height.inner_meters())
|
||||
}
|
||||
|
||||
/// Converts the boundary to map-space.
|
||||
pub fn to_bounds(&self) -> Bounds {
|
||||
let mut b = Bounds::new();
|
||||
b.update(Pt2D::new(0.0, 0.0));
|
||||
b.update(self.get_max_world_pt());
|
||||
b
|
||||
}
|
||||
|
||||
/// Convert all points to map-space, failing if any points are outside this boundary.
|
||||
pub fn try_convert(&self, pts: &[LonLat]) -> Option<Vec<Pt2D>> {
|
||||
let mut result = Vec::new();
|
||||
for gps in pts {
|
||||
if !self.contains(*gps) {
|
||||
return None;
|
||||
}
|
||||
result.push(gps.to_pt(self));
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Convert all points to map-space. The points may be outside this boundary.
|
||||
pub fn convert(&self, pts: &[LonLat]) -> Vec<Pt2D> {
|
||||
pts.iter().map(|gps| gps.to_pt(self)).collect()
|
||||
}
|
||||
|
||||
/// Convert map-space points back to `LonLat`s. This is only valid if the `GPSBounds` used
|
||||
/// is the same as the one used to originally produce the `Pt2D`s.
|
||||
pub fn convert_back(&self, pts: &[Pt2D]) -> Vec<LonLat> {
|
||||
pts.iter().map(|pt| pt.to_gps(self)).collect()
|
||||
}
|
||||
|
||||
pub fn convert_back_xy(&self, x: f64, y: f64) -> LonLat {
|
||||
let (width, height) = {
|
||||
let pt = self.get_max_world_pt();
|
||||
(pt.x(), pt.y())
|
||||
};
|
||||
|
||||
let lon = (x / width * (self.max_lon - self.min_lon)) + self.min_lon;
|
||||
let lat = self.min_lat + ((self.max_lat - self.min_lat) * (height - y) / height);
|
||||
LonLat::new(lon, lat)
|
||||
}
|
||||
|
||||
/// Returns points in order covering this boundary.
|
||||
pub fn get_rectangle(&self) -> Vec<LonLat> {
|
||||
vec![
|
||||
LonLat::new(self.min_lon, self.min_lat),
|
||||
LonLat::new(self.max_lon, self.min_lat),
|
||||
LonLat::new(self.max_lon, self.max_lat),
|
||||
LonLat::new(self.min_lon, self.max_lat),
|
||||
LonLat::new(self.min_lon, self.min_lat),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Convenient wrapper around rstar
|
||||
#[derive(Clone)]
|
||||
pub struct QuadTree<T>(RTree<GeomWithData<Rectangle<[f64; 2]>, T>>);
|
||||
|
||||
impl<T> QuadTree<T> {
|
||||
pub fn new() -> Self {
|
||||
Self(RTree::new())
|
||||
}
|
||||
|
||||
pub fn builder() -> QuadTreeBuilder<T> {
|
||||
QuadTreeBuilder::new()
|
||||
}
|
||||
|
||||
/// Slow, prefer bulk_load or QuadTreeBuilder
|
||||
pub fn insert(&mut self, entry: GeomWithData<Rectangle<[f64; 2]>, T>) {
|
||||
self.0.insert(entry);
|
||||
}
|
||||
pub fn insert_with_box(&mut self, data: T, bbox: Bounds) {
|
||||
self.0.insert(bbox.as_bbox(data));
|
||||
}
|
||||
|
||||
pub fn bulk_load(entries: Vec<GeomWithData<Rectangle<[f64; 2]>, T>>) -> Self {
|
||||
Self(RTree::bulk_load(entries))
|
||||
}
|
||||
|
||||
pub fn query_bbox_borrow(&self, bbox: Bounds) -> impl Iterator<Item = &T> + '_ {
|
||||
let envelope = AABB::from_corners([bbox.min_x, bbox.min_y], [bbox.max_x, bbox.max_y]);
|
||||
self.0
|
||||
.locate_in_envelope_intersecting(&envelope)
|
||||
.map(|x| &x.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> QuadTree<T> {
|
||||
pub fn query_bbox(&self, bbox: Bounds) -> impl Iterator<Item = T> + '_ {
|
||||
let envelope = AABB::from_corners([bbox.min_x, bbox.min_y], [bbox.max_x, bbox.max_y]);
|
||||
self.0
|
||||
.locate_in_envelope_intersecting(&envelope)
|
||||
.map(|x| x.data)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> QuadTree<T> {
|
||||
// TODO Inefficient
|
||||
pub fn remove(&mut self, data: T) -> Option<T> {
|
||||
self.0
|
||||
.remove_with_selection_function(Selector(data))
|
||||
.map(|item| item.data)
|
||||
}
|
||||
}
|
||||
|
||||
struct Selector<T>(T);
|
||||
|
||||
impl<T: PartialEq> SelectionFunction<GeomWithData<Rectangle<[f64; 2]>, T>> for Selector<T> {
|
||||
fn should_unpack_parent(&self, _envelope: &AABB<[f64; 2]>) -> bool {
|
||||
// TODO Maybe we can ask the caller to estimate where the previous object was?
|
||||
true
|
||||
}
|
||||
|
||||
fn should_unpack_leaf(&self, leaf: &GeomWithData<Rectangle<[f64; 2]>, T>) -> bool {
|
||||
self.0 == leaf.data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct QuadTreeBuilder<T>(Vec<GeomWithData<Rectangle<[f64; 2]>, T>>);
|
||||
|
||||
impl<T> QuadTreeBuilder<T> {
|
||||
pub fn new() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
pub fn add(&mut self, entry: GeomWithData<Rectangle<[f64; 2]>, T>) {
|
||||
self.0.push(entry);
|
||||
}
|
||||
pub fn add_with_box(&mut self, data: T, bbox: Bounds) {
|
||||
self.0.push(bbox.as_bbox(data));
|
||||
}
|
||||
|
||||
pub fn build(self) -> QuadTree<T> {
|
||||
QuadTree::bulk_load(self.0)
|
||||
}
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Angle, Bounds, Distance, Polygon, Pt2D, Ring, Tessellation};
|
||||
|
||||
const TRIANGLES_PER_CIRCLE: usize = 60;
|
||||
|
||||
/// A circle, defined by a center and radius.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Circle {
|
||||
pub center: Pt2D,
|
||||
pub radius: Distance,
|
||||
}
|
||||
|
||||
impl Circle {
|
||||
/// Creates a circle.
|
||||
pub fn new(center: Pt2D, radius: Distance) -> Circle {
|
||||
Circle { center, radius }
|
||||
}
|
||||
|
||||
/// True if the point is inside the circle.
|
||||
pub fn contains_pt(&self, pt: Pt2D) -> bool {
|
||||
// avoid sqrt by squaring radius instead
|
||||
(pt.x() - self.center.x()).powi(2) + (pt.y() - self.center.y()).powi(2)
|
||||
< self.radius.inner_meters().powi(2)
|
||||
}
|
||||
|
||||
/// Get the boundary containing this circle.
|
||||
pub fn get_bounds(&self) -> Bounds {
|
||||
Bounds {
|
||||
min_x: self.center.x() - self.radius.inner_meters(),
|
||||
max_x: self.center.x() + self.radius.inner_meters(),
|
||||
min_y: self.center.y() - self.radius.inner_meters(),
|
||||
max_y: self.center.y() + self.radius.inner_meters(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the circle as a polygon.
|
||||
pub fn to_polygon(&self) -> Polygon {
|
||||
self.to_ring().into_polygon()
|
||||
}
|
||||
|
||||
/// Renders some percent, between [0, 1], of the circle. The shape starts from 0 degrees.
|
||||
pub fn to_partial_tessellation(&self, percent_full: f64) -> Tessellation {
|
||||
#![allow(clippy::float_cmp)]
|
||||
assert!((0. ..=1.).contains(&percent_full));
|
||||
let mut pts = vec![self.center];
|
||||
let mut indices = Vec::new();
|
||||
for i in 0..TRIANGLES_PER_CIRCLE {
|
||||
pts.push(self.center.project_away(
|
||||
self.radius,
|
||||
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * percent_full * 360.0),
|
||||
));
|
||||
indices.push(0);
|
||||
indices.push(i + 1);
|
||||
if i != TRIANGLES_PER_CIRCLE - 1 {
|
||||
indices.push(i + 2);
|
||||
} else if percent_full == 1.0 {
|
||||
indices.push(1);
|
||||
} else {
|
||||
indices.pop();
|
||||
indices.pop();
|
||||
}
|
||||
}
|
||||
Tessellation::new(pts, indices)
|
||||
}
|
||||
|
||||
/// Returns the ring around the circle.
|
||||
fn to_ring(&self) -> Ring {
|
||||
let mut pts: Vec<Pt2D> = (0..=TRIANGLES_PER_CIRCLE)
|
||||
.map(|i| {
|
||||
self.center.project_away(
|
||||
self.radius,
|
||||
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * 360.0),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// With some radii, we get duplicate adjacent points
|
||||
pts.dedup();
|
||||
Ring::must_new(pts)
|
||||
}
|
||||
|
||||
/// Creates an outline around the circle, strictly contained with the circle's original radius.
|
||||
pub fn to_outline(&self, thickness: Distance) -> Result<Polygon> {
|
||||
if self.radius <= thickness {
|
||||
bail!(
|
||||
"Can't make Circle outline with radius {} and thickness {}",
|
||||
self.radius,
|
||||
thickness
|
||||
);
|
||||
}
|
||||
|
||||
let bigger = self.to_ring();
|
||||
let smaller = Circle::new(self.center, self.radius - thickness).to_ring();
|
||||
Ok(Polygon::with_holes(bigger, vec![smaller]))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Circle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Circle({}, {})", self.center, self.radius)
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//! Conversions between this crate and `geo`. Long-term, we should think about directly using `geo`
|
||||
//! or wrapping it, but in the meantime...
|
||||
//!
|
||||
//! TODO Also, there's no consistency between standalone methods like this and From/Into impls.
|
||||
|
||||
use crate::Pt2D;
|
||||
|
||||
pub fn pts_to_line_string(raw_pts: &[Pt2D]) -> geo::LineString {
|
||||
let pts: Vec<geo::Point> = raw_pts
|
||||
.iter()
|
||||
.map(|pt| geo::Point::new(pt.x(), pt.y()))
|
||||
.collect();
|
||||
pts.into()
|
||||
}
|
@ -1,309 +0,0 @@
|
||||
use std::{cmp, f64, fmt, ops};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{deserialize_f64, serialize_f64, trim_f64, Duration, Speed, UnitFmt, EPSILON_DIST};
|
||||
|
||||
/// A distance, in meters. Can be negative.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Distance(
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")] f64,
|
||||
);
|
||||
|
||||
// By construction, Distance is a finite f64 with trimmed precision.
|
||||
impl Eq for Distance {}
|
||||
|
||||
#[allow(clippy::derive_ord_xor_partial_ord)] // false positive
|
||||
impl Ord for Distance {
|
||||
fn cmp(&self, other: &Distance) -> cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Distance {
|
||||
pub const ZERO: Distance = Distance::const_meters(0.0);
|
||||
|
||||
/// Creates a distance in meters.
|
||||
pub fn meters(value: f64) -> Distance {
|
||||
if !value.is_finite() {
|
||||
panic!("Bad Distance {}", value);
|
||||
}
|
||||
|
||||
Distance(trim_f64(value))
|
||||
}
|
||||
|
||||
// TODO Can't panic inside a const fn, seemingly. Don't pass in anything bad!
|
||||
pub const fn const_meters(value: f64) -> Distance {
|
||||
Distance(value)
|
||||
}
|
||||
|
||||
/// Creates a distance in inches.
|
||||
pub fn inches(value: f64) -> Distance {
|
||||
Distance::meters(0.0254 * value)
|
||||
}
|
||||
|
||||
/// Creates a distance in miles.
|
||||
pub fn miles(value: f64) -> Distance {
|
||||
Distance::meters(1609.34 * value)
|
||||
}
|
||||
|
||||
/// Creates a distance in centimeters.
|
||||
pub fn centimeters(value: usize) -> Distance {
|
||||
Distance::meters((value as f64) / 100.0)
|
||||
}
|
||||
|
||||
/// Creates a distance in feet.
|
||||
pub fn feet(value: f64) -> Distance {
|
||||
Distance::meters(value * 0.3048)
|
||||
}
|
||||
|
||||
/// Returns the absolute value of this distance.
|
||||
pub fn abs(self) -> Distance {
|
||||
if self.0 > 0.0 {
|
||||
self
|
||||
} else {
|
||||
Distance(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the square root of this distance.
|
||||
pub fn sqrt(self) -> Distance {
|
||||
Distance::meters(self.0.sqrt())
|
||||
}
|
||||
|
||||
/// Returns the distance in meters. Prefer to work with type-safe `Distance`s.
|
||||
// TODO Remove if possible.
|
||||
pub fn inner_meters(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the distance in feet.
|
||||
pub fn to_feet(self) -> f64 {
|
||||
self.0 * 3.28084
|
||||
}
|
||||
|
||||
/// Returns the distance in miles.
|
||||
pub fn to_miles(self) -> f64 {
|
||||
self.to_feet() / 5280.0
|
||||
}
|
||||
|
||||
/// Describes the distance according to formatting rules. Rounds to 1 decimal place for both
|
||||
/// small (feet and meters) and large (miles and kilometers) units.
|
||||
pub fn to_string(self, fmt: &UnitFmt) -> String {
|
||||
if fmt.metric {
|
||||
if self.0 < 1000.0 {
|
||||
format!("{}m", (self.0 * 10.0).round() / 10.0)
|
||||
} else {
|
||||
let km = self.0 / 1000.0;
|
||||
format!("{}km", (km * 10.0).round() / 10.0)
|
||||
}
|
||||
} else {
|
||||
let feet = self.to_feet();
|
||||
let miles = self.to_miles();
|
||||
if miles >= 0.1 {
|
||||
format!("{} miles", (miles * 10.0).round() / 10.0)
|
||||
} else {
|
||||
format!("{} ft", (feet * 10.0).round() / 10.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculates a percentage, usually in [0.0, 1.0], of self / other. If the denominator is
|
||||
/// zero, returns 0%.
|
||||
pub fn safe_percent(self, other: Distance) -> f64 {
|
||||
if other == Distance::ZERO {
|
||||
return 0.0;
|
||||
}
|
||||
self / other
|
||||
}
|
||||
|
||||
/// Rounds this distance up to a higher, more "even" value to use for buckets along a plot's
|
||||
/// axis. Always rounds for imperial units (feet).
|
||||
pub fn round_up_for_axis(self) -> Distance {
|
||||
let ft = self.to_feet();
|
||||
let miles = ft / 5280.0;
|
||||
if ft <= 0.0 {
|
||||
Distance::ZERO
|
||||
} else if ft <= 10.0 {
|
||||
Distance::feet(ft.ceil())
|
||||
} else if ft <= 100.0 {
|
||||
Distance::feet(10.0 * (ft / 10.0).ceil())
|
||||
} else if miles < 0.1 {
|
||||
Distance::feet(100.0 * (ft / 100.0).ceil())
|
||||
} else if miles <= 1.0 {
|
||||
Distance::miles((miles * 10.0).ceil() / 10.0)
|
||||
} else if miles <= 10.0 {
|
||||
Distance::miles(miles.ceil())
|
||||
} else if miles <= 100.0 {
|
||||
Distance::miles(10.0 * (miles / 10.0).ceil())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn to_u64(self) -> u64 {
|
||||
(self.0 / EPSILON_DIST.0) as u64
|
||||
}
|
||||
|
||||
pub(crate) fn from_u64(x: u64) -> Distance {
|
||||
(x as f64) * EPSILON_DIST
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Distance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}m", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Distance {
|
||||
type Output = Distance;
|
||||
|
||||
fn add(self, other: Distance) -> Distance {
|
||||
Distance::meters(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Distance {
|
||||
fn add_assign(&mut self, other: Distance) {
|
||||
*self = *self + other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Distance {
|
||||
type Output = Distance;
|
||||
|
||||
fn sub(self, other: Distance) -> Distance {
|
||||
Distance::meters(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Distance {
|
||||
type Output = Distance;
|
||||
|
||||
fn neg(self) -> Distance {
|
||||
Distance::meters(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::SubAssign for Distance {
|
||||
fn sub_assign(&mut self, other: Distance) {
|
||||
*self = *self - other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f64> for Distance {
|
||||
type Output = Distance;
|
||||
|
||||
fn mul(self, scalar: f64) -> Distance {
|
||||
Distance::meters(self.0 * scalar)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Distance> for f64 {
|
||||
type Output = Distance;
|
||||
|
||||
fn mul(self, other: Distance) -> Distance {
|
||||
Distance::meters(self * other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::MulAssign<f64> for Distance {
|
||||
fn mul_assign(&mut self, other: f64) {
|
||||
*self = *self * other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<Distance> for Distance {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, other: Distance) -> f64 {
|
||||
if other == Distance::ZERO {
|
||||
panic!("Can't divide {} / {}", self, other);
|
||||
}
|
||||
self.0 / other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f64> for Distance {
|
||||
type Output = Distance;
|
||||
|
||||
fn div(self, scalar: f64) -> Distance {
|
||||
if scalar == 0.0 {
|
||||
panic!("Can't divide {} / {}", self, scalar);
|
||||
}
|
||||
Distance::meters(self.0 / scalar)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<Speed> for Distance {
|
||||
type Output = Duration;
|
||||
|
||||
fn div(self, other: Speed) -> Duration {
|
||||
if other == Speed::ZERO {
|
||||
panic!("Can't divide {} / 0 mph", self);
|
||||
}
|
||||
Duration::seconds(self.0 / other.inner_meters_per_second())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for Distance {
|
||||
fn sum<I>(iter: I) -> Distance
|
||||
where
|
||||
I: Iterator<Item = Distance>,
|
||||
{
|
||||
let mut sum = Distance::ZERO;
|
||||
for x in iter {
|
||||
sum += x;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Distance {
|
||||
fn default() -> Distance {
|
||||
Distance::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn round_up_for_axis() {
|
||||
let fmt = UnitFmt {
|
||||
metric: false,
|
||||
round_durations: false,
|
||||
};
|
||||
|
||||
for (input, expected) in [
|
||||
(-3.0, 0.0),
|
||||
(0.0, 0.0),
|
||||
(3.2, 4.0),
|
||||
(30.2, 40.0),
|
||||
(300.2, 400.0),
|
||||
(
|
||||
Distance::miles(0.13).to_feet(),
|
||||
Distance::miles(0.2).to_feet(),
|
||||
),
|
||||
(
|
||||
Distance::miles(0.64).to_feet(),
|
||||
Distance::miles(0.7).to_feet(),
|
||||
),
|
||||
(
|
||||
Distance::miles(2.6).to_feet(),
|
||||
Distance::miles(3.0).to_feet(),
|
||||
),
|
||||
(
|
||||
Distance::miles(2.9).to_feet(),
|
||||
Distance::miles(3.0).to_feet(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
Distance::feet(input).round_up_for_axis().to_string(&fmt),
|
||||
Distance::feet(expected).to_string(&fmt)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,438 +0,0 @@
|
||||
use std::{cmp, ops};
|
||||
|
||||
use anyhow::Result;
|
||||
use instant::Instant;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{deserialize_f64, serialize_f64, trim_f64, Distance, Speed, UnitFmt};
|
||||
|
||||
/// A duration, in seconds. Can be negative.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Duration(
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")] f64,
|
||||
);
|
||||
|
||||
// By construction, Duration is a finite f64 with trimmed precision.
|
||||
impl Eq for Duration {}
|
||||
|
||||
#[allow(clippy::derive_ord_xor_partial_ord)] // false positive
|
||||
impl Ord for Duration {
|
||||
fn cmp(&self, other: &Duration) -> cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Duration {
|
||||
pub const ZERO: Duration = Duration::const_seconds(0.0);
|
||||
pub const EPSILON: Duration = Duration::const_seconds(0.0001);
|
||||
|
||||
/// Creates a duration in seconds.
|
||||
pub fn seconds(value: f64) -> Duration {
|
||||
if !value.is_finite() {
|
||||
panic!("Bad Duration {}", value);
|
||||
}
|
||||
|
||||
Duration(trim_f64(value))
|
||||
}
|
||||
|
||||
/// Creates a duration in minutes.
|
||||
pub fn minutes(mins: usize) -> Duration {
|
||||
Duration::seconds((mins as f64) * 60.0)
|
||||
}
|
||||
|
||||
/// Creates a duration in hours.
|
||||
pub fn hours(hours: usize) -> Duration {
|
||||
Duration::seconds((hours as f64) * 3600.0)
|
||||
}
|
||||
|
||||
/// Creates a duration in minutes.
|
||||
pub fn f64_minutes(mins: f64) -> Duration {
|
||||
Duration::seconds(mins * 60.0)
|
||||
}
|
||||
|
||||
/// Creates a duration in milliseconds.
|
||||
pub fn milliseconds(value: f64) -> Duration {
|
||||
Duration::seconds(value / 1000.0)
|
||||
}
|
||||
|
||||
pub const fn const_seconds(value: f64) -> Duration {
|
||||
Duration(value)
|
||||
}
|
||||
|
||||
pub(crate) fn to_u64(self) -> u64 {
|
||||
(self.0 / Duration::EPSILON.0) as u64
|
||||
}
|
||||
|
||||
pub(crate) fn from_u64(x: u64) -> Duration {
|
||||
(x as f64) * Duration::EPSILON
|
||||
}
|
||||
|
||||
pub fn abs(&self) -> Self {
|
||||
Self(self.0.abs())
|
||||
}
|
||||
|
||||
/// Returns the duration in seconds. Prefer working in typesafe `Duration`s.
|
||||
// TODO Remove if possible.
|
||||
pub fn inner_seconds(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Splits the duration into (hours, minutes, seconds, deciseconds).
|
||||
// TODO Could share some of this with Time -- the representations are the same
|
||||
fn get_parts(self) -> (usize, usize, usize, usize) {
|
||||
// Force positive
|
||||
let mut remainder = self.inner_seconds().abs();
|
||||
let hours = (remainder / 3600.0).floor();
|
||||
remainder -= hours * 3600.0;
|
||||
let minutes = (remainder / 60.0).floor();
|
||||
remainder -= minutes * 60.0;
|
||||
let seconds = remainder.floor();
|
||||
remainder -= seconds;
|
||||
let decis = (remainder / 0.1).round();
|
||||
|
||||
(
|
||||
hours as usize,
|
||||
minutes as usize,
|
||||
seconds as usize,
|
||||
decis as usize,
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses a duration such as "3:00" to `Duration::minutes(3)`.
|
||||
// TODO This is NOT the inverse of Display!
|
||||
pub fn parse(string: &str) -> Result<Duration> {
|
||||
let parts: Vec<&str> = string.split(':').collect();
|
||||
if parts.is_empty() {
|
||||
bail!("Duration {}: no :'s", string);
|
||||
}
|
||||
|
||||
let mut seconds: f64 = 0.0;
|
||||
if parts.last().unwrap().contains('.') {
|
||||
let last_parts: Vec<&str> = parts.last().unwrap().split('.').collect();
|
||||
if last_parts.len() != 2 {
|
||||
bail!("Duration {}: no . in last part", string);
|
||||
}
|
||||
seconds += last_parts[1].parse::<f64>()? / 10.0;
|
||||
seconds += last_parts[0].parse::<f64>()?;
|
||||
} else {
|
||||
seconds += parts.last().unwrap().parse::<f64>()?;
|
||||
}
|
||||
|
||||
match parts.len() {
|
||||
1 => Ok(Duration::seconds(seconds)),
|
||||
2 => {
|
||||
seconds += 60.0 * parts[0].parse::<f64>()?;
|
||||
Ok(Duration::seconds(seconds))
|
||||
}
|
||||
3 => {
|
||||
seconds += 60.0 * parts[1].parse::<f64>()?;
|
||||
seconds += 3600.0 * parts[0].parse::<f64>()?;
|
||||
Ok(Duration::seconds(seconds))
|
||||
}
|
||||
_ => bail!("Duration {}: weird number of parts", string),
|
||||
}
|
||||
}
|
||||
|
||||
/// If two durations are within this amount, they'll print as if they're the same.
|
||||
pub fn epsilon_eq(self, other: Duration) -> bool {
|
||||
let eps = Duration::seconds(0.1);
|
||||
match self.cmp(&other) {
|
||||
cmp::Ordering::Greater => self - other < eps,
|
||||
cmp::Ordering::Less => other - self < eps,
|
||||
cmp::Ordering::Equal => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration elapsed from this moment in real time.
|
||||
pub fn realtime_elapsed(since: Instant) -> Duration {
|
||||
let dt = since.elapsed();
|
||||
Duration::seconds((dt.as_secs() as f64) + (f64::from(dt.subsec_nanos()) * 1e-9))
|
||||
}
|
||||
|
||||
/// Rounds a duration up to the nearest whole number multiple.
|
||||
pub fn round_up(self, multiple: Duration) -> Duration {
|
||||
let remainder = self % multiple;
|
||||
if remainder == Duration::ZERO {
|
||||
self
|
||||
} else {
|
||||
self + multiple - remainder
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration as a number of minutes, rounded up.
|
||||
pub fn num_minutes_rounded_up(self) -> usize {
|
||||
let (hrs, mins, secs, rem) = self.get_parts();
|
||||
let mut result = mins + 60 * hrs;
|
||||
if secs != 0 || rem != 0 {
|
||||
result += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// TODO Do something fancier? http://vis.stanford.edu/papers/tick-labels
|
||||
// TODO Unit test me
|
||||
/// Returns (rounded max, the boundaries)
|
||||
pub fn make_intervals_for_max(self, num_labels: usize) -> (Duration, Vec<Duration>) {
|
||||
// Example 1: 43 minutes, max 5 labels... raw_mins_per_interval is 8.6
|
||||
let raw_mins_per_interval = (self.num_minutes_rounded_up() as f64) / (num_labels as f64);
|
||||
// So then this rounded up to 10 minutes
|
||||
let mut mins_per_interval = Duration::seconds(60.0 * raw_mins_per_interval)
|
||||
.round_up(Duration::minutes(5))
|
||||
.num_minutes_rounded_up();
|
||||
|
||||
// Example 2: 8 minutes, max 5 labels... raw_mins_per_interval is 1.6
|
||||
// If we're under 25 minutes, this is going to be weird.
|
||||
if self < (num_labels as f64) * Duration::minutes(5) {
|
||||
// rounded up to 5 mins? 1 min increments
|
||||
// up to 10? 2 min increments
|
||||
// up to 15? 3
|
||||
// up to 20? 4
|
||||
// then after that the normal behavior
|
||||
mins_per_interval = (self.round_up(Duration::minutes(5)) / (num_labels as f64))
|
||||
.num_minutes_rounded_up();
|
||||
}
|
||||
|
||||
let max = (num_labels as f64) * Duration::minutes(mins_per_interval);
|
||||
let labels = (0..=num_labels)
|
||||
.map(|i| Duration::minutes(i * mins_per_interval))
|
||||
.collect();
|
||||
|
||||
if max < self {
|
||||
panic!(
|
||||
"Wait max of {} with {} labels wound up with rounded max of {}",
|
||||
self, num_labels, max
|
||||
);
|
||||
}
|
||||
(max, labels)
|
||||
}
|
||||
|
||||
/// Shows only the largest unit (hours, minute, seconds), rounded to `precision` decimal points.
|
||||
///
|
||||
/// ```
|
||||
/// use geom::Duration;
|
||||
/// assert_eq!(Duration::seconds(3600.0).to_rounded_string(0), "1hr");
|
||||
/// assert_eq!(Duration::seconds(3600.0).to_rounded_string(1), "1.0hr");
|
||||
/// assert_eq!(Duration::seconds(7800.0).to_rounded_string(0), "2hr");
|
||||
/// assert_eq!(Duration::seconds(800.0).to_rounded_string(1), "13.3min");
|
||||
/// assert_eq!(Duration::seconds(-800.0).to_rounded_string(1), "-13.3min");
|
||||
/// assert_eq!(Duration::seconds(0.0).to_rounded_string(0), "0");
|
||||
/// assert_eq!(Duration::seconds(12.5).to_rounded_string(1), "12.5s");
|
||||
/// assert_eq!(Duration::seconds(12.5).to_rounded_string(2), "12.50s");
|
||||
/// ```
|
||||
pub fn to_rounded_string(self, precision: usize) -> String {
|
||||
let (hours, minutes, seconds, remainder) = self.get_parts();
|
||||
if hours == 0 && minutes == 0 && seconds == 0 && remainder == 0 {
|
||||
return "0".to_string();
|
||||
}
|
||||
|
||||
let sign = if self < Duration::ZERO { "-" } else { "" };
|
||||
|
||||
let (whole, part, unit) = {
|
||||
if hours != 0 {
|
||||
let whole = hours as f64;
|
||||
let part = minutes as f64 / 60.0;
|
||||
let unit = "hr";
|
||||
(whole, part, unit)
|
||||
} else if minutes != 0 {
|
||||
let whole = minutes as f64;
|
||||
let part = seconds as f64 / 60.0;
|
||||
let unit = "min";
|
||||
(whole, part, unit)
|
||||
} else {
|
||||
let whole = seconds as f64;
|
||||
let part = remainder as f64 / 10.0;
|
||||
let unit = "s";
|
||||
(whole, part, unit)
|
||||
}
|
||||
};
|
||||
|
||||
let number = format!("{:.1$}", whole + part, precision);
|
||||
return format!("{}{}{}", sign, number, unit);
|
||||
}
|
||||
|
||||
/// Describes the duration according to formatting rules.
|
||||
pub fn to_string(self, fmt: &UnitFmt) -> String {
|
||||
let mut s = String::new();
|
||||
if self < Duration::ZERO {
|
||||
s = "-".to_string();
|
||||
}
|
||||
let (hours, minutes, seconds, remainder) = self.get_parts();
|
||||
if hours == 0 && minutes == 0 && seconds == 0 && remainder == 0 {
|
||||
return "0s".to_string();
|
||||
}
|
||||
|
||||
if hours != 0 {
|
||||
s = format!("{}{}hr ", s, hours);
|
||||
}
|
||||
if minutes != 0 {
|
||||
s = format!("{}{}min ", s, minutes);
|
||||
}
|
||||
if remainder != 0 {
|
||||
if fmt.round_durations {
|
||||
s = format!("{}{}s", s, seconds);
|
||||
} else {
|
||||
s = format!("{}{}.{:1}s", s, seconds, remainder);
|
||||
}
|
||||
} else if seconds != 0 {
|
||||
s = format!("{}{}s", s, seconds);
|
||||
}
|
||||
// Trim trailing whitespace, in case we have non-zero hours/minutes, but zero seconds
|
||||
s.trim_end().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Duration {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
(*self).to_string(&UnitFmt {
|
||||
metric: false,
|
||||
round_durations: false
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn add(self, other: Duration) -> Duration {
|
||||
Duration::seconds(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign for Duration {
|
||||
fn add_assign(&mut self, other: Duration) {
|
||||
*self = *self + other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::SubAssign for Duration {
|
||||
fn sub_assign(&mut self, other: Duration) {
|
||||
*self = *self - other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, other: Duration) -> Duration {
|
||||
Duration::seconds(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn neg(self) -> Duration {
|
||||
Duration::seconds(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f64> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn mul(self, other: f64) -> Duration {
|
||||
Duration::seconds(self.0 * other)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Both of these work. Use a macro or crate to define both, so we don't have to worry about
|
||||
// order for commutative things like multiplication. :P
|
||||
impl ops::Mul<Duration> for f64 {
|
||||
type Output = Duration;
|
||||
|
||||
fn mul(self, other: Duration) -> Duration {
|
||||
Duration::seconds(self * other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Speed> for Duration {
|
||||
type Output = Distance;
|
||||
|
||||
fn mul(self, other: Speed) -> Distance {
|
||||
Distance::meters(self.0 * other.inner_meters_per_second())
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<Duration> for Duration {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, other: Duration) -> f64 {
|
||||
if other.0 == 0.0 {
|
||||
panic!("Can't divide {} / {}", self, other);
|
||||
}
|
||||
self.0 / other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<f64> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn div(self, other: f64) -> Duration {
|
||||
if other == 0.0 {
|
||||
panic!("Can't divide {} / {}", self, other);
|
||||
}
|
||||
Duration::seconds(self.0 / other)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Rem<Duration> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn rem(self, other: Duration) -> Duration {
|
||||
Duration::seconds(self.0 % other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for Duration {
|
||||
fn sum<I>(iter: I) -> Duration
|
||||
where
|
||||
I: Iterator<Item = Duration>,
|
||||
{
|
||||
let mut sum = Duration::ZERO;
|
||||
for x in iter {
|
||||
sum += x;
|
||||
}
|
||||
sum
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Duration {
|
||||
fn default() -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn print_durations() {
|
||||
let dont_round = UnitFmt {
|
||||
metric: false,
|
||||
round_durations: false,
|
||||
};
|
||||
let round = UnitFmt {
|
||||
metric: false,
|
||||
round_durations: true,
|
||||
};
|
||||
|
||||
assert_eq!("0s", Duration::ZERO.to_string(&dont_round));
|
||||
assert_eq!("0s", Duration::seconds(0.001).to_string(&dont_round));
|
||||
assert_eq!(
|
||||
"1min 30.1s",
|
||||
Duration::seconds(90.123).to_string(&dont_round)
|
||||
);
|
||||
assert_eq!("1min 30s", Duration::seconds(90.123).to_string(&round));
|
||||
assert_eq!(
|
||||
"2hr 33min 5s",
|
||||
(Duration::hours(2) + Duration::minutes(33) + Duration::seconds(5.0))
|
||||
.to_string(&dont_round)
|
||||
);
|
||||
assert_eq!("3hr", Duration::hours(3).to_string(&dont_round));
|
||||
assert_eq!("42min", Duration::minutes(42).to_string(&dont_round));
|
||||
}
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use geo::{ClosestPoint, Contains, EuclideanDistance, Intersects};
|
||||
|
||||
use crate::conversions::pts_to_line_string;
|
||||
use crate::{Bounds, Distance, Polygon, Pt2D, QuadTree};
|
||||
|
||||
// TODO Maybe use https://crates.io/crates/spatial-join proximity maps
|
||||
|
||||
/// A quad-tree to quickly find the closest points to some polylines.
|
||||
#[derive(Clone)]
|
||||
pub struct FindClosest<K> {
|
||||
// TODO maybe any type of geo:: thing
|
||||
geometries: BTreeMap<K, geo::LineString>,
|
||||
quadtree: QuadTree<K>,
|
||||
}
|
||||
|
||||
impl<K> FindClosest<K>
|
||||
where
|
||||
K: Clone + Ord + std::fmt::Debug,
|
||||
{
|
||||
pub fn new() -> FindClosest<K> {
|
||||
FindClosest {
|
||||
geometries: BTreeMap::new(),
|
||||
quadtree: QuadTree::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an object to the quadtree, remembering some key associated with the points.
|
||||
/// TODO This doesn't properly handle single points, and will silently fail by never returning
|
||||
/// any matches.
|
||||
pub fn add(&mut self, key: K, pts: &[Pt2D]) {
|
||||
self.geometries.insert(key.clone(), pts_to_line_string(pts));
|
||||
self.quadtree.insert_with_box(key, Bounds::from(pts));
|
||||
}
|
||||
|
||||
/// Adds the outer ring of a polygon to the quadtree.
|
||||
pub fn add_polygon(&mut self, key: K, polygon: &Polygon) {
|
||||
self.add(key, polygon.get_outer_ring().points());
|
||||
}
|
||||
|
||||
/// For every object within some distance of a query point, return the (object's key, point on
|
||||
/// the object's polyline, distance away).
|
||||
pub fn all_close_pts(
|
||||
&self,
|
||||
query_pt: Pt2D,
|
||||
max_dist_away: Distance,
|
||||
) -> Vec<(K, Pt2D, Distance)> {
|
||||
let query_geom = geo::Point::new(query_pt.x(), query_pt.y());
|
||||
|
||||
self.quadtree
|
||||
.query_bbox_borrow(
|
||||
Polygon::rectangle_centered(query_pt, max_dist_away, max_dist_away).get_bounds(),
|
||||
)
|
||||
.filter_map(|key| {
|
||||
if let geo::Closest::SinglePoint(pt) =
|
||||
self.geometries[key].closest_point(&query_geom)
|
||||
{
|
||||
let dist = Distance::meters(pt.euclidean_distance(&query_geom));
|
||||
if dist <= max_dist_away {
|
||||
Some((key.clone(), Pt2D::new(pt.x(), pt.y()), dist))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if self.geometries[key].contains(&query_geom) {
|
||||
// TODO Yay, FindClosest has a bug. :P
|
||||
Some((key.clone(), query_pt, Distance::ZERO))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finds the closest point on the existing geometry to the query pt.
|
||||
pub fn closest_pt(&self, query_pt: Pt2D, max_dist_away: Distance) -> Option<(K, Pt2D)> {
|
||||
self.all_close_pts(query_pt, max_dist_away)
|
||||
.into_iter()
|
||||
.min_by_key(|(_, _, dist)| *dist)
|
||||
.map(|(k, pt, _)| (k, pt))
|
||||
}
|
||||
|
||||
/// Find all objects with a point inside the query polygon
|
||||
pub fn all_points_inside(&self, query: &Polygon) -> Vec<K> {
|
||||
let query_geo: geo::Polygon = query.clone().into();
|
||||
|
||||
self.quadtree
|
||||
.query_bbox_borrow(query.get_bounds())
|
||||
.filter_map(|key| {
|
||||
if self.geometries[key].intersects(&query_geo) {
|
||||
Some(key.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
169
geom/src/gps.rs
169
geom/src/gps.rs
@ -1,169 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use geojson::{GeoJson, Value};
|
||||
use ordered_float::NotNan;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Distance, GPSBounds, Pt2D};
|
||||
|
||||
/// Represents a (longitude, latitude) point.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
||||
pub struct LonLat {
|
||||
longitude: NotNan<f64>,
|
||||
latitude: NotNan<f64>,
|
||||
}
|
||||
|
||||
impl LonLat {
|
||||
/// Note the order of arguments!
|
||||
pub fn new(lon: f64, lat: f64) -> LonLat {
|
||||
LonLat {
|
||||
longitude: NotNan::new(lon).unwrap(),
|
||||
latitude: NotNan::new(lat).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the longitude of this point.
|
||||
pub fn x(self) -> f64 {
|
||||
self.longitude.into_inner()
|
||||
}
|
||||
|
||||
/// Returns the latitude of this point.
|
||||
pub fn y(self) -> f64 {
|
||||
self.latitude.into_inner()
|
||||
}
|
||||
|
||||
/// Transform this to a world-space point. Can go out of bounds.
|
||||
pub fn to_pt(self, b: &GPSBounds) -> Pt2D {
|
||||
let (width, height) = {
|
||||
let pt = b.get_max_world_pt();
|
||||
(pt.x(), pt.y())
|
||||
};
|
||||
|
||||
let x = (self.x() - b.min_lon) / (b.max_lon - b.min_lon) * width;
|
||||
// Invert y, so that the northernmost latitude is 0. Screen drawing order, not Cartesian
|
||||
// grid.
|
||||
let y = height - ((self.y() - b.min_lat) / (b.max_lat - b.min_lat) * height);
|
||||
Pt2D::new(x, y)
|
||||
}
|
||||
|
||||
/// Returns the Haversine distance to another point.
|
||||
pub(crate) fn gps_dist(self, other: LonLat) -> Distance {
|
||||
let earth_radius_m = 6_371_000.0;
|
||||
let lon1 = self.x().to_radians();
|
||||
let lon2 = other.x().to_radians();
|
||||
let lat1 = self.y().to_radians();
|
||||
let lat2 = other.y().to_radians();
|
||||
|
||||
let delta_lat = lat2 - lat1;
|
||||
let delta_lon = lon2 - lon1;
|
||||
|
||||
let a = (delta_lat / 2.0).sin().powi(2)
|
||||
+ (delta_lon / 2.0).sin().powi(2) * lat1.cos() * lat2.cos();
|
||||
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
|
||||
Distance::meters(earth_radius_m * c)
|
||||
}
|
||||
|
||||
/// Pretty meaningless units, for comparing distances very roughly
|
||||
pub fn fast_dist(self, other: LonLat) -> NotNan<f64> {
|
||||
NotNan::new((self.x() - other.x()).powi(2) + (self.y() - other.y()).powi(2)).unwrap()
|
||||
}
|
||||
|
||||
/// Finds the average of a set of coordinates.
|
||||
pub fn center(pts: &[LonLat]) -> LonLat {
|
||||
if pts.is_empty() {
|
||||
panic!("Can't find center of 0 points");
|
||||
}
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
for pt in pts {
|
||||
x += pt.x();
|
||||
y += pt.y();
|
||||
}
|
||||
let len = pts.len() as f64;
|
||||
LonLat::new(x / len, y / len)
|
||||
}
|
||||
|
||||
/// Parses a WKT-style line-string into a list of coordinates.
|
||||
pub fn parse_wkt_linestring(raw: &str) -> Option<Vec<LonLat>> {
|
||||
// Input is something like LINESTRING (-111.9263026 33.4245036, -111.9275146 33.4245016,
|
||||
// -111.9278751 33.4233106)
|
||||
let mut pts = Vec::new();
|
||||
// -111.9446 33.425474, -111.9442814 33.4254737, -111.9442762 33.426894
|
||||
for pair in raw
|
||||
.strip_prefix("LINESTRING (")?
|
||||
.strip_suffix(')')?
|
||||
.split(", ")
|
||||
{
|
||||
let mut nums = Vec::new();
|
||||
for x in pair.split(' ') {
|
||||
nums.push(x.parse::<f64>().ok()?);
|
||||
}
|
||||
if nums.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
pts.push(LonLat::new(nums[0], nums[1]));
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
Some(pts)
|
||||
}
|
||||
|
||||
/// Extract polygons from a raw GeoJSON string. For multipolygons, only returns the first
|
||||
/// member. If the GeoJSON feature has a property called `name`, this will also be returned.
|
||||
pub fn parse_geojson_polygons(raw: String) -> Result<Vec<(Vec<LonLat>, Option<String>)>> {
|
||||
let geojson = raw.parse::<GeoJson>()?;
|
||||
let features = match geojson {
|
||||
GeoJson::Feature(feature) => vec![feature],
|
||||
GeoJson::FeatureCollection(feature_collection) => feature_collection.features,
|
||||
_ => bail!("Unexpected geojson: {:?}", geojson),
|
||||
};
|
||||
let mut polygons = Vec::new();
|
||||
for mut feature in features {
|
||||
let points = match feature.geometry.take().map(|g| g.value) {
|
||||
Some(Value::MultiPolygon(multi_polygon)) => multi_polygon[0][0].clone(),
|
||||
Some(Value::Polygon(polygon)) => polygon[0].clone(),
|
||||
_ => bail!("Unexpected feature: {:?}", feature),
|
||||
};
|
||||
let name = feature
|
||||
.property("name")
|
||||
.and_then(|value| value.as_str())
|
||||
.map(|x| x.to_string());
|
||||
polygons.push((
|
||||
points
|
||||
.into_iter()
|
||||
.map(|pt| LonLat::new(pt[0], pt[1]))
|
||||
.collect(),
|
||||
name,
|
||||
));
|
||||
}
|
||||
Ok(polygons)
|
||||
}
|
||||
|
||||
/// Reads a GeoJSON file and returns coordinates from the one polygon contained.
|
||||
pub fn read_geojson_polygon(path: &str) -> Result<Vec<LonLat>> {
|
||||
let raw = fs_err::read_to_string(path)?;
|
||||
let mut list = Self::parse_geojson_polygons(raw)?;
|
||||
if list.len() != 1 {
|
||||
bail!("{path} doesn't contain exactly one polygon");
|
||||
}
|
||||
Ok(list.pop().unwrap().0)
|
||||
}
|
||||
|
||||
pub fn to_geojson(self) -> geojson::Geometry {
|
||||
geojson::Geometry::new(geojson::Value::Point(vec![self.x(), self.y()]))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LonLat {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "LonLat({0}, {1})", self.x(), self.y())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LonLat> for geo::Point {
|
||||
fn from(pt: LonLat) -> Self {
|
||||
geo::Point::new(pt.x(), pt.y())
|
||||
}
|
||||
}
|
208
geom/src/lib.rs
208
geom/src/lib.rs
@ -1,208 +0,0 @@
|
||||
#![allow(clippy::new_without_default)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate anyhow;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub use crate::angle::Angle;
|
||||
pub use crate::bounds::{Bounds, GPSBounds, QuadTree, QuadTreeBuilder};
|
||||
pub use crate::circle::Circle;
|
||||
pub use crate::distance::Distance;
|
||||
pub use crate::duration::Duration;
|
||||
pub use crate::find_closest::FindClosest;
|
||||
pub use crate::gps::LonLat;
|
||||
pub use crate::line::{InfiniteLine, Line};
|
||||
pub use crate::percent::Percent;
|
||||
pub use crate::polygon::Polygon;
|
||||
pub use crate::polyline::{ArrowCap, PolyLine};
|
||||
pub use crate::pt::{HashablePt2D, Pt2D};
|
||||
pub use crate::ring::Ring;
|
||||
pub use crate::speed::Speed;
|
||||
pub use crate::stats::{HgramValue, Histogram, Statistic};
|
||||
pub use crate::tessellation::{Tessellation, Triangle};
|
||||
pub use crate::time::Time;
|
||||
|
||||
mod angle;
|
||||
mod bounds;
|
||||
mod circle;
|
||||
mod conversions;
|
||||
mod distance;
|
||||
mod duration;
|
||||
mod find_closest;
|
||||
mod gps;
|
||||
mod line;
|
||||
mod percent;
|
||||
mod polygon;
|
||||
mod polyline;
|
||||
mod pt;
|
||||
mod ring;
|
||||
mod speed;
|
||||
mod stats;
|
||||
mod tessellation;
|
||||
mod time;
|
||||
mod utils;
|
||||
|
||||
// About 0.4 inches... which is quite tiny on the scale of things. :)
|
||||
pub const EPSILON_DIST: Distance = Distance::const_meters(0.01);
|
||||
|
||||
/// Reduce the precision of an f64. This helps ensure serialization is idempotent (everything is
|
||||
/// exactly the same before and after saving/loading). Ideally we'd use some kind of proper
|
||||
/// fixed-precision type instead of f64.
|
||||
pub fn trim_f64(x: f64) -> f64 {
|
||||
(x * 10_000.0).round() / 10_000.0
|
||||
}
|
||||
|
||||
/// Serializes a trimmed `f64` as an `i32` to save space.
|
||||
fn serialize_f64<S: Serializer>(x: &f64, s: S) -> Result<S::Ok, S::Error> {
|
||||
// So a trimmed f64's range becomes 2**31 / 10,000 =~ 214,000, which is plenty
|
||||
// We MUST round here, the same as trim_f64. The unit test demonstrates why.
|
||||
let int = (x * 10_000.0).round() as i32;
|
||||
int.serialize(s)
|
||||
}
|
||||
|
||||
/// Deserializes a trimmed `f64` from an `i32`.
|
||||
fn deserialize_f64<'de, D: Deserializer<'de>>(d: D) -> Result<f64, D::Error> {
|
||||
let x = <i32>::deserialize(d)?;
|
||||
Ok(x as f64 / 10_000.0)
|
||||
}
|
||||
|
||||
/// Specifies how to stringify different geom objects.
|
||||
#[derive(Clone, Serialize, Deserialize, Copy)]
|
||||
pub struct UnitFmt {
|
||||
/// Round `Duration`s to a whole number of seconds.
|
||||
pub round_durations: bool,
|
||||
/// Display in metric; US imperial otherwise.
|
||||
pub metric: bool,
|
||||
}
|
||||
|
||||
impl UnitFmt {
|
||||
/// Default settings using metric.
|
||||
pub fn metric() -> Self {
|
||||
Self {
|
||||
round_durations: true,
|
||||
metric: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Default settings using imperial.
|
||||
pub fn imperial() -> Self {
|
||||
Self {
|
||||
round_durations: true,
|
||||
metric: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CornerRadii {
|
||||
pub top_left: f64,
|
||||
pub top_right: f64,
|
||||
pub bottom_right: f64,
|
||||
pub bottom_left: f64,
|
||||
}
|
||||
|
||||
impl CornerRadii {
|
||||
pub fn uniform(radius: f64) -> Self {
|
||||
Self {
|
||||
top_left: radius,
|
||||
top_right: radius,
|
||||
bottom_right: radius,
|
||||
bottom_left: radius,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self::uniform(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<f64> for CornerRadii {
|
||||
fn from(uniform: f64) -> Self {
|
||||
Self::uniform(uniform)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CornerRadii {
|
||||
fn default() -> Self {
|
||||
Self::zero()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a GeoJson with one feature per geometry, with the specified properties.
|
||||
// TODO Rethink after https://github.com/georust/geojson/issues/170
|
||||
pub fn geometries_with_properties_to_geojson(
|
||||
input: Vec<(
|
||||
geojson::Geometry,
|
||||
serde_json::Map<String, serde_json::Value>,
|
||||
)>,
|
||||
) -> geojson::GeoJson {
|
||||
let mut features = Vec::new();
|
||||
for (geom, properties) in input {
|
||||
let mut f = geojson::Feature::from(geom);
|
||||
f.properties = Some(properties);
|
||||
features.push(f);
|
||||
}
|
||||
geojson::GeoJson::from(geojson::FeatureCollection {
|
||||
bbox: None,
|
||||
features,
|
||||
foreign_members: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a GeoJson with one feature per geometry, and no properties.
|
||||
pub fn geometries_to_geojson(input: Vec<geojson::Geometry>) -> geojson::GeoJson {
|
||||
let mut features = Vec::new();
|
||||
for geom in input {
|
||||
features.push(geojson::Feature::from(geom));
|
||||
}
|
||||
geojson::GeoJson::from(geojson::FeatureCollection {
|
||||
bbox: None,
|
||||
features,
|
||||
foreign_members: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{Rng, SeedableRng};
|
||||
|
||||
#[test]
|
||||
fn f64_trimming() {
|
||||
let mut rng = rand_xorshift::XorShiftRng::seed_from_u64(42);
|
||||
for _ in 0..1_000 {
|
||||
// Generate a random point
|
||||
let input = Pt2D::new(
|
||||
rng.gen_range(-214_000.00..214_000.0),
|
||||
rng.gen_range(-214_000.00..214_000.0),
|
||||
);
|
||||
// Round-trip to JSON and bincode
|
||||
let json_roundtrip: Pt2D =
|
||||
serde_json::from_slice(serde_json::to_string(&input).unwrap().as_bytes()).unwrap();
|
||||
let bincode_roundtrip: Pt2D =
|
||||
bincode::deserialize(&bincode::serialize(&input).unwrap()).unwrap();
|
||||
|
||||
// Make sure everything is EXACTLY equal
|
||||
if !exactly_eq(input, json_roundtrip) || !exactly_eq(input, bincode_roundtrip) {
|
||||
panic!("Round-tripping mismatch!\ninput= {:?}\njson_roundtrip= {:?}\nbincode_roundtrip={:?}", input,json_roundtrip, bincode_roundtrip);
|
||||
}
|
||||
}
|
||||
|
||||
// Hardcode a particular case, where we can hand-verify that it trims to 4 decimal places
|
||||
let input = Pt2D::new(1.2345678, 9.876543);
|
||||
let json_roundtrip: Pt2D =
|
||||
serde_json::from_slice(serde_json::to_string(&input).unwrap().as_bytes()).unwrap();
|
||||
let bincode_roundtrip: Pt2D =
|
||||
bincode::deserialize(&bincode::serialize(&input).unwrap()).unwrap();
|
||||
assert_eq!(input.x(), 1.2346);
|
||||
assert_eq!(input.y(), 9.8765);
|
||||
assert!(exactly_eq(input, json_roundtrip));
|
||||
assert!(exactly_eq(input, bincode_roundtrip));
|
||||
}
|
||||
|
||||
// Don't use the PartialEq implementation, which does an epsilon check
|
||||
fn exactly_eq(pt1: Pt2D, pt2: Pt2D) -> bool {
|
||||
pt1.x() == pt2.x() && pt1.y() == pt2.y()
|
||||
}
|
||||
}
|
279
geom/src/line.rs
279
geom/src/line.rs
@ -1,279 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Angle, Distance, PolyLine, Polygon, Pt2D, EPSILON_DIST};
|
||||
|
||||
/// A line segment.
|
||||
// TODO Rename?
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
|
||||
pub struct Line(Pt2D, Pt2D);
|
||||
|
||||
impl Line {
|
||||
/// Creates a line segment between two points, which must not be the same
|
||||
pub fn new(pt1: Pt2D, pt2: Pt2D) -> Result<Line> {
|
||||
if pt1.dist_to(pt2) <= EPSILON_DIST {
|
||||
bail!("Line from {:?} to {:?} too small", pt1, pt2);
|
||||
}
|
||||
Ok(Line(pt1, pt2))
|
||||
}
|
||||
|
||||
/// Equivalent to `Line::new(pt1, pt2).unwrap()`. Use this to effectively document an assertion
|
||||
/// at the call-site.
|
||||
pub fn must_new(pt1: Pt2D, pt2: Pt2D) -> Line {
|
||||
Line::new(pt1, pt2).unwrap()
|
||||
}
|
||||
|
||||
/// Returns an infinite line passing through this line's two points.
|
||||
pub fn infinite(&self) -> InfiniteLine {
|
||||
InfiniteLine(self.0, self.1)
|
||||
}
|
||||
|
||||
/// Returns the first point in this line segment.
|
||||
pub fn pt1(&self) -> Pt2D {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the second point in this line segment.
|
||||
pub fn pt2(&self) -> Pt2D {
|
||||
self.1
|
||||
}
|
||||
|
||||
/// Returns the two points in this line segment.
|
||||
pub fn points(&self) -> Vec<Pt2D> {
|
||||
vec![self.0, self.1]
|
||||
}
|
||||
|
||||
/// Returns a polyline containing these two points.
|
||||
pub fn to_polyline(&self) -> PolyLine {
|
||||
PolyLine::must_new(self.points())
|
||||
}
|
||||
|
||||
/// Returns a thick line segment.
|
||||
pub fn make_polygons(&self, thickness: Distance) -> Polygon {
|
||||
self.to_polyline().make_polygons(thickness)
|
||||
}
|
||||
|
||||
/// Length of the line segment
|
||||
pub fn length(&self) -> Distance {
|
||||
self.pt1().dist_to(self.pt2())
|
||||
}
|
||||
|
||||
/// If two line segments intersect -- including endpoints -- return the point where they hit.
|
||||
/// Undefined if the two lines have more than one intersection point!
|
||||
// TODO Also return the distance along self
|
||||
pub fn intersection(&self, other: &Line) -> Option<Pt2D> {
|
||||
// From http://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
||||
if is_counter_clockwise(self.pt1(), other.pt1(), other.pt2())
|
||||
== is_counter_clockwise(self.pt2(), other.pt1(), other.pt2())
|
||||
|| is_counter_clockwise(self.pt1(), self.pt2(), other.pt1())
|
||||
== is_counter_clockwise(self.pt1(), self.pt2(), other.pt2())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let hit = self.infinite().intersection(&other.infinite())?;
|
||||
if self.contains_pt(hit) {
|
||||
// TODO and other contains pt, then we dont need ccw check thing
|
||||
Some(hit)
|
||||
} else {
|
||||
// TODO Should be impossible, but I was hitting it somewhere
|
||||
println!(
|
||||
"{} and {} intersect, but first line doesn't contain_pt({})",
|
||||
self, other, hit
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine if two line segments intersect, but more so than just two endpoints touching.
|
||||
pub fn crosses(&self, other: &Line) -> bool {
|
||||
#[allow(clippy::suspicious_operation_groupings)] // false positive
|
||||
if self.pt1() == other.pt1()
|
||||
|| self.pt1() == other.pt2()
|
||||
|| self.pt2() == other.pt1()
|
||||
|| self.pt2() == other.pt2()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.intersection(other).is_some()
|
||||
}
|
||||
|
||||
/// If the line segment intersects with an infinite line -- including endpoints -- return the
|
||||
/// point where they hit. Undefined if the segment and infinite line intersect at more than one
|
||||
/// point!
|
||||
// TODO Also return the distance along self
|
||||
pub fn intersection_infinite(&self, other: &InfiniteLine) -> Option<Pt2D> {
|
||||
let hit = self.infinite().intersection(other)?;
|
||||
if self.contains_pt(hit) {
|
||||
Some(hit)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Perpendicularly shifts the line over to the right. Width must be non-negative.
|
||||
pub fn shift_right(&self, width: Distance) -> Line {
|
||||
assert!(width >= Distance::ZERO);
|
||||
let angle = self.angle().rotate_degs(90.0);
|
||||
Line::must_new(
|
||||
self.pt1().project_away(width, angle),
|
||||
self.pt2().project_away(width, angle),
|
||||
)
|
||||
}
|
||||
|
||||
/// Perpendicularly shifts the line over to the left. Width must be non-negative.
|
||||
pub fn shift_left(&self, width: Distance) -> Line {
|
||||
assert!(width >= Distance::ZERO);
|
||||
let angle = self.angle().rotate_degs(-90.0);
|
||||
Line::must_new(
|
||||
self.pt1().project_away(width, angle),
|
||||
self.pt2().project_away(width, angle),
|
||||
)
|
||||
}
|
||||
|
||||
/// Perpendicularly shifts the line to the right if positive or left if negative.
|
||||
pub fn shift_either_direction(&self, width: Distance) -> Line {
|
||||
if width >= Distance::ZERO {
|
||||
self.shift_right(width)
|
||||
} else {
|
||||
self.shift_left(-width)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reversed line segment
|
||||
pub fn reversed(&self) -> Line {
|
||||
Line::must_new(self.pt2(), self.pt1())
|
||||
}
|
||||
|
||||
/// The angle of the line segment, from the first to the second point
|
||||
pub fn angle(&self) -> Angle {
|
||||
self.pt1().angle_to(self.pt2())
|
||||
}
|
||||
|
||||
/// Returns a point along the line segment, unless the distance exceeds the segment's length.
|
||||
pub fn dist_along(&self, dist: Distance) -> Result<Pt2D> {
|
||||
let len = self.length();
|
||||
if dist < Distance::ZERO || dist > len {
|
||||
bail!("dist_along({}) of a length {} line", dist, len);
|
||||
}
|
||||
self.percent_along(dist / len)
|
||||
}
|
||||
/// Equivalent to `self.dist_along(dist).unwrap()`. Use this to document an assertion at the
|
||||
/// call-site.
|
||||
pub fn must_dist_along(&self, dist: Distance) -> Pt2D {
|
||||
self.dist_along(dist).unwrap()
|
||||
}
|
||||
|
||||
pub fn unbounded_dist_along(&self, dist: Distance) -> Pt2D {
|
||||
self.unbounded_percent_along(dist / self.length())
|
||||
}
|
||||
|
||||
pub fn unbounded_percent_along(&self, percent: f64) -> Pt2D {
|
||||
Pt2D::new(
|
||||
self.pt1().x() + percent * (self.pt2().x() - self.pt1().x()),
|
||||
self.pt1().y() + percent * (self.pt2().y() - self.pt1().y()),
|
||||
)
|
||||
}
|
||||
pub fn percent_along(&self, percent: f64) -> Result<Pt2D> {
|
||||
if !(0.0..=1.0).contains(&percent) {
|
||||
bail!("percent_along({}) of some line outside [0, 1]", percent);
|
||||
}
|
||||
Ok(self.unbounded_percent_along(percent))
|
||||
}
|
||||
|
||||
pub fn slice(&self, from: Distance, to: Distance) -> Result<Line> {
|
||||
if from < Distance::ZERO || to < Distance::ZERO || from >= to {
|
||||
bail!("slice({}, {}) makes no sense", from, to);
|
||||
}
|
||||
Line::new(self.dist_along(from)?, self.dist_along(to)?)
|
||||
}
|
||||
|
||||
/// Returns a subset of this line, with two percentages along the line's length.
|
||||
pub fn percent_slice(&self, from: f64, to: f64) -> Result<Line> {
|
||||
self.slice(from * self.length(), to * self.length())
|
||||
}
|
||||
|
||||
pub fn middle(&self) -> Result<Pt2D> {
|
||||
self.dist_along(self.length() / 2.0)
|
||||
}
|
||||
|
||||
pub fn contains_pt(&self, pt: Pt2D) -> bool {
|
||||
self.dist_along_of_point(pt).is_some()
|
||||
}
|
||||
|
||||
pub fn dist_along_of_point(&self, pt: Pt2D) -> Option<Distance> {
|
||||
let dist1 = self.pt1().raw_dist_to(pt);
|
||||
let dist2 = pt.raw_dist_to(self.pt2());
|
||||
let length = self.pt1().raw_dist_to(self.pt2());
|
||||
if (dist1 + dist2 - length).abs() < EPSILON_DIST.inner_meters() {
|
||||
Some(Distance::meters(dist1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn percent_along_of_point(&self, pt: Pt2D) -> Option<f64> {
|
||||
let dist = self.dist_along_of_point(pt)?;
|
||||
Some(dist / self.length())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Line {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "Line::new(")?;
|
||||
writeln!(f, " Pt2D::new({}, {}),", self.0.x(), self.0.y())?;
|
||||
writeln!(f, " Pt2D::new({}, {}),", self.1.x(), self.1.y())?;
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
||||
|
||||
fn is_counter_clockwise(pt1: Pt2D, pt2: Pt2D, pt3: Pt2D) -> bool {
|
||||
(pt3.y() - pt1.y()) * (pt2.x() - pt1.x()) > (pt2.y() - pt1.y()) * (pt3.x() - pt1.x())
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct InfiniteLine(Pt2D, Pt2D);
|
||||
|
||||
impl InfiniteLine {
|
||||
/// Fails for parallel lines.
|
||||
// https://stackoverflow.com/a/565282 by way of
|
||||
// https://github.com/ucarion/line_intersection/blob/master/src/lib.rs
|
||||
pub fn intersection(&self, other: &InfiniteLine) -> Option<Pt2D> {
|
||||
#![allow(clippy::many_single_char_names)]
|
||||
fn cross(a: (f64, f64), b: (f64, f64)) -> f64 {
|
||||
a.0 * b.1 - a.1 * b.0
|
||||
}
|
||||
|
||||
let p = self.0;
|
||||
let q = other.0;
|
||||
let r = (self.1.x() - self.0.x(), self.1.y() - self.0.y());
|
||||
let s = (other.1.x() - other.0.x(), other.1.y() - other.0.y());
|
||||
|
||||
let r_cross_s = cross(r, s);
|
||||
let q_minus_p = (q.x() - p.x(), q.y() - p.y());
|
||||
//let q_minus_p_cross_r = cross(q_minus_p, r);
|
||||
|
||||
if r_cross_s == 0.0 {
|
||||
// Parallel
|
||||
None
|
||||
} else {
|
||||
let t = cross(q_minus_p, (s.0 / r_cross_s, s.1 / r_cross_s));
|
||||
//let u = cross(q_minus_p, Pt2D::new(r.x() / r_cross_s, r.y() / r_cross_s));
|
||||
Some(Pt2D::new(p.x() + t * r.0, p.y() + t * r.1))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pt_angle(pt: Pt2D, angle: Angle) -> InfiniteLine {
|
||||
Line::must_new(pt, pt.project_away(Distance::meters(1.0), angle)).infinite()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InfiniteLine {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "InfiniteLine::new(")?;
|
||||
writeln!(f, " Pt2D::new({}, {}),", self.0.x(), self.0.y())?;
|
||||
writeln!(f, " Pt2D::new({}, {}),", self.1.x(), self.1.y())?;
|
||||
write!(f, ")")
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Most of the time, [0, 1]. But some callers may go outside this range.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Percent(f64);
|
||||
|
||||
impl Percent {
|
||||
pub fn inner(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn int(x: usize) -> Percent {
|
||||
if x > 100 {
|
||||
panic!("Percent::int({}) too big", x);
|
||||
}
|
||||
Percent((x as f64) / 100.0)
|
||||
}
|
||||
|
||||
pub fn of(x: usize, total: usize) -> Percent {
|
||||
Percent((x as f64) / (total as f64))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Percent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:.2}%", self.0 * 100.0)
|
||||
}
|
||||
}
|
@ -1,559 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use geo::{
|
||||
Area, BooleanOps, Contains, ConvexHull, Intersects, MapCoordsInPlace, SimplifyVwPreserve,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
Angle, Bounds, CornerRadii, Distance, GPSBounds, LonLat, PolyLine, Pt2D, Ring, Tessellation,
|
||||
Triangle,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Polygon {
|
||||
// [0] is the outer/exterior ring, and the others are holes/interiors
|
||||
pub(crate) rings: Vec<Ring>,
|
||||
// For performance reasons, some callers may want to generate the polygon's Rings and
|
||||
// Tessellation at the same time, instead of using earcutr.
|
||||
pub(crate) tessellation: Option<Tessellation>,
|
||||
}
|
||||
|
||||
impl Polygon {
|
||||
pub fn with_holes(outer: Ring, mut inner: Vec<Ring>) -> Self {
|
||||
inner.insert(0, outer);
|
||||
Self {
|
||||
rings: inner,
|
||||
tessellation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_rings(rings: Vec<Ring>) -> Self {
|
||||
assert!(!rings.is_empty());
|
||||
Self {
|
||||
rings,
|
||||
tessellation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pretessellated(rings: Vec<Ring>, tessellation: Tessellation) -> Self {
|
||||
Self {
|
||||
rings,
|
||||
tessellation: Some(tessellation),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_geojson(raw: &[Vec<Vec<f64>>]) -> Result<Self> {
|
||||
let mut rings = Vec::new();
|
||||
for pts in raw {
|
||||
let transformed: Vec<Pt2D> =
|
||||
pts.iter().map(|pair| Pt2D::new(pair[0], pair[1])).collect();
|
||||
rings.push(Ring::new(transformed)?);
|
||||
}
|
||||
Ok(Self::from_rings(rings))
|
||||
}
|
||||
|
||||
pub fn from_triangle(tri: &Triangle) -> Self {
|
||||
Ring::must_new(vec![tri.pt1, tri.pt2, tri.pt3, tri.pt1]).into_polygon()
|
||||
}
|
||||
|
||||
pub fn triangles(&self) -> Vec<Triangle> {
|
||||
Tessellation::from(self.clone()).triangles()
|
||||
}
|
||||
|
||||
/// Does this polygon contain the point in its interior?
|
||||
pub fn contains_pt(&self, pt: Pt2D) -> bool {
|
||||
self.to_geo().contains(&geo::Point::from(pt))
|
||||
}
|
||||
|
||||
pub fn get_bounds(&self) -> Bounds {
|
||||
// Interiors should always be strictly contained within the polygon's exterior
|
||||
Bounds::from(self.get_outer_ring().points())
|
||||
}
|
||||
|
||||
/// Transformations must preserve Rings.
|
||||
fn transform<F: Fn(&Pt2D) -> Pt2D>(&self, f: F) -> Result<Self> {
|
||||
let mut rings = Vec::new();
|
||||
for ring in &self.rings {
|
||||
rings.push(Ring::new(ring.points().iter().map(&f).collect())?);
|
||||
}
|
||||
Ok(Self {
|
||||
rings,
|
||||
tessellation: self.tessellation.clone().take().map(|mut t| {
|
||||
t.transform(f);
|
||||
t
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn translate(&self, dx: f64, dy: f64) -> Self {
|
||||
self.transform(|pt| pt.offset(dx, dy))
|
||||
.expect("translate shouldn't collapse Rings")
|
||||
}
|
||||
|
||||
/// When `factor` is small, this may collapse Rings and thus fail.
|
||||
pub fn scale(&self, factor: f64) -> Result<Self> {
|
||||
self.transform(|pt| Pt2D::new(pt.x() * factor, pt.y() * factor))
|
||||
}
|
||||
|
||||
/// When `factor` is known to be over 1, then scaling can't fail.
|
||||
pub fn must_scale(&self, factor: f64) -> Self {
|
||||
if factor < 1.0 {
|
||||
panic!("must_scale({factor}) might collapse Rings. Use scale()");
|
||||
}
|
||||
self.transform(|pt| Pt2D::new(pt.x() * factor, pt.y() * factor))
|
||||
.expect("must_scale collapsed a Ring")
|
||||
}
|
||||
|
||||
pub fn rotate(&self, angle: Angle) -> Self {
|
||||
self.rotate_around(angle, self.center())
|
||||
}
|
||||
|
||||
pub fn rotate_around(&self, angle: Angle, pivot: Pt2D) -> Self {
|
||||
self.transform(|pt| {
|
||||
let origin_pt = Pt2D::new(pt.x() - pivot.x(), pt.y() - pivot.y());
|
||||
let (sin, cos) = angle.normalized_radians().sin_cos();
|
||||
Pt2D::new(
|
||||
pivot.x() + origin_pt.x() * cos - origin_pt.y() * sin,
|
||||
pivot.y() + origin_pt.y() * cos + origin_pt.x() * sin,
|
||||
)
|
||||
})
|
||||
.expect("rotate_around shouldn't collapse Rings")
|
||||
}
|
||||
|
||||
pub fn centered_on(&self, center: Pt2D) -> Self {
|
||||
let bounds = self.get_bounds();
|
||||
let dx = center.x() - bounds.width() / 2.0;
|
||||
let dy = center.y() - bounds.height() / 2.0;
|
||||
self.translate(dx, dy)
|
||||
}
|
||||
|
||||
pub fn get_outer_ring(&self) -> &Ring {
|
||||
&self.rings[0]
|
||||
}
|
||||
|
||||
pub fn into_outer_ring(mut self) -> Ring {
|
||||
self.rings.remove(0)
|
||||
}
|
||||
|
||||
/// Returns the arithmetic mean of the outer ring's points. The result could wind up inside a
|
||||
/// hole in the polygon. Consider using `polylabel` too.
|
||||
pub fn center(&self) -> Pt2D {
|
||||
let mut pts = self.get_outer_ring().clone().into_points();
|
||||
pts.pop();
|
||||
Pt2D::center(&pts)
|
||||
}
|
||||
|
||||
/// Top-left at the origin. Doesn't take Distance, because this is usually pixels, actually.
|
||||
pub fn maybe_rectangle(width: f64, height: f64) -> Result<Self> {
|
||||
Ring::new(vec![
|
||||
Pt2D::new(0.0, 0.0),
|
||||
Pt2D::new(width, 0.0),
|
||||
Pt2D::new(width, height),
|
||||
Pt2D::new(0.0, height),
|
||||
Pt2D::new(0.0, 0.0),
|
||||
])
|
||||
.map(|ring| ring.into_polygon())
|
||||
}
|
||||
|
||||
/// Top-left at the origin. Doesn't take Distance, because this is usually pixels, actually.
|
||||
/// Note this will panic if `width` or `height` is 0.
|
||||
pub fn rectangle(width: f64, height: f64) -> Self {
|
||||
Self::maybe_rectangle(width, height).unwrap()
|
||||
}
|
||||
|
||||
pub fn rectangle_centered(center: Pt2D, width: Distance, height: Distance) -> Self {
|
||||
Self::rectangle(width.inner_meters(), height.inner_meters()).translate(
|
||||
center.x() - width.inner_meters() / 2.0,
|
||||
center.y() - height.inner_meters() / 2.0,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn rectangle_two_corners(pt1: Pt2D, pt2: Pt2D) -> Option<Self> {
|
||||
if Pt2D::new(pt1.x(), 0.0) == Pt2D::new(pt2.x(), 0.0)
|
||||
|| Pt2D::new(0.0, pt1.y()) == Pt2D::new(0.0, pt2.y())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let (x1, width) = if pt1.x() < pt2.x() {
|
||||
(pt1.x(), pt2.x() - pt1.x())
|
||||
} else {
|
||||
(pt2.x(), pt1.x() - pt2.x())
|
||||
};
|
||||
let (y1, height) = if pt1.y() < pt2.y() {
|
||||
(pt1.y(), pt2.y() - pt1.y())
|
||||
} else {
|
||||
(pt2.y(), pt1.y() - pt2.y())
|
||||
};
|
||||
Some(Self::rectangle(width, height).translate(x1, y1))
|
||||
}
|
||||
|
||||
/// Top-left at the origin. Doesn't take Distance, because this is usually pixels, actually.
|
||||
pub fn maybe_rounded_rectangle<R: Into<CornerRadii>>(w: f64, h: f64, r: R) -> Option<Self> {
|
||||
let r = r.into();
|
||||
let max_r = r
|
||||
.top_left
|
||||
.max(r.top_right)
|
||||
.max(r.bottom_right)
|
||||
.max(r.bottom_left);
|
||||
if 2.0 * max_r > w || 2.0 * max_r > h {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut pts = vec![];
|
||||
|
||||
const RESOLUTION: usize = 5;
|
||||
let mut arc = |r: f64, center: Pt2D, angle1_degs: f64, angle2_degs: f64| {
|
||||
for i in 0..=RESOLUTION {
|
||||
let angle = Angle::degrees(
|
||||
angle1_degs + (angle2_degs - angle1_degs) * ((i as f64) / (RESOLUTION as f64)),
|
||||
);
|
||||
pts.push(center.project_away(Distance::meters(r), angle.invert_y()));
|
||||
}
|
||||
};
|
||||
|
||||
arc(r.top_left, Pt2D::new(r.top_left, r.top_left), 180.0, 90.0);
|
||||
arc(
|
||||
r.top_right,
|
||||
Pt2D::new(w - r.top_right, r.top_right),
|
||||
90.0,
|
||||
0.0,
|
||||
);
|
||||
arc(
|
||||
r.bottom_right,
|
||||
Pt2D::new(w - r.bottom_right, h - r.bottom_right),
|
||||
360.0,
|
||||
270.0,
|
||||
);
|
||||
arc(
|
||||
r.bottom_left,
|
||||
Pt2D::new(r.bottom_left, h - r.bottom_left),
|
||||
270.0,
|
||||
180.0,
|
||||
);
|
||||
// Close it off
|
||||
pts.push(Pt2D::new(0.0, r.top_left));
|
||||
|
||||
// If the radius was maximized, then some of the edges will be zero length.
|
||||
pts.dedup();
|
||||
|
||||
Some(Ring::must_new(pts).into_polygon())
|
||||
}
|
||||
|
||||
/// A rectangle, two sides of which are fully rounded half-circles.
|
||||
pub fn pill(w: f64, h: f64) -> Self {
|
||||
let r = w.min(h) / 2.0;
|
||||
Self::maybe_rounded_rectangle(w, h, r).unwrap()
|
||||
}
|
||||
|
||||
/// Top-left at the origin. Doesn't take Distance, because this is usually pixels, actually.
|
||||
/// If it's not possible to apply the specified radius, fallback to a regular rectangle.
|
||||
pub fn rounded_rectangle<R: Into<CornerRadii>>(w: f64, h: f64, r: R) -> Self {
|
||||
Self::maybe_rounded_rectangle(w, h, r).unwrap_or_else(|| Self::rectangle(w, h))
|
||||
}
|
||||
|
||||
/// Union all of the polygons into one geo::MultiPolygon
|
||||
pub fn union_all_into_multipolygon(mut list: Vec<Self>) -> geo::MultiPolygon {
|
||||
// TODO Not sure why this happened, or if this is really valid to construct...
|
||||
if list.is_empty() {
|
||||
return geo::MultiPolygon(Vec::new());
|
||||
}
|
||||
|
||||
let mut result = geo::MultiPolygon(vec![list.pop().unwrap().into()]);
|
||||
for p in list {
|
||||
result = result.union(&p.into());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn intersection(&self, other: &Self) -> Result<Vec<Self>> {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
from_multi(self.to_geo().intersection(&other.to_geo()))
|
||||
})) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
println!("BooleanOps crashed: {err:?}");
|
||||
bail!("BooleanOps crashed: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn difference(&self, other: &Self) -> Result<Vec<Self>> {
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
from_multi(self.to_geo().difference(&other.to_geo()))
|
||||
})) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
println!("BooleanOps crashed: {err:?}");
|
||||
bail!("BooleanOps crashed: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn convex_hull(list: Vec<Self>) -> Result<Self> {
|
||||
let mp: geo::MultiPolygon = list.into_iter().map(|p| p.to_geo()).collect();
|
||||
mp.convex_hull().try_into()
|
||||
}
|
||||
|
||||
pub fn concave_hull(points: Vec<Pt2D>, concavity: u32) -> Result<Self> {
|
||||
use geo::k_nearest_concave_hull::KNearestConcaveHull;
|
||||
let points: Vec<geo::Point> = points.iter().map(|p| geo::Point::from(*p)).collect();
|
||||
points.k_nearest_concave_hull(concavity).try_into()
|
||||
}
|
||||
|
||||
/// Find the "pole of inaccessibility" -- the most distant internal point from the polygon
|
||||
/// outline
|
||||
pub fn polylabel(&self) -> Pt2D {
|
||||
let pt = polylabel::polylabel(&self.to_geo(), &1.0).unwrap();
|
||||
Pt2D::new(pt.x(), pt.y())
|
||||
}
|
||||
|
||||
/// Do two polygons intersect at all?
|
||||
pub fn intersects(&self, other: &Self) -> bool {
|
||||
self.to_geo().intersects(&other.to_geo())
|
||||
}
|
||||
|
||||
/// Does this polygon intersect a polyline?
|
||||
pub fn intersects_polyline(&self, pl: &PolyLine) -> bool {
|
||||
self.to_geo().intersects(&pl.to_geo())
|
||||
}
|
||||
|
||||
pub(crate) fn get_rings(&self) -> &[Ring] {
|
||||
&self.rings
|
||||
}
|
||||
|
||||
/// Creates the outline around the polygon (both the exterior and holes), with the thickness
|
||||
/// half straddling the polygon and half of it just outside.
|
||||
///
|
||||
/// Returns a `Tessellation` that may union together the outline from the exterior and multiple
|
||||
/// holes. Callers that need a `Polygon` must call `to_outline` on the individual `Rings`.
|
||||
pub fn to_outline(&self, thickness: Distance) -> Tessellation {
|
||||
Tessellation::union_all(
|
||||
self.rings
|
||||
.iter()
|
||||
.map(|r| Tessellation::from(r.to_outline(thickness)))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Usually m^2, unless the polygon is in screen-space
|
||||
pub fn area(&self) -> f64 {
|
||||
// Don't use signed_area, since we may work with polygons that have different orientations
|
||||
self.to_geo().unsigned_area()
|
||||
}
|
||||
|
||||
/// Doesn't handle multiple crossings in and out.
|
||||
pub fn clip_polyline(&self, input: &PolyLine) -> Option<Vec<Pt2D>> {
|
||||
let hits = self.get_outer_ring().all_intersections(input);
|
||||
|
||||
if hits.is_empty() {
|
||||
// All the points must be inside, or none
|
||||
if self.contains_pt(input.first_pt()) {
|
||||
Some(input.points().clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if hits.len() == 1 {
|
||||
// Which end?
|
||||
if self.contains_pt(input.first_pt()) {
|
||||
input
|
||||
.get_slice_ending_at(hits[0])
|
||||
.map(|pl| pl.into_points())
|
||||
} else {
|
||||
input
|
||||
.get_slice_starting_at(hits[0])
|
||||
.map(|pl| pl.into_points())
|
||||
}
|
||||
} else if hits.len() == 2 {
|
||||
Some(input.trim_to_endpts(hits[0], hits[1]).into_points())
|
||||
} else {
|
||||
// TODO Not handled
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Only handles a few cases
|
||||
pub fn clip_ring(&self, input: &Ring) -> Option<Vec<Pt2D>> {
|
||||
let ring = self.get_outer_ring();
|
||||
let hits = ring.all_intersections(&PolyLine::unchecked_new(input.clone().into_points()));
|
||||
|
||||
if hits.is_empty() {
|
||||
// If the first point is inside, then all must be
|
||||
if self.contains_pt(input.points()[0]) {
|
||||
return Some(input.points().clone());
|
||||
}
|
||||
} else if hits.len() == 2 {
|
||||
let (pl1, pl2) = input.get_both_slices_btwn(hits[0], hits[1])?;
|
||||
|
||||
// One of these should be partly outside the polygon. The endpoints won't be in the
|
||||
// polygon itself, but they'll be on the ring.
|
||||
if pl1
|
||||
.points()
|
||||
.iter()
|
||||
.all(|pt| self.contains_pt(*pt) || ring.contains_pt(*pt))
|
||||
{
|
||||
return Some(pl1.into_points());
|
||||
}
|
||||
if pl2
|
||||
.points()
|
||||
.iter()
|
||||
.all(|pt| self.contains_pt(*pt) || ring.contains_pt(*pt))
|
||||
{
|
||||
return Some(pl2.into_points());
|
||||
}
|
||||
// Huh?
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Optionally map the world-space points back to GPS.
|
||||
pub fn to_geojson(&self, gps: Option<&GPSBounds>) -> geojson::Geometry {
|
||||
let mut geom: geo::Geometry = self.to_geo().into();
|
||||
if let Some(ref gps_bounds) = gps {
|
||||
geom.map_coords_in_place(|c| {
|
||||
let gps = Pt2D::new(c.x, c.y).to_gps(gps_bounds);
|
||||
(gps.x(), gps.y()).into()
|
||||
});
|
||||
}
|
||||
|
||||
geojson::Geometry {
|
||||
bbox: None,
|
||||
value: geojson::Value::from(&geom),
|
||||
foreign_members: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts all polygons from raw bytes representing a GeoJSON file, along with the string
|
||||
/// key/value properties. Only the first polygon from multipolygons is returned. If
|
||||
/// `require_in_bounds` is set, then the polygon must completely fit within the `gps_bounds`.
|
||||
pub fn from_geojson_bytes(
|
||||
raw_bytes: &[u8],
|
||||
gps_bounds: &GPSBounds,
|
||||
require_in_bounds: bool,
|
||||
) -> Result<Vec<(Self, BTreeMap<String, String>)>> {
|
||||
let raw_string = std::str::from_utf8(raw_bytes)?;
|
||||
let geojson = raw_string.parse::<geojson::GeoJson>()?;
|
||||
let features = match geojson {
|
||||
geojson::GeoJson::Feature(feature) => vec![feature],
|
||||
geojson::GeoJson::FeatureCollection(collection) => collection.features,
|
||||
_ => bail!("Unexpected geojson: {:?}", geojson),
|
||||
};
|
||||
|
||||
let mut results = Vec::new();
|
||||
for feature in features {
|
||||
if let Some(geom) = &feature.geometry {
|
||||
let raw_pts = match &geom.value {
|
||||
geojson::Value::Polygon(pts) => pts,
|
||||
// If there are multiple, just use the first
|
||||
geojson::Value::MultiPolygon(polygons) => &polygons[0],
|
||||
_ => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
// TODO Handle holes
|
||||
let gps_pts: Vec<LonLat> = raw_pts[0]
|
||||
.iter()
|
||||
.map(|pt| LonLat::new(pt[0], pt[1]))
|
||||
.collect();
|
||||
let pts = if !require_in_bounds {
|
||||
gps_bounds.convert(&gps_pts)
|
||||
} else if let Some(pts) = gps_bounds.try_convert(&gps_pts) {
|
||||
pts
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(ring) = Ring::new(pts) {
|
||||
let mut tags = BTreeMap::new();
|
||||
for (key, value) in feature.properties_iter() {
|
||||
if let Some(value) = value.as_str() {
|
||||
tags.insert(key.to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
results.push((ring.into_polygon(), tags));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// If simplification fails, just keep the original polygon
|
||||
pub fn simplify(&self, epsilon: f64) -> Self {
|
||||
self.to_geo()
|
||||
.simplify_vw_preserve(&epsilon)
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| self.clone())
|
||||
}
|
||||
|
||||
/// An arbitrary placeholder value, when Option types aren't worthwhile
|
||||
pub fn dummy() -> Self {
|
||||
Self::rectangle(0.1, 0.1)
|
||||
}
|
||||
|
||||
// A less verbose way of invoking the From/Into impl. Note this hides a potentially expensive
|
||||
// clone.
|
||||
fn to_geo(&self) -> geo::Polygon {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
/// Convert to `geo` and also map from world-space to WGS84
|
||||
pub fn to_geo_wgs84(&self, gps_bounds: &GPSBounds) -> geo::Polygon<f64> {
|
||||
let mut p = self.to_geo();
|
||||
p.map_coords_in_place(|c| {
|
||||
let gps = Pt2D::new(c.x, c.y).to_gps(gps_bounds);
|
||||
(gps.x(), gps.y()).into()
|
||||
});
|
||||
p
|
||||
}
|
||||
|
||||
pub fn from_geo_wgs84(mut polygon: geo::Polygon<f64>, gps_bounds: &GPSBounds) -> Result<Self> {
|
||||
polygon.map_coords_in_place(|c| {
|
||||
let pt = LonLat::new(c.x, c.y).to_pt(gps_bounds);
|
||||
(pt.x(), pt.y()).into()
|
||||
});
|
||||
polygon.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Polygon {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "Polygon with {} rings", self.rings.len())?;
|
||||
for ring in &self.rings {
|
||||
writeln!(f, " {}", ring)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<geo::Polygon> for Polygon {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(poly: geo::Polygon) -> Result<Self> {
|
||||
let (exterior, interiors) = poly.into_inner();
|
||||
let mut holes = Vec::new();
|
||||
for linestring in interiors {
|
||||
holes.push(Ring::try_from(linestring)?);
|
||||
}
|
||||
Ok(Polygon::with_holes(Ring::try_from(exterior)?, holes))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Polygon> for geo::Polygon {
|
||||
fn from(mut poly: Polygon) -> Self {
|
||||
let exterior = poly.rings.remove(0);
|
||||
let interiors: Vec<geo::LineString> =
|
||||
poly.rings.into_iter().map(geo::LineString::from).collect();
|
||||
Self::new(exterior.into(), interiors)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_multi(multi: geo::MultiPolygon) -> Result<Vec<Polygon>> {
|
||||
let mut result = Vec::new();
|
||||
for polygon in multi {
|
||||
result.push(Polygon::try_from(polygon)?);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
1225
geom/src/polyline.rs
1225
geom/src/polyline.rs
File diff suppressed because it is too large
Load Diff
192
geom/src/pt.rs
192
geom/src/pt.rs
@ -1,192 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use geo::Simplify;
|
||||
use ordered_float::NotNan;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::conversions::pts_to_line_string;
|
||||
use crate::{
|
||||
deserialize_f64, serialize_f64, trim_f64, Angle, Distance, GPSBounds, LonLat, EPSILON_DIST,
|
||||
};
|
||||
|
||||
/// This represents world-space in meters.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct Pt2D {
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")]
|
||||
x: f64,
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")]
|
||||
y: f64,
|
||||
}
|
||||
|
||||
impl std::cmp::PartialEq for Pt2D {
|
||||
fn eq(&self, other: &Pt2D) -> bool {
|
||||
self.approx_eq(*other, EPSILON_DIST)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pt2D {
|
||||
pub fn new(x: f64, y: f64) -> Pt2D {
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
panic!("Bad Pt2D {}, {}", x, y);
|
||||
}
|
||||
|
||||
// TODO enforce >=0
|
||||
|
||||
Pt2D {
|
||||
x: trim_f64(x),
|
||||
y: trim_f64(y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self::new(0.0, 0.0)
|
||||
}
|
||||
|
||||
// TODO This is a small first step...
|
||||
pub fn approx_eq(self, other: Pt2D, threshold: Distance) -> bool {
|
||||
self.dist_to(other) <= threshold
|
||||
}
|
||||
|
||||
/// Can go out of bounds.
|
||||
pub fn to_gps(self, b: &GPSBounds) -> LonLat {
|
||||
b.convert_back_xy(self.x(), self.y())
|
||||
}
|
||||
|
||||
pub fn x(self) -> f64 {
|
||||
self.x
|
||||
}
|
||||
|
||||
pub fn y(self) -> f64 {
|
||||
self.y
|
||||
}
|
||||
|
||||
/// If distance is negative, this projects a point in theta.opposite()
|
||||
pub fn project_away(self, dist: Distance, theta: Angle) -> Pt2D {
|
||||
let (sin, cos) = theta.normalized_radians().sin_cos();
|
||||
Pt2D::new(
|
||||
self.x() + dist.inner_meters() * cos,
|
||||
self.y() + dist.inner_meters() * sin,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn raw_dist_to(self, to: Pt2D) -> f64 {
|
||||
((self.x() - to.x()).powi(2) + (self.y() - to.y()).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
pub fn dist_to(self, to: Pt2D) -> Distance {
|
||||
Distance::meters(self.raw_dist_to(to))
|
||||
}
|
||||
|
||||
/// Pretty meaningless units, for comparing distances very roughly
|
||||
pub fn fast_dist(self, other: Pt2D) -> NotNan<f64> {
|
||||
NotNan::new((self.x() - other.x()).powi(2) + (self.y() - other.y()).powi(2)).unwrap()
|
||||
}
|
||||
|
||||
pub fn angle_to(self, to: Pt2D) -> Angle {
|
||||
// DON'T invert y here
|
||||
Angle::new_rads((to.y() - self.y()).atan2(to.x() - self.x()))
|
||||
}
|
||||
|
||||
pub fn offset(self, dx: f64, dy: f64) -> Pt2D {
|
||||
Pt2D::new(self.x() + dx, self.y() + dy)
|
||||
}
|
||||
|
||||
pub fn center(pts: &[Pt2D]) -> Pt2D {
|
||||
if pts.is_empty() {
|
||||
panic!("Can't find center of 0 points");
|
||||
}
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
for pt in pts {
|
||||
x += pt.x();
|
||||
y += pt.y();
|
||||
}
|
||||
let len = pts.len() as f64;
|
||||
Pt2D::new(x / len, y / len)
|
||||
}
|
||||
|
||||
// Temporary until Pt2D has proper resolution.
|
||||
pub fn approx_dedupe(pts: Vec<Pt2D>, threshold: Distance) -> Vec<Pt2D> {
|
||||
// Just use dedup() on the Vec.
|
||||
assert_ne!(threshold, EPSILON_DIST);
|
||||
let mut result: Vec<Pt2D> = Vec::new();
|
||||
for pt in pts {
|
||||
if result.is_empty() || !result.last().unwrap().approx_eq(pt, threshold) {
|
||||
result.push(pt);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn to_hashable(self) -> HashablePt2D {
|
||||
HashablePt2D {
|
||||
x_nan: NotNan::new(self.x()).unwrap(),
|
||||
y_nan: NotNan::new(self.y()).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simplifies a list of points using Ramer-Douglas-Peuckr
|
||||
pub fn simplify_rdp(pts: Vec<Pt2D>, epsilon: f64) -> Vec<Pt2D> {
|
||||
let mut pts = pts_to_line_string(&pts)
|
||||
.simplify(&epsilon)
|
||||
.into_points()
|
||||
.into_iter()
|
||||
.map(|pt| pt.into())
|
||||
.collect::<Vec<_>>();
|
||||
// TODO Not sure why, but from geo 0.23 to 0.24, this became necessary?
|
||||
pts.dedup();
|
||||
pts
|
||||
}
|
||||
|
||||
pub fn to_geojson(self, gps: Option<&GPSBounds>) -> geojson::Geometry {
|
||||
if let Some(gps) = gps {
|
||||
self.to_gps(gps).to_geojson()
|
||||
} else {
|
||||
geojson::Geometry::new(geojson::Value::Point(vec![self.x(), self.y()]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Pt2D {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Pt2D({0}, {1})", self.x(), self.y())
|
||||
}
|
||||
}
|
||||
|
||||
/// This represents world space, NOT LonLat.
|
||||
// TODO So rename it HashablePair or something
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
|
||||
pub struct HashablePt2D {
|
||||
x_nan: NotNan<f64>,
|
||||
y_nan: NotNan<f64>,
|
||||
}
|
||||
|
||||
impl HashablePt2D {
|
||||
pub fn to_pt2d(self) -> Pt2D {
|
||||
Pt2D::new(self.x_nan.into_inner(), self.y_nan.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pt2D> for geo::Coord {
|
||||
fn from(pt: Pt2D) -> Self {
|
||||
geo::Coord { x: pt.x, y: pt.y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pt2D> for geo::Point {
|
||||
fn from(pt: Pt2D) -> Self {
|
||||
geo::Point::new(pt.x, pt.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<geo::Coord> for Pt2D {
|
||||
fn from(coord: geo::Coord) -> Self {
|
||||
Pt2D::new(coord.x, coord.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<geo::Point> for Pt2D {
|
||||
fn from(point: geo::Point) -> Self {
|
||||
Pt2D::new(point.x(), point.y())
|
||||
}
|
||||
}
|
331
geom/src/ring.rs
331
geom/src/ring.rs
@ -1,331 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::fmt::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Distance, GPSBounds, Line, PolyLine, Polygon, Pt2D, Tessellation};
|
||||
|
||||
/// Maybe a misnomer, but like a PolyLine, but closed.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Ring {
|
||||
// first equals last
|
||||
pts: Vec<Pt2D>,
|
||||
}
|
||||
|
||||
impl Ring {
|
||||
pub fn new(pts: Vec<Pt2D>) -> Result<Ring> {
|
||||
if pts.len() < 3 {
|
||||
bail!("Can't make a ring with < 3 points");
|
||||
}
|
||||
if pts[0] != *pts.last().unwrap() {
|
||||
bail!("Can't make a ring with mismatching first/last points");
|
||||
}
|
||||
|
||||
if let Some(pair) = pts.windows(2).find(|pair| pair[0] == pair[1]) {
|
||||
bail!("Ring has duplicate adjacent points near {}", pair[0]);
|
||||
}
|
||||
|
||||
let result = Ring { pts };
|
||||
|
||||
let mut seen_pts = HashSet::new();
|
||||
for pt in result.pts.iter().skip(1) {
|
||||
if seen_pts.contains(&pt.to_hashable()) {
|
||||
bail!("Ring has repeat non-adjacent points near {}", pt);
|
||||
}
|
||||
seen_pts.insert(pt.to_hashable());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Use with caution. Ignores duplicate points
|
||||
pub fn unsafe_deduping_new(mut pts: Vec<Pt2D>) -> Result<Ring> {
|
||||
pts.dedup();
|
||||
if pts.len() < 3 {
|
||||
bail!("Can't make a ring with < 3 points");
|
||||
}
|
||||
if pts[0] != *pts.last().unwrap() {
|
||||
bail!("Can't make a ring with mismatching first/last points");
|
||||
}
|
||||
|
||||
if let Some(pair) = pts.windows(2).find(|pair| pair[0] == pair[1]) {
|
||||
bail!("Ring has duplicate adjacent points near {}", pair[0]);
|
||||
}
|
||||
|
||||
let result = Ring { pts };
|
||||
|
||||
let mut seen_pts = HashSet::new();
|
||||
for pt in result.pts.iter().skip(1) {
|
||||
if seen_pts.contains(&pt.to_hashable()) {
|
||||
// Just spam logs instead of crashing
|
||||
println!("Ring has repeat non-adjacent points near {}", pt);
|
||||
}
|
||||
seen_pts.insert(pt.to_hashable());
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn must_new(pts: Vec<Pt2D>) -> Ring {
|
||||
Ring::new(pts).unwrap()
|
||||
}
|
||||
|
||||
/// First dedupes adjacent points
|
||||
pub fn deduping_new(mut pts: Vec<Pt2D>) -> Result<Self> {
|
||||
pts.dedup();
|
||||
Self::new(pts)
|
||||
}
|
||||
|
||||
pub fn as_polyline(&self) -> PolyLine {
|
||||
PolyLine::unchecked_new(self.pts.clone())
|
||||
}
|
||||
|
||||
/// Draws the ring with some thickness, with half of it straddling the interor of the ring, and
|
||||
/// half on the outside.
|
||||
pub fn to_outline(&self, thickness: Distance) -> Tessellation {
|
||||
// TODO Has a weird corner. Use the polygon offset thing instead?
|
||||
self.as_polyline().thicken_tessellation(thickness)
|
||||
}
|
||||
|
||||
pub fn into_polygon(self) -> Polygon {
|
||||
Polygon::with_holes(self, Vec::new())
|
||||
}
|
||||
|
||||
pub fn points(&self) -> &Vec<Pt2D> {
|
||||
&self.pts
|
||||
}
|
||||
pub fn into_points(self) -> Vec<Pt2D> {
|
||||
self.pts
|
||||
}
|
||||
|
||||
/// Be careful with the order of results. Hits on an earlier line segment of other show up
|
||||
/// first, but if the ring hits a line segment at multiple points, who knows. Dedupes.
|
||||
pub fn all_intersections(&self, other: &PolyLine) -> Vec<Pt2D> {
|
||||
let mut hits = Vec::new();
|
||||
let mut seen = HashSet::new();
|
||||
for l1 in other.lines() {
|
||||
for l2 in self
|
||||
.pts
|
||||
.windows(2)
|
||||
.map(|pair| Line::must_new(pair[0], pair[1]))
|
||||
{
|
||||
if let Some(pt) = l1.intersection(&l2) {
|
||||
if !seen.contains(&pt.to_hashable()) {
|
||||
hits.push(pt);
|
||||
seen.insert(pt.to_hashable());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hits
|
||||
}
|
||||
|
||||
pub(crate) fn get_both_slices_btwn(
|
||||
&self,
|
||||
pt1: Pt2D,
|
||||
pt2: Pt2D,
|
||||
) -> Option<(PolyLine, PolyLine)> {
|
||||
assert!(pt1 != pt2);
|
||||
let pl = self.as_polyline();
|
||||
|
||||
let mut dist1 = pl.dist_along_of_point(pt1)?.0;
|
||||
let mut dist2 = pl.dist_along_of_point(pt2)?.0;
|
||||
if dist1 > dist2 {
|
||||
std::mem::swap(&mut dist1, &mut dist2);
|
||||
}
|
||||
if dist1 == dist2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// TODO If we reversed the points, we need to reverse these results! Argh
|
||||
let candidate1 = pl.maybe_exact_slice(dist1, dist2).ok()?;
|
||||
let candidate2 = pl
|
||||
.maybe_exact_slice(dist2, pl.length())
|
||||
.ok()?
|
||||
.must_extend(pl.maybe_exact_slice(Distance::ZERO, dist1).ok()?);
|
||||
Some((candidate1, candidate2))
|
||||
}
|
||||
|
||||
/// Assuming both points are somewhere along the ring, return the points in between the two, by
|
||||
/// tracing along the ring in the longer or shorter direction (depending on `longer`). If both
|
||||
/// points are the same, returns `None`. The result is oriented from `pt1` to `pt2`.
|
||||
pub fn get_slice_between(&self, pt1: Pt2D, pt2: Pt2D, longer: bool) -> Option<PolyLine> {
|
||||
if pt1 == pt2 {
|
||||
return None;
|
||||
}
|
||||
let (candidate1, candidate2) = self.get_both_slices_btwn(pt1, pt2)?;
|
||||
let slice = if longer == (candidate1.length() > candidate2.length()) {
|
||||
candidate1
|
||||
} else {
|
||||
candidate2
|
||||
};
|
||||
if slice.first_pt() == pt1 {
|
||||
Some(slice)
|
||||
} else {
|
||||
// TODO Do we want to be paranoid here? Or just do the fix in get_both_slices_btwn
|
||||
// directly?
|
||||
Some(slice.reversed())
|
||||
}
|
||||
}
|
||||
|
||||
/// Assuming both points are somewhere along the ring, return the points in between the two, by
|
||||
/// tracing along the ring in the shorter direction. If both points are the same, returns
|
||||
/// `None`. The result is oriented from `pt1` to `pt2`.
|
||||
pub fn get_shorter_slice_between(&self, pt1: Pt2D, pt2: Pt2D) -> Option<PolyLine> {
|
||||
self.get_slice_between(pt1, pt2, false)
|
||||
}
|
||||
|
||||
// TODO Rmove this one, fix all callers
|
||||
pub fn get_shorter_slice_btwn(&self, pt1: Pt2D, pt2: Pt2D) -> Option<PolyLine> {
|
||||
let (candidate1, candidate2) = self.get_both_slices_btwn(pt1, pt2)?;
|
||||
if candidate1.length() < candidate2.length() {
|
||||
Some(candidate1)
|
||||
} else {
|
||||
Some(candidate2)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract all PolyLines and Rings. Doesn't handle crazy double loops and stuff.
|
||||
pub fn split_points(pts: &[Pt2D]) -> Result<(Vec<PolyLine>, Vec<Ring>)> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut intersections = HashSet::new();
|
||||
for pt in pts {
|
||||
let pt = pt.to_hashable();
|
||||
if seen.contains(&pt) {
|
||||
intersections.insert(pt);
|
||||
} else {
|
||||
seen.insert(pt);
|
||||
}
|
||||
}
|
||||
intersections.insert(pts[0].to_hashable());
|
||||
intersections.insert(pts.last().unwrap().to_hashable());
|
||||
|
||||
let mut polylines = Vec::new();
|
||||
let mut rings = Vec::new();
|
||||
let mut current = Vec::new();
|
||||
for pt in pts.iter().cloned() {
|
||||
current.push(pt);
|
||||
if intersections.contains(&pt.to_hashable()) && current.len() > 1 {
|
||||
if current[0] == pt && current.len() >= 3 {
|
||||
rings.push(Ring::new(current.drain(..).collect())?);
|
||||
} else {
|
||||
polylines.push(PolyLine::new(current.drain(..).collect())?);
|
||||
}
|
||||
current.push(pt);
|
||||
}
|
||||
}
|
||||
Ok((polylines, rings))
|
||||
}
|
||||
|
||||
pub fn contains_pt(&self, pt: Pt2D) -> bool {
|
||||
self.as_polyline().dist_along_of_point(pt).is_some()
|
||||
}
|
||||
|
||||
/// Produces a GeoJSON polygon, optionally mapping the world-space points back to GPS.
|
||||
pub fn to_geojson(&self, gps: Option<&GPSBounds>) -> geojson::Geometry {
|
||||
let mut pts = Vec::new();
|
||||
if let Some(gps) = gps {
|
||||
for pt in gps.convert_back(&self.pts) {
|
||||
pts.push(vec![pt.x(), pt.y()]);
|
||||
}
|
||||
} else {
|
||||
for pt in &self.pts {
|
||||
pts.push(vec![pt.x(), pt.y()]);
|
||||
}
|
||||
}
|
||||
geojson::Geometry::new(geojson::Value::Polygon(vec![pts]))
|
||||
}
|
||||
|
||||
/// Translates the ring by a fixed offset.
|
||||
pub fn translate(mut self, dx: f64, dy: f64) -> Ring {
|
||||
for pt in &mut self.pts {
|
||||
*pt = pt.offset(dx, dy);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Find the "pole of inaccessibility" -- the most distant internal point from the polygon
|
||||
/// outline
|
||||
pub fn polylabel(&self) -> Pt2D {
|
||||
// TODO Refactor to_geo?
|
||||
let polygon = geo::Polygon::new(
|
||||
geo::LineString::from(
|
||||
self.pts
|
||||
.iter()
|
||||
.map(|pt| geo::Point::new(pt.x(), pt.y()))
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
Vec::new(),
|
||||
);
|
||||
let pt = polylabel::polylabel(&polygon, &1.0).unwrap();
|
||||
Pt2D::new(pt.x(), pt.y())
|
||||
}
|
||||
|
||||
/// Look for "bad" rings that double back on themselves. These're likely to cause many
|
||||
/// downstream problems. "Bad" means the order of points doesn't match the order when sorting
|
||||
/// by angle from the center.
|
||||
///
|
||||
/// TODO I spot many false positives. Look for better definitions -- maybe self-intersecting
|
||||
/// polygons?
|
||||
pub fn doubles_back(&self) -> bool {
|
||||
// Skip the first=last point
|
||||
let mut orig = self.pts.clone();
|
||||
orig.pop();
|
||||
// Polylabel is better than center; sometimes the center is very close to an edge
|
||||
let center = self.polylabel();
|
||||
|
||||
let mut sorted = orig.clone();
|
||||
sorted.sort_by_key(|pt| pt.angle_to(center).normalized_degrees() as i64);
|
||||
|
||||
// Align things again
|
||||
while sorted[0] != orig[0] {
|
||||
sorted.rotate_right(1);
|
||||
}
|
||||
|
||||
// Do they match up?
|
||||
orig != sorted
|
||||
}
|
||||
|
||||
/// Print the coordinates of this ring as a `geo::LineString` for easy bug reports
|
||||
pub fn as_geo_linestring(&self) -> String {
|
||||
let mut output = String::new();
|
||||
writeln!(output, "let line_string = geo_types::line_string![").unwrap();
|
||||
for pt in &self.pts {
|
||||
writeln!(output, " (x: {}, y: {}),", pt.x(), pt.y()).unwrap();
|
||||
}
|
||||
writeln!(output, "];").unwrap();
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ring {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "Ring::new(vec![")?;
|
||||
for pt in &self.pts {
|
||||
writeln!(f, " Pt2D::new({}, {}),", pt.x(), pt.y())?;
|
||||
}
|
||||
write!(f, "])")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Ring> for geo::LineString {
|
||||
fn from(ring: Ring) -> Self {
|
||||
let coords = ring
|
||||
.pts
|
||||
.into_iter()
|
||||
.map(geo::Coord::from)
|
||||
.collect::<Vec<_>>();
|
||||
Self(coords)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<geo::LineString> for Ring {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(line_string: geo::LineString) -> Result<Self, Self::Error> {
|
||||
let pts: Vec<Pt2D> = line_string.0.into_iter().map(Pt2D::from).collect();
|
||||
Self::deduping_new(pts)
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
use std::{cmp, ops};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{deserialize_f64, serialize_f64, trim_f64, Distance, Duration, UnitFmt};
|
||||
|
||||
/// In meters per second. Can be negative.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Speed(
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")] f64,
|
||||
);
|
||||
|
||||
// By construction, Speed is a finite f64 with trimmed precision.
|
||||
impl Eq for Speed {}
|
||||
|
||||
#[allow(clippy::derive_ord_xor_partial_ord)] // false positive
|
||||
impl Ord for Speed {
|
||||
fn cmp(&self, other: &Speed) -> cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Speed {
|
||||
pub const ZERO: Speed = Speed::const_meters_per_second(0.0);
|
||||
|
||||
pub fn meters_per_second(value: f64) -> Speed {
|
||||
if !value.is_finite() {
|
||||
panic!("Bad Speed {}", value);
|
||||
}
|
||||
|
||||
Speed(trim_f64(value))
|
||||
}
|
||||
|
||||
pub const fn const_meters_per_second(value: f64) -> Speed {
|
||||
Speed(value)
|
||||
}
|
||||
|
||||
pub fn miles_per_hour(value: f64) -> Speed {
|
||||
Speed::meters_per_second(0.44704 * value)
|
||||
}
|
||||
|
||||
pub fn km_per_hour(value: f64) -> Speed {
|
||||
Speed::meters_per_second(0.277778 * value)
|
||||
}
|
||||
|
||||
pub fn from_dist_time(d: Distance, t: Duration) -> Speed {
|
||||
Speed::meters_per_second(d.inner_meters() / t.inner_seconds())
|
||||
}
|
||||
|
||||
// TODO Remove if possible.
|
||||
pub fn inner_meters_per_second(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn to_miles_per_hour(self) -> f64 {
|
||||
self.0 * 2.23694
|
||||
}
|
||||
|
||||
/// Describes the speed according to formatting rules.
|
||||
pub fn to_string(self, fmt: &UnitFmt) -> String {
|
||||
if fmt.metric {
|
||||
format!("{} km/h", (self.0 * 3.6).round())
|
||||
} else {
|
||||
format!("{} mph", self.to_miles_per_hour().round())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add for Speed {
|
||||
type Output = Speed;
|
||||
|
||||
fn add(self, other: Speed) -> Speed {
|
||||
Speed::meters_per_second(self.0 + other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Speed {
|
||||
type Output = Speed;
|
||||
|
||||
fn sub(self, other: Speed) -> Speed {
|
||||
Speed::meters_per_second(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div for Speed {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, other: Speed) -> f64 {
|
||||
self.0 / other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Neg for Speed {
|
||||
type Output = Speed;
|
||||
|
||||
fn neg(self) -> Speed {
|
||||
Speed::meters_per_second(-self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<f64> for Speed {
|
||||
type Output = Speed;
|
||||
|
||||
fn mul(self, scalar: f64) -> Speed {
|
||||
Speed::meters_per_second(self.0 * scalar)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Speed> for f64 {
|
||||
type Output = Speed;
|
||||
|
||||
fn mul(self, other: Speed) -> Speed {
|
||||
Speed::meters_per_second(self * other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Mul<Duration> for Speed {
|
||||
type Output = Distance;
|
||||
|
||||
fn mul(self, other: Duration) -> Distance {
|
||||
Distance::meters(self.0 * other.inner_seconds())
|
||||
}
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Distance, Duration};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub enum Statistic {
|
||||
Min,
|
||||
Mean,
|
||||
P50,
|
||||
P90,
|
||||
P99,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl Statistic {
|
||||
pub fn all() -> Vec<Statistic> {
|
||||
vec![
|
||||
Statistic::Min,
|
||||
Statistic::Mean,
|
||||
Statistic::P50,
|
||||
Statistic::P90,
|
||||
Statistic::P99,
|
||||
Statistic::Max,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Statistic {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Statistic::Min => write!(f, "minimum"),
|
||||
Statistic::Mean => write!(f, "mean"),
|
||||
Statistic::P50 => write!(f, "50%ile"),
|
||||
Statistic::P90 => write!(f, "90%ile"),
|
||||
Statistic::P99 => write!(f, "99%ile"),
|
||||
Statistic::Max => write!(f, "maximum"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HgramValue<T>: Copy + std::cmp::Ord + std::fmt::Display {
|
||||
// TODO Weird name because I can't figure out associated type mess in FanChart
|
||||
fn hgram_zero() -> T;
|
||||
fn to_u64(self) -> u64;
|
||||
fn from_u64(x: u64) -> T;
|
||||
}
|
||||
|
||||
impl HgramValue<Duration> for Duration {
|
||||
fn hgram_zero() -> Duration {
|
||||
Duration::ZERO
|
||||
}
|
||||
fn to_u64(self) -> u64 {
|
||||
self.to_u64()
|
||||
}
|
||||
fn from_u64(x: u64) -> Duration {
|
||||
Duration::from_u64(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl HgramValue<Distance> for Distance {
|
||||
fn hgram_zero() -> Distance {
|
||||
Distance::ZERO
|
||||
}
|
||||
fn to_u64(self) -> u64 {
|
||||
self.to_u64()
|
||||
}
|
||||
fn from_u64(x: u64) -> Distance {
|
||||
Distance::from_u64(x)
|
||||
}
|
||||
}
|
||||
|
||||
impl HgramValue<u16> for u16 {
|
||||
fn hgram_zero() -> u16 {
|
||||
0
|
||||
}
|
||||
fn to_u64(self) -> u64 {
|
||||
self as u64
|
||||
}
|
||||
fn from_u64(x: u64) -> u16 {
|
||||
u16::try_from(x).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl HgramValue<usize> for usize {
|
||||
fn hgram_zero() -> usize {
|
||||
0
|
||||
}
|
||||
fn to_u64(self) -> u64 {
|
||||
self as u64
|
||||
}
|
||||
fn from_u64(x: u64) -> usize {
|
||||
x as usize
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Maybe consider having a type-safe NonEmptyHistogram.
|
||||
#[derive(Clone)]
|
||||
pub struct Histogram<T: HgramValue<T>> {
|
||||
count: usize,
|
||||
histogram: histogram::Histogram,
|
||||
min: T,
|
||||
max: T,
|
||||
}
|
||||
|
||||
impl<T: HgramValue<T>> Default for Histogram<T> {
|
||||
fn default() -> Histogram<T> {
|
||||
Histogram {
|
||||
count: 0,
|
||||
histogram: Default::default(),
|
||||
min: T::hgram_zero(),
|
||||
max: T::hgram_zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: HgramValue<T>> Histogram<T> {
|
||||
pub fn new() -> Histogram<T> {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn add(&mut self, x: T) {
|
||||
if self.count == 0 {
|
||||
self.min = x;
|
||||
self.max = x;
|
||||
} else {
|
||||
self.min = self.min.min(x);
|
||||
self.max = self.max.max(x);
|
||||
}
|
||||
self.count += 1;
|
||||
self.histogram
|
||||
.increment(x.to_u64())
|
||||
.map_err(|err| format!("Can't add {}: {}", x, err))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, x: T) {
|
||||
// TODO This doesn't update min/max! Why are we tracking that ourselves? Do we not trust
|
||||
// the lossiness of the underlying histogram?
|
||||
self.count -= 1;
|
||||
self.histogram
|
||||
.decrement(x.to_u64())
|
||||
.map_err(|err| format!("Can't remove {}: {}", x, err))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn describe(&self) -> String {
|
||||
if self.count == 0 {
|
||||
return "no data yet".to_string();
|
||||
}
|
||||
|
||||
format!(
|
||||
"{} count, 50%ile {}, 90%ile {}, 99%ile {}, min {}, mean {}, max {}",
|
||||
crate::utils::prettyprint_usize(self.count),
|
||||
self.select(Statistic::P50).unwrap(),
|
||||
self.select(Statistic::P90).unwrap(),
|
||||
self.select(Statistic::P99).unwrap(),
|
||||
self.select(Statistic::Min).unwrap(),
|
||||
self.select(Statistic::Mean).unwrap(),
|
||||
self.select(Statistic::Max).unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// None if empty
|
||||
pub fn percentile(&self, p: f64) -> Option<T> {
|
||||
if self.count == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(T::from_u64(self.histogram.percentile(p).unwrap()))
|
||||
}
|
||||
|
||||
pub fn select(&self, stat: Statistic) -> Option<T> {
|
||||
if self.count == 0 {
|
||||
return None;
|
||||
}
|
||||
let raw = match stat {
|
||||
Statistic::P50 => self.histogram.percentile(50.0).unwrap(),
|
||||
Statistic::P90 => self.histogram.percentile(90.0).unwrap(),
|
||||
Statistic::P99 => self.histogram.percentile(99.0).unwrap(),
|
||||
Statistic::Min => {
|
||||
return Some(self.min);
|
||||
}
|
||||
Statistic::Mean => self.histogram.mean().unwrap(),
|
||||
Statistic::Max => {
|
||||
return Some(self.max);
|
||||
}
|
||||
};
|
||||
Some(T::from_u64(raw))
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
||||
/// Could implement PartialEq, but be a bit more clear how approximate this is
|
||||
pub fn seems_eq(&self, other: &Histogram<T>) -> bool {
|
||||
self.describe() == other.describe()
|
||||
}
|
||||
}
|
@ -1,264 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Angle, Bounds, GPSBounds, Polygon, Pt2D};
|
||||
|
||||
// Only serializable for Polygons that precompute a tessellation
|
||||
/// A tessellated polygon, ready for rendering.
|
||||
#[derive(PartialEq, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Tessellation {
|
||||
/// These points aren't in any meaningful order. It's not generally possible to reconstruct a
|
||||
/// `Polygon` from this.
|
||||
pub(crate) points: Vec<Pt2D>,
|
||||
/// Groups of three indices make up the triangles
|
||||
indices: Vec<u16>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Triangle {
|
||||
pub pt1: Pt2D,
|
||||
pub pt2: Pt2D,
|
||||
pub pt3: Pt2D,
|
||||
}
|
||||
|
||||
impl From<Polygon> for Tessellation {
|
||||
fn from(mut polygon: Polygon) -> Self {
|
||||
if let Some(tessellation) = polygon.tessellation.take() {
|
||||
return tessellation;
|
||||
}
|
||||
|
||||
let geojson_style: Vec<Vec<Vec<f64>>> = polygon
|
||||
.rings
|
||||
.iter()
|
||||
.map(|ring| {
|
||||
ring.points()
|
||||
.iter()
|
||||
.map(|pt| vec![pt.x(), pt.y()])
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let (vertices, holes, dims) = earcutr::flatten(&geojson_style);
|
||||
let indices = downsize(earcutr::earcut(&vertices, &holes, dims).unwrap());
|
||||
|
||||
Self {
|
||||
points: vertices
|
||||
.chunks(2)
|
||||
.map(|pair| Pt2D::new(pair[0], pair[1]))
|
||||
.collect(),
|
||||
indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<geo::Polygon> for Tessellation {
|
||||
fn from(poly: geo::Polygon) -> Self {
|
||||
// geo::Polygon -> geom::Polygon may fail, so we can't just do two hops. We can tessellate
|
||||
// something even if it has invalid Rings.
|
||||
let (exterior, mut interiors) = poly.into_inner();
|
||||
interiors.insert(0, exterior);
|
||||
|
||||
let geojson_style: Vec<Vec<Vec<f64>>> = interiors
|
||||
.into_iter()
|
||||
.map(|ring| {
|
||||
ring.into_inner()
|
||||
.into_iter()
|
||||
.map(|pt| vec![pt.x, pt.y])
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
let (vertices, holes, dims) = earcutr::flatten(&geojson_style);
|
||||
let indices = earcutr::earcut(&vertices, &holes, dims).unwrap();
|
||||
|
||||
let points = vertices
|
||||
.chunks(2)
|
||||
.map(|pair| Pt2D::new(pair[0], pair[1]))
|
||||
.collect();
|
||||
|
||||
Self::new(points, indices)
|
||||
}
|
||||
}
|
||||
|
||||
impl Tessellation {
|
||||
pub fn new(points: Vec<Pt2D>, indices: Vec<usize>) -> Self {
|
||||
Tessellation {
|
||||
points,
|
||||
indices: downsize(indices),
|
||||
}
|
||||
}
|
||||
|
||||
/// The `points` are not necessarily a `Ring`, which has strict requirements about no duplicate
|
||||
/// points. We can render various types of invalid polygon.
|
||||
pub fn from_ring(points: Vec<Pt2D>) -> Self {
|
||||
assert!(points.len() >= 3);
|
||||
|
||||
let mut vertices = Vec::new();
|
||||
for pt in &points {
|
||||
vertices.push(pt.x());
|
||||
vertices.push(pt.y());
|
||||
}
|
||||
let indices = downsize(earcutr::earcut(&vertices, &Vec::new(), 2).unwrap());
|
||||
|
||||
Self { points, indices }
|
||||
}
|
||||
|
||||
/// Returns (points, indices) for rendering
|
||||
pub fn consume(self) -> (Vec<Pt2D>, Vec<u16>) {
|
||||
(self.points, self.indices)
|
||||
}
|
||||
|
||||
pub fn triangles(&self) -> Vec<Triangle> {
|
||||
let mut triangles: Vec<Triangle> = Vec::new();
|
||||
for slice in self.indices.chunks_exact(3) {
|
||||
triangles.push(Triangle {
|
||||
pt1: self.points[slice[0] as usize],
|
||||
pt2: self.points[slice[1] as usize],
|
||||
pt3: self.points[slice[2] as usize],
|
||||
});
|
||||
}
|
||||
triangles
|
||||
}
|
||||
|
||||
pub fn get_bounds(&self) -> Bounds {
|
||||
Bounds::from(&self.points)
|
||||
}
|
||||
|
||||
pub fn center(&self) -> Pt2D {
|
||||
self.get_bounds().center()
|
||||
}
|
||||
|
||||
pub(crate) fn transform<F: Fn(&Pt2D) -> Pt2D>(&mut self, f: F) {
|
||||
for pt in &mut self.points {
|
||||
*pt = f(pt);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate(&mut self, dx: f64, dy: f64) {
|
||||
self.transform(|pt| pt.offset(dx, dy));
|
||||
}
|
||||
|
||||
pub fn scale(&mut self, factor: f64) {
|
||||
self.transform(|pt| Pt2D::new(pt.x() * factor, pt.y() * factor));
|
||||
}
|
||||
|
||||
pub fn scale_xy(&mut self, x_factor: f64, y_factor: f64) {
|
||||
self.transform(|pt| Pt2D::new(pt.x() * x_factor, pt.y() * y_factor))
|
||||
}
|
||||
|
||||
pub fn rotate(&mut self, angle: Angle) {
|
||||
self.rotate_around(angle, self.center())
|
||||
}
|
||||
|
||||
pub fn rotate_around(&mut self, angle: Angle, pivot: Pt2D) {
|
||||
self.transform(|pt| {
|
||||
let origin_pt = Pt2D::new(pt.x() - pivot.x(), pt.y() - pivot.y());
|
||||
let (sin, cos) = angle.normalized_radians().sin_cos();
|
||||
Pt2D::new(
|
||||
pivot.x() + origin_pt.x() * cos - origin_pt.y() * sin,
|
||||
pivot.y() + origin_pt.y() * cos + origin_pt.x() * sin,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Equivalent to `self.scale(scale).translate(translate_x, translate_y).rotate_around(rotate,
|
||||
/// pivot)`, but modifies the polygon in-place and is faster.
|
||||
pub fn inplace_multi_transform(
|
||||
&mut self,
|
||||
scale: f64,
|
||||
translate_x: f64,
|
||||
translate_y: f64,
|
||||
rotate: Angle,
|
||||
pivot: Pt2D,
|
||||
) {
|
||||
let (sin, cos) = rotate.normalized_radians().sin_cos();
|
||||
|
||||
for pt in &mut self.points {
|
||||
// Scale
|
||||
let x = scale * pt.x();
|
||||
let y = scale * pt.y();
|
||||
// Translate
|
||||
let x = x + translate_x;
|
||||
let y = y + translate_y;
|
||||
// Rotate
|
||||
let origin_pt = Pt2D::new(x - pivot.x(), y - pivot.y());
|
||||
*pt = Pt2D::new(
|
||||
pivot.x() + origin_pt.x() * cos - origin_pt.y() * sin,
|
||||
pivot.y() + origin_pt.y() * cos + origin_pt.x() * sin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(self, other: Self) -> Self {
|
||||
let mut points = self.points;
|
||||
let mut indices = self.indices;
|
||||
let offset = points.len() as u16;
|
||||
points.extend(other.points);
|
||||
for idx in other.indices {
|
||||
indices.push(offset + idx);
|
||||
}
|
||||
Self { points, indices }
|
||||
}
|
||||
|
||||
pub fn union_all(mut list: Vec<Self>) -> Self {
|
||||
let mut result = list.pop().unwrap();
|
||||
for p in list {
|
||||
result = result.union(p);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Produces a GeoJSON multipolygon consisting of individual triangles. Optionally map the
|
||||
/// world-space points back to GPS.
|
||||
pub fn to_geojson(&self, gps: Option<&GPSBounds>) -> geojson::Geometry {
|
||||
let mut polygons = Vec::new();
|
||||
for triangle in self.triangles() {
|
||||
let raw_pts = vec![triangle.pt1, triangle.pt2, triangle.pt3, triangle.pt1];
|
||||
let mut pts = Vec::new();
|
||||
if let Some(gps) = gps {
|
||||
for pt in gps.convert_back(&raw_pts) {
|
||||
pts.push(vec![pt.x(), pt.y()]);
|
||||
}
|
||||
} else {
|
||||
for pt in raw_pts {
|
||||
pts.push(vec![pt.x(), pt.y()]);
|
||||
}
|
||||
}
|
||||
polygons.push(vec![pts]);
|
||||
}
|
||||
|
||||
geojson::Geometry::new(geojson::Value::MultiPolygon(polygons))
|
||||
}
|
||||
|
||||
// TODO This only makes sense for something vaguely Ring-like
|
||||
fn to_geo(&self) -> geo::Polygon {
|
||||
let exterior = crate::conversions::pts_to_line_string(&self.points);
|
||||
geo::Polygon::new(exterior, Vec::new())
|
||||
}
|
||||
|
||||
// TODO After making to_outline return a real Polygon, get rid of this
|
||||
pub fn difference(&self, other: &Tessellation) -> Result<Vec<Polygon>> {
|
||||
use geo::BooleanOps;
|
||||
|
||||
// TODO Remove after https://github.com/georust/geo/issues/913
|
||||
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
crate::polygon::from_multi(self.to_geo().difference(&other.to_geo()))
|
||||
})) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
println!("BooleanOps crashed: {err:?}");
|
||||
bail!("BooleanOps crashed: {err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn downsize(input: Vec<usize>) -> Vec<u16> {
|
||||
let mut output = Vec::new();
|
||||
for x in input {
|
||||
if let Ok(x) = u16::try_from(x) {
|
||||
output.push(x);
|
||||
} else {
|
||||
panic!("{} can't fit in u16, some polygon is too huge", x);
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
226
geom/src/time.rs
226
geom/src/time.rs
@ -1,226 +0,0 @@
|
||||
use std::{cmp, ops};
|
||||
|
||||
use anyhow::Result;
|
||||
use ordered_float::NotNan;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{deserialize_f64, serialize_f64, trim_f64, Duration};
|
||||
|
||||
/// In seconds since midnight. Can't be negative.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Time(
|
||||
#[serde(serialize_with = "serialize_f64", deserialize_with = "deserialize_f64")] f64,
|
||||
);
|
||||
|
||||
// By construction, Time is a finite f64 with trimmed precision.
|
||||
impl Eq for Time {}
|
||||
|
||||
#[allow(clippy::derive_ord_xor_partial_ord)] // false positive
|
||||
impl Ord for Time {
|
||||
fn cmp(&self, other: &Time) -> cmp::Ordering {
|
||||
self.partial_cmp(other).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_hash_xor_eq)] // false positive
|
||||
impl std::hash::Hash for Time {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
NotNan::new(self.0).unwrap().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Time {
|
||||
pub const START_OF_DAY: Time = Time(0.0);
|
||||
|
||||
// No direct public constructors. Explicitly do Time::START_OF_DAY + duration.
|
||||
fn seconds_since_midnight(value: f64) -> Time {
|
||||
if !value.is_finite() || value < 0.0 {
|
||||
panic!("Bad Time {}", value);
|
||||
}
|
||||
|
||||
Time(trim_f64(value))
|
||||
}
|
||||
|
||||
/// (hours, minutes, seconds, centiseconds)
|
||||
fn get_parts(self) -> (usize, usize, usize, usize) {
|
||||
let mut remainder = self.0;
|
||||
let hours = (remainder / 3600.0).floor();
|
||||
remainder -= hours * 3600.0;
|
||||
let minutes = (remainder / 60.0).floor();
|
||||
remainder -= minutes * 60.0;
|
||||
let seconds = remainder.floor();
|
||||
remainder -= seconds;
|
||||
let centis = (remainder / 0.1).floor();
|
||||
|
||||
(
|
||||
hours as usize,
|
||||
minutes as usize,
|
||||
seconds as usize,
|
||||
centis as usize,
|
||||
)
|
||||
}
|
||||
/// Rounded down. 6:59:00 is hour 6.
|
||||
pub fn get_hours(self) -> usize {
|
||||
self.get_parts().0
|
||||
}
|
||||
|
||||
pub fn ampm_tostring(self) -> String {
|
||||
let (mut hours, minutes, seconds, _) = self.get_parts();
|
||||
let next_day = if hours >= 24 {
|
||||
let days = hours / 24;
|
||||
hours %= 24;
|
||||
format!(" (+{} days)", days)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let suffix = if hours < 12 { "AM" } else { "PM" };
|
||||
if hours == 0 {
|
||||
hours = 12;
|
||||
} else if hours >= 24 {
|
||||
hours -= 24;
|
||||
} else if hours > 12 {
|
||||
hours -= 12;
|
||||
}
|
||||
|
||||
format!(
|
||||
"{:02}:{:02}:{:02} {}{}",
|
||||
hours, minutes, seconds, suffix, next_day
|
||||
)
|
||||
}
|
||||
|
||||
pub fn as_filename(self) -> String {
|
||||
let (hours, minutes, seconds, remainder) = self.get_parts();
|
||||
format!(
|
||||
"{0:02}h{1:02}m{2:02}.{3:01}s",
|
||||
hours, minutes, seconds, remainder
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse(string: &str) -> Result<Time> {
|
||||
let parts: Vec<&str> = string.split(':').collect();
|
||||
if parts.is_empty() {
|
||||
bail!("Time {}: no :'s", string);
|
||||
}
|
||||
|
||||
let mut seconds = parts.last().unwrap().parse::<f64>()?;
|
||||
match parts.len() {
|
||||
1 => Ok(Time::seconds_since_midnight(seconds)),
|
||||
2 => {
|
||||
// They're really minutes
|
||||
seconds *= 60.0;
|
||||
seconds += 3600.0 * parts[0].parse::<f64>()?;
|
||||
Ok(Time::seconds_since_midnight(seconds))
|
||||
}
|
||||
3 => {
|
||||
seconds += 60.0 * parts[1].parse::<f64>()?;
|
||||
seconds += 3600.0 * parts[0].parse::<f64>()?;
|
||||
Ok(Time::seconds_since_midnight(seconds))
|
||||
}
|
||||
_ => bail!("Time {}: weird number of parts", string),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO These are a little weird, so don't operator overload yet
|
||||
pub fn percent_of(self, p: f64) -> Time {
|
||||
if !(0.0..=1.0).contains(&p) {
|
||||
panic!("Bad percent_of input: {}", p);
|
||||
}
|
||||
Time::seconds_since_midnight(self.0 * p)
|
||||
}
|
||||
|
||||
pub fn to_percent(self, other: Time) -> f64 {
|
||||
self.0 / other.0
|
||||
}
|
||||
|
||||
/// For RNG range generation. Don't abuse.
|
||||
pub fn inner_seconds(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn clamped_sub(self, dt: Duration) -> Time {
|
||||
Time::seconds_since_midnight((self.0 - dt.inner_seconds()).max(0.0))
|
||||
}
|
||||
|
||||
pub fn round_seconds(self, s: f64) -> Time {
|
||||
Time::seconds_since_midnight(s * (self.0 / s).round())
|
||||
}
|
||||
}
|
||||
|
||||
// 24-hour format by default
|
||||
impl std::fmt::Display for Time {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let (hours, minutes, seconds, remainder) = self.get_parts();
|
||||
write!(
|
||||
f,
|
||||
"{0:02}:{1:02}:{2:02}.{3:01}",
|
||||
hours, minutes, seconds, remainder
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Add<Duration> for Time {
|
||||
type Output = Time;
|
||||
|
||||
fn add(self, other: Duration) -> Time {
|
||||
Time::seconds_since_midnight(self.0 + other.inner_seconds())
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::AddAssign<Duration> for Time {
|
||||
fn add_assign(&mut self, other: Duration) {
|
||||
*self = *self + other;
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub<Duration> for Time {
|
||||
type Output = Time;
|
||||
|
||||
fn sub(self, other: Duration) -> Time {
|
||||
Time::seconds_since_midnight(self.0 - other.inner_seconds())
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Sub for Time {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, other: Time) -> Duration {
|
||||
Duration::seconds(self.0 - other.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse() {
|
||||
assert_eq!(
|
||||
Time::START_OF_DAY + Duration::seconds(42.3),
|
||||
Time::parse("42.3").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Time::START_OF_DAY + Duration::hours(7) + Duration::minutes(30),
|
||||
Time::parse("07:30").unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
Time::START_OF_DAY
|
||||
+ Duration::hours(7)
|
||||
+ Duration::minutes(30)
|
||||
+ Duration::seconds(5.0),
|
||||
Time::parse("07:30:05").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_hours() {
|
||||
assert_eq!((Time::START_OF_DAY + Duration::hours(6)).get_hours(), 6);
|
||||
assert_eq!(
|
||||
(Time::START_OF_DAY + Duration::hours(6) + Duration::seconds(1.0)).get_hours(),
|
||||
6
|
||||
);
|
||||
assert_eq!(
|
||||
(Time::START_OF_DAY + Duration::hours(6) + Duration::minutes(59)).get_hours(),
|
||||
6
|
||||
);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
// These're copied from abstutil! geom should be split into its own repository at some point, and
|
||||
// abstutil brings in too many dependencies.
|
||||
|
||||
pub fn prettyprint_usize(x: usize) -> String {
|
||||
let num = format!("{}", x);
|
||||
let mut result = String::new();
|
||||
let mut i = num.len();
|
||||
for c in num.chars() {
|
||||
result.push(c);
|
||||
i -= 1;
|
||||
if i > 0 && i % 3 == 0 {
|
||||
result.push(',');
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
@ -9,7 +9,7 @@ abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
hyper = { version = "0.14.26", features = ["full"] }
|
||||
lazy_static = "1.4.0"
|
||||
log = { workspace = true }
|
||||
|
@ -18,7 +18,7 @@ csv = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
gdal = { version = "0.14.0", optional = true, features = ["bindgen"] }
|
||||
kml = { path = "../kml" }
|
||||
log = { workspace = true }
|
||||
|
@ -9,7 +9,7 @@ abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
roxmltree = { version = "0.19.0", features=["std"] }
|
||||
serde = { workspace = true }
|
||||
|
@ -21,7 +21,7 @@ contour = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
instant = { workspace = true }
|
||||
log = { workspace = true }
|
||||
lyon = "1.0.1"
|
||||
|
@ -11,7 +11,7 @@ anyhow = { workspace = true }
|
||||
enumset = { version = "1.1.3", features=["serde"] }
|
||||
fast_paths = { git = "https://github.com/easbar/fast_paths", rev = "9a954e02f01ed16939d3c4a2dc9dd3fb4f6c03ee"}
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
kml = { path = "../kml" }
|
||||
log = { workspace = true }
|
||||
lyon = "1.0.1"
|
||||
|
@ -13,7 +13,7 @@ wasm = ["getrandom/js", "js-sys", "map_gui/wasm", "wasm-bindgen", "web-sys", "wi
|
||||
[dependencies]
|
||||
abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
js-sys = { version = "0.3.51", optional = true }
|
||||
log = { workspace = true }
|
||||
|
@ -12,7 +12,7 @@ flatgeobuf = { version = "3.25.0" }
|
||||
futures = { workspace = true }
|
||||
geo = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
geozero = "0.9.9"
|
||||
log = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||
[dependencies]
|
||||
abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
osm2streets = { git = "https://github.com/a-b-street/osm2streets" }
|
||||
popgetter = { git = "https://github.com/dabreegster/popgetter/" }
|
||||
|
@ -11,7 +11,7 @@ anyhow = { workspace = true }
|
||||
ctrlc = { version = "3.4.1", optional = true }
|
||||
downcast-rs = "1.2.0"
|
||||
enum_dispatch = "0.3.12"
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
instant = { workspace = true }
|
||||
libm = "0.2.8"
|
||||
log = { workspace = true }
|
||||
|
@ -7,7 +7,7 @@ edition = "2021"
|
||||
abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
anyhow = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
rand = "0.8.5"
|
||||
|
@ -11,7 +11,7 @@ anyhow = { workspace = true }
|
||||
blockfinding = { path = "../blockfinding" }
|
||||
convert_osm = { path = "../convert_osm" }
|
||||
fs-err = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
rand = { workspace = true }
|
||||
sim = { path = "../sim" }
|
||||
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
abstutil = { path = "../abstutil" }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
log = { workspace = true }
|
||||
map_model = { path = "../map_model" }
|
||||
rand = { workspace = true }
|
||||
|
@ -21,7 +21,7 @@ fs-err = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
futures-channel = { workspace = true }
|
||||
geojson = { workspace = true }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
glow = "0.12.1"
|
||||
glutin = { git = "https://github.com/rust-windowing/glutin", optional = true, rev = "2bffbf52d6b4f4c32adc463818e10ac8082948e4" }
|
||||
htmlescape = "0.3.1"
|
||||
|
@ -14,7 +14,7 @@ wasm = ["getrandom/js", "wasm-bindgen", "widgetry/wasm-backend"]
|
||||
[dependencies]
|
||||
abstio = { path = "../abstio" }
|
||||
abstutil = { path = "../abstutil" }
|
||||
geom = { path = "../geom" }
|
||||
geom = { workspace = true }
|
||||
getrandom = { workspace = true, optional = true }
|
||||
log = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
Loading…
Reference in New Issue
Block a user