Refactor the steep streets layer, add it as an option to the ungap tool.

Code and usability issues accumulating as more things cram into this
view... tackling that next.
This commit is contained in:
Dustin Carlino 2021-08-24 11:52:16 -07:00
parent bddbe8af99
commit 06b74dbe55
3 changed files with 175 additions and 123 deletions

View File

@ -48,6 +48,28 @@ impl Layer for SteepStreets {
impl SteepStreets {
pub fn new(ctx: &mut EventCtx, app: &App) -> SteepStreets {
let (colorer, steepest, uphill_legend) = SteepStreets::make_colorer(ctx, app);
let (unzoomed, zoomed, legend) = colorer.build(ctx);
let panel = Panel::new_builder(Widget::col(vec![
header(ctx, "Steep streets"),
uphill_legend,
legend,
format!("Steepest road: {:.0}% incline", steepest * 100.0).text_widget(ctx),
]))
.aligned_pair(PANEL_PLACEMENT)
.build(ctx);
SteepStreets {
tooltip: None,
unzoomed,
zoomed,
panel,
}
}
/// Also returns the steepest street and a row explaining the uphill arrows
pub fn make_colorer<'a>(ctx: &mut EventCtx, app: &'a App) -> (ColorDiscrete<'a>, f64, Widget) {
let mut colorer = ColorDiscrete::new(
app,
vec![
@ -108,7 +130,6 @@ impl SteepStreets {
}
}
colorer.unzoomed.append(arrows);
let (unzoomed, zoomed, legend) = colorer.build(ctx);
let pt = Pt2D::new(0.0, 0.0);
let panel_arrow = PolyLine::must_new(vec![
@ -118,26 +139,14 @@ impl SteepStreets {
])
.make_polygons(thickness)
.scale(5.0);
let panel = Panel::new_builder(Widget::col(vec![
header(ctx, "Steep streets"),
Widget::row(vec![
GeomBatch::from(vec![(ctx.style().text_primary_color, panel_arrow)])
.autocrop()
.into_widget(ctx),
"points uphill".text_widget(ctx).centered_vert(),
]),
legend,
format!("Steepest road: {:.0}% incline", steepest * 100.0).text_widget(ctx),
]))
.aligned_pair(PANEL_PLACEMENT)
.build(ctx);
let uphill_legend = Widget::row(vec![
GeomBatch::from(vec![(ctx.style().text_primary_color, panel_arrow)])
.autocrop()
.into_widget(ctx),
"points uphill".text_widget(ctx).centered_vert(),
]);
SteepStreets {
tooltip: None,
unzoomed,
zoomed,
panel,
}
(colorer, steepest, uphill_legend)
}
}
@ -198,7 +207,7 @@ impl ElevationContours {
high = high.max(i.elevation);
}
let (closest_elevation, unzoomed) = make_elevation_contours(ctx, app, low, high);
let (closest_elevation, unzoomed) = ElevationContours::make_contours(ctx, app, low, high);
let panel = Panel::new_builder(Widget::col(vec![
header(ctx, "Elevation"),
@ -219,92 +228,92 @@ impl ElevationContours {
panel,
}
}
}
pub fn make_elevation_contours(
ctx: &mut EventCtx,
app: &App,
low: Distance,
high: Distance,
) -> (FindClosest<Distance>, Drawable) {
let bounds = app.primary.map.get_bounds();
let mut closest = FindClosest::new(bounds);
let mut batch = GeomBatch::new();
pub fn make_contours(
ctx: &mut EventCtx,
app: &App,
low: Distance,
high: Distance,
) -> (FindClosest<Distance>, Drawable) {
let bounds = app.primary.map.get_bounds();
let mut closest = FindClosest::new(bounds);
let mut batch = GeomBatch::new();
ctx.loading_screen("generate contours", |_, timer| {
timer.start("gather input");
ctx.loading_screen("generate contours", |_, timer| {
timer.start("gather input");
let resolution_m = 30.0;
// Elevation in meters
let mut grid: Grid<f64> = Grid::new(
(bounds.width() / resolution_m).ceil() as usize,
(bounds.height() / resolution_m).ceil() as usize,
0.0,
);
let resolution_m = 30.0;
// Elevation in meters
let mut grid: Grid<f64> = Grid::new(
(bounds.width() / resolution_m).ceil() as usize,
(bounds.height() / resolution_m).ceil() as usize,
0.0,
);
// Since gaps in the grid mess stuff up, just fill out each grid cell. Explicitly do the
// interpolation to the nearest measurement we have.
for i in app.primary.map.all_intersections() {
// TODO Or maybe even just the center?
closest.add(i.elevation, i.polygon.points());
}
let mut indices = Vec::new();
for x in 0..grid.width {
for y in 0..grid.height {
indices.push((x, y));
// Since gaps in the grid mess stuff up, just fill out each grid cell. Explicitly do the
// interpolation to the nearest measurement we have.
for i in app.primary.map.all_intersections() {
// TODO Or maybe even just the center?
closest.add(i.elevation, i.polygon.points());
}
}
for (idx, elevation) in timer.parallelize("fill out grid", indices, |(x, y)| {
let pt = Pt2D::new((x as f64) * resolution_m, (y as f64) * resolution_m);
let elevation = match closest.closest_pt(pt, INTERSECTION_SEARCH_RADIUS) {
Some((e, _)) => e,
// No intersections nearby... assume ocean?
None => Distance::ZERO,
};
(grid.idx(x, y), elevation)
}) {
grid.data[idx] = elevation.inner_meters();
}
timer.stop("gather input");
let mut indices = Vec::new();
for x in 0..grid.width {
for y in 0..grid.height {
indices.push((x, y));
}
}
for (idx, elevation) in timer.parallelize("fill out grid", indices, |(x, y)| {
let pt = Pt2D::new((x as f64) * resolution_m, (y as f64) * resolution_m);
let elevation = match closest.closest_pt(pt, INTERSECTION_SEARCH_RADIUS) {
Some((e, _)) => e,
// No intersections nearby... assume ocean?
None => Distance::ZERO,
};
(grid.idx(x, y), elevation)
}) {
grid.data[idx] = elevation.inner_meters();
}
timer.stop("gather input");
timer.start("calculate contours");
// Generate polygons covering the contour line where the cost in the grid crosses these
// threshold values.
let mut thresholds: Vec<f64> = Vec::new();
let mut x = low;
while x < high {
thresholds.push(x.inner_meters());
x += CONTOUR_STEP_SIZE;
}
// And color the polygon for each threshold
let scale = ColorScale(vec![Color::WHITE, Color::RED]);
let colors: Vec<Color> = (0..thresholds.len())
.map(|i| scale.eval((i as f64) / (thresholds.len() as f64)))
.collect();
let smooth = false;
let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
let features = c.contours(&grid.data, &thresholds).unwrap();
timer.stop("calculate contours");
timer.start("calculate contours");
// Generate polygons covering the contour line where the cost in the grid crosses these
// threshold values.
let mut thresholds: Vec<f64> = Vec::new();
let mut x = low;
while x < high {
thresholds.push(x.inner_meters());
x += CONTOUR_STEP_SIZE;
}
// And color the polygon for each threshold
let scale = ColorScale(vec![Color::WHITE, Color::RED]);
let colors: Vec<Color> = (0..thresholds.len())
.map(|i| scale.eval((i as f64) / (thresholds.len() as f64)))
.collect();
let smooth = false;
let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
let features = c.contours(&grid.data, &thresholds).unwrap();
timer.stop("calculate contours");
timer.start_iter("draw", features.len());
for (feature, color) in features.into_iter().zip(colors) {
timer.next();
match feature.geometry.unwrap().value {
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(p) = Polygon::from_geojson(&p) {
let poly = p.scale(resolution_m);
if let Ok(x) = poly.to_outline(Distance::meters(5.0)) {
batch.push(Color::BLACK.alpha(0.5), x);
timer.start_iter("draw", features.len());
for (feature, color) in features.into_iter().zip(colors) {
timer.next();
match feature.geometry.unwrap().value {
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(p) = Polygon::from_geojson(&p) {
let poly = p.scale(resolution_m);
if let Ok(x) = poly.to_outline(Distance::meters(5.0)) {
batch.push(Color::BLACK.alpha(0.5), x);
}
batch.push(color.alpha(0.1), poly);
}
batch.push(color.alpha(0.1), poly);
}
}
_ => unreachable!(),
}
_ => unreachable!(),
}
}
});
});
(closest, batch.upload(ctx))
(closest, batch.upload(ctx))
}
}

View File

@ -35,6 +35,7 @@ pub struct ExploreMap {
labels: Option<DrawRoadLabels>,
edits_layer: Drawable,
elevation: bool,
steep_streets: Option<Drawable>,
// TODO Once widgetry buttons can take custom enums, that'd be perfect here
road_types: HashMap<String, Drawable>,
@ -60,12 +61,13 @@ impl ExploreMap {
Box::new(ExploreMap {
top_panel: Panel::empty(ctx),
bottom_right_panel: make_bottom_right_panel(ctx, app, true, true, false),
bottom_right_panel: make_bottom_right_panel(ctx, app, true, true, false, false),
magnifying_glass: MagnifyingGlass::new(ctx),
bike_network_layer: Some(DrawNetworkLayer::new()),
labels: Some(DrawRoadLabels::new()),
edits_layer: Drawable::empty(ctx),
elevation: false,
steep_streets: None,
road_types: HashMap::new(),
zoom_enabled_cache_key: zoom_enabled_cache_key(ctx),
@ -79,6 +81,7 @@ impl ExploreMap {
if name == "bike network"
|| name == "road labels"
|| name == "elevation"
|| name == "steep streets"
|| name.starts_with("about ")
{
return;
@ -267,13 +270,13 @@ impl State<App> for ExploreMap {
"zoom map out" => {
ctx.canvas.center_zoom(-8.0);
debug!("clicked zoomed out to: {}", ctx.canvas.cam_zoom);
self.bottom_right_panel = make_bottom_right_panel(ctx, app, self.bike_network_layer.is_some(), self.labels.is_some(), self.elevation);
self.bottom_right_panel = make_bottom_right_panel(ctx, app, self.bike_network_layer.is_some(), self.labels.is_some(), self.elevation, self.steep_streets.is_some());
return Transition::Keep;
},
"zoom map in" => {
ctx.canvas.center_zoom(8.0);
debug!("clicked zoomed in to: {}", ctx.canvas.cam_zoom);
self.bottom_right_panel = make_bottom_right_panel(ctx, app, self.bike_network_layer.is_some(), self.labels.is_some(), self.elevation);
self.bottom_right_panel = make_bottom_right_panel(ctx, app, self.bike_network_layer.is_some(), self.labels.is_some(), self.elevation, self.steep_streets.is_some());
return Transition::Keep;
},
_ => unreachable!(),
@ -302,6 +305,7 @@ impl State<App> for ExploreMap {
self.bike_network_layer.is_some(),
self.labels.is_some(),
self.elevation,
self.steep_streets.is_some(),
);
if self.elevation {
let name = app.primary.map.get_name().clone();
@ -313,13 +317,41 @@ impl State<App> for ExploreMap {
high = high.max(i.elevation);
}
// TODO Maybe also draw the uphill arrows on the steepest streets?
let value = crate::layer::elevation::make_elevation_contours(
let value = crate::layer::elevation::ElevationContours::make_contours(
ctx, app, low, high,
);
app.session.elevation_contours.set(name, value);
}
}
}
"steep streets" => {
if self.bottom_right_panel.is_checked("steep streets") {
let (colorer, _, uphill_legend) =
crate::layer::elevation::SteepStreets::make_colorer(ctx, app);
// Make a horizontal legend for the incline
let mut legend: Vec<Widget> = colorer
.categories
.iter()
.map(|(label, color)| {
legend_batch(ctx, *color, Text::from(Line(label).fg(Color::WHITE)))
.into_widget(ctx)
})
.collect();
legend.push(uphill_legend);
let legend = Widget::custom_row(legend);
self.bottom_right_panel
.replace(ctx, "steep streets legend", legend);
self.steep_streets = Some(colorer.unzoomed.upload(ctx));
} else {
self.steep_streets = None;
self.bottom_right_panel.replace(
ctx,
"steep streets legend",
Text::new().into_widget(ctx),
);
}
}
_ => unreachable!(),
},
_ => {}
@ -336,6 +368,7 @@ impl State<App> for ExploreMap {
self.bike_network_layer.is_some(),
self.labels.is_some(),
self.elevation,
self.steep_streets.is_some(),
);
self.zoom_enabled_cache_key = zoom_enabled_cache_key(ctx);
}
@ -350,7 +383,6 @@ impl State<App> for ExploreMap {
if let Some(ref n) = self.bike_network_layer {
n.draw(g, app);
}
// TODO Might be useful to toggle these off
if let Some(ref l) = self.labels {
l.draw(g, app);
}
@ -360,6 +392,9 @@ impl State<App> for ExploreMap {
g.redraw(draw);
}
}
if let Some(ref draw) = self.steep_streets {
g.redraw(draw);
}
self.magnifying_glass.draw(g, app);
if let Some(name) = self.bottom_right_panel.currently_hovering() {
@ -466,7 +501,7 @@ fn make_top_panel(ctx: &mut EventCtx, app: &App) -> Panel {
ctx.style()
.btn_solid_primary
.icon_text("system/assets/tools/pencil.svg", "Create new bike lanes")
.hotkey(Key::S)
.hotkey(Key::C)
.build_def(ctx),
ctx.style()
.btn_outline
@ -520,6 +555,7 @@ fn make_legend(
bike_network: bool,
labels: bool,
elevation: bool,
steep_streets: bool,
) -> Widget {
Widget::col(vec![
Widget::custom_row(vec![
@ -538,23 +574,25 @@ fn make_legend(
// TODO Distinguish door-zone bike lanes?
// TODO Call out bike turning boxes?
// TODO Call out bike signals?
Toggle::checkbox(ctx, "road labels", None, labels),
Widget::custom_row(vec![
Widget::row(vec![
Toggle::checkbox(ctx, "elevation", Key::E, elevation),
ctx.style()
.btn_plain
.icon("system/assets/tools/info.svg")
.build_widget(ctx, "about the elevation data")
.centered_vert(),
Text::new()
.into_widget(ctx)
.named("current elevation")
.centered_vert(),
]),
// TODO Probably a collisions layer, or the alternate "steep streets"
])
.evenly_spaced(),
Toggle::checkbox(ctx, "road labels", Key::L, labels),
Widget::row(vec![
Toggle::checkbox(ctx, "elevation", Key::E, elevation),
ctx.style()
.btn_plain
.icon("system/assets/tools/info.svg")
.build_widget(ctx, "about the elevation data")
.centered_vert(),
Text::new()
.into_widget(ctx)
.named("current elevation")
.centered_vert(),
]),
Widget::row(vec![
Toggle::checkbox(ctx, "steep streets", Key::S, steep_streets),
// A placeholder
Text::new().into_widget(ctx).named("steep streets legend"),
]),
// TODO Probably a collisions layer
])
}
@ -564,10 +602,11 @@ fn make_bottom_right_panel(
bike_network: bool,
labels: bool,
elevation: bool,
steep_streets: bool,
) -> Panel {
Panel::new_builder(Widget::col(vec![
make_zoom_controls(ctx).align_right().padding_right(16),
make_legend(ctx, app, bike_network, labels, elevation)
make_legend(ctx, app, bike_network, labels, elevation, steep_streets)
.padding(16)
.bg(ctx.style().panel_bg),
]))
@ -575,10 +614,10 @@ fn make_bottom_right_panel(
.build_custom(ctx)
}
fn legend_item(ctx: &mut EventCtx, color: Color, label: &str) -> Widget {
fn legend_batch(ctx: &mut EventCtx, color: Color, txt: Text) -> GeomBatch {
// TODO Height of the "trail" button is slightly too low!
// Text with padding and a background color
let (mut batch, hitbox) = Text::from(Line(label))
let (mut batch, hitbox) = txt
.render(ctx)
.batch()
.container()
@ -590,7 +629,11 @@ fn legend_item(ctx: &mut EventCtx, color: Color, label: &str) -> Widget {
})
.into_geom(ctx, None);
batch.unshift(color, hitbox);
batch
}
fn legend_item(ctx: &mut EventCtx, color: Color, label: &str) -> Widget {
let batch = legend_batch(ctx, color, Text::from(Line(label)));
return ButtonBuilder::new()
.custom_batch(batch.clone(), ControlState::Default)
.custom_batch(

View File

@ -13,7 +13,7 @@ pub struct ColorDiscrete<'a> {
pub unzoomed: GeomBatch,
pub zoomed: GeomBatch,
// Store both, so we can build the legend in the original order later
categories: Vec<(String, Color)>,
pub categories: Vec<(String, Color)>,
colors: HashMap<String, Color>,
}