game/devtools/
kml.rs

1// TODO Rename -- this is for KML, CSV, GeoJSON
2
3use 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            // Enable to write a smaller .bin only with the shapes matching the bounds.
48            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
211/// Loads and clips objects to the current map. Also returns the dataset name.
212fn 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    // TODO Use the bytes; don't re-read here. This is necessary to work on web.
222    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            // Assuming this is some huge file, conveniently convert the extract to .bin.
227            // The new file will show up as untracked in git, so it'll be obvious this
228            // happened.
229            // Except don't do this for Seattle collisions; that's separately transformed into a
230            // different binary format!
231            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        // TODO If the below isn't true, show it as an outline instead? Can't make that a Polygon,
324        // though
325        // if attribs.get("spatial_type") == Some(&"Polygon".to_string()) {
326        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}