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
12pub 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 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 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 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 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}