Browse all neighborhoods for the LTN tool [rebuild] [release]

This commit is contained in:
Dustin Carlino 2021-09-26 13:53:13 -07:00
parent 7fccf09f45
commit ceed02b7d4
4 changed files with 161 additions and 36 deletions

View File

@ -420,7 +420,7 @@ fn finish_app_setup(
let layers = ungap::Layers::new(ctx, app);
vec![ungap::ExploreMap::new_state(ctx, app, layers)]
} else if setup.ltn {
vec![ltn::Viewer::start_anywhere(ctx, app)]
vec![ltn::BrowseNeighborhoods::new_state(ctx, app)]
} else {
// Not attempting to keep the primary and secondary simulations synchronized at the same
// time yet. Just handle this one startup case, so we can switch maps without constantly

View File

@ -45,16 +45,29 @@ impl Neighborhood {
}
}
let mut n = Neighborhood {
Neighborhood {
interior,
perimeter,
borders,
modal_filters: BTreeSet::new(),
rat_runs: Vec::new(),
};
n.rat_runs = n.find_rat_runs(map);
n
}
}
// Just finds a sampling of rat runs, not necessarily all of them
pub fn calculate_rat_runs(&mut self, map: &Map) {
// Just flood from each border and see if we can reach another border.
//
// We might be able to do this in one pass, seeding the queue with all borders. But I think
// the "visited" bit would get tangled up between different possibilities...
self.rat_runs = self
.borders
.iter()
.flat_map(|i| self.rat_run_from(map, *i))
.collect();
self.rat_runs
.sort_by(|a, b| a.length_ratio.partial_cmp(&b.length_ratio).unwrap());
}
pub fn toggle_modal_filter(&mut self, map: &Map, r: RoadID) {
@ -63,7 +76,7 @@ impl Neighborhood {
} else {
self.modal_filters.insert(r);
}
self.rat_runs = self.find_rat_runs(map);
self.calculate_rat_runs(map);
}
pub fn is_interior_road(r: RoadID, map: &Map) -> bool {
@ -75,21 +88,6 @@ impl Neighborhood {
.any(|l| PathConstraints::Car.can_use(l, map))
}
// Just returns a sampling of rat runs, not necessarily all of them
fn find_rat_runs(&self, map: &Map) -> Vec<RatRun> {
// Just flood from each border and see if we can reach another border.
//
// We might be able to do this in one pass, seeding the queue with all borders. But I think
// the "visited" bit would get tangled up between different possibilities...
let mut runs: Vec<RatRun> = self
.borders
.iter()
.flat_map(|i| self.rat_run_from(map, *i))
.collect();
runs.sort_by(|a, b| a.length_ratio.partial_cmp(&b.length_ratio).unwrap());
runs
}
fn rat_run_from(&self, map: &Map, start: IntersectionID) -> Option<RatRun> {
// We don't need a priority queue
let mut back_refs = HashMap::new();

125
game/src/ltn/browse.rs Normal file
View File

@ -0,0 +1,125 @@
use std::collections::BTreeSet;
use map_gui::tools::{nice_map_name, CityPicker};
use map_gui::ID;
use widgetry::{
lctrl, Color, Drawable, EventCtx, GeomBatch, GfxCtx, HorizontalAlignment, Key, Line, Outcome,
Panel, State, TextExt, VerticalAlignment, Widget,
};
use super::{Neighborhood, Viewer};
use crate::app::{App, Transition};
use crate::common::intersections_from_roads;
pub struct BrowseNeighborhoods {
panel: Panel,
draw_neighborhoods: Drawable,
}
impl BrowseNeighborhoods {
pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
let panel = Panel::new_builder(Widget::col(vec![
Widget::row(vec![
Line("LTN tool").small_heading().into_widget(ctx),
ctx.style()
.btn_popup_icon_text(
"system/assets/tools/map.svg",
nice_map_name(app.primary.map.get_name()),
)
.hotkey(lctrl(Key::L))
.build_widget(ctx, "change map")
.centered_vert()
.align_right(),
]),
"Click a neighborhood".text_widget(ctx),
]))
.aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
.build(ctx);
let draw_neighborhoods = calculate_neighborhoods(app).upload(ctx);
Box::new(BrowseNeighborhoods {
panel,
draw_neighborhoods,
})
}
}
impl State<App> for BrowseNeighborhoods {
fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
ctx.canvas_movement();
if ctx.redo_mouseover() {
app.primary.current_selection =
match app.mouseover_unzoomed_roads_and_intersections(ctx) {
x @ Some(ID::Road(_)) => x,
Some(ID::Lane(l)) => Some(ID::Road(l.road)),
_ => None,
};
}
if let Some(ID::Road(r)) = app.primary.current_selection {
if Neighborhood::is_interior_road(r, &app.primary.map) && ctx.normal_left_click() {
return Transition::Replace(Viewer::start_from_road(ctx, app, r));
}
}
if let Outcome::Clicked(x) = self.panel.event(ctx) {
match x.as_ref() {
"change map" => {
return Transition::Push(CityPicker::new_state(
ctx,
app,
Box::new(|ctx, app| {
Transition::Replace(BrowseNeighborhoods::new_state(ctx, app))
}),
));
}
_ => unreachable!(),
}
}
Transition::Keep
}
fn draw(&self, g: &mut GfxCtx, _: &App) {
self.panel.draw(g);
g.redraw(&self.draw_neighborhoods);
}
}
fn calculate_neighborhoods(app: &App) -> GeomBatch {
let map = &app.primary.map;
let mut unvisited = BTreeSet::new();
let mut batch = GeomBatch::new();
let colors = [
Color::BLUE,
Color::ORANGE,
Color::PURPLE,
Color::RED,
Color::GREEN,
Color::CYAN,
];
let mut num_neighborhoods = 0;
for r in map.all_roads() {
if Neighborhood::is_interior_road(r.id, map) {
unvisited.insert(r.id);
}
}
while !unvisited.is_empty() {
let start = *unvisited.iter().next().unwrap();
let neighborhood = Neighborhood::from_road(map, start);
// TODO Either use that 4-color theorem and actually guarantee no adjacent same-color ones,
// or change the style to have a clear outline around each
let color = colors[num_neighborhoods % colors.len()];
num_neighborhoods += 1;
for i in intersections_from_roads(&neighborhood.interior, map) {
batch.push(color, map.get_i(i).polygon.clone());
}
for r in neighborhood.interior {
batch.push(color, map.get_r(r).get_thick_polygon());
unvisited.remove(&r);
}
}
batch
}

View File

@ -11,8 +11,10 @@ use widgetry::{
use crate::app::{App, Transition};
use crate::common::intersections_from_roads;
pub use browse::BrowseNeighborhoods;
mod algorithms;
mod browse;
pub struct Viewer {
panel: Panel,
@ -42,20 +44,9 @@ struct RatRun {
}
impl Viewer {
pub fn start_anywhere(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
// Find some residential road to start on
let r = app
.primary
.map
.all_roads()
.iter()
.find(|r| Neighborhood::is_interior_road(r.id, &app.primary.map))
.unwrap();
Viewer::start_from_road(ctx, app, r.id)
}
fn start_from_road(ctx: &mut EventCtx, app: &App, start: RoadID) -> Box<dyn State<App>> {
let neighborhood = Neighborhood::from_road(&app.primary.map, start);
let mut neighborhood = Neighborhood::from_road(&app.primary.map, start);
neighborhood.calculate_rat_runs(&app.primary.map);
let (draw_neighborhood, legend) = neighborhood.render(ctx, app);
let panel = Panel::new_builder(Widget::col(vec![
Widget::row(vec![
@ -70,6 +61,11 @@ impl Viewer {
.centered_vert()
.align_right(),
]),
ctx.style()
.btn_outline
.text("Browse neighborhoods")
.hotkey(Key::B)
.build_def(ctx),
legend,
Text::new().into_widget(ctx).named("rat runs"),
]))
@ -195,7 +191,9 @@ impl State<App> for Viewer {
return Transition::Push(CityPicker::new_state(
ctx,
app,
Box::new(|ctx, app| Transition::Replace(Viewer::start_anywhere(ctx, app))),
Box::new(|ctx, app| {
Transition::Replace(BrowseNeighborhoods::new_state(ctx, app))
}),
));
}
"previous rat run" => {
@ -206,6 +204,9 @@ impl State<App> for Viewer {
self.current_rat_run_idx += 1;
self.recalculate(ctx, app);
}
"Browse neighborhoods" => {
return Transition::Replace(BrowseNeighborhoods::new_state(ctx, app));
}
_ => unreachable!(),
}
}
@ -263,8 +264,9 @@ impl Neighborhood {
impl RatRun {
fn roads<'a>(&'a self, map: &'a Map) -> impl Iterator<Item = &'a Road> {
// TODO Find the neighborhoods that aren't being defined right, instead of flat_map here
self.path
.windows(2)
.map(move |pair| map.get_i(pair[0]).find_road_between(pair[1], map).unwrap())
.flat_map(move |pair| map.get_i(pair[0]).find_road_between(pair[1], map))
}
}