game/layer/
elevation.rs

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    /// Also returns the steepest street and a row explaining the uphill arrows
68    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            // Draw arrows pointing uphill
99            // TODO Draw V's, not arrows.
100            // TODO Or try gradient colors.
101            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    /// Returns the colored categories used and a row explaining the uphill arrows
127    pub fn make_legend(ctx: &mut EventCtx) -> (Vec<(&'static str, Color)>, Widget) {
128        let categories = vec![
129            // Colors and buckets from https://github.com/ITSLeeds/slopes
130            ("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            // Elevation in meters
251            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            // Since gaps in the grid mess stuff up, just fill out each grid cell. Explicitly do the
258            // interpolation to the nearest measurement we have.
259            for i in app.primary.map.all_intersections() {
260                // TODO Or maybe even just the center?
261                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                    // No intersections nearby... assume ocean?
274                    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            // Generate polygons covering the contour line where the cost in the grid crosses these
284            // threshold values.
285            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            // And color the polygon for each threshold
292            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}