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 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 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 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
91pub fn make_heatmap(
93 ctx: &mut EventCtx,
94 batch: &mut GeomBatch,
95 bounds: &Bounds,
96 pts: Vec<Pt2D>,
97 opts: &HeatmapOptions,
98) -> Widget {
99 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 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 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 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 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 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 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 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
247pub struct Grid<T> {
249 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 pub fn idx(&self, x: usize, y: usize) -> usize {
266 y * self.width + x
267 }
268
269 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 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
293pub fn draw_isochrone(
296 map: &Map,
297 time_to_reach_building: &HashMap<BuildingID, Duration>,
298 thresholds: &[f64],
299 colors: &[Color],
300) -> GeomBatch {
301 let bounds = map.get_bounds();
304 let resolution_m = 100.0;
305 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 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 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 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}