1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::app::App;
use crate::common::heatmap::Grid;
use crate::game::{State, Transition};
use ezgui::{
    hotkey, Btn, Color, Composite, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key,
    Line, Outcome, VerticalAlignment, Widget,
};
use geom::{Distance, Polygon};
use map_model::{connectivity, BuildingID};

// TODO Move cursor live
pub struct IsochroneViewer {
    composite: Composite,
    draw: Drawable,
}

impl IsochroneViewer {
    pub fn new(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Box<dyn State> {
        let draw = make_isochrone(ctx, app, start);
        Box::new(IsochroneViewer {
            composite: Composite::new(Widget::col(vec![
                Widget::row(vec![
                    Line("Isochrone").small_heading().draw(ctx),
                    Btn::text_fg("X")
                        .build(ctx, "close", hotkey(Key::Escape))
                        .align_right(),
                ]),
                // TODO legend, mode picker
            ]))
            .aligned(HorizontalAlignment::Center, VerticalAlignment::Top)
            .build(ctx),
            draw,
        })
    }
}

impl State for IsochroneViewer {
    fn event(&mut self, ctx: &mut EventCtx, _: &mut App) -> Transition {
        ctx.canvas_movement();

        match self.composite.event(ctx) {
            Outcome::Clicked(x) => match x.as_ref() {
                "close" => {
                    return Transition::Pop;
                }
                _ => unreachable!(),
            },
            _ => {}
        }

        Transition::Keep
    }

    fn draw(&self, g: &mut GfxCtx, _: &App) {
        g.redraw(&self.draw);
        self.composite.draw(g);
    }
}

fn make_isochrone(ctx: &mut EventCtx, app: &App, start: BuildingID) -> Drawable {
    let bounds = app.primary.map.get_bounds();
    let resolution_m = 100.0;
    // Distance in meters
    let mut grid: Grid<f64> = Grid::new(
        (bounds.width() / resolution_m).ceil() as usize,
        (bounds.height() / resolution_m).ceil() as usize,
        0.0,
    );

    for (b, cost) in connectivity::all_costs_from(&app.primary.map, start) {
        let pt = app.primary.map.get_b(b).polygon.center();
        let idx = grid.idx(
            ((pt.x() - bounds.min_x) / resolution_m) as usize,
            ((pt.y() - bounds.min_y) / resolution_m) as usize,
        );
        // Don't add! If two buildings map to the same cell, should pick a finer resolution.
        grid.data[idx] = cost.inner_meters();
    }

    let thresholds = vec![
        0.1,
        Distance::miles(0.5).inner_meters(),
        Distance::miles(3.0).inner_meters(),
        Distance::miles(6.0).inner_meters(),
    ];
    let colors = vec![
        Color::BLACK.alpha(0.5),
        Color::GREEN.alpha(0.5),
        Color::BLUE.alpha(0.5),
        Color::RED.alpha(0.5),
    ];
    let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, false);
    let mut batch = GeomBatch::new();
    for (feature, color) in c
        .contours(&grid.data, &thresholds)
        .unwrap()
        .into_iter()
        .zip(colors)
    {
        match feature.geometry.unwrap().value {
            geojson::Value::MultiPolygon(polygons) => {
                for p in polygons {
                    batch.push(color, Polygon::from_geojson(&p).scale(resolution_m));
                }
            }
            _ => unreachable!(),
        }
    }

    batch.upload(ctx)
}