game/edit/
zones.rs

1use std::collections::BTreeSet;
2
3use enumset::EnumSet;
4use maplit::btreeset;
5
6use map_gui::tools::{checkbox_per_mode, intersections_from_roads, ColorDiscrete};
7use map_model::{AccessRestrictions, CommonEndpoint, PathConstraints, RoadID};
8use synthpop::TripMode;
9use widgetry::mapspace::ToggleZoomed;
10use widgetry::{
11    Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, Text,
12    VerticalAlignment, Widget,
13};
14
15use crate::app::{App, Transition};
16use crate::common::CommonState;
17use crate::common::RoadSelector;
18use crate::edit::apply_map_edits;
19
20pub struct ZoneEditor {
21    panel: Panel,
22    selector: RoadSelector,
23    allow_through_traffic: BTreeSet<TripMode>,
24    draw: ToggleZoomed,
25
26    orig_members: BTreeSet<RoadID>,
27}
28
29impl ZoneEditor {
30    pub fn new_state(ctx: &mut EventCtx, app: &mut App, start: RoadID) -> Box<dyn State<App>> {
31        let start = app.primary.map.get_r(start);
32        let members = if let Some(z) = start.get_zone(&app.primary.map) {
33            z.members.clone()
34        } else {
35            // Starting a new zone
36            btreeset! { start.id }
37        };
38        let allow_through_traffic = start
39            .access_restrictions
40            .allow_through_traffic
41            .into_iter()
42            .map(TripMode::from_constraints)
43            .collect();
44
45        let (draw, legend) = draw_zone(ctx, app, &members);
46        let orig_members = members.clone();
47        let selector = RoadSelector::new(ctx, app, members);
48
49        Box::new(ZoneEditor {
50            panel: Panel::new_builder(Widget::col(vec![
51                Line("Editing restricted access zone")
52                    .small_heading()
53                    .into_widget(ctx),
54                selector.make_controls(ctx).named("selector"),
55                legend,
56                make_instructions(ctx, &allow_through_traffic).named("instructions"),
57                checkbox_per_mode(ctx, app, &allow_through_traffic),
58                Widget::custom_row(vec![
59                    ctx.style()
60                        .btn_solid_primary
61                        .text("Apply")
62                        .hotkey(Key::Enter)
63                        .build_def(ctx),
64                    ctx.style()
65                        .btn_solid_destructive
66                        .text("Cancel")
67                        .hotkey(Key::Escape)
68                        .build_def(ctx),
69                ])
70                .evenly_spaced(),
71            ]))
72            .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
73            .build(ctx),
74            orig_members,
75            selector,
76            allow_through_traffic,
77            draw,
78        })
79    }
80}
81
82// TODO Handle splitting/merging zones.
83impl State<App> for ZoneEditor {
84    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
85        match self.panel.event(ctx) {
86            Outcome::Clicked(x) => match x.as_ref() {
87                "Apply" => {
88                    let mut edits = app.primary.map.get_edits().clone();
89
90                    // Roads deleted from the zone
91                    for r in self.orig_members.difference(&self.selector.roads) {
92                        edits
93                            .commands
94                            .push(app.primary.map.edit_road_cmd(*r, |new| {
95                                new.access_restrictions = AccessRestrictions::new();
96                            }));
97                    }
98
99                    let mut allow_through_traffic = self
100                        .allow_through_traffic
101                        .iter()
102                        .map(|m| m.to_constraints())
103                        .collect::<EnumSet<_>>();
104                    // The original allow_through_traffic always includes this, and there's no way
105                    // to exclude it, so stay consistent.
106                    allow_through_traffic.insert(PathConstraints::Train);
107                    let new_access_restrictions = AccessRestrictions {
108                        allow_through_traffic,
109                    };
110                    for r in &self.selector.roads {
111                        let old_access_restrictions =
112                            app.primary.map.get_r(*r).access_restrictions.clone();
113                        if old_access_restrictions != new_access_restrictions {
114                            edits
115                                .commands
116                                .push(app.primary.map.edit_road_cmd(*r, |new| {
117                                    new.access_restrictions = new_access_restrictions.clone();
118                                }));
119                        }
120                    }
121
122                    apply_map_edits(ctx, app, edits);
123                    return Transition::Pop;
124                }
125                "Cancel" => {
126                    return Transition::Pop;
127                }
128                x => {
129                    if self.selector.event(ctx, app, Some(x)) {
130                        let new_controls = self.selector.make_controls(ctx);
131                        self.panel.replace(ctx, "selector", new_controls);
132                        self.draw = draw_zone(ctx, app, &self.selector.roads).0;
133                    }
134                }
135            },
136            Outcome::Changed(_) => {
137                let mut new_allow_through_traffic = BTreeSet::new();
138                for m in TripMode::all() {
139                    if self.panel.is_checked(m.ongoing_verb()) {
140                        new_allow_through_traffic.insert(m);
141                    }
142                }
143                let instructions = make_instructions(ctx, &new_allow_through_traffic);
144                self.panel.replace(ctx, "instructions", instructions);
145                self.allow_through_traffic = new_allow_through_traffic;
146            }
147            _ => {
148                if self.selector.event(ctx, app, None) {
149                    let new_controls = self.selector.make_controls(ctx);
150                    self.panel.replace(ctx, "selector", new_controls);
151                    self.draw = draw_zone(ctx, app, &self.selector.roads).0;
152                }
153            }
154        }
155
156        Transition::Keep
157    }
158
159    fn draw(&self, g: &mut GfxCtx, app: &App) {
160        // TODO The currently selected road is covered up pretty badly
161        self.draw.draw(g);
162        self.panel.draw(g);
163        self.selector.draw(g, app, false);
164        CommonState::draw_osd(g, app);
165    }
166}
167
168fn draw_zone(ctx: &mut EventCtx, app: &App, members: &BTreeSet<RoadID>) -> (ToggleZoomed, Widget) {
169    let mut colorer = ColorDiscrete::new(
170        app,
171        vec![
172            ("restricted road", Color::CYAN),
173            ("entrance/exit", Color::BLUE),
174        ],
175    );
176    let map = &app.primary.map;
177    for r in members {
178        let r = map.get_r(*r);
179        colorer.add_r(r.id, "restricted road");
180        for next in map.get_next_roads(r.id) {
181            if !members.contains(&next) {
182                if let CommonEndpoint::One(i) = r.common_endpoint(map.get_r(next)) {
183                    colorer.add_i(i, "entrance/exit");
184                }
185            }
186        }
187    }
188    for i in intersections_from_roads(members, &app.primary.map) {
189        colorer.add_i(i, "restricted road");
190    }
191    colorer.build(ctx)
192}
193
194fn make_instructions(ctx: &mut EventCtx, allow_through_traffic: &BTreeSet<TripMode>) -> Widget {
195    if allow_through_traffic == &TripMode::all().into_iter().collect() {
196        Text::from(
197            "Through-traffic is allowed for everyone, meaning this is just a normal public road. \
198             Would you like to restrict it?",
199        )
200        .wrap_to_pct(ctx, 30)
201        .into_widget(ctx)
202    } else {
203        Line("Trips may start or end in this zone, but through-traffic is only allowed for:")
204            .into_widget(ctx)
205    }
206}