ltn/render/cells.rs
1use std::collections::{HashSet, VecDeque};
2
3use geom::{Bounds, Distance, Polygon};
4use map_gui::tools::Grid;
5use map_model::Map;
6use widgetry::{Color, GeomBatch};
7
8use crate::render::colors;
9use crate::Neighbourhood;
10
11const RESOLUTION_M: f64 = 10.0;
12
13pub struct RenderCells {
14 /// Rarely, this might be empty if the area is very small
15 pub polygons_per_cell: Vec<Vec<Polygon>>,
16 /// Colors per cell, such that adjacent cells are colored differently
17 pub colors: Vec<Color>,
18
19 boundary_polygon: Polygon,
20}
21
22struct RenderCellsBuilder {
23 /// The grid only covers the boundary polygon of the neighbourhood. The values are cell indices,
24 /// and `Some(num_cells)` marks the boundary of the neighbourhood.
25 grid: Grid<Option<usize>>,
26 colors: Vec<Color>,
27 /// Bounds of the neighbourhood boundary polygon
28 bounds: Bounds,
29
30 boundary_polygon: Polygon,
31}
32
33impl RenderCells {
34 /// Partition a neighbourhood's boundary polygon based on the cells. This discretizes space into
35 /// a grid, and then extracts a polygon from the raster. The results don't look perfect, but
36 /// it's fast.
37 pub fn new(map: &Map, neighbourhood: &Neighbourhood) -> RenderCells {
38 RenderCellsBuilder::new(map, neighbourhood).finalize()
39 }
40
41 /// Draw cells as areas with different colors. The colors are meaningless, but the same color
42 /// won't be shared between adjacent cells.
43 pub fn draw_colored_areas(&self) -> GeomBatch {
44 let mut batch = GeomBatch::new();
45 for (color, polygons) in self.colors.iter().zip(self.polygons_per_cell.iter()) {
46 for poly in polygons {
47 batch.push(*color, poly.clone());
48 }
49 }
50 batch
51 }
52
53 /// Draw the boundary between cells as a thick outline. It's meant to look like the
54 /// neighbourhood is split into disconnected islands.
55 pub fn draw_island_outlines(&self) -> GeomBatch {
56 let neighbourhood_boundary = self
57 .boundary_polygon
58 .get_outer_ring()
59 .to_outline(Distance::meters(25.0));
60
61 let mut batch = GeomBatch::new();
62 for (cell_color, polygons) in self.colors.iter().zip(self.polygons_per_cell.iter()) {
63 for poly in polygons {
64 // If the cell is disconnected, keep drawing this as an area to point out the
65 // problem
66 if *cell_color == colors::DISCONNECTED_CELL {
67 batch.push(*cell_color, poly.clone());
68 continue;
69 }
70
71 let boundary = poly.get_outer_ring().to_outline(Distance::meters(5.0));
72
73 let color = cell_color.alpha(1.0).shade(0.2);
74 // If possible, try to erase where the cell boundary touches the perimeter road.
75 if let Ok(list) = boundary.difference(&neighbourhood_boundary) {
76 batch.extend(color, list);
77 } else {
78 batch.push(color, boundary);
79 }
80 }
81 }
82 batch
83 }
84
85 /// Per cell, convert all polygons to a `geo::MultiPolygon`. Leave the coordinate system as map-space.
86 pub fn to_multipolygons(&self) -> Vec<geo::MultiPolygon> {
87 self.polygons_per_cell
88 .clone()
89 .into_iter()
90 .map(Polygon::union_all_into_multipolygon)
91 .collect()
92 }
93}
94
95impl RenderCellsBuilder {
96 fn new(map: &Map, neighbourhood: &Neighbourhood) -> RenderCellsBuilder {
97 let boundary_polygon = neighbourhood.boundary_polygon.clone();
98 // Make a 2D grid covering the polygon. Each tile in the grid contains a cell index, which
99 // will become a color by the end. None means no cell is assigned yet.
100 let bounds = boundary_polygon.get_bounds();
101 let mut grid: Grid<Option<usize>> = Grid::new(
102 (bounds.width() / RESOLUTION_M).ceil() as usize,
103 (bounds.height() / RESOLUTION_M).ceil() as usize,
104 None,
105 );
106
107 // Initially fill out the grid based on the roads in each cell
108 let mut warn_leak = true;
109 for (cell_idx, cell) in neighbourhood.cells.iter().enumerate() {
110 for (r, interval) in &cell.roads {
111 let road = map.get_r(*r);
112 // Some roads with a filter are _very_ short, and this fails. The connecting roads
113 // on either side should contribute a grid cell and wind up fine.
114 if let Ok(slice) = road
115 .center_pts
116 .maybe_exact_slice(interval.start, interval.end)
117 {
118 // Walk along the center line. We could look at the road's thickness and fill
119 // out points based on that, but the diffusion should take care of it.
120 for (pt, _) in
121 slice.step_along(Distance::meters(RESOLUTION_M / 2.0), Distance::ZERO)
122 {
123 let grid_idx = grid.idx(
124 ((pt.x() - bounds.min_x) / RESOLUTION_M) as usize,
125 ((pt.y() - bounds.min_y) / RESOLUTION_M) as usize,
126 );
127 // Due to tunnels/bridges, sometimes a road belongs to a neighbourhood, but
128 // leaks outside the neighbourhood's boundary. Avoid crashing. The real fix
129 // is to better define boundaries in the face of z-order changes.
130 //
131 // Example is https://www.openstreetmap.org/way/87298633
132 if grid_idx >= grid.data.len() {
133 if warn_leak {
134 warn!(
135 "{} leaks outside its neighbourhood's boundary polygon, near {}",
136 road.id, pt
137 );
138 // In some neighbourhoods, there are so many warnings that logging
139 // causes noticeable slowdown!
140 warn_leak = false;
141 }
142 continue;
143 }
144
145 // If roads from two different cells are close enough to clobber
146 // originally, oh well?
147 grid.data[grid_idx] = Some(cell_idx);
148 }
149 }
150 }
151 }
152 // Also mark the boundary polygon, so we can prevent the diffusion from "leaking" outside
153 // the area. The grid covers the rectangular bounds of the polygon. Rather than make an
154 // enum with 3 cases, just assign a new index to mean "boundary."
155 let boundary_marker = neighbourhood.cells.len();
156 for (pt, _) in
157 geom::PolyLine::unchecked_new(boundary_polygon.get_outer_ring().clone().into_points())
158 .step_along(Distance::meters(RESOLUTION_M / 2.0), Distance::ZERO)
159 {
160 // TODO Refactor helpers to transform between map-space and the grid tiles. Possibly
161 // Grid should know about this.
162 let grid_idx = grid.idx(
163 ((pt.x() - bounds.min_x) / RESOLUTION_M) as usize,
164 ((pt.y() - bounds.min_y) / RESOLUTION_M) as usize,
165 );
166 grid.data[grid_idx] = Some(boundary_marker);
167 }
168
169 let adjacencies = diffusion(&mut grid, boundary_marker);
170 let mut cell_colors = color_cells(neighbourhood.cells.len(), adjacencies);
171
172 // Color some special cells
173 for (idx, cell) in neighbourhood.cells.iter().enumerate() {
174 if cell.is_disconnected() {
175 cell_colors[idx] = colors::DISCONNECTED_CELL;
176 }
177 }
178
179 RenderCellsBuilder {
180 grid,
181 colors: cell_colors,
182 bounds,
183
184 boundary_polygon,
185 }
186 }
187
188 fn finalize(self) -> RenderCells {
189 let mut result = RenderCells {
190 polygons_per_cell: Vec::new(),
191 colors: Vec::new(),
192 boundary_polygon: self.boundary_polygon,
193 };
194
195 for (idx, color) in self.colors.into_iter().enumerate() {
196 // contour will find where the grid is >= a threshold value. The main grid has one
197 // number per cell, so we can't directly use it -- the area >= some cell index is
198 // meaningless. Per cell, make a new grid that just has that cell.
199 let grid: Grid<f64> = Grid {
200 width: self.grid.width,
201 height: self.grid.height,
202 data: self
203 .grid
204 .data
205 .iter()
206 .map(
207 |maybe_cell| {
208 if maybe_cell == &Some(idx) {
209 1.0
210 } else {
211 0.0
212 }
213 },
214 )
215 .collect(),
216 };
217
218 let smooth = false;
219 let contour_builder =
220 contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
221 let thresholds = vec![1.0];
222
223 let mut cell_polygons = Vec::new();
224 for contour in contour_builder.contours(&grid.data, &thresholds).unwrap() {
225 let (polygons, _) = contour.into_inner();
226 for p in polygons {
227 if let Ok(poly) = Polygon::try_from(p) {
228 cell_polygons.push(
229 poly.must_scale(RESOLUTION_M)
230 .translate(self.bounds.min_x, self.bounds.min_y),
231 );
232 }
233 }
234 }
235
236 // Sometimes one cell "leaks" out of the neighbourhood boundary. Not sure why. But we
237 // can just clip the result.
238 let mut clipped = Vec::new();
239 for p in cell_polygons {
240 // If clipping fails, just use the original polygon.
241 if let Ok(list) = p.intersection(&result.boundary_polygon) {
242 clipped.extend(list);
243 } else {
244 clipped.push(p);
245 }
246 }
247
248 result.polygons_per_cell.push(clipped);
249 result.colors.push(color);
250 }
251
252 result
253 }
254}
255
256/// Returns a set of adjacent indices. The pairs are symmetric -- (x, y) and (y, x) will both be
257/// populated. Adjacency with boundary_marker doesn't count.
258fn diffusion(grid: &mut Grid<Option<usize>>, boundary_marker: usize) -> HashSet<(usize, usize)> {
259 // Grid indices to propagate
260 let mut queue: VecDeque<usize> = VecDeque::new();
261
262 // Initially seed the queue with all colored tiles
263 for (idx, value) in grid.data.iter().enumerate() {
264 if let Some(x) = value {
265 // Don't expand the boundary tiles
266 if *x != boundary_marker {
267 queue.push_back(idx);
268 }
269 }
270 }
271
272 let mut adjacencies = HashSet::new();
273
274 while !queue.is_empty() {
275 let current_idx = queue.pop_front().unwrap();
276 let current_color = grid.data[current_idx].unwrap();
277 let (current_x, current_y) = grid.xy(current_idx);
278 // Don't flood to diagonal neighbors. That would usually result in "leaking" out past the
279 // boundary tiles when the boundary polygon isn't axis-aligned.
280 // TODO But this still does "leak" out sometimes -- the cell covering 22nd/Lynn, for
281 // example.
282 for (next_x, next_y) in grid.orthogonal_neighbors(current_x, current_y) {
283 let next_idx = grid.idx(next_x, next_y);
284 if let Some(prev_color) = grid.data[next_idx] {
285 // If the color doesn't match our current_color, we've found the border between two
286 // cells.
287 if current_color != prev_color
288 && current_color != boundary_marker
289 && prev_color != boundary_marker
290 {
291 adjacencies.insert((current_color, prev_color));
292 adjacencies.insert((prev_color, current_color));
293 }
294 // If a color has been assigned, don't flood any further.
295 } else {
296 grid.data[next_idx] = Some(current_color);
297 queue.push_back(next_idx);
298 }
299 }
300 }
301
302 adjacencies
303}
304
305fn color_cells(num_cells: usize, adjacencies: HashSet<(usize, usize)>) -> Vec<Color> {
306 // This is the same greedy logic as Perimeter::calculate_coloring
307 let mut assigned_colors = Vec::new();
308 for this_idx in 0..num_cells {
309 let mut available_colors: Vec<bool> =
310 std::iter::repeat(true).take(colors::CELLS.len()).collect();
311 // Find all neighbors
312 for other_idx in 0..num_cells {
313 if adjacencies.contains(&(this_idx, other_idx)) {
314 // We assign colors in order, so any neighbor index smaller than us has been
315 // chosen
316 if other_idx < this_idx {
317 available_colors[assigned_colors[other_idx]] = false;
318 }
319 }
320 }
321
322 // If there are multiple colors available, prefer one that hasn't been used anywhere yet.
323 // Cells far apart shouldn't seem related to the user.
324 let mut choice = None;
325 let mut backup = None;
326 for (idx, available) in available_colors.into_iter().enumerate() {
327 if !available {
328 continue;
329 }
330 if assigned_colors.iter().any(|x| *x == idx) {
331 if backup.is_none() {
332 backup = Some(idx);
333 }
334 } else {
335 choice = Some(idx);
336 break;
337 }
338 }
339 assigned_colors.push(
340 choice
341 .or(backup)
342 .unwrap_or_else(|| assigned_colors.len() % colors::CELLS.len()),
343 );
344 }
345 assigned_colors
346 .into_iter()
347 .map(|idx| colors::CELLS[idx].alpha(0.8))
348 .collect()
349}