map_gui/tools/
heatmap.rs

1use std::collections::HashMap;
2
3use geom::{Bounds, Duration, Histogram, Polygon, Pt2D, Statistic};
4use map_model::{BuildingID, Map};
5use widgetry::tools::{ColorLegend, ColorScale};
6use widgetry::{
7    Choice, Color, EventCtx, GeomBatch, Panel, RoundedF64, Spinner, TextExt, Toggle, Widget,
8};
9
10const NEIGHBORS: [[isize; 2]; 9] = [
11    [0, 0],
12    [-1, 1],
13    [-1, 0],
14    [-1, -1],
15    [0, -1],
16    [1, -1],
17    [1, 0],
18    [1, 1],
19    [0, 1],
20];
21
22#[derive(Clone, PartialEq)]
23pub struct HeatmapOptions {
24    // In meters
25    resolution: f64,
26    radius: f64,
27    smoothing: bool,
28    contours: bool,
29    color_scheme: String,
30}
31
32impl HeatmapOptions {
33    pub fn new() -> HeatmapOptions {
34        HeatmapOptions {
35            resolution: 10.0,
36            radius: 3.0,
37            smoothing: true,
38            contours: true,
39            color_scheme: "Turbo".to_string(),
40        }
41    }
42
43    pub fn to_controls(&self, ctx: &mut EventCtx, legend: Widget) -> Vec<Widget> {
44        vec![
45            // TODO Display the value...
46            Widget::row(vec![
47                "Resolution (meters)".text_widget(ctx).centered_vert(),
48                Spinner::f64_widget(ctx, "resolution", (1.0, 100.0), self.resolution, 1.0)
49                    .align_right(),
50            ]),
51            Widget::row(vec![
52                "Radius (resolution multiplier)"
53                    .text_widget(ctx)
54                    .centered_vert(),
55                Spinner::f64_widget(ctx, "radius", (0.0, 10.0), self.radius, 1.0).align_right(),
56            ]),
57            Toggle::switch(ctx, "smoothing", None, self.smoothing),
58            Toggle::switch(ctx, "contours", None, self.contours),
59            Widget::row(vec![
60                "Color scheme".text_widget(ctx).centered_vert(),
61                Widget::dropdown(
62                    ctx,
63                    "Color scheme",
64                    self.color_scheme.clone(),
65                    vec!["Turbo", "Inferno", "Warm", "Cool", "Oranges", "Spectral"]
66                        .into_iter()
67                        .map(Choice::string)
68                        .collect(),
69                ),
70            ]),
71            legend,
72        ]
73    }
74
75    pub fn from_controls(c: &Panel) -> HeatmapOptions {
76        // Did we just change?
77        if c.has_widget("resolution") {
78            HeatmapOptions {
79                resolution: c.spinner::<RoundedF64>("resolution").0,
80                radius: c.spinner::<RoundedF64>("radius").0,
81                smoothing: c.is_checked("smoothing"),
82                contours: c.is_checked("contours"),
83                color_scheme: c.dropdown_value("Color scheme"),
84            }
85        } else {
86            HeatmapOptions::new()
87        }
88    }
89}
90
91// Returns a legend
92pub fn make_heatmap(
93    ctx: &mut EventCtx,
94    batch: &mut GeomBatch,
95    bounds: &Bounds,
96    pts: Vec<Pt2D>,
97    opts: &HeatmapOptions,
98) -> Widget {
99    // 7 colors, 8 labels
100    let num_colors = 7;
101    let gradient = match opts.color_scheme.as_ref() {
102        "Turbo" => colorous::TURBO,
103        "Inferno" => colorous::INFERNO,
104        "Warm" => colorous::WARM,
105        "Cool" => colorous::COOL,
106        "Oranges" => colorous::ORANGES,
107        "Spectral" => colorous::SPECTRAL,
108        _ => unreachable!(),
109    };
110    let colors: Vec<Color> = (0..num_colors)
111        .map(|i| {
112            let c = gradient.eval_rational(i, num_colors);
113            Color::rgb(c.r as usize, c.g as usize, c.b as usize)
114        })
115        .collect();
116
117    if pts.is_empty() {
118        let labels = std::iter::repeat("0".to_string())
119            .take(num_colors + 1)
120            .collect();
121        return ColorLegend::gradient(ctx, &ColorScale(colors), labels);
122    }
123
124    // At each point, add a 2D Gaussian kernel centered at the point.
125    let mut raw_grid: Grid<f64> = Grid::new(
126        (bounds.width() / opts.resolution).ceil() as usize,
127        (bounds.height() / opts.resolution).ceil() as usize,
128        0.0,
129    );
130    for pt in pts {
131        let base_x = ((pt.x() - bounds.min_x) / opts.resolution) as isize;
132        let base_y = ((pt.y() - bounds.min_y) / opts.resolution) as isize;
133        let denom = 2.0 * (opts.radius / 2.0).powi(2);
134
135        let r = opts.radius as isize;
136        for x in base_x - r..=base_x + r {
137            for y in base_y - r..=base_y + r {
138                let loc_r2 = (x - base_x).pow(2) + (y - base_y).pow(2);
139                if x > 0
140                    && y > 0
141                    && x < (raw_grid.width as isize)
142                    && y < (raw_grid.height as isize)
143                    && loc_r2 <= r * r
144                {
145                    // https://en.wikipedia.org/wiki/Gaussian_function#Two-dimensional_Gaussian_function
146                    let value = (-(((x - base_x) as f64).powi(2) / denom
147                        + ((y - base_y) as f64).powi(2) / denom))
148                        .exp();
149                    let idx = raw_grid.idx(x as usize, y as usize);
150                    raw_grid.data[idx] += value;
151                }
152            }
153        }
154    }
155
156    let mut grid: Grid<f64> = Grid::new(
157        (bounds.width() / opts.resolution).ceil() as usize,
158        (bounds.height() / opts.resolution).ceil() as usize,
159        0.0,
160    );
161    if opts.smoothing {
162        for y in 0..raw_grid.height {
163            for x in 0..raw_grid.width {
164                let mut div = 1;
165                let idx = grid.idx(x, y);
166                grid.data[idx] = raw_grid.data[idx];
167                for offset in &NEIGHBORS {
168                    let next_x = x as isize + offset[0];
169                    let next_y = y as isize + offset[1];
170                    if next_x > 0
171                        && next_y > 0
172                        && next_x < (raw_grid.width as isize)
173                        && next_y < (raw_grid.height as isize)
174                    {
175                        div += 1;
176                        let next_idx = grid.idx(next_x as usize, next_y as usize);
177                        grid.data[idx] += raw_grid.data[next_idx];
178                    }
179                }
180                grid.data[idx] /= div as f64;
181            }
182        }
183    } else {
184        grid = raw_grid;
185    }
186
187    let mut distrib = Histogram::new();
188    for count in &grid.data {
189        // TODO Just truncate the decimal?
190        distrib.add(*count as usize);
191    }
192
193    if opts.contours {
194        let max = distrib.select(Statistic::Max).unwrap() as f64;
195        let mut thresholds: Vec<f64> = (0..=5).map(|i| (i as f64) / 5.0 * max).collect();
196        // Skip 0; it'll cover the entire map. But have a low value to distinguish
197        // nothing/something.
198        thresholds[0] = 0.1;
199        let contour_builder =
200            contour::ContourBuilder::new(grid.width as u32, grid.height as u32, false);
201        for contour in contour_builder.contours(&grid.data, &thresholds).unwrap() {
202            let (geometry, threshold) = contour.into_inner();
203
204            let c = gradient.eval_continuous(threshold / max);
205            // Don't block the map underneath
206            let color = Color::rgb(c.r as usize, c.g as usize, c.b as usize).alpha(0.6);
207
208            for geo_poly in geometry {
209                if let Ok(poly) = Polygon::try_from(geo_poly) {
210                    batch.push(color, poly.must_scale(opts.resolution));
211                }
212            }
213        }
214    } else {
215        // Now draw rectangles
216        let square = Polygon::rectangle(opts.resolution, opts.resolution);
217        for y in 0..grid.height {
218            for x in 0..grid.width {
219                let count = grid.data[grid.idx(x, y)];
220                if count > 0.0 {
221                    let pct = (count as f64) / (distrib.select(Statistic::Max).unwrap() as f64);
222                    let c = gradient.eval_continuous(pct);
223                    // Don't block the map underneath
224                    let color = Color::rgb(c.r as usize, c.g as usize, c.b as usize).alpha(0.6);
225                    batch.push(
226                        color,
227                        square
228                            .translate((x as f64) * opts.resolution, (y as f64) * opts.resolution),
229                    );
230                }
231            }
232        }
233    }
234
235    let mut labels = vec!["0".to_string()];
236    for i in 1..=num_colors {
237        let pct = (i as f64) / (num_colors as f64);
238        labels.push(
239            (pct * (distrib.select(Statistic::Max).unwrap() as f64))
240                .round()
241                .to_string(),
242        );
243    }
244    ColorLegend::gradient(ctx, &ColorScale(colors), labels)
245}
246
247/// A 2D grid containing some arbitrary data.
248pub struct Grid<T> {
249    /// Logically represents a 2D vector. Row-major ordering.
250    pub data: Vec<T>,
251    pub width: usize,
252    pub height: usize,
253}
254
255impl<T: Copy> Grid<T> {
256    pub fn new(width: usize, height: usize, default: T) -> Grid<T> {
257        Grid {
258            data: std::iter::repeat(default).take(width * height).collect(),
259            width,
260            height,
261        }
262    }
263
264    /// Calculate the index from a given (x, y). Doesn't do any bounds checking.
265    pub fn idx(&self, x: usize, y: usize) -> usize {
266        y * self.width + x
267    }
268
269    /// The inverse of `idx`. No bounds checking.
270    pub fn xy(&self, idx: usize) -> (usize, usize) {
271        let y = idx / self.width;
272        let x = idx % self.width;
273        (x, y)
274    }
275
276    /// From one tile, calculate the 4 orthogonal neighbors. Includes bounds checking.
277    pub fn orthogonal_neighbors(&self, center_x: usize, center_y: usize) -> Vec<(usize, usize)> {
278        let center_x = center_x as isize;
279        let center_y = center_y as isize;
280        let mut results = Vec::new();
281        for (dx, dy) in [(-1, 0), (0, -1), (0, 1), (1, 0)] {
282            let x = center_x + dx;
283            let y = center_y + dy;
284            if x < 0 || (x as usize) >= self.width || y < 0 || (y as usize) >= self.height {
285                continue;
286            }
287            results.push((x as usize, y as usize));
288        }
289        results
290    }
291}
292
293// TODO Refactor the variations of this.
294/// Thresholds are Durations, in units of seconds
295pub fn draw_isochrone(
296    map: &Map,
297    time_to_reach_building: &HashMap<BuildingID, Duration>,
298    thresholds: &[f64],
299    colors: &[Color],
300) -> GeomBatch {
301    // To generate the polygons covering areas between 0-5 mins, 5-10 mins, etc, we have to feed
302    // in a 2D grid of costs. Use a 100x100 meter resolution.
303    let bounds = map.get_bounds();
304    let resolution_m = 100.0;
305    // The costs we're storing are currently durations, but the contour crate needs f64, so
306    // just store the number of seconds.
307    let mut grid: Grid<f64> = Grid::new(
308        (bounds.width() / resolution_m).ceil() as usize,
309        (bounds.height() / resolution_m).ceil() as usize,
310        0.0,
311    );
312
313    for (b, cost) in time_to_reach_building {
314        // What grid cell does the building belong to?
315        let pt = map.get_b(*b).polygon.center();
316        let idx = grid.idx(
317            ((pt.x() - bounds.min_x) / resolution_m) as usize,
318            ((pt.y() - bounds.min_y) / resolution_m) as usize,
319        );
320        // Don't add! If two buildings map to the same cell, we should pick a finer resolution.
321        grid.data[idx] = cost.inner_seconds();
322    }
323
324    let smooth = false;
325    let contour_builder =
326        contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
327    let mut batch = GeomBatch::new();
328    // The last feature returned will be larger than the last threshold value. We don't want to
329    // display that at all. zip() will omit this last pair, since colors.len() ==
330    // thresholds.len() - 1.
331    //
332    // TODO Actually, this still isn't working. I think each polygon is everything > the
333    // threshold, not everything between two thresholds?
334    for (contour, color) in contour_builder
335        .contours(&grid.data, thresholds)
336        .unwrap()
337        .into_iter()
338        .zip(colors)
339    {
340        let (polygons, _) = contour.into_inner();
341        for p in polygons {
342            if let Ok(poly) = Polygon::try_from(p) {
343                batch.push(*color, poly.must_scale(resolution_m));
344            }
345        }
346    }
347
348    batch
349}