1use crate::ID;
2use geom::{Angle, Distance, FindClosest, PolyLine, Polygon, Pt2D};
3use map_gui::tools::{ColorDiscrete, Grid};
4use widgetry::mapspace::ToggleZoomed;
5use widgetry::tools::ColorScale;
6use widgetry::{Color, EventCtx, GeomBatch, GfxCtx, Panel, Text, TextExt, Widget};
7
8use crate::app::App;
9use crate::layer::{header, Layer, LayerOutcome, PANEL_PLACEMENT};
10
11pub struct SteepStreets {
12 tooltip: Option<Text>,
13 draw: ToggleZoomed,
14 panel: Panel,
15}
16
17impl Layer for SteepStreets {
18 fn name(&self) -> Option<&'static str> {
19 Some("steep streets")
20 }
21 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
22 if ctx.redo_mouseover() {
23 self.tooltip = None;
24 if let Some(ID::Road(r)) = app.mouseover_unzoomed_roads_and_intersections(ctx) {
25 self.tooltip = Some(Text::from(format!(
26 "{:.1}% incline",
27 app.primary.map.get_r(r).percent_incline.abs() * 100.0
28 )));
29 }
30 }
31
32 <dyn Layer>::simple_event(ctx, &mut self.panel)
33 }
34 fn draw(&self, g: &mut GfxCtx, _: &App) {
35 self.panel.draw(g);
36 self.draw.draw(g);
37 if let Some(ref txt) = self.tooltip {
38 g.draw_mouse_tooltip(txt.clone());
39 }
40 }
41 fn draw_minimap(&self, g: &mut GfxCtx) {
42 g.redraw(&self.draw.unzoomed);
43 }
44}
45
46impl SteepStreets {
47 pub fn new(ctx: &mut EventCtx, app: &App) -> SteepStreets {
48 let (colorer, steepest, uphill_legend) = SteepStreets::make_colorer(ctx, app);
49 let (draw, legend) = colorer.build(ctx);
50
51 let panel = Panel::new_builder(Widget::col(vec![
52 header(ctx, "Steep streets"),
53 uphill_legend,
54 legend,
55 format!("Steepest road: {:.0}% incline", steepest * 100.0).text_widget(ctx),
56 ]))
57 .aligned_pair(PANEL_PLACEMENT)
58 .build(ctx);
59
60 SteepStreets {
61 tooltip: None,
62 draw,
63 panel,
64 }
65 }
66
67 pub fn make_colorer<'a>(ctx: &mut EventCtx, app: &'a App) -> (ColorDiscrete<'a>, f64, Widget) {
69 let (categories, uphill_legend) = SteepStreets::make_legend(ctx);
70 let mut colorer = ColorDiscrete::new(app, categories);
71
72 let arrow_len = Distance::meters(5.0);
73 let thickness = Distance::meters(2.0);
74 let mut steepest = 0.0_f64;
75 let mut arrows = GeomBatch::new();
76 for r in app.primary.map.all_roads() {
77 if r.is_light_rail() {
78 continue;
79 }
80 let pct = r.percent_incline.abs();
81 steepest = steepest.max(pct);
82
83 let bucket = if pct < 0.03 {
84 "0-3% (flat)"
85 } else if pct < 0.05 {
86 "3-5%"
87 } else if pct < 0.08 {
88 "5-8%"
89 } else if pct < 0.1 {
90 "8-10%"
91 } else if pct < 0.2 {
92 "10-20%"
93 } else {
94 ">20% (steep)"
95 };
96 colorer.add_r(r.id, bucket);
97
98 if pct < 0.03 {
102 continue;
103 }
104 let mut pl = r.center_pts.clone();
105 if r.percent_incline < 0.0 {
106 pl = pl.reversed();
107 }
108
109 for (pt, angle) in pl.step_along(Distance::meters(15.0), arrow_len) {
110 arrows.push(
111 Color::WHITE,
112 PolyLine::must_new(vec![
113 pt.project_away(arrow_len, angle.rotate_degs(-135.0)),
114 pt,
115 pt.project_away(arrow_len, angle.rotate_degs(135.0)),
116 ])
117 .make_polygons(thickness),
118 );
119 }
120 }
121 colorer.draw.unzoomed.append(arrows);
122
123 (colorer, steepest, uphill_legend)
124 }
125
126 pub fn make_legend(ctx: &mut EventCtx) -> (Vec<(&'static str, Color)>, Widget) {
128 let categories = vec![
129 ("0-3% (flat)", Color::hex("#296B07")),
131 ("3-5%", Color::hex("#689A03")),
132 ("5-8%", Color::hex("#EB9A04")),
133 ("8-10%", Color::hex("#D30800")),
134 ("10-20%", Color::hex("#980104")),
135 (">20% (steep)", Color::hex("#680605")),
136 ];
137
138 let arrow_len = Distance::meters(5.0);
139 let thickness = Distance::meters(2.0);
140 let pt = Pt2D::new(0.0, 0.0);
141 let panel_arrow = PolyLine::must_new(vec![
142 pt.project_away(arrow_len, Angle::degrees(-135.0)),
143 pt,
144 pt.project_away(arrow_len, Angle::degrees(135.0)),
145 ])
146 .make_polygons(thickness)
147 .must_scale(5.0);
148 let uphill_legend = Widget::row(vec![
149 GeomBatch::from(vec![(ctx.style().text_primary_color, panel_arrow)])
150 .autocrop()
151 .into_widget(ctx),
152 "points uphill".text_widget(ctx).centered_vert(),
153 ]);
154
155 (categories, uphill_legend)
156 }
157}
158
159const INTERSECTION_SEARCH_RADIUS: Distance = Distance::const_meters(300.0);
160const CONTOUR_STEP_SIZE: Distance = Distance::const_meters(15.0);
161
162pub struct ElevationContours {
163 tooltip: Option<Text>,
164 closest_elevation: FindClosest<Distance>,
165 draw: ToggleZoomed,
166 panel: Panel,
167}
168
169impl Layer for ElevationContours {
170 fn name(&self) -> Option<&'static str> {
171 Some("elevation")
172 }
173 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Option<LayerOutcome> {
174 if ctx.redo_mouseover() {
175 self.tooltip = None;
176 if ctx.canvas.is_unzoomed() {
177 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
178 if let Some((elevation, _)) = self
179 .closest_elevation
180 .closest_pt(pt, INTERSECTION_SEARCH_RADIUS)
181 {
182 self.tooltip = Some(Text::from(format!(
183 "Elevation: {}",
184 elevation.to_string(&app.opts.units)
185 )));
186 }
187 }
188 }
189 }
190
191 <dyn Layer>::simple_event(ctx, &mut self.panel)
192 }
193 fn draw(&self, g: &mut GfxCtx, _: &App) {
194 self.panel.draw(g);
195 self.draw.draw(g);
196 if let Some(ref txt) = self.tooltip {
197 g.draw_mouse_tooltip(txt.clone());
198 }
199 }
200 fn draw_minimap(&self, g: &mut GfxCtx) {
201 g.redraw(&self.draw.unzoomed);
202 }
203}
204
205impl ElevationContours {
206 pub fn new(ctx: &mut EventCtx, app: &App) -> ElevationContours {
207 let mut low = Distance::ZERO;
208 let mut high = Distance::ZERO;
209 for i in app.primary.map.all_intersections() {
210 low = low.min(i.elevation);
211 high = high.max(i.elevation);
212 }
213
214 let (closest_elevation, draw) = ElevationContours::make_contours(ctx, app, low, high);
215
216 let panel = Panel::new_builder(Widget::col(vec![
217 header(ctx, "Elevation"),
218 format!(
219 "Elevation from {} to {}",
220 low.to_string(&app.opts.units),
221 high.to_string(&app.opts.units)
222 )
223 .text_widget(ctx),
224 ]))
225 .aligned_pair(PANEL_PLACEMENT)
226 .build(ctx);
227
228 ElevationContours {
229 tooltip: None,
230 closest_elevation,
231 draw,
232 panel,
233 }
234 }
235
236 pub fn make_contours(
237 ctx: &mut EventCtx,
238 app: &App,
239 low: Distance,
240 high: Distance,
241 ) -> (FindClosest<Distance>, ToggleZoomed) {
242 let bounds = app.primary.map.get_bounds();
243 let mut closest = FindClosest::new();
244 let mut draw = ToggleZoomed::builder();
245
246 ctx.loading_screen("generate contours", |_, timer| {
247 timer.start("gather input");
248
249 let resolution_m = 30.0;
250 let mut grid: Grid<f64> = Grid::new(
252 (bounds.width() / resolution_m).ceil() as usize,
253 (bounds.height() / resolution_m).ceil() as usize,
254 0.0,
255 );
256
257 for i in app.primary.map.all_intersections() {
260 closest.add_polygon(i.elevation, &i.polygon);
262 }
263 let mut indices = Vec::new();
264 for x in 0..grid.width {
265 for y in 0..grid.height {
266 indices.push((x, y));
267 }
268 }
269 for (idx, elevation) in timer.parallelize("fill out grid", indices, |(x, y)| {
270 let pt = Pt2D::new((x as f64) * resolution_m, (y as f64) * resolution_m);
271 let elevation = match closest.closest_pt(pt, INTERSECTION_SEARCH_RADIUS) {
272 Some((e, _)) => e,
273 None => Distance::ZERO,
275 };
276 (grid.idx(x, y), elevation)
277 }) {
278 grid.data[idx] = elevation.inner_meters();
279 }
280 timer.stop("gather input");
281
282 timer.start("calculate contours");
283 let mut thresholds: Vec<f64> = Vec::new();
286 let mut x = low;
287 while x < high {
288 thresholds.push(x.inner_meters());
289 x += CONTOUR_STEP_SIZE;
290 }
291 let scale = ColorScale(vec![Color::WHITE, Color::RED]);
293 let colors: Vec<Color> = (0..thresholds.len())
294 .map(|i| scale.eval((i as f64) / (thresholds.len() as f64)))
295 .collect();
296 let smooth = false;
297 let contour_builder =
298 contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
299 let contours = contour_builder.contours(&grid.data, &thresholds).unwrap();
300 timer.stop("calculate contours");
301
302 timer.start_iter("draw", contours.len());
303 for (contour, color) in contours.into_iter().zip(colors) {
304 timer.next();
305 let (polygons, _) = contour.into_inner();
306 for p in polygons {
307 if let Ok(p) = Polygon::try_from(p) {
308 let poly = p.must_scale(resolution_m);
309 draw.unzoomed.push(
310 Color::BLACK.alpha(0.5),
311 poly.to_outline(Distance::meters(5.0)),
312 );
313 draw.unzoomed.push(color.alpha(0.1), poly);
314 }
315 }
316 }
317 });
318
319 (closest, draw.build(ctx))
320 }
321}