map_gui/
colors.rs

1//! A color scheme groups colors used for different map, dynamic, and UI elements in one place, to
2//! encourage deduplication. The player can also switch between different color schemes.
3
4use std::io::Write;
5
6use anyhow::Result;
7use fs_err::File;
8use serde::{Deserialize, Serialize};
9
10use map_model::osm::RoadRank;
11use map_model::{LaneType, Map};
12use widgetry::tools::ColorScale;
13use widgetry::{Choice, Color, EventCtx, Fill, Style, Texture};
14
15use crate::tools::loading_tips;
16
17// I've gone back and forth how to organize color scheme code. I was previously against having one
18// centralized place with all definitions, because careful naming or comments are needed to explain
19// the context of a definition. That's unnecessary when the color is defined in the one place it's
20// used. But that was before we started consolidating the color palette in designs, and before we
21// started rapidly iterating on totally different schemes.
22//
23// For the record, the compiler catches typos with this approach, but I don't think I had a single
24// bug that took more than 30s to catch and fix in ~1.5 years of the untyped string key. ;)
25//
26// TODO There are plenty of colors left that aren't captured here. :(
27
28#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
29pub enum ColorSchemeChoice {
30    DayMode,
31    NightMode,
32    Textured,
33    ClassicDayMode,
34    LTN,
35}
36
37impl ColorSchemeChoice {
38    pub fn choices() -> Vec<Choice<ColorSchemeChoice>> {
39        vec![
40            Choice::new("day mode", ColorSchemeChoice::DayMode),
41            Choice::new("night mode", ColorSchemeChoice::NightMode),
42            Choice::new("textured", ColorSchemeChoice::Textured),
43            Choice::new("classic", ColorSchemeChoice::ClassicDayMode),
44            Choice::new("LTN", ColorSchemeChoice::LTN),
45        ]
46    }
47
48    pub fn parse(x: &str) -> Result<ColorSchemeChoice> {
49        let mut options = Vec::new();
50        for c in ColorSchemeChoice::choices() {
51            options.push(c.label.clone());
52            if c.label == x {
53                return Ok(c.data);
54            }
55        }
56        bail!(
57            "Invalid --color_scheme={}. Choices: {}",
58            x,
59            options.join(", ")
60        );
61    }
62}
63
64pub struct ColorScheme {
65    scheme: ColorSchemeChoice,
66
67    pub road_outlines: bool,
68    pub road_class_colors: bool,
69    pub show_buildings_in_minimap: bool,
70
71    // UI
72    pub panel_bg: Color,
73    pub inner_panel_bg: Color,
74    pub day_time_slider: Color,
75    pub night_time_slider: Color,
76    pub selected: Color,
77    pub current_object: Color,
78    pub perma_selected_object: Color,
79    pub fade_map_dark: Color,
80    gui_style: Style,
81    pub minimap_cursor_border: Color,
82    pub minimap_cursor_bg: Option<Color>,
83
84    // Roads
85    driving_lane: Color,
86    bus_lane: Color,
87    parking_lane: Color,
88    bike_lane: Color,
89    sidewalk: Color,
90    pub sidewalk_lines: Color,
91    pub general_road_marking: Color,
92    road_center_line: Color,
93    pub light_rail_track: Color,
94    pub private_road: Option<Color>,
95    pub unzoomed_highway: Color,
96    pub unzoomed_arterial: Color,
97    pub unzoomed_residential: Color,
98    pub unzoomed_cycleway: Color,
99    pub unzoomed_footway: Color,
100    footway: Color,
101    shared_use: Color,
102
103    // Intersections
104    pub normal_intersection: Color,
105    pub stop_sign: Color,
106    pub stop_sign_pole: Color,
107    pub signal_protected_turn: Color,
108    pub signal_permitted_turn: Color,
109    pub signal_banned_turn: Color,
110    pub signal_box: Color,
111    pub signal_spinner: Color,
112    pub signal_turn_block_bg: Color,
113
114    // Problems encountered on a trip
115    pub slowest_intersection: Color,
116    pub slower_intersection: Color,
117    pub slow_intersection: Color,
118
119    // Other static elements
120    pub void_background: Color,
121    pub map_background: Fill,
122    pub unzoomed_interesting_intersection: Color,
123    pub residential_building: Color,
124    pub commercial_building: Color,
125    pub building_outline: Color,
126    pub parking_lot: Color,
127    pub grass: Fill,
128    pub water: Fill,
129    pub study_area: Fill,
130
131    // Unzoomed dynamic elements
132    pub unzoomed_car: Color,
133    pub unzoomed_bike: Color,
134    pub unzoomed_bus: Color,
135    pub unzoomed_pedestrian: Color,
136
137    // Agents
138    agent_colors: Vec<Color>,
139    pub route: Color,
140    pub turn_arrow: Color,
141    pub brake_light: Color,
142    pub bus_body: Color,
143    pub bus_label: Color,
144    pub train_body: Color,
145    pub ped_head: Color,
146    pub ped_foot: Color,
147    pub ped_preparing_bike_body: Color,
148    pub ped_crowd: Color,
149    pub bike_frame: Color,
150    pub parked_car: Color,
151
152    // Layers
153    pub good_to_bad_red: ColorScale,
154    pub good_to_bad_green: ColorScale,
155    pub bus_layer: Color,
156    pub edits_layer: Color,
157
158    // Misc
159    pub parking_trip: Color,
160    pub bike_trip: Color,
161    pub bus_trip: Color,
162    pub before_changes: Color,
163    pub after_changes: Color,
164}
165
166impl ColorScheme {
167    pub fn new(ctx: &mut EventCtx, scheme: ColorSchemeChoice) -> ColorScheme {
168        let mut cs = match scheme {
169            ColorSchemeChoice::DayMode => ColorScheme::day_mode(),
170            ColorSchemeChoice::NightMode => ColorScheme::night_mode(),
171            ColorSchemeChoice::Textured => ColorScheme::textured(),
172            ColorSchemeChoice::ClassicDayMode => ColorScheme::classic(),
173            ColorSchemeChoice::LTN => ColorScheme::ltn(),
174        };
175        cs.scheme = scheme;
176        ctx.set_style(cs.gui_style.clone());
177        cs
178    }
179
180    fn classic() -> ColorScheme {
181        let mut cs = Self::light_background(Style::light_bg());
182        cs.scheme = ColorSchemeChoice::ClassicDayMode;
183        cs
184    }
185
186    fn light_background(mut gui_style: Style) -> ColorScheme {
187        gui_style.loading_tips = loading_tips();
188        ColorScheme {
189            scheme: ColorSchemeChoice::DayMode,
190
191            road_outlines: false,
192            road_class_colors: false,
193            show_buildings_in_minimap: true,
194
195            // UI
196            panel_bg: gui_style.panel_bg,
197            inner_panel_bg: gui_style.section_bg,
198            day_time_slider: hex("#F4DA22"),
199            night_time_slider: hex("#12409D"),
200            selected: Color::RED.alpha(0.7),
201            current_object: Color::WHITE,
202            perma_selected_object: Color::BLUE,
203            fade_map_dark: Color::BLACK.alpha(0.6),
204            minimap_cursor_border: Color::BLACK,
205            minimap_cursor_bg: None,
206            gui_style,
207
208            // Roads
209            driving_lane: Color::BLACK,
210            bus_lane: Color::rgb(190, 74, 76),
211            parking_lane: Color::grey(0.2),
212            bike_lane: Color::rgb(15, 125, 75),
213            sidewalk: Color::grey(0.8),
214            sidewalk_lines: Color::grey(0.7),
215            general_road_marking: Color::WHITE,
216            road_center_line: Color::YELLOW,
217            light_rail_track: hex("#844204"),
218            private_road: Some(hex("#F0B0C0")),
219            unzoomed_highway: hex("#E892A2"),
220            unzoomed_arterial: hex("#FFC73E"),
221            unzoomed_residential: Color::WHITE,
222            unzoomed_cycleway: hex("#0F7D4B"),
223            unzoomed_footway: hex("#DED68A"),
224            // TODO Distinguish shared use and footway unzoomed or zoomed?
225            footway: hex("#DED68A"),
226            shared_use: hex("#DED68A"),
227
228            // Intersections
229            normal_intersection: Color::grey(0.2),
230            stop_sign: Color::RED,
231            stop_sign_pole: Color::grey(0.5),
232            signal_protected_turn: hex("#72CE36"),
233            signal_permitted_turn: hex("#4CA7E9"),
234            signal_banned_turn: hex("#EB3223"),
235            signal_box: Color::grey(0.5),
236            signal_spinner: hex("#F2994A"),
237            signal_turn_block_bg: Color::grey(0.6),
238
239            // Problems encountered on a trip
240            slowest_intersection: Color::RED,
241            slower_intersection: Color::YELLOW,
242            slow_intersection: Color::GREEN,
243
244            // Other static elements
245            void_background: Color::BLACK,
246            map_background: Color::grey(0.87).into(),
247            unzoomed_interesting_intersection: Color::BLACK,
248            residential_building: hex("#C4C1BC"),
249            commercial_building: hex("#9FABA7"),
250            building_outline: hex("#938E85"),
251            parking_lot: Color::grey(0.7),
252            grass: hex("#94C84A").into(),
253            water: hex("#A4C8EA").into(),
254            study_area: hex("#96830C").into(),
255
256            // Unzoomed dynamic elements
257            unzoomed_car: hex("#FE5f55"),
258            unzoomed_bike: hex("#90BE6D"),
259            unzoomed_bus: hex("#FFD166"),
260            unzoomed_pedestrian: hex("#457B9D"),
261
262            // Agents
263            agent_colors: vec![
264                hex("#5C45A0"),
265                hex("#3E8BC3"),
266                hex("#E1BA13"),
267                hex("#96322F"),
268                hex("#00A27B"),
269            ],
270            route: Color::ORANGE.alpha(0.5),
271            turn_arrow: hex("#DF8C3D"),
272            brake_light: hex("#FF1300"),
273            bus_body: Color::rgb(50, 133, 117),
274            bus_label: Color::rgb(249, 206, 24),
275            train_body: hex("#42B6E9"),
276            ped_head: Color::rgb(139, 69, 19),
277            ped_foot: Color::BLACK,
278            ped_preparing_bike_body: Color::rgb(255, 0, 144),
279            ped_crowd: Color::rgb_f(0.2, 0.7, 0.7),
280            bike_frame: hex("#AAA9AD"),
281            parked_car: hex("#938E85"),
282
283            // Layers
284            good_to_bad_red: ColorScale(vec![hex("#F19A93"), hex("#A32015")]),
285            good_to_bad_green: ColorScale(vec![hex("#BEDB92"), hex("#397A4C")]),
286            bus_layer: hex("#4CA7E9"),
287            edits_layer: hex("#12409D"),
288
289            // Misc
290            parking_trip: hex("#4E30A6"),
291            bike_trip: Color::rgb(15, 125, 75),
292            bus_trip: Color::rgb(190, 74, 76),
293            before_changes: Color::BLUE,
294            after_changes: Color::RED,
295        }
296    }
297
298    // Shamelessly adapted from https://github.com/Uriopass/Egregoria
299    fn night_mode() -> ColorScheme {
300        let mut cs = ColorScheme::classic();
301        cs.scheme = ColorSchemeChoice::NightMode;
302        cs.gui_style = widgetry::Style::dark_bg();
303
304        cs.void_background = hex("#200A24");
305        cs.map_background = Color::BLACK.into();
306        cs.grass = hex("#243A1F").into();
307        cs.water = hex("#21374E").into();
308        cs.residential_building = hex("#2C422E");
309        cs.commercial_building = hex("#5D5F97");
310
311        cs.driving_lane = hex("#404040");
312        cs.parking_lane = hex("#353535");
313        cs.sidewalk = hex("#6B6B6B");
314        cs.general_road_marking = hex("#B1B1B1");
315        cs.normal_intersection = cs.driving_lane;
316        cs.road_center_line = cs.general_road_marking;
317
318        cs.parking_lot = cs.sidewalk;
319        cs.unzoomed_highway = cs.parking_lane;
320        cs.unzoomed_arterial = cs.sidewalk;
321        cs.unzoomed_residential = cs.driving_lane;
322        cs.unzoomed_interesting_intersection = cs.unzoomed_highway;
323        cs.stop_sign = hex("#A32015");
324        cs.private_road = Some(hex("#9E757F"));
325        cs.study_area = hex("#D9B002").into();
326
327        cs.panel_bg = cs.gui_style.panel_bg;
328        cs.inner_panel_bg = cs.panel_bg.alpha(1.0);
329        cs.minimap_cursor_border = Color::WHITE;
330        cs.minimap_cursor_bg = Some(Color::rgba(238, 112, 46, 0.2));
331
332        cs
333    }
334
335    fn textured() -> ColorScheme {
336        let mut cs = ColorScheme::day_mode();
337        cs.scheme = ColorSchemeChoice::Textured;
338        cs.grass = Texture::GRASS.into();
339        cs.water = Texture::STILL_WATER.into();
340        cs.map_background = Texture::CONCRETE.into();
341        cs
342    }
343
344    fn day_mode() -> ColorScheme {
345        let mut cs = Self::light_background(Style::light_bg());
346        cs.scheme = ColorSchemeChoice::DayMode;
347        cs.road_outlines = true;
348        cs.road_class_colors = true;
349        cs.show_buildings_in_minimap = false;
350
351        cs.map_background = hex("#EEE5C8").into();
352        cs.grass = hex("#BED4A3").into();
353        cs.water = hex("#6384D6").into();
354
355        cs.sidewalk = hex("#A9A9A9");
356        cs.sidewalk_lines = hex("#989898");
357
358        cs.unzoomed_arterial = hex("#F6A483");
359
360        cs.residential_building = hex("#C5D2E5");
361        cs.commercial_building = hex("#99AECC");
362
363        cs
364    }
365
366    fn ltn() -> ColorScheme {
367        let mut cs = ColorScheme::day_mode();
368        cs.scheme = ColorSchemeChoice::LTN;
369        cs.private_road = None;
370        cs.fade_map_dark = Color::BLACK.alpha(0.3);
371
372        // Based on Mapbox light scheme: https://www.mapbox.com/maps/light
373        cs.map_background = hex("#F6F6F4").into();
374        // Water is #CAD2D3, but more blue
375        cs.water = hex("#c7d7d9").into();
376        // #ECEEED, but more green
377        cs.grass = hex("#ddebe4").into();
378        // Many maps would use line thickness to distinguish main and local roads, but we're stuck
379        // with geometric interpretation, so use black.
380        cs.unzoomed_highway = Color::BLACK;
381        cs.unzoomed_arterial = Color::BLACK;
382        cs.unzoomed_residential = Color::WHITE;
383        cs.unzoomed_cycleway = Color::CLEAR;
384        cs.unzoomed_footway = Color::CLEAR;
385        cs.light_rail_track = Color::CLEAR;
386
387        // The colors of cells will show through these, de-emphasizing them
388        cs.parking_lot = Color::BLACK.alpha(0.2);
389        cs.residential_building = Color::BLACK.alpha(0.3);
390        cs.commercial_building = Color::BLACK.alpha(0.5);
391
392        cs.gui_style.panel_bg = Color::WHITE;
393        cs.panel_bg = cs.gui_style.panel_bg;
394
395        cs
396    }
397}
398
399impl ColorScheme {
400    pub fn rotating_color_plot(&self, idx: usize) -> Color {
401        modulo_color(
402            &[
403                Color::RED,
404                Color::BLUE,
405                Color::GREEN,
406                Color::PURPLE,
407                Color::BLACK,
408            ],
409            idx,
410        )
411    }
412
413    pub fn rotating_color_agents(&self, idx: usize) -> Color {
414        modulo_color(&self.agent_colors, idx)
415    }
416
417    pub fn unzoomed_road_surface(&self, rank: RoadRank) -> Color {
418        match rank {
419            RoadRank::Highway => self.unzoomed_highway,
420            RoadRank::Arterial => self.unzoomed_arterial,
421            RoadRank::Local => self.unzoomed_residential,
422        }
423    }
424
425    pub fn zoomed_road_surface(&self, lane: LaneType, rank: RoadRank) -> Color {
426        let main_asphalt = if self.road_class_colors {
427            match rank {
428                RoadRank::Highway => Color::grey(0.3),
429                RoadRank::Arterial => Color::grey(0.4),
430                RoadRank::Local => Color::grey(0.5),
431            }
432        } else {
433            self.driving_lane
434        };
435        let parking_asphalt = if self.road_class_colors {
436            main_asphalt
437        } else {
438            self.parking_lane
439        };
440
441        match lane {
442            LaneType::Driving => main_asphalt,
443            LaneType::Bus => self.bus_lane,
444            LaneType::Parking => parking_asphalt,
445            LaneType::Sidewalk | LaneType::Shoulder => self.sidewalk,
446            LaneType::Biking => self.bike_lane,
447            LaneType::SharedLeftTurn => main_asphalt,
448            LaneType::Construction => parking_asphalt,
449            LaneType::LightRail => unreachable!(),
450            LaneType::Buffer(_) => main_asphalt,
451            LaneType::Footway => self.footway,
452            LaneType::SharedUse => self.shared_use,
453        }
454    }
455    pub fn zoomed_intersection_surface(&self, rank: RoadRank) -> Color {
456        if self.road_class_colors {
457            self.zoomed_road_surface(LaneType::Driving, rank)
458        } else {
459            self.normal_intersection
460        }
461    }
462
463    pub fn curb(&self, rank: RoadRank) -> Color {
464        // The curb should be darker than the asphalt to stand out
465        match rank {
466            RoadRank::Highway => Color::grey(0.2),
467            RoadRank::Arterial => Color::grey(0.3),
468            RoadRank::Local => Color::grey(0.4),
469        }
470    }
471
472    pub fn road_center_line(&self, map: &Map) -> Color {
473        // TODO A more robust approach is to offload this question to osm2lanes, and color by
474        // separators
475        if map.get_name().city.country == "gb" {
476            self.general_road_marking
477        } else {
478            self.road_center_line
479        }
480    }
481
482    // These two could try to use serde, but... Color serializes with a separate RGB by default,
483    // and changing it to use a nice hex string is way too hard.
484    pub fn export(&self, path: &str) -> Result<()> {
485        let mut f = File::create(path)?;
486        writeln!(f, "unzoomed_highway {}", self.unzoomed_highway.as_hex())?;
487        writeln!(f, "unzoomed_arterial {}", self.unzoomed_arterial.as_hex())?;
488        writeln!(
489            f,
490            "unzoomed_residential {}",
491            self.unzoomed_residential.as_hex()
492        )?;
493        writeln!(f, "unzoomed_cycleway {}", self.unzoomed_cycleway.as_hex())?;
494        writeln!(f, "unzoomed_footway {}", self.unzoomed_footway.as_hex())?;
495        writeln!(
496            f,
497            "residential_building {}",
498            self.residential_building.as_hex()
499        )?;
500        writeln!(
501            f,
502            "commercial_building {}",
503            self.commercial_building.as_hex()
504        )?;
505        if let Fill::Color(c) = self.grass {
506            writeln!(f, "grass {}", c.as_hex())?;
507        }
508        if let Fill::Color(c) = self.water {
509            writeln!(f, "water {}", c.as_hex())?;
510        }
511        Ok(())
512    }
513
514    pub fn import(&mut self, path: &str) -> Result<()> {
515        let raw = String::from_utf8(abstio::slurp_file(path)?)?;
516        let mut colors = Vec::new();
517        for line in raw.split('\n') {
518            if line.is_empty() {
519                continue;
520            }
521            let mut parts = line.split(' ');
522            parts.next();
523            colors.push(Color::hex(parts.next().unwrap()));
524        }
525
526        self.unzoomed_highway = colors[0];
527        self.unzoomed_arterial = colors[1];
528        self.unzoomed_residential = colors[2];
529        self.unzoomed_cycleway = colors[3];
530        self.unzoomed_footway = colors[4];
531        self.residential_building = colors[5];
532        self.commercial_building = colors[6];
533        self.grass = Fill::Color(colors[7]);
534        self.water = Fill::Color(colors[8]);
535
536        Ok(())
537    }
538}
539
540fn modulo_color(colors: &[Color], idx: usize) -> Color {
541    colors[idx % colors.len()]
542}
543
544// Convenience
545fn hex(x: &str) -> Color {
546    Color::hex(x)
547}