Make Polygon::scale fallible. #951

Now there's no possible way to construct an invalid Polygon; every one
has Rings!
This commit is contained in:
Dustin Carlino 2022-09-01 17:26:32 +01:00
parent 7bba08113f
commit 081bd769fa
7 changed files with 65 additions and 45 deletions

View File

@ -1,7 +1,7 @@
use std::collections::{BTreeMap, BTreeSet};
use abstutil::prettyprint_usize;
use geom::{ArrowCap, Distance, Duration, PolyLine, Polygon, Time};
use geom::{ArrowCap, Distance, Duration, PolyLine, Polygon, Tessellation, Time};
use map_gui::options::TrafficSignalStyle;
use map_gui::render::traffic_signal::draw_signal_stage;
use map_model::{IntersectionID, IntersectionType, StageType};
@ -193,28 +193,29 @@ fn current_demand_body(ctx: &mut EventCtx, app: &App, id: IntersectionID) -> Wid
}
let mut batch = GeomBatch::new();
let polygon = app.primary.map.get_i(id).polygon.clone();
let mut polygon = Tessellation::from(app.primary.map.get_i(id).polygon.clone());
let bounds = polygon.get_bounds();
// Pick a zoom so that we fit a fixed width in pixels
let zoom = (0.25 * ctx.canvas.window_width) / bounds.width();
batch.push(
app.cs.normal_intersection,
polygon.translate(-bounds.min_x, -bounds.min_y).scale(zoom),
);
polygon.translate(-bounds.min_x, -bounds.min_y);
polygon.scale(zoom);
batch.push(app.cs.normal_intersection, polygon);
let mut tooltips = Vec::new();
let mut outlines = Vec::new();
for (pl, demand) in demand_per_movement {
let percent = (demand as f64) / (total_demand as f64);
let arrow = pl
if let Ok(arrow) = pl
.make_arrow(percent * Distance::meters(3.0), ArrowCap::Triangle)
.translate(-bounds.min_x, -bounds.min_y)
.scale(zoom);
if let Ok(p) = arrow.to_outline(Distance::meters(1.0)) {
outlines.push(p);
.scale(zoom)
{
if let Ok(p) = arrow.to_outline(Distance::meters(1.0)) {
outlines.push(p);
}
batch.push(Color::hex("#A3A3A3"), arrow.clone());
tooltips.push((arrow, Text::from(prettyprint_usize(demand)), None));
}
batch.push(Color::hex("#A3A3A3"), arrow.clone());
tooltips.push((arrow, Text::from(prettyprint_usize(demand)), None));
}
batch.extend(Color::WHITE, outlines);

View File

@ -144,7 +144,7 @@ impl SteepStreets {
pt.project_away(arrow_len, Angle::degrees(135.0)),
])
.make_polygons(thickness)
.scale(5.0);
.must_scale(5.0);
let uphill_legend = Widget::row(vec![
GeomBatch::from(vec![(ctx.style().text_primary_color, panel_arrow)])
.autocrop()
@ -305,7 +305,7 @@ impl ElevationContours {
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(p) = Polygon::from_geojson(&p) {
let poly = p.scale(resolution_m);
let poly = p.must_scale(resolution_m);
if let Ok(x) = poly.to_outline(Distance::meters(5.0)) {
draw.unzoomed.push(Color::BLACK.alpha(0.5), x);
}

View File

@ -214,7 +214,9 @@ impl TimePanel {
// on the right, except at the very end (for the last 'radius' pixels). And
// when the width is too small for the radius, this messes up.
progress_bar.push(bar_bg, Polygon::rectangle(bar_width, bar_height));
progress_bar.push(bar_fg, Polygon::rectangle(finished_width, bar_height));
if let Ok(p) = Polygon::maybe_rectangle(finished_width, bar_height) {
progress_bar.push(bar_fg, p);
}
if let Some(baseline_finished_width) = baseline_finished_width {
if baseline_finished_width > 0.0 {

View File

@ -233,7 +233,7 @@ impl RenderCellsBuilder {
for p in polygons {
if let Ok(poly) = Polygon::from_geojson(&p) {
cell_polygons.push(
poly.scale(RESOLUTION_M)
poly.must_scale(RESOLUTION_M)
.translate(self.bounds.min_x, self.bounds.min_y),
);
}

View File

@ -92,31 +92,40 @@ impl Polygon {
Bounds::from(&self.points)
}
fn transform<F: Fn(&Pt2D) -> Pt2D>(&self, f: F) -> Self {
Self {
/// Transformations must preserve Rings.
fn transform<F: Fn(&Pt2D) -> Pt2D>(&self, f: F) -> Result<Self> {
let mut rings = None;
if let Some(ref existing_rings) = self.rings {
let mut transformed = Vec::new();
for ring in existing_rings {
transformed.push(Ring::new(ring.points().iter().map(&f).collect())?);
}
rings = Some(transformed);
}
Ok(Self {
points: self.points.iter().map(&f).collect(),
indices: self.indices.clone(),
rings: self.rings.as_ref().map(|rings| {
rings
.iter()
// When scaling, rings may collapse entirely; just give up on preserving in
// that case.
.filter_map(|ring| Ring::new(ring.points().iter().map(&f).collect()).ok())
.collect()
}),
}
rings,
})
}
pub fn translate(&self, dx: f64, dy: f64) -> Self {
self.transform(|pt| pt.offset(dx, dy))
.expect("translate shouldn't collapse Rings")
}
pub fn scale(&self, factor: f64) -> Self {
/// 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))
}
pub fn scale_xy(&self, x_factor: f64, y_factor: f64) -> Self {
self.transform(|pt| Pt2D::new(pt.x() * x_factor, pt.y() * 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 {
@ -132,6 +141,7 @@ impl Polygon {
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 {
@ -180,15 +190,21 @@ impl Polygon {
}
/// Top-left at the origin. Doesn't take Distance, because this is usually pixels, actually.
pub fn rectangle(width: f64, height: f64) -> Self {
Ring::must_new(vec![
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),
])
.into_polygon()
.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 {

View File

@ -64,16 +64,17 @@ impl<A: AppLike + 'static> CityPicker<A> {
let mut tooltips = Vec::new();
for (name, polygon) in city.districts {
if &name != app.map().get_name() {
batch.push(
outline_color,
polygon.to_outline(Distance::meters(200.0)).unwrap(),
);
let polygon = polygon.scale(zoom);
tooltips.push((
polygon.clone(),
Text::from(nice_map_name(&name)),
Some(ClickOutcome::Custom(Box::new(name))),
));
if let Ok(zoomed_polygon) = polygon.scale(zoom) {
batch.push(
outline_color,
polygon.to_outline(Distance::meters(200.0)).unwrap(),
);
tooltips.push((
zoomed_polygon,
Text::from(nice_map_name(&name)),
Some(ClickOutcome::Custom(Box::new(name))),
));
}
}
}
DrawWithTooltips::new_widget(

View File

@ -212,7 +212,7 @@ pub fn make_heatmap(
let color = Color::rgb(c.r as usize, c.g as usize, c.b as usize).alpha(0.6);
for p in polygons {
if let Ok(poly) = Polygon::from_geojson(&p) {
batch.push(color, poly.scale(opts.resolution));
batch.push(color, poly.must_scale(opts.resolution));
}
}
}
@ -348,7 +348,7 @@ pub fn draw_isochrone(
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(poly) = Polygon::from_geojson(&p) {
batch.push(*color, poly.scale(resolution_m));
batch.push(*color, poly.must_scale(resolution_m));
}
}
}