map_gui/tools/
navigate.rs

1use std::collections::HashSet;
2
3use map_model::RoadID;
4use widgetry::{
5    Autocomplete, Color, DrawBaselayer, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome,
6    Panel, State, Text, Transition, Widget,
7};
8
9use crate::tools::grey_out_map;
10use crate::{AppLike, ID};
11
12// TODO Canonicalize names, handling abbreviations like east/e and street/st
13pub struct Navigator {
14    panel: Panel,
15    target_zoom: f64,
16}
17
18impl Navigator {
19    pub fn new_state<A: AppLike + 'static>(ctx: &mut EventCtx, app: &A) -> Box<dyn State<A>> {
20        Self::new_state_with_target_zoom(ctx, app, ctx.canvas.settings.min_zoom_for_detail)
21    }
22
23    pub fn new_state_with_target_zoom<A: AppLike + 'static>(
24        ctx: &mut EventCtx,
25        app: &A,
26        target_zoom: f64,
27    ) -> Box<dyn State<A>> {
28        Box::new(Navigator {
29            target_zoom,
30            panel: Panel::new_builder(Widget::col(vec![
31                Widget::row(vec![
32                    Line("Enter a street name").small_heading().into_widget(ctx),
33                    ctx.style().btn_close_widget(ctx),
34                ]),
35                Autocomplete::new_widget(
36                    ctx,
37                    app.map()
38                        .all_roads()
39                        .iter()
40                        .map(|r| (r.get_name(app.opts().language.as_ref()), r.id))
41                        .collect(),
42                    10,
43                )
44                .named("street"),
45                ctx.style()
46                    .btn_outline
47                    .text("Search by business name or address")
48                    .hotkey(Key::Tab)
49                    .build_def(ctx),
50            ]))
51            .build(ctx),
52        })
53    }
54}
55
56impl<A: AppLike + 'static> State<A> for Navigator {
57    fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition<A> {
58        if let Outcome::Clicked(x) = self.panel.event(ctx) {
59            match x.as_ref() {
60                "close" => {
61                    return Transition::Pop;
62                }
63                "Search by business name or address" => {
64                    return Transition::Replace(SearchBuildings::new_state(
65                        ctx,
66                        app,
67                        self.target_zoom,
68                    ));
69                }
70                _ => unreachable!(),
71            }
72        }
73        if let Some(roads) = self.panel.autocomplete_done("street") {
74            if roads.is_empty() {
75                return Transition::Pop;
76            }
77            return Transition::Replace(CrossStreet::new_state(ctx, app, roads, self.target_zoom));
78        }
79
80        if self.panel.clicked_outside(ctx) {
81            return Transition::Pop;
82        }
83
84        Transition::Keep
85    }
86
87    fn draw_baselayer(&self) -> DrawBaselayer {
88        DrawBaselayer::PreviousState
89    }
90
91    fn draw(&self, g: &mut GfxCtx, app: &A) {
92        grey_out_map(g, app);
93        self.panel.draw(g);
94    }
95}
96
97struct CrossStreet {
98    first: Vec<RoadID>,
99    panel: Panel,
100    draw: Drawable,
101    target_zoom: f64,
102}
103
104impl CrossStreet {
105    fn new_state<A: AppLike + 'static>(
106        ctx: &mut EventCtx,
107        app: &A,
108        first: Vec<RoadID>,
109        target_zoom: f64,
110    ) -> Box<dyn State<A>> {
111        let map = app.map();
112        let mut cross_streets = HashSet::new();
113        let mut batch = GeomBatch::new();
114        for r in &first {
115            let road = map.get_r(*r);
116            batch.push(Color::RED, road.get_thick_polygon());
117            for i in [road.src_i, road.dst_i] {
118                for cross in &map.get_i(i).roads {
119                    cross_streets.insert(*cross);
120                }
121            }
122        }
123        // Roads share intersections, so of course there'll be overlap here.
124        for r in &first {
125            cross_streets.remove(r);
126        }
127
128        Box::new(CrossStreet {
129            panel: Panel::new_builder(Widget::col(vec![
130                Widget::row(vec![
131                    {
132                        let mut txt = Text::from(Line("What cross street?").small_heading());
133                        // TODO This isn't so clear...
134                        txt.add_line(format!(
135                            "(Or just quit to go to {})",
136                            map.get_r(first[0]).get_name(app.opts().language.as_ref()),
137                        ));
138                        txt.into_widget(ctx)
139                    },
140                    ctx.style().btn_close_widget(ctx),
141                ]),
142                Autocomplete::new_widget(
143                    ctx,
144                    cross_streets
145                        .into_iter()
146                        .map(|r| (map.get_r(r).get_name(app.opts().language.as_ref()), r))
147                        .collect(),
148                    10,
149                )
150                .named("street"),
151            ]))
152            .build(ctx),
153            first,
154            draw: ctx.upload(batch),
155            target_zoom,
156        })
157    }
158}
159
160impl<A: AppLike + 'static> State<A> for CrossStreet {
161    fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition<A> {
162        let map = app.map();
163
164        if let Outcome::Clicked(x) = self.panel.event(ctx) {
165            match x.as_ref() {
166                "close" => {
167                    // Just warp to somewhere on the first road
168                    let pt = map.get_r(self.first[0]).center_pts.middle();
169                    return Transition::Replace(app.make_warper(
170                        ctx,
171                        pt,
172                        Some(self.target_zoom),
173                        None,
174                    ));
175                }
176                _ => unreachable!(),
177            }
178        }
179        if let Some(roads) = self.panel.autocomplete_done("street") {
180            // Find the best match
181            let mut found = None;
182            'OUTER: for r1 in &self.first {
183                let r1 = map.get_r(*r1);
184                for i in [r1.src_i, r1.dst_i] {
185                    if map.get_i(i).roads.iter().any(|r2| roads.contains(r2)) {
186                        found = Some(i);
187                        break 'OUTER;
188                    }
189                }
190            }
191            if let Some(i) = found {
192                let pt = map.get_i(i).polygon.center();
193                return Transition::Replace(app.make_warper(
194                    ctx,
195                    pt,
196                    Some(self.target_zoom),
197                    Some(ID::Intersection(i)),
198                ));
199            } else {
200                return Transition::Pop;
201            }
202        }
203
204        if self.panel.clicked_outside(ctx) {
205            return Transition::Pop;
206        }
207
208        Transition::Keep
209    }
210
211    fn draw_baselayer(&self) -> DrawBaselayer {
212        DrawBaselayer::PreviousState
213    }
214
215    fn draw(&self, g: &mut GfxCtx, app: &A) {
216        g.redraw(&self.draw);
217        grey_out_map(g, app);
218        self.panel.draw(g);
219    }
220}
221
222struct SearchBuildings {
223    panel: Panel,
224    target_zoom: f64,
225}
226
227impl SearchBuildings {
228    fn new_state<A: AppLike + 'static>(
229        ctx: &mut EventCtx,
230        app: &A,
231        target_zoom: f64,
232    ) -> Box<dyn State<A>> {
233        Box::new(SearchBuildings {
234            target_zoom,
235            panel: Panel::new_builder(Widget::col(vec![
236                Widget::row(vec![
237                    Line("Enter a business name or address")
238                        .small_heading()
239                        .into_widget(ctx),
240                    ctx.style().btn_close_widget(ctx),
241                ]),
242                Autocomplete::new_widget(
243                    ctx,
244                    app.map()
245                        .all_buildings()
246                        .iter()
247                        .flat_map(|b| {
248                            let mut results = Vec::new();
249                            if !b.address.starts_with("???") {
250                                results.push((b.address.clone(), b.id));
251                            }
252                            if let Some(ref names) = b.name {
253                                results.push((
254                                    names.get(app.opts().language.as_ref()).to_string(),
255                                    b.id,
256                                ));
257                            }
258                            for a in &b.amenities {
259                                results.push((
260                                    format!(
261                                        "{} (at {})",
262                                        a.names.get(app.opts().language.as_ref()),
263                                        b.address
264                                    ),
265                                    b.id,
266                                ));
267                            }
268                            results
269                        })
270                        .collect(),
271                    10,
272                )
273                .named("bldg"),
274                ctx.style()
275                    .btn_outline
276                    .text("Search for streets")
277                    .hotkey(Key::Tab)
278                    .build_def(ctx),
279            ]))
280            .build(ctx),
281        })
282    }
283}
284
285impl<A: AppLike + 'static> State<A> for SearchBuildings {
286    fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition<A> {
287        if let Outcome::Clicked(x) = self.panel.event(ctx) {
288            match x.as_ref() {
289                "close" => {
290                    return Transition::Pop;
291                }
292                "Search for streets" => {
293                    return Transition::Replace(Navigator::new_state_with_target_zoom(
294                        ctx,
295                        app,
296                        self.target_zoom,
297                    ));
298                }
299                _ => unreachable!(),
300            }
301        }
302        if let Some(bldgs) = self.panel.autocomplete_done("bldg") {
303            if bldgs.is_empty() {
304                return Transition::Pop;
305            }
306            let b = app.map().get_b(bldgs[0]);
307            let pt = b.label_center;
308            return Transition::Replace(app.make_warper(
309                ctx,
310                pt,
311                Some(self.target_zoom),
312                Some(ID::Building(bldgs[0])),
313            ));
314        }
315
316        if self.panel.clicked_outside(ctx) {
317            return Transition::Pop;
318        }
319
320        Transition::Keep
321    }
322
323    fn draw_baselayer(&self) -> DrawBaselayer {
324        DrawBaselayer::PreviousState
325    }
326
327    fn draw(&self, g: &mut GfxCtx, app: &A) {
328        grey_out_map(g, app);
329        self.panel.draw(g);
330    }
331}