1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::OnceLock;
4
5use regex::Regex;
6
7use abstutil::Timer;
8use geom::{Angle, Bounds, Distance, Polygon, Pt2D, QuadTree};
9use map_model::{osm, Road, RoadID};
10use widgetry::mapspace::PerZoom;
11use widgetry::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, Line, Text};
12
13use crate::AppLike;
14
15pub struct DrawRoadLabels {
19 per_zoom: RefCell<Option<PerZoom>>,
20 include_roads: Box<dyn Fn(&Road) -> bool>,
21 fg_color: Color,
22 outline_color: Color,
23}
24
25impl DrawRoadLabels {
26 pub fn new(include_roads: Box<dyn Fn(&Road) -> bool>) -> Self {
28 Self {
29 per_zoom: Default::default(),
30 include_roads,
31 fg_color: Color::WHITE,
32 outline_color: Color::BLACK,
33 }
34 }
35
36 pub fn only_major_roads() -> Self {
38 Self::new(Box::new(|r| {
39 r.get_rank() != osm::RoadRank::Local && !r.is_light_rail()
40 }))
41 }
42
43 pub fn light_background(mut self) -> Self {
44 self.fg_color = Color::BLACK;
45 self.outline_color = Color::WHITE;
46 self
47 }
48
49 pub fn draw(&self, g: &mut GfxCtx, app: &dyn AppLike) {
50 let mut per_zoom = self.per_zoom.borrow_mut();
51 if per_zoom.is_none() {
52 *per_zoom = Some(PerZoom::new(g.canvas.settings.min_zoom_for_detail, 0.1));
53 }
54 let per_zoom = per_zoom.as_mut().unwrap();
55
56 let (zoom, idx) = per_zoom.discretize_zoom(g.canvas.cam_zoom);
57 let draw = &mut per_zoom.draw_per_zoom[idx];
58 if draw.is_none() {
59 *draw = Some(self.render(g, app, zoom));
60 }
61 g.redraw(draw.as_ref().unwrap());
62 }
63
64 fn render(&self, g: &mut GfxCtx, app: &dyn AppLike, zoom: f64) -> Drawable {
65 let mut batch = GeomBatch::new();
66 let map = app.map();
67
68 let text_scale = 1.0 / zoom;
71
72 let mut quadtree = QuadTree::new();
73
74 'ROAD: for r in map.all_roads() {
75 if !(self.include_roads)(r) || r.length() < Distance::meters(30.0) {
76 continue;
77 }
78
79 let name = if let Some(x) = simplify_name(r.get_name(app.opts().language.as_ref())) {
80 x
81 } else {
82 continue;
83 };
84 let (pt, angle) = r.center_pts.must_dist_along(r.length() / 2.0);
85
86 let big_bounds = cheaply_overestimate_bounds(&name, text_scale, pt, angle);
88 if quadtree.query_bbox(big_bounds).next().is_some() {
89 continue 'ROAD;
90 }
91 quadtree.insert_with_box((), big_bounds);
92
93 let txt = Text::from(
95 Line(&name)
96 .big_heading_plain()
97 .fg(self.fg_color)
98 .outlined(self.outline_color),
99 );
100 batch.append(txt.render_autocropped(g).multi_transform(
101 text_scale,
102 pt,
103 angle.reorient(),
104 ));
105 }
106
107 g.upload(batch)
108 }
109}
110
111static SIMPLIFY_PATTERNS: OnceLock<Vec<(Regex, String)>> = OnceLock::new();
112
113fn simplify_name(mut x: String) -> Option<String> {
115 if x == "???" || x.starts_with("Exit for ") {
117 return None;
118 }
119
120 for (search, replace_with) in SIMPLIFY_PATTERNS.get_or_init(simplify_patterns).iter() {
121 x = search.replace(&x, replace_with).to_string();
123 }
124
125 Some(x)
126}
127
128fn simplify_patterns() -> Vec<(Regex, String)> {
129 let mut replace = Vec::new();
130
131 for (long, short) in [
132 ("Northeast", "NE"),
133 ("Northwest", "NW"),
134 ("Southeast", "SE"),
135 ("Southwest", "SW"),
136 ("North", "N"),
138 ("South", "S"),
139 ("East", "E"),
140 ("West", "W"),
141 ] {
142 replace.push((
144 Regex::new(&format!("^{long} ")).unwrap(),
145 format!("{short} "),
146 ));
147 replace.push((
148 Regex::new(&format!(" {long}$")).unwrap(),
149 format!(" {short}"),
150 ));
151 }
152
153 for (long, short) in [
154 ("Street", "St"),
155 ("Boulevard", "Blvd"),
156 ("Avenue", "Ave"),
157 ("Place", "Pl"),
158 ] {
159 replace.push((
161 Regex::new(&format!("{}$", long)).unwrap(),
162 short.to_string(),
163 ));
164 replace.push((
166 Regex::new(&format!(" {} ", long)).unwrap(),
167 format!(" {} ", short),
168 ));
169 }
170
171 replace
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_simplify_name() {
180 for (input, want) in [
181 ("Northeast Northgate Way", "NE Northgate Way"),
182 ("South 42nd Street", "S 42nd St"),
183 ("Northcote Road", "Northcote Road"),
184 ] {
185 let got = simplify_name(input.to_string()).unwrap();
186 if got != want {
187 panic!("simplify_name({}) = {}; expected {}", input, got, want);
188 }
189 }
190 }
191}
192
193fn cheaply_overestimate_bounds(text: &str, text_scale: f64, center: Pt2D, angle: Angle) -> Bounds {
194 let letter_width = 30.0 * text_scale;
196 let letter_height = 30.0 * text_scale;
197
198 Polygon::rectangle_centered(
199 center,
200 Distance::meters(letter_width * text.len() as f64),
201 Distance::meters(letter_height),
202 )
203 .rotate(angle.reorient())
204 .get_bounds()
205}
206
207pub struct DrawSimpleRoadLabels {
210 draw: Drawable,
211 include_roads: Box<dyn Fn(&Road) -> bool>,
212 fg_color: Color,
213
214 pub label_covers_road: HashMap<RoadID, (Distance, Distance)>,
215}
216
217impl DrawSimpleRoadLabels {
218 pub fn new(
220 ctx: &mut EventCtx,
221 app: &dyn AppLike,
222 fg_color: Color,
223 include_roads: Box<dyn Fn(&Road) -> bool>,
224 ) -> Self {
225 let mut labels = Self {
226 draw: Drawable::empty(ctx),
227 include_roads,
228 fg_color,
229 label_covers_road: HashMap::new(),
230 };
231 ctx.loading_screen("label roads", |ctx, timer| {
232 labels.render(ctx, app, timer);
233 });
234 labels
235 }
236
237 pub fn empty(ctx: &EventCtx) -> Self {
238 Self {
239 draw: Drawable::empty(ctx),
240 include_roads: Box::new(|_| false),
241 fg_color: Color::CLEAR,
242 label_covers_road: HashMap::new(),
243 }
244 }
245
246 pub fn only_major_roads(ctx: &mut EventCtx, app: &dyn AppLike, fg_color: Color) -> Self {
248 Self::new(
249 ctx,
250 app,
251 fg_color,
252 Box::new(|r| r.get_rank() != osm::RoadRank::Local && !r.is_light_rail()),
253 )
254 }
255
256 pub fn all_roads(ctx: &mut EventCtx, app: &dyn AppLike, fg_color: Color) -> Self {
257 Self::new(ctx, app, fg_color, Box::new(|_| true))
258 }
259
260 pub fn draw(&self, g: &mut GfxCtx) {
261 g.redraw(&self.draw);
262 }
263
264 fn render(&mut self, ctx: &mut EventCtx, app: &dyn AppLike, timer: &mut Timer) {
265 let mut batch = GeomBatch::new();
266 let map = app.map();
267
268 timer.start_iter("render roads", map.all_roads().len());
269 for r in map.all_roads() {
270 timer.next();
271 if !(self.include_roads)(r) || r.length() < Distance::meters(30.0) || r.zorder < 0 {
273 continue;
274 }
275
276 let name = if let Some(x) = simplify_name(r.get_name(app.opts().language.as_ref())) {
277 x
278 } else {
279 continue;
280 };
281
282 let txt_batch = Text::from(Line(&name)).render_autocropped(ctx);
283 if txt_batch.is_empty() {
284 continue;
286 }
287 let txt_bounds = txt_batch.get_bounds();
288
289 let outline_thickness = Distance::meters(2.0);
301 let road_width = (r.get_width() - 2.0 * outline_thickness).inner_meters();
302 let road_length = (0.9 * r.length()).inner_meters();
304
305 let mut scale = road_width / txt_bounds.height();
307
308 if txt_bounds.width() * scale > road_length {
310 scale = road_length / txt_bounds.width();
311 }
313
314 let center = r.length() / 2.0;
317 let half_width = Distance::meters(scale * txt_bounds.width() / 2.0);
318 self.label_covers_road
319 .insert(r.id, (center - half_width, center + half_width));
320
321 let quadrant = r.center_pts.quadrant();
333 let shift_dir = if quadrant == 2 || quadrant == 3 {
334 -1.0
335 } else {
336 1.0
337 };
338 let mut curve = r
343 .center_pts
344 .shift_either_direction(Distance::meters(shift_dir * road_width / 2.0))
345 .unwrap();
346 if quadrant == 2 || quadrant == 3 {
347 curve = curve.reversed();
348 }
349
350 batch.append(
351 Line(&name)
352 .fg(self.fg_color)
353 .render_curvey(ctx, &curve, scale),
354 );
355 }
356
357 self.draw = ctx.upload(batch);
358 }
359}