1use std::collections::{BTreeMap, HashMap, HashSet};
4
5use abstutil::{prettyprint_usize, Timer};
6use geom::{ArrowCap, Circle, Distance, PolyLine, Polygon, Pt2D, QuadTree, Ring};
7use kml::{ExtraShape, ExtraShapes};
8use map_gui::colors::ColorScheme;
9use map_gui::tools::FilePicker;
10use map_model::BuildingID;
11use widgetry::tools::PopupMsg;
12use widgetry::{
13 lctrl, Choice, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line,
14 Outcome, Panel, State, Text, TextBox, TextExt, VerticalAlignment, Widget,
15};
16
17use crate::app::{App, Transition};
18
19pub struct ViewKML {
20 panel: Panel,
21 objects: Vec<Object>,
22 draw: Drawable,
23
24 selected: Vec<usize>,
25 quadtree: QuadTree<usize>,
26 draw_query: Drawable,
27}
28
29struct Object {
30 polygon: Polygon,
31 color: Color,
32 attribs: BTreeMap<String, String>,
33
34 osm_bldg: Option<BuildingID>,
35}
36
37const RADIUS: Distance = Distance::const_meters(5.0);
38const THICKNESS: Distance = Distance::const_meters(2.0);
39
40impl ViewKML {
41 pub fn new_state(
42 ctx: &mut EventCtx,
43 app: &App,
44 file: Option<(String, Vec<u8>)>,
45 ) -> Box<dyn State<App>> {
46 ctx.loading_screen("load kml", |ctx, timer| {
47 let dump_clipped_shapes = false;
49 let (dataset_name, objects) = load_objects(app, file, dump_clipped_shapes, timer);
50
51 let mut batch = GeomBatch::new();
52 let mut quadtree = QuadTree::builder();
53 timer.start_iter("render shapes", objects.len());
54 for (idx, obj) in objects.iter().enumerate() {
55 timer.next();
56 quadtree.add_with_box(idx, obj.polygon.get_bounds());
57 batch.push(obj.color, obj.polygon.clone());
58 }
59
60 let mut choices = vec![Choice::string("None")];
61 if dataset_name == "parcels" {
62 choices.push(Choice::string("parcels without buildings"));
63 choices.push(Choice::string(
64 "parcels without buildings and trips or parking",
65 ));
66 choices.push(Choice::string("parcels with multiple buildings"));
67 choices.push(Choice::string("parcels with >1 households"));
68 choices.push(Choice::string("parcels with parking"));
69 }
70
71 Box::new(ViewKML {
72 draw: ctx.upload(batch),
73 panel: Panel::new_builder(Widget::col(vec![
74 Widget::row(vec![
75 Line("KML viewer").small_heading().into_widget(ctx),
76 ctx.style().btn_close_widget(ctx),
77 ]),
78 format!(
79 "{}: {} objects",
80 dataset_name,
81 prettyprint_usize(objects.len())
82 )
83 .text_widget(ctx),
84 ctx.style()
85 .btn_outline
86 .text("load KML file")
87 .hotkey(lctrl(Key::L))
88 .build_def(ctx),
89 Widget::row(vec![
90 "Query:".text_widget(ctx),
91 Widget::dropdown(ctx, "query", "None".to_string(), choices),
92 ]),
93 Widget::row(vec![
94 "Key=value filter:".text_widget(ctx),
95 TextBox::widget(ctx, "filter", String::new(), false, 10),
96 ]),
97 "Query matches 0 objects".text_widget(ctx).named("matches"),
98 "Mouseover an object to examine it"
99 .text_widget(ctx)
100 .named("mouseover"),
101 ]))
102 .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
103 .build(ctx),
104 objects,
105 quadtree: quadtree.build(),
106 selected: Vec::new(),
107 draw_query: Drawable::empty(ctx),
108 })
109 })
110 }
111}
112
113impl State<App> for ViewKML {
114 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
115 ctx.canvas_movement();
116 if ctx.redo_mouseover() {
117 let mut new_selected = Vec::new();
118 if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
119 for idx in self
120 .quadtree
121 .query_bbox(Circle::new(pt, Distance::meters(3.0)).get_bounds())
122 {
123 if self.objects[idx].polygon.contains_pt(pt) {
124 new_selected.push(idx);
125 }
126 }
127 }
128 if new_selected != self.selected {
129 self.selected = new_selected;
130 if self.selected.is_empty() {
131 let details = "Mouseover an object to examine it".text_widget(ctx);
132 self.panel.replace(ctx, "mouseover", details);
133 } else {
134 let mut txt = Text::new();
135 if self.selected.len() > 1 {
136 txt.add_line(format!("Selecting {} objects", self.selected.len()));
137 }
138 for idx in &self.selected {
139 if self.selected.len() > 1 {
140 txt.add_line(Line("Object").small_heading());
141 }
142 for (k, v) in &self.objects[*idx].attribs {
143 txt.add_line(format!("{} = {}", k, v));
144 }
145 }
146 let details = txt.into_widget(ctx);
147 self.panel.replace(ctx, "mouseover", details);
148 }
149 }
150 }
151 if !self.selected.is_empty() && ctx.normal_left_click() {
152 let mut lines = Vec::new();
153 let title = format!("{} objects", self.selected.len());
154 for idx in self.selected.drain(..) {
155 lines.push("Object".to_string());
156 lines.push(String::new());
157 for (k, v) in &self.objects[idx].attribs {
158 lines.push(format!("{} = {}", k, v));
159 }
160 lines.push(String::new());
161 }
162 lines.pop();
163 return Transition::Push(PopupMsg::new_state(ctx, &title, lines));
164 }
165
166 match self.panel.event(ctx) {
167 Outcome::Clicked(x) => match x.as_ref() {
168 "close" => {
169 return Transition::Pop;
170 }
171 "load KML file" => {
172 return pick_file(ctx, app);
173 }
174 _ => unreachable!(),
175 },
176 Outcome::Changed(_) => {
177 let mut query: String = self.panel.dropdown_value("query");
178 let filter = self.panel.text_box("filter");
179 if query == "None" && !filter.is_empty() {
180 query = filter;
181 }
182 let (batch, cnt) = make_query(app, &self.objects, &query);
183 self.draw_query = ctx.upload(batch);
184 self.panel.replace(
185 ctx,
186 "matches",
187 format!("Query matches {} objects", cnt).text_widget(ctx),
188 );
189 }
190 _ => {}
191 }
192
193 Transition::Keep
194 }
195
196 fn draw(&self, g: &mut GfxCtx, app: &App) {
197 g.redraw(&self.draw);
198 g.redraw(&self.draw_query);
199 self.panel.draw(g);
200
201 for idx in &self.selected {
202 let obj = &self.objects[*idx];
203 g.draw_polygon(Color::BLUE, obj.polygon.clone());
204 if let Some(b) = obj.osm_bldg {
205 g.draw_polygon(Color::GREEN, app.primary.map.get_b(b).polygon.clone());
206 }
207 }
208 }
209}
210
211fn load_objects(
213 app: &App,
214 file: Option<(String, Vec<u8>)>,
215 dump_clipped_shapes: bool,
216 timer: &mut Timer,
217) -> (String, Vec<Object>) {
218 let map = &app.primary.map;
219 let bounds = map.get_gps_bounds();
220
221 let path = file.map(|x| x.0);
223 let raw_shapes = if let Some(ref path) = path {
224 if path.ends_with(".kml") {
225 let shapes = kml::load(path.clone(), bounds, true, timer).unwrap();
226 if !path.ends_with("input/us/seattle/collisions.kml") {
232 abstio::write_binary(path.replace(".kml", ".bin"), &shapes);
233 }
234 shapes
235 } else if path.ends_with(".csv") {
236 let shapes = ExtraShapes::load_csv(path.clone(), bounds, timer).unwrap();
237 abstio::write_binary(path.replace(".csv", ".bin"), &shapes);
238 shapes
239 } else if path.ends_with(".geojson") || path.ends_with(".json") {
240 let require_in_bounds = false;
241 let shapes =
242 ExtraShapes::load_geojson_no_clipping(path.clone(), bounds, require_in_bounds)
243 .unwrap();
244 abstio::write_binary(
245 path.replace(".geojson", ".bin").replace(".json", ".bin"),
246 &shapes,
247 );
248 shapes
249 } else if path.ends_with(".bin") {
250 abstio::read_binary::<ExtraShapes>(path.to_string(), timer)
251 } else {
252 ExtraShapes { shapes: Vec::new() }
253 }
254 } else {
255 ExtraShapes { shapes: Vec::new() }
256 };
257 let boundary = map.get_boundary_polygon();
258 let dataset_name = path
259 .as_ref()
260 .map(abstutil::basename)
261 .unwrap_or_else(|| "no file".to_string());
262 let bldg_lookup: HashMap<String, BuildingID> = map
263 .all_buildings()
264 .iter()
265 .map(|b| (b.orig_id.inner_id().to_string(), b.id))
266 .collect();
267 let cs = &app.cs;
268
269 let pairs: Vec<(Object, ExtraShape)> = timer
270 .parallelize(
271 "convert shapes",
272 raw_shapes.shapes.into_iter().enumerate().collect(),
273 |(idx, shape)| {
274 let pts = bounds.convert(&shape.points);
275 if pts.iter().any(|pt| boundary.contains_pt(*pt)) {
276 Some((
277 make_object(
278 cs,
279 &bldg_lookup,
280 shape.attributes.clone(),
281 pts,
282 &dataset_name,
283 idx,
284 ),
285 shape,
286 ))
287 } else {
288 None
289 }
290 },
291 )
292 .into_iter()
293 .flatten()
294 .collect();
295 let mut objects = Vec::new();
296 let mut clipped_shapes = Vec::new();
297 for (obj, shape) in pairs {
298 objects.push(obj);
299 clipped_shapes.push(shape);
300 }
301 if path.is_some() && dump_clipped_shapes {
302 abstio::write_binary(
303 format!("{}_clipped_for_{}.bin", dataset_name, map.get_name().map),
304 &clipped_shapes,
305 );
306 }
307
308 (dataset_name, objects)
309}
310
311fn make_object(
312 cs: &ColorScheme,
313 bldg_lookup: &HashMap<String, BuildingID>,
314 attribs: BTreeMap<String, String>,
315 pts: Vec<Pt2D>,
316 dataset_name: &str,
317 obj_idx: usize,
318) -> Object {
319 let mut color = Color::RED.alpha(0.8);
320 let polygon = if pts.len() == 1 {
321 Circle::new(pts[0], RADIUS).to_polygon()
322 } else if let Ok(ring) = Ring::new(pts.clone()) {
323 color = cs.rotating_color_plot(obj_idx).alpha(0.8);
327 ring.into_polygon()
328 } else {
329 let backup = pts[0];
330 match PolyLine::new(pts) {
331 Ok(pl) => pl.make_arrow(THICKNESS, ArrowCap::Triangle),
332 Err(err) => {
333 println!(
334 "Object with attribs {:?} has messed up geometry: {}",
335 attribs, err
336 );
337 Circle::new(backup, RADIUS).to_polygon()
338 }
339 }
340 };
341
342 let mut osm_bldg = None;
343 if dataset_name == "parcels" {
344 if let Some(bldg) = attribs.get("osm_bldg") {
345 if let Some(id) = bldg_lookup.get(bldg) {
346 osm_bldg = Some(*id);
347 }
348 }
349 }
350
351 Object {
352 polygon,
353 color,
354 attribs,
355 osm_bldg,
356 }
357}
358
359fn make_query(app: &App, objects: &[Object], query: &str) -> (GeomBatch, usize) {
360 let mut batch = GeomBatch::new();
361 let mut cnt = 0;
362 let color = Color::BLUE.alpha(0.8);
363 match query {
364 "None" => {}
365 "parcels without buildings" => {
366 for obj in objects {
367 if obj.osm_bldg.is_none() {
368 cnt += 1;
369 batch.push(color, obj.polygon.clone());
370 }
371 }
372 }
373 "parcels without buildings and trips or parking" => {
374 for obj in objects {
375 if obj.osm_bldg.is_none()
376 && (obj.attribs.contains_key("households")
377 || obj.attribs.contains_key("parking"))
378 {
379 cnt += 1;
380 batch.push(color, obj.polygon.clone());
381 }
382 }
383 }
384 "parcels with multiple buildings" => {
385 let mut seen = HashSet::new();
386 for obj in objects {
387 if let Some(b) = obj.osm_bldg {
388 if seen.contains(&b) {
389 cnt += 1;
390 batch.push(color, app.primary.map.get_b(b).polygon.clone());
391 } else {
392 seen.insert(b);
393 }
394 }
395 }
396 }
397 "parcels with >1 households" => {
398 for obj in objects {
399 if let Some(hh) = obj.attribs.get("households") {
400 if hh != "1" {
401 cnt += 1;
402 batch.push(color, obj.polygon.clone());
403 }
404 }
405 }
406 }
407 "parcels with parking" => {
408 for obj in objects {
409 if obj.attribs.contains_key("parking") {
410 cnt += 1;
411 batch.push(color, obj.polygon.clone());
412 }
413 }
414 }
415 x => {
416 for obj in objects {
417 for (k, v) in &obj.attribs {
418 if format!("{}={}", k, v).contains(x) {
419 batch.push(color, obj.polygon.clone());
420 break;
421 }
422 }
423 }
424 }
425 }
426 (batch, cnt)
427}
428
429fn pick_file(ctx: &mut EventCtx, app: &App) -> Transition {
430 Transition::Push(FilePicker::new_state(
431 ctx,
432 Some(app.primary.map.get_city_name().input_path("")),
433 Box::new(|ctx, app, maybe_file| {
434 if let Ok(Some(file)) = maybe_file {
435 Transition::Multi(vec![
436 Transition::Pop,
437 Transition::Replace(ViewKML::new_state(ctx, app, Some(file))),
438 ])
439 } else {
440 Transition::Pop
441 }
442 }),
443 ))
444}