Instead of drawing LTN cells as a grid, post-process the grid into a

simple polygon using contouring. This vastly speeds up the GeoJSON
export feature.
This commit is contained in:
Dustin Carlino 2022-01-22 16:42:13 +00:00
parent 2271924d2d
commit f7e8bca517
6 changed files with 97 additions and 74 deletions

1
Cargo.lock generated
View File

@ -2119,6 +2119,7 @@ dependencies = [
"abstio",
"abstutil",
"anyhow",
"contour",
"fs-err",
"geo",
"geojson",

View File

@ -15,6 +15,7 @@ wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"]
abstio = { path = "../abstio" }
abstutil = { path = "../abstutil" }
anyhow = "1.0.38"
contour = "0.4.0"
fs-err = "2.6.0"
geo = "0.18"
geojson = { version = "0.22.0", features = ["geo-types"] }

View File

@ -5,8 +5,8 @@ use geom::Distance;
use map_gui::tools::{CityPicker, DrawRoadLabels, Navigator, PopupMsg, URLManager};
use widgetry::mapspace::{ToggleZoomed, World, WorldOutcome};
use widgetry::{
Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Outcome, Panel, RewriteColor, State,
TextExt, Toggle, VerticalAlignment, Widget,
Choice, Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Outcome, Panel, State, TextExt,
Toggle, VerticalAlignment, Widget,
};
use super::{Neighborhood, NeighborhoodID, Partitioning};
@ -104,9 +104,7 @@ impl State<App> for BrowseNeighborhoods {
return Transition::Push(Navigator::new_state(ctx, app));
}
"Export to GeoJSON" => {
let result = ctx.loading_screen("export LTNs", |ctx, timer| {
super::export::write_geojson_file(ctx, app, timer)
});
let result = super::export::write_geojson_file(ctx, app);
return Transition::Push(match result {
Ok(path) => PopupMsg::new_state(
ctx,
@ -170,9 +168,7 @@ fn make_world(ctx: &mut EventCtx, app: &App, timer: &mut Timer) -> World<Neighbo
// tried greying out everything else, but then the view is too jumpy.
let neighborhood = Neighborhood::new(ctx, app, *id);
let render_cells = super::draw_cells::RenderCells::new(map, &neighborhood);
let hovered_batch = render_cells
.draw_grid()
.color(RewriteColor::ChangeAlpha(0.8));
let hovered_batch = render_cells.draw();
world
.add(*id)
.hitbox(block.polygon.clone())

View File

@ -150,7 +150,7 @@ fn make_world(ctx: &mut EventCtx, app: &App, neighborhood: &Neighborhood) -> Wor
let render_cells = super::draw_cells::RenderCells::new(map, neighborhood);
if app.session.draw_cells_as_areas {
world.draw_master_batch(ctx, render_cells.draw_grid());
world.draw_master_batch(ctx, render_cells.draw());
} else {
let mut draw = GeomBatch::new();
for (idx, cell) in neighborhood.cells.iter().enumerate() {

View File

@ -1,7 +1,6 @@
use std::collections::{HashSet, VecDeque};
use abstutil::Timer;
use geom::{Bounds, Distance, Polygon, Pt2D};
use geom::{Bounds, Distance, Polygon};
use map_gui::tools::Grid;
use map_model::Map;
use widgetry::{Color, GeomBatch};
@ -24,21 +23,52 @@ const DISCONNECTED_COLOR: Color = Color::RED;
const RESOLUTION_M: f64 = 10.0;
pub struct RenderCells {
polygons_per_cell: Vec<Vec<Polygon>>,
/// Colors per cell, such that adjacent cells are colored differently
pub colors: Vec<Color>,
}
struct RenderCellsBuilder {
/// The grid only covers the boundary polygon of the neighborhood. The values are cell indices,
/// and `Some(num_cells)` marks the boundary of the neighborhood.
grid: Grid<Option<usize>>,
/// Colors per cell, such that adjacent cells are colored differently
pub colors: Vec<Color>,
colors: Vec<Color>,
/// Bounds of the neighborhood boundary polygon
bounds: Bounds,
/// The number of cells, used as a sentinel value in the grid
boundary_marker: usize,
}
/// Partition a neighborhood's boundary polygon based on the cells. This discretizes
/// space into a grid, so the results don't look perfect, but it's fast.
impl RenderCells {
/// Partition a neighborhood's boundary polygon based on the cells. This discretizes space into
/// a grid, and then extracts a polygon from the raster. The results don't look perfect, but
/// it's fast.
pub fn new(map: &Map, neighborhood: &Neighborhood) -> RenderCells {
RenderCellsBuilder::new(map, neighborhood).finalize()
}
// TODO It'd look nicer to render the cells "underneath" the roads and intersections, at the
// layer where areas are shown now
pub fn draw(&self) -> GeomBatch {
let mut batch = GeomBatch::new();
for (color, polygons) in self.colors.iter().zip(self.polygons_per_cell.iter()) {
for poly in polygons {
batch.push(color.alpha(0.5), poly.clone());
}
}
batch
}
/// Per cell, convert all polygons to a `geo::MultiPolygon`. Leave the coordinate system as map-space.
pub fn to_multipolygons(&self) -> Vec<geo::MultiPolygon<f64>> {
self.polygons_per_cell
.clone()
.into_iter()
.map(Polygon::union_all_into_multipolygon)
.collect()
}
}
impl RenderCellsBuilder {
fn new(map: &Map, neighborhood: &Neighborhood) -> RenderCellsBuilder {
let boundary_polygon = neighborhood
.orig_perimeter
.clone()
@ -116,71 +146,67 @@ impl RenderCells {
}
}
RenderCells {
RenderCellsBuilder {
grid,
colors: cell_colors,
bounds,
boundary_marker,
}
}
/// Just draw rectangles based on the grid
pub fn draw_grid(&self) -> GeomBatch {
// TODO We should be able to generate actual polygons per cell using the contours crate
// TODO Also it'd look nicer to render this "underneath" the roads and intersections, at the
// layer where areas are shown now
let mut batch = GeomBatch::new();
for (idx, value) in self.grid.data.iter().enumerate() {
if let Some(cell_idx) = value {
if *cell_idx == self.boundary_marker {
continue;
}
let (x, y) = self.grid.xy(idx);
let tile_center = Pt2D::new(
self.bounds.min_x + RESOLUTION_M * (x as f64 + 0.5),
self.bounds.min_y + RESOLUTION_M * (y as f64 + 0.5),
);
batch.push(
self.colors[*cell_idx].alpha(0.5),
Polygon::rectangle_centered(
tile_center,
Distance::meters(RESOLUTION_M),
Distance::meters(RESOLUTION_M),
),
);
}
}
batch
}
fn finalize(self) -> RenderCells {
let mut result = RenderCells {
polygons_per_cell: Vec::new(),
colors: Vec::new(),
};
/// Per cell, glue together all of the rectangles into a single multipolygon
pub fn to_multipolygons(&self, timer: &mut Timer) -> Vec<geo::MultiPolygon<f64>> {
let mut polygons_per_cell: Vec<Vec<Polygon>> = std::iter::repeat_with(Vec::new)
.take(self.boundary_marker)
.collect();
for (idx, value) in self.grid.data.iter().enumerate() {
if let Some(cell_idx) = value {
if *cell_idx == self.boundary_marker {
continue;
for (idx, color) in self.colors.into_iter().enumerate() {
// contour will find where the grid is >= a threshold value. The main grid has one
// number per cell, so we can't directly use it -- the area >= some cell index is
// meaningless. Per cell, make a new grid that just has that cell.
let grid: Grid<f64> = Grid {
width: self.grid.width,
height: self.grid.height,
data: self
.grid
.data
.iter()
.map(
|maybe_cell| {
if maybe_cell == &Some(idx) {
1.0
} else {
0.0
}
},
)
.collect(),
};
let smooth = false;
let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
let thresholds = vec![1.0];
let mut cell_polygons = Vec::new();
for feature in c.contours(&grid.data, &thresholds).unwrap() {
match feature.geometry.unwrap().value {
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(poly) = Polygon::from_geojson(&p) {
cell_polygons.push(
poly.scale(RESOLUTION_M)
.translate(self.bounds.min_x, self.bounds.min_y),
);
}
}
}
_ => unreachable!(),
}
let (x, y) = self.grid.xy(idx);
let tile_center = Pt2D::new(
self.bounds.min_x + RESOLUTION_M * (x as f64 + 0.5),
self.bounds.min_y + RESOLUTION_M * (y as f64 + 0.5),
);
polygons_per_cell[*cell_idx].push(Polygon::rectangle_centered(
tile_center,
Distance::meters(RESOLUTION_M),
Distance::meters(RESOLUTION_M),
));
}
result.polygons_per_cell.push(cell_polygons);
result.colors.push(color);
}
timer.parallelize(
"Unioning polygons for one cell",
polygons_per_cell,
Polygon::union_all_into_multipolygon,
)
result
}
}

View File

@ -1,13 +1,12 @@
use anyhow::Result;
use abstutil::Timer;
use geom::{PolyLine, Pt2D};
use widgetry::EventCtx;
use super::Neighborhood;
use crate::App;
pub fn write_geojson_file(ctx: &EventCtx, app: &App, timer: &mut Timer) -> Result<String> {
pub fn write_geojson_file(ctx: &EventCtx, app: &App) -> Result<String> {
if cfg!(target_arch = "wasm32") {
bail!("Export only supported in the installed version");
}
@ -37,7 +36,7 @@ pub fn write_geojson_file(ctx: &EventCtx, app: &App, timer: &mut Timer) -> Resul
// Cells per neighborhood
let render_cells =
super::draw_cells::RenderCells::new(map, &Neighborhood::new(ctx, app, *id));
for (idx, multipolygon) in render_cells.to_multipolygons(timer).into_iter().enumerate() {
for (idx, multipolygon) in render_cells.to_multipolygons().into_iter().enumerate() {
let mut feature = Feature {
bbox: None,
geometry: Some(Geometry {