mirror of
https://github.com/a-b-street/abstreet.git
synced 2024-11-23 17:07:12 +03:00
Document as much of geom as I can before my battery dies. And a mechanical API changes, Angle::new_degs -> Angle::degrees
This commit is contained in:
parent
262c48721b
commit
af70904377
@ -243,7 +243,7 @@ pub fn draw_occupants(details: &mut Details, app: &App, id: BuildingID, focus: O
|
||||
// Lies
|
||||
id: PedestrianID(person.0),
|
||||
pos,
|
||||
facing: Angle::new_degs(90.0),
|
||||
facing: Angle::degrees(90.0),
|
||||
waiting_for_turn: None,
|
||||
preparing_bike: false,
|
||||
// Both hands and feet!
|
||||
|
@ -209,7 +209,7 @@ pub fn trips(
|
||||
.scale(1.5);
|
||||
|
||||
if !open_trips.contains_key(t) {
|
||||
icon = icon.rotate(Angle::new_degs(180.0));
|
||||
icon = icon.rotate(Angle::degrees(180.0));
|
||||
}
|
||||
|
||||
icon.batch().container().align_right().margin_right(10)
|
||||
|
@ -77,10 +77,10 @@ impl DrawBuilding {
|
||||
}
|
||||
x => {
|
||||
let angle = match x {
|
||||
CameraAngle::IsometricNE => Angle::new_degs(-45.0),
|
||||
CameraAngle::IsometricNW => Angle::new_degs(-135.0),
|
||||
CameraAngle::IsometricSE => Angle::new_degs(45.0),
|
||||
CameraAngle::IsometricSW => Angle::new_degs(135.0),
|
||||
CameraAngle::IsometricNE => Angle::degrees(-45.0),
|
||||
CameraAngle::IsometricNW => Angle::degrees(-135.0),
|
||||
CameraAngle::IsometricSE => Angle::degrees(45.0),
|
||||
CameraAngle::IsometricSW => Angle::degrees(135.0),
|
||||
CameraAngle::TopDown | CameraAngle::Abstract => unreachable!(),
|
||||
};
|
||||
|
||||
|
@ -47,8 +47,8 @@ impl DrawBusStop {
|
||||
batch.push(
|
||||
cs.stop_sign_pole,
|
||||
Line::new(
|
||||
center.project_away(RADIUS, Angle::new_degs(90.0)),
|
||||
center.project_away(1.5 * RADIUS, Angle::new_degs(90.0)),
|
||||
center.project_away(RADIUS, Angle::degrees(90.0)),
|
||||
center.project_away(1.5 * RADIUS, Angle::degrees(90.0)),
|
||||
)
|
||||
.unwrap()
|
||||
.make_polygons(Distance::meters(0.3)),
|
||||
|
@ -126,14 +126,14 @@ impl DrawLane {
|
||||
GeomBatch::load_svg(g.prerender, "system/assets/map/bus_only.svg")
|
||||
.scale(0.06)
|
||||
.centered_on(pt)
|
||||
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
|
||||
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
|
||||
);
|
||||
} else if lane.is_biking() {
|
||||
draw.append(
|
||||
GeomBatch::load_svg(g.prerender, "system/assets/meters/bike.svg")
|
||||
.scale(0.06)
|
||||
.centered_on(pt)
|
||||
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
|
||||
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
|
||||
);
|
||||
} else if lane.lane_type == LaneType::SharedLeftTurn {
|
||||
draw.append(
|
||||
@ -141,7 +141,7 @@ impl DrawLane {
|
||||
.autocrop()
|
||||
.scale(0.003)
|
||||
.centered_on(pt)
|
||||
.rotate(angle.shortest_rotation_towards(Angle::new_degs(-90.0))),
|
||||
.rotate(angle.shortest_rotation_towards(Angle::degrees(-90.0))),
|
||||
);
|
||||
} else if lane.lane_type == LaneType::Construction {
|
||||
// TODO Still not quite centered right, but close enough
|
||||
@ -152,7 +152,7 @@ impl DrawLane {
|
||||
)
|
||||
.scale(0.05)
|
||||
.rotate_around_batch_center(
|
||||
angle.shortest_rotation_towards(Angle::new_degs(-90.0)),
|
||||
angle.shortest_rotation_towards(Angle::degrees(-90.0)),
|
||||
)
|
||||
.autocrop()
|
||||
.centered_on(pt),
|
||||
|
@ -254,6 +254,6 @@ fn crosswalk_icon(geom: &PolyLine) -> (Pt2D, Angle) {
|
||||
let l = Line::must_new(geom.points()[1], geom.points()[2]);
|
||||
(
|
||||
l.dist_along(Distance::meters(1.0)).unwrap_or(l.pt1()),
|
||||
l.angle().shortest_rotation_towards(Angle::new_degs(90.0)),
|
||||
l.angle().shortest_rotation_towards(Angle::degrees(90.0)),
|
||||
)
|
||||
}
|
||||
|
17
geom/README.md
Normal file
17
geom/README.md
Normal file
@ -0,0 +1,17 @@
|
||||
# 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`.
|
@ -2,7 +2,7 @@ use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Stores in radians
|
||||
/// An angle, stored in radians.
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)]
|
||||
pub struct Angle(f64);
|
||||
|
||||
@ -15,10 +15,12 @@ impl Angle {
|
||||
Angle((rads * 10_000_000.0).round() / 10_000_000.0)
|
||||
}
|
||||
|
||||
pub fn new_degs(degs: f64) -> Angle {
|
||||
/// 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)
|
||||
}
|
||||
@ -27,18 +29,22 @@ impl 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()
|
||||
}
|
||||
@ -47,11 +53,12 @@ impl Angle {
|
||||
/// normalize to be [0, 360].
|
||||
pub fn shortest_rotation_towards(self, other: Angle) -> Angle {
|
||||
// https://math.stackexchange.com/questions/110080/shortest-way-to-achieve-target-angle
|
||||
Angle::new_degs(
|
||||
Angle::degrees(
|
||||
((self.normalized_degrees() - other.normalized_degrees() + 540.0) % 360.0) - 180.0,
|
||||
)
|
||||
}
|
||||
|
||||
/// True if this angle is within some degrees of another, accounting for rotation
|
||||
pub fn approx_eq(self, other: Angle, within_degrees: f64) -> bool {
|
||||
// https://math.stackexchange.com/questions/110080/shortest-way-to-achieve-target-angle
|
||||
// This yields [-180, 180]
|
||||
|
@ -4,6 +4,7 @@ use aabb_quadtree::geom::{Point, Rect};
|
||||
|
||||
use crate::{LonLat, Polygon, Pt2D, Ring};
|
||||
|
||||
/// Represents a rectangular boundary of `Pt2D` points.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Bounds {
|
||||
pub min_x: f64,
|
||||
@ -13,6 +14,7 @@ pub struct Bounds {
|
||||
}
|
||||
|
||||
impl Bounds {
|
||||
/// A boundary including no points.
|
||||
pub fn new() -> Bounds {
|
||||
Bounds {
|
||||
min_x: f64::MAX,
|
||||
@ -22,6 +24,7 @@ impl Bounds {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a boundary covering some points.
|
||||
pub fn from(pts: &Vec<Pt2D>) -> Bounds {
|
||||
let mut b = Bounds::new();
|
||||
for pt in pts {
|
||||
@ -30,6 +33,7 @@ impl Bounds {
|
||||
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());
|
||||
@ -37,15 +41,18 @@ impl Bounds {
|
||||
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));
|
||||
}
|
||||
|
||||
/// 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 `aabb_quadtree`.
|
||||
pub fn as_bbox(&self) -> Rect {
|
||||
Rect {
|
||||
top_left: Point {
|
||||
@ -59,6 +66,7 @@ impl Bounds {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a rectangle covering this boundary.
|
||||
pub fn get_rectangle(&self) -> Polygon {
|
||||
Ring::must_new(vec![
|
||||
Pt2D::new(self.min_x, self.min_y),
|
||||
@ -70,13 +78,18 @@ impl Bounds {
|
||||
.to_polygon()
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@ -85,6 +98,9 @@ impl Bounds {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(crate) min_lon: f64,
|
||||
@ -94,6 +110,7 @@ pub struct GPSBounds {
|
||||
}
|
||||
|
||||
impl GPSBounds {
|
||||
/// A boundary including no points.
|
||||
pub fn new() -> GPSBounds {
|
||||
GPSBounds {
|
||||
min_lon: f64::MAX,
|
||||
@ -103,6 +120,7 @@ impl GPSBounds {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a boundary covering some points.
|
||||
pub fn from(pts: Vec<LonLat>) -> GPSBounds {
|
||||
let mut b = GPSBounds::new();
|
||||
for pt in pts {
|
||||
@ -111,6 +129,7 @@ impl GPSBounds {
|
||||
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());
|
||||
@ -118,6 +137,7 @@ impl GPSBounds {
|
||||
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
|
||||
@ -125,15 +145,17 @@ impl GPSBounds {
|
||||
&& 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_meters(LonLat::new(self.max_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_meters(LonLat::new(self.min_lon, self.max_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));
|
||||
@ -141,7 +163,7 @@ impl GPSBounds {
|
||||
b
|
||||
}
|
||||
|
||||
/// Fails if points are out-of-bounds.
|
||||
/// Convert all points to map-space, failing if any points are outside this boundary.
|
||||
pub fn try_convert(&self, pts: &Vec<LonLat>) -> Option<Vec<Pt2D>> {
|
||||
let mut result = Vec::new();
|
||||
for pt in pts {
|
||||
@ -153,18 +175,14 @@ impl GPSBounds {
|
||||
Some(result)
|
||||
}
|
||||
|
||||
/// Results can be out-of-bounds.
|
||||
/// Convert all points to map-space. The points may be outside this boundary.
|
||||
pub fn convert(&self, pts: &Vec<LonLat>) -> Vec<Pt2D> {
|
||||
pts.iter().map(|pt| Pt2D::from_gps(*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: &Vec<Pt2D>) -> Vec<LonLat> {
|
||||
pts.iter().map(|pt| pt.to_gps(self)).collect()
|
||||
}
|
||||
|
||||
pub fn approx_eq(&self, other: &GPSBounds) -> bool {
|
||||
LonLat::new(self.min_lon, self.min_lat).approx_eq(LonLat::new(other.min_lon, other.min_lat))
|
||||
&& LonLat::new(self.max_lon, self.max_lat)
|
||||
.approx_eq(LonLat::new(other.max_lon, other.max_lat))
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ use crate::{Angle, Bounds, Distance, Polygon, Pt2D, Ring};
|
||||
|
||||
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,
|
||||
@ -13,16 +14,19 @@ pub struct Circle {
|
||||
}
|
||||
|
||||
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(),
|
||||
@ -32,17 +36,20 @@ impl Circle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the circle as a polygon.
|
||||
pub fn to_polygon(&self) -> Polygon {
|
||||
self.to_partial_polygon(1.0)
|
||||
}
|
||||
|
||||
/// Renders some percent, between [0, 1], of the circle as a polygon. The polygon starts from 0
|
||||
/// degrees.
|
||||
pub fn to_partial_polygon(&self, percent_full: f64) -> Polygon {
|
||||
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::new_degs((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * percent_full * 360.0),
|
||||
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * percent_full * 360.0),
|
||||
));
|
||||
indices.push(0);
|
||||
indices.push(i + 1);
|
||||
@ -58,20 +65,21 @@ impl Circle {
|
||||
Polygon::precomputed(pts, indices)
|
||||
}
|
||||
|
||||
/// Returns the ring around the circle.
|
||||
fn to_ring(&self) -> Ring {
|
||||
Ring::must_new(
|
||||
(0..=TRIANGLES_PER_CIRCLE)
|
||||
.map(|i| {
|
||||
self.center.project_away(
|
||||
self.radius,
|
||||
Angle::new_degs((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * 360.0),
|
||||
Angle::degrees((i as f64) / (TRIANGLES_PER_CIRCLE as f64) * 360.0),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Draws an outline around the circle, strictly contained with the circle's original radius.
|
||||
/// Creates an outline around the circle, strictly contained with the circle's original radius.
|
||||
pub fn to_outline(&self, thickness: Distance) -> Result<Polygon, String> {
|
||||
if self.radius <= thickness {
|
||||
return Err(format!(
|
||||
|
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{trim_f64, Duration, Speed, UnitFmt};
|
||||
|
||||
/// In meters. Can be negative.
|
||||
/// A distance, in meters. Can be negative.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Distance(f64);
|
||||
|
||||
@ -19,6 +19,7 @@ impl Ord for Distance {
|
||||
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);
|
||||
@ -32,18 +33,22 @@ impl 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)
|
||||
}
|
||||
|
||||
/// Returns the absolute value of this distance.
|
||||
pub fn abs(self) -> Distance {
|
||||
if self.0 > 0.0 {
|
||||
self
|
||||
@ -52,15 +57,18 @@ impl Distance {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Describes the distance according to formatting rules.
|
||||
pub fn to_string(self, fmt: &UnitFmt) -> String {
|
||||
if fmt.metric {
|
||||
// TODO Round values to nearest meter, and km
|
||||
@ -76,6 +84,7 @@ impl Distance {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the largest of the two inputs.
|
||||
pub fn max(self, other: Distance) -> Distance {
|
||||
if self >= other {
|
||||
self
|
||||
@ -84,6 +93,7 @@ impl Distance {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the smallest of the two inputs.
|
||||
pub fn min(self, other: Distance) -> Distance {
|
||||
if self <= other {
|
||||
self
|
||||
@ -95,7 +105,6 @@ impl Distance {
|
||||
|
||||
impl fmt::Display for Distance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// TODO This is harder to localize
|
||||
write!(f, "{}m", self.0)
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use abstutil::elapsed_seconds;
|
||||
|
||||
use crate::{trim_f64, Distance, Speed};
|
||||
|
||||
/// In seconds. Can be negative.
|
||||
/// A duration, in seconds. Can be negative.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct Duration(f64);
|
||||
|
||||
@ -24,6 +24,7 @@ impl Duration {
|
||||
pub const ZERO: Duration = Duration::const_seconds(0.0);
|
||||
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);
|
||||
@ -32,14 +33,17 @@ impl Duration {
|
||||
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)
|
||||
}
|
||||
@ -56,13 +60,14 @@ impl Duration {
|
||||
(x as f64) * Duration::EPSILON
|
||||
}
|
||||
|
||||
/// 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, centiseconds).
|
||||
// TODO Could share some of this with Time -- the representations are the same
|
||||
// (hours, minutes, seconds, centiseconds)
|
||||
fn get_parts(self) -> (usize, usize, usize, usize) {
|
||||
// Force positive
|
||||
let mut remainder = self.inner_seconds().abs();
|
||||
@ -82,6 +87,7 @@ impl Duration {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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, Box<dyn Error>> {
|
||||
let parts: Vec<&str> = string.split(':').collect();
|
||||
@ -128,10 +134,12 @@ impl Duration {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration elapsed from this moment in real time.
|
||||
pub fn realtime_elapsed(since: Instant) -> Duration {
|
||||
Duration::seconds(elapsed_seconds(since))
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@ -141,6 +149,7 @@ impl Duration {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
@ -10,6 +10,7 @@ use crate::{Bounds, Distance, Pt2D};
|
||||
|
||||
// TODO Maybe use https://crates.io/crates/spatial-join proximity maps
|
||||
|
||||
/// A quad-tree to quickly find the closest points to some polylines.
|
||||
pub struct FindClosest<K> {
|
||||
// TODO maybe any type of geo:: thing
|
||||
geometries: BTreeMap<K, geo::LineString<f64>>,
|
||||
@ -20,6 +21,7 @@ impl<K> FindClosest<K>
|
||||
where
|
||||
K: Clone + Ord + std::fmt::Debug,
|
||||
{
|
||||
/// Creates the quad-tree, limited to points contained in the boundary.
|
||||
pub fn new(bounds: &Bounds) -> FindClosest<K> {
|
||||
FindClosest {
|
||||
geometries: BTreeMap::new(),
|
||||
@ -27,12 +29,15 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an object to the quadtree, remembering some key associated with the points.
|
||||
pub fn add(&mut self, key: K, pts: &Vec<Pt2D>) {
|
||||
self.geometries.insert(key.clone(), pts_to_line_string(pts));
|
||||
self.quadtree
|
||||
.insert_with_box(key, Bounds::from(pts).as_bbox());
|
||||
}
|
||||
|
||||
/// 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,
|
||||
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::Distance;
|
||||
|
||||
/// longitude is x, latitude is y
|
||||
/// Represents a (longitude, latitude) point.
|
||||
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
||||
pub struct LonLat {
|
||||
longitude: NotNan<f64>,
|
||||
@ -16,6 +16,7 @@ pub struct LonLat {
|
||||
}
|
||||
|
||||
impl LonLat {
|
||||
/// Note the order of arguments!
|
||||
pub fn new(lon: f64, lat: f64) -> LonLat {
|
||||
LonLat {
|
||||
longitude: NotNan::new(lon).unwrap(),
|
||||
@ -23,16 +24,18 @@ impl LonLat {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
pub fn gps_dist_meters(self, other: LonLat) -> Distance {
|
||||
// Haversine distance
|
||||
/// 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();
|
||||
@ -53,11 +56,8 @@ impl LonLat {
|
||||
NotNan::new((self.x() - other.x()).powi(2) + (self.y() - other.y()).powi(2)).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn approx_eq(self, other: LonLat) -> bool {
|
||||
let epsilon = 1e-8;
|
||||
(self.x() - other.x()).abs() < epsilon && (self.y() - other.y()).abs() < epsilon
|
||||
}
|
||||
|
||||
/// Parses a file in the https://wiki.openstreetmap.org/wiki/Osmosis/Polygon_Filter_File_Format
|
||||
/// and returns all points.
|
||||
pub fn read_osmosis_polygon(path: String) -> Result<Vec<LonLat>, Box<dyn Error>> {
|
||||
let f = File::open(&path)?;
|
||||
let mut pts = Vec::new();
|
||||
|
@ -5,50 +5,63 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{Angle, Distance, PolyLine, Polygon, Pt2D, EPSILON_DIST};
|
||||
|
||||
/// Segment, technically. Should rename.
|
||||
/// 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. None if the points are the same.
|
||||
pub fn new(pt1: Pt2D, pt2: Pt2D) -> Option<Line> {
|
||||
if pt1.dist_to(pt2) <= EPSILON_DIST {
|
||||
return None;
|
||||
}
|
||||
Some(Line(pt1, pt2))
|
||||
}
|
||||
// Just to be more clear at the call-site
|
||||
|
||||
/// 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/
|
||||
@ -74,7 +87,7 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
/// An intersection that isn't just two endpoints touching
|
||||
/// Determine if two line segments intersect, but more so than just two endpoints touching.
|
||||
pub fn crosses(&self, other: &Line) -> bool {
|
||||
if self.pt1() == other.pt1()
|
||||
|| self.pt1() == other.pt2()
|
||||
@ -86,6 +99,9 @@ impl Line {
|
||||
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)?;
|
||||
@ -96,6 +112,7 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@ -105,6 +122,7 @@ impl Line {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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);
|
||||
@ -114,6 +132,7 @@ impl Line {
|
||||
)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@ -122,14 +141,17 @@ impl Line {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reversed line segment
|
||||
pub fn reverse(&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) -> Option<Pt2D> {
|
||||
let len = self.length();
|
||||
if dist < Distance::ZERO || dist > len {
|
||||
@ -137,6 +159,8 @@ impl Line {
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
@ -242,7 +242,7 @@ impl Polygon {
|
||||
const RESOLUTION: usize = 5;
|
||||
let mut arc = |center: Pt2D, angle1_degs: f64, angle2_degs: f64| {
|
||||
for i in 0..=RESOLUTION {
|
||||
let angle = Angle::new_degs(
|
||||
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()));
|
||||
|
@ -103,7 +103,7 @@ impl CompareTimes {
|
||||
let y_label = {
|
||||
let label = Text::from(Line(format!("{} (minutes)", y_name.into())))
|
||||
.render(ctx)
|
||||
.rotate(Angle::new_degs(90.0))
|
||||
.rotate(Angle::degrees(90.0))
|
||||
.autocrop();
|
||||
// The text is already scaled; don't use Widget::draw_batch and scale it again.
|
||||
JustDraw::wrap(ctx, label).centered_vert().margin_right(5)
|
||||
|
@ -154,7 +154,7 @@ impl FanChart {
|
||||
// TODO Need ticks now to actually see where this goes
|
||||
let batch = Text::from(Line(t.to_string()))
|
||||
.render(ctx)
|
||||
.rotate(Angle::new_degs(-15.0))
|
||||
.rotate(Angle::degrees(-15.0))
|
||||
.autocrop();
|
||||
// The text is already scaled; don't use Widget::draw_batch and scale it again.
|
||||
row.push(JustDraw::wrap(ctx, batch));
|
||||
|
@ -175,7 +175,7 @@ impl<T: Yvalue<T>> LinePlot<T> {
|
||||
// TODO Need ticks now to actually see where this goes
|
||||
let batch = Text::from(Line(t.to_string()))
|
||||
.render(ctx)
|
||||
.rotate(Angle::new_degs(-15.0))
|
||||
.rotate(Angle::degrees(-15.0))
|
||||
.autocrop();
|
||||
// The text is already scaled; don't use Widget::draw_batch and scale it again.
|
||||
row.push(JustDraw::wrap(ctx, batch));
|
||||
|
@ -152,7 +152,7 @@ impl ScatterPlot {
|
||||
// TODO Need ticks now to actually see where this goes
|
||||
let batch = Text::from(Line(t.to_string()))
|
||||
.render(ctx)
|
||||
.rotate(Angle::new_degs(-15.0))
|
||||
.rotate(Angle::degrees(-15.0))
|
||||
.autocrop();
|
||||
// The text is already scaled; don't use Widget::draw_batch and scale it again.
|
||||
row.push(JustDraw::wrap(ctx, batch));
|
||||
|
@ -262,7 +262,7 @@ fn setup_scrollable_canvas(ctx: &mut EventCtx) -> Drawable {
|
||||
.render_to_batch(&ctx.prerender)
|
||||
.scale(2.0)
|
||||
.centered_on(Pt2D::new(600.0, 500.0))
|
||||
.rotate(Angle::new_degs(-30.0)),
|
||||
.rotate(Angle::degrees(-30.0)),
|
||||
);
|
||||
|
||||
let mut rng = if cfg!(target_arch = "wasm32") {
|
||||
|
Loading…
Reference in New Issue
Block a user