map_gui/render/
road.rs

1use std::cell::RefCell;
2
3use geom::{Bounds, Distance, Pt2D, Tessellation};
4use map_model::{Building, LaneType, Map, Road, RoadID, NORMAL_LANE_THICKNESS};
5use widgetry::{Color, Drawable, GeomBatch, GfxCtx, Line, Prerender, Text};
6
7use crate::colors::ColorSchemeChoice;
8use crate::options::CameraAngle;
9use crate::render::lane::DrawLane;
10use crate::render::{DrawOptions, Renderable};
11use crate::{AppLike, ID};
12
13// The default font size is too large; shrink it down to fit on roads better.
14const LABEL_SCALE_FACTOR: f64 = 0.1;
15// Making the label follow the road's curvature usually looks better, but sometimes the letters
16// squish together, so keep this experiment disabled for now.
17const DRAW_CURVEY_LABEL: bool = false;
18
19pub struct DrawRoad {
20    pub id: RoadID,
21    zorder: isize,
22
23    draw: RefCell<Option<Drawable>>,
24    pub lanes: Vec<DrawLane>,
25}
26
27impl DrawRoad {
28    pub fn new(r: &Road) -> DrawRoad {
29        DrawRoad {
30            id: r.id,
31            zorder: r.zorder,
32            draw: RefCell::new(None),
33            lanes: r.lanes.iter().map(|l| DrawLane::new(l, r)).collect(),
34        }
35    }
36
37    pub fn render<P: AsRef<Prerender>>(&self, prerender: &P, app: &dyn AppLike) -> GeomBatch {
38        let prerender = prerender.as_ref();
39        let r = app.map().get_r(self.id);
40
41        if r.is_light_rail() {
42            // No label or center-line
43            return GeomBatch::new();
44        }
45        let name = r.get_name(app.opts().language.as_ref());
46        let mut batch;
47        if r.length() >= Distance::meters(30.0) && name != "???" {
48            // Render a label, so split the center-line into two pieces
49            let text_width =
50                Distance::meters(Text::from(&name).rendered_width(prerender) * LABEL_SCALE_FACTOR);
51            batch = render_center_line(app, r, Some(text_width));
52
53            if DRAW_CURVEY_LABEL {
54                let fg = Color::WHITE;
55                if r.center_pts.quadrant() > 1 && r.center_pts.quadrant() < 4 {
56                    batch.append(Line(name).fg(fg).outlined(Color::BLACK).render_curvey(
57                        prerender,
58                        &r.center_pts.reversed(),
59                        LABEL_SCALE_FACTOR,
60                    ));
61                } else {
62                    batch.append(Line(name).fg(fg).outlined(Color::BLACK).render_curvey(
63                        prerender,
64                        &r.center_pts,
65                        LABEL_SCALE_FACTOR,
66                    ));
67                }
68            } else {
69                let mut center_line_color = app.cs().road_center_line(app.map());
70                if r.is_private() && app.cs().private_road.is_some() {
71                    center_line_color = center_line_color.lerp(app.cs().private_road.unwrap(), 0.5)
72                }
73                let txt = Text::from(Line(name).fg(center_line_color));
74                let (pt, angle) = r.center_pts.must_dist_along(r.length() / 2.0);
75                batch.append(
76                    txt.render_autocropped(prerender)
77                        .scale(LABEL_SCALE_FACTOR)
78                        .centered_on(pt)
79                        .rotate_around_batch_center(angle.reorient()),
80                );
81            }
82        } else {
83            // No label
84            batch = render_center_line(app, r, None);
85        }
86
87        // Driveways of connected buildings. These are grouped by road to limit what has to be
88        // recalculated when road edits cause buildings to re-snap.
89        for b in app.map().road_to_buildings(self.id) {
90            draw_building_driveway(app, app.map().get_b(*b), &mut batch);
91        }
92
93        batch
94    }
95
96    pub fn clear_rendering(&mut self) {
97        *self.draw.borrow_mut() = None;
98        for l in &mut self.lanes {
99            l.clear_rendering();
100        }
101    }
102}
103
104impl Renderable for DrawRoad {
105    fn get_id(&self) -> ID {
106        ID::Road(self.id)
107    }
108
109    fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike, _: &DrawOptions) {
110        let mut draw = self.draw.borrow_mut();
111        if draw.is_none() {
112            *draw = Some(g.upload(self.render(g, app)));
113        }
114        g.redraw(draw.as_ref().unwrap());
115    }
116
117    fn get_outline(&self, map: &Map) -> Tessellation {
118        // Highlight the entire thing, not just an outline
119        Tessellation::from(map.get_r(self.id).get_thick_polygon())
120    }
121
122    fn get_bounds(&self, map: &Map) -> Bounds {
123        map.get_r(self.id).get_thick_polygon().get_bounds()
124    }
125
126    fn contains_pt(&self, pt: Pt2D, map: &Map) -> bool {
127        map.get_r(self.id).get_thick_polygon().contains_pt(pt)
128    }
129
130    fn get_zorder(&self) -> isize {
131        self.zorder
132    }
133}
134
135/// If `text_width` is defined, don't draw the center line in the middle of the road for this
136/// amount of space
137fn render_center_line(app: &dyn AppLike, r: &Road, text_width: Option<Distance>) -> GeomBatch {
138    let mut center_line_color = app.cs().road_center_line(app.map());
139    if r.is_private() && app.cs().private_road.is_some() {
140        center_line_color = center_line_color.lerp(app.cs().private_road.unwrap(), 0.5)
141    }
142
143    let mut batch = GeomBatch::new();
144
145    // Draw a center line every time two driving/bike/bus lanes of opposite direction are adjacent.
146    let mut width = Distance::ZERO;
147    for pair in r.lanes.windows(2) {
148        width += pair[0].width;
149        if pair[0].dir != pair[1].dir
150            && pair[0].lane_type.is_for_moving_vehicles()
151            && pair[1].lane_type.is_for_moving_vehicles()
152        {
153            let pl = r.shift_from_left_side(width).unwrap();
154            if let Some(text_width) = text_width {
155                // Draw dashed lines from the start of the road to where the text label begins,
156                // then another set of dashes from where the text label ends to the end of the road
157                let first_segment_distance = (pl.length() - text_width) / 2.0;
158                let last_segment_distance = first_segment_distance + text_width;
159                for slice in [
160                    pl.slice(Distance::ZERO, first_segment_distance),
161                    pl.slice(last_segment_distance, pl.length()),
162                ] {
163                    if let Ok((line, _)) = slice {
164                        batch.extend(
165                            center_line_color,
166                            line.dashed_lines(
167                                Distance::meters(0.25),
168                                Distance::meters(2.0),
169                                Distance::meters(1.0),
170                            ),
171                        );
172                    }
173                }
174            } else {
175                // Uninterrupted center line covering the entire road
176                batch.extend(
177                    center_line_color,
178                    pl.dashed_lines(
179                        Distance::meters(0.25),
180                        Distance::meters(2.0),
181                        Distance::meters(1.0),
182                    ),
183                );
184            }
185        }
186    }
187
188    batch
189}
190
191fn draw_building_driveway(app: &dyn AppLike, bldg: &Building, batch: &mut GeomBatch) {
192    if app.opts().camera_angle == CameraAngle::Abstract || !app.opts().show_building_driveways {
193        return;
194    }
195
196    // Trim the driveway away from the sidewalk's center line, so that it doesn't overlap.  For
197    // now, this cleanup is visual; it doesn't belong in the map_model layer.
198    let orig_pl = &bldg.driveway_geom;
199    let driveway = orig_pl
200        .slice(
201            Distance::ZERO,
202            orig_pl.length() - app.map().get_l(bldg.sidewalk()).width / 2.0,
203        )
204        .map(|(pl, _)| pl)
205        .unwrap_or_else(|_| orig_pl.clone());
206    if driveway.length() > Distance::meters(0.1) {
207        batch.push(
208            if app.opts().color_scheme == ColorSchemeChoice::NightMode {
209                Color::hex("#4B4B4B")
210            } else {
211                app.cs().zoomed_road_surface(
212                    LaneType::Sidewalk,
213                    app.map().get_parent(bldg.sidewalk()).get_rank(),
214                )
215            },
216            driveway.make_polygons(NORMAL_LANE_THICKNESS),
217        );
218    }
219}