diff --git a/Cargo.lock b/Cargo.lock index 344c46b194..e5396b395b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,9 +123,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0df63cb2955042487fad3aefd2c6e3ae7389ac5dc1beb28921de0b69f779d4" +checksum = "ee67c11feeac938fae061b232e38e0b6d94f97a9df10e6271319325ac4c56a86" [[package]] name = "approx" @@ -1224,6 +1224,7 @@ version = "0.1.0" dependencies = [ "aabb-quadtree", "abstutil", + "anyhow", "built", "chrono", "collisions", @@ -2171,6 +2172,7 @@ version = "0.1.0" dependencies = [ "aabb-quadtree", "abstutil", + "anyhow", "colorous", "contour", "flate2", @@ -2185,6 +2187,7 @@ dependencies = [ "reqwest", "serde", "sim", + "tokio", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -2912,7 +2915,6 @@ dependencies = [ "rand_xorshift", "serde_json", "sim", - "tokio", ] [[package]] @@ -3476,9 +3478,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" +checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6" dependencies = [ "libc", ] diff --git a/game/Cargo.toml b/game/Cargo.toml index 1781eead7d..85583ff995 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -17,6 +17,7 @@ wasm = ["map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"] [dependencies] aabb-quadtree = "0.1.0" abstutil = { path = "../abstutil" } +anyhow = "1.0.37" built = { version = "0.4.3", optional = true, features=["chrono"] } chrono = "0.4.15" collisions = { path = "../collisions" } diff --git a/game/src/sandbox/gameplay/mod.rs b/game/src/sandbox/gameplay/mod.rs index 88e24879d8..b3b44e8b4d 100644 --- a/game/src/sandbox/gameplay/mod.rs +++ b/game/src/sandbox/gameplay/mod.rs @@ -1,3 +1,6 @@ +use core::future::Future; +use core::pin::Pin; + use rand_xorshift::XorShiftRng; use abstutil::{MapName, Timer}; @@ -81,6 +84,19 @@ pub enum LoadScenario { Nothing, Path(String), Scenario(Scenario), + // wasm futures are not `Send`, since they all ultimately run on the browser's single threaded + // runloop + #[cfg(target_arch = "wasm32")] + Future(Pin Scenario>>>>>), + #[cfg(not(target_arch = "wasm32"))] + Future( + Pin< + Box< + dyn Send + + Future Scenario>>>, + >, + >, + ), } impl GameplayMode { @@ -118,11 +134,27 @@ impl GameplayMode { } else if name == "home_to_work" { LoadScenario::Scenario(ScenarioGenerator::proletariat_robot(map, &mut rng, timer)) } else if name == "census" { - let config = popdat::Config::default(); - LoadScenario::Scenario( - popdat::generate_scenario("typical monday", config, map, &mut rng) - .expect("unable to build census scenario"), - ) + let map_area = map.get_boundary_polygon().clone(); + let map_bounds = map.get_gps_bounds().clone(); + let mut rng = sim::fork_rng(&mut rng); + + LoadScenario::Future(Box::pin(async move { + let areas = popdat::CensusArea::fetch_all_for_map(&map_area, &map_bounds).await?; + + let scenario_from_app: Box Scenario> = + Box::new(move |app: &App| { + let config = popdat::Config::default(); + popdat::generate_scenario( + "typical monday", + areas, + config, + &app.primary.map, + &mut rng, + ) + }); + + Ok(scenario_from_app) + })) } else { LoadScenario::Path(abstutil::path_scenario(map.get_name(), &name)) } diff --git a/game/src/sandbox/mod.rs b/game/src/sandbox/mod.rs index 22a3f4102b..85c3642e2f 100644 --- a/game/src/sandbox/mod.rs +++ b/game/src/sandbox/mod.rs @@ -678,6 +678,28 @@ impl State for SandboxLoader { self.stage = Some(LoadStage::GotScenario(scenario)); continue; } + gameplay::LoadScenario::Future(future) => { + use map_gui::load::FutureLoader; + return Transition::Push(FutureLoader::::new( + ctx, + Box::pin(future), + "Loading Scenario", + Box::new(|_, _, scenario| { + // TODO show error/retry alert? + let scenario = + scenario.expect("failed to load scenario from future"); + Transition::Multi(vec![ + Transition::Pop, + Transition::ModifyState(Box::new(|state, _, app| { + let loader = + state.downcast_mut::().unwrap(); + app.primary.scenario = Some(scenario.clone()); + loader.stage = Some(LoadStage::GotScenario(scenario)); + })), + ]) + }), + )); + } gameplay::LoadScenario::Path(path) => { // Reuse the cached scenario, if possible. if let Some(ref scenario) = app.primary.scenario { diff --git a/map_gui/Cargo.toml b/map_gui/Cargo.toml index 3b57af669f..bfeb0553a8 100644 --- a/map_gui/Cargo.toml +++ b/map_gui/Cargo.toml @@ -5,8 +5,8 @@ authors = ["Dustin Carlino "] edition = "2018" [features] -native = ["reqwest"] -wasm = ["futures", "futures-channel", "js-sys", "wasm-bindgen", "wasm-bindgen-futures", "web-sys"] +native = ["reqwest", "tokio"] +wasm = ["js-sys", "wasm-bindgen", "wasm-bindgen-futures", "web-sys"] # Just a marker to not use localhost URLs wasm_s3 = [] # A marker to use a named release from S3 instead of dev for updating files @@ -15,11 +15,12 @@ release_s3 = [] [dependencies] aabb-quadtree = "0.1.0" abstutil = { path = "../abstutil" } +anyhow = "1.0.37" colorous = "1.0.3" contour = "0.3.0" flate2 = "1.0.19" -futures = { version = "0.3.8", optional = true } -futures-channel = { version = "0.3.8", optional = true } +futures = { version = "0.3.8" } +futures-channel = { version = "0.3.8"} geojson = "0.21.0" geom = { path = "../geom" } instant = "0.1.7" @@ -29,6 +30,7 @@ map_model = { path = "../map_model" } reqwest = { version = "0.10.8", optional = true, default-features=false, features=["blocking", "rustls-tls"] } serde = "1.0.116" sim = { path = "../sim" } +tokio = { version ="0.2", features=["rt-core"], optional = true } wasm-bindgen = { version = "0.2.68", optional = true } wasm-bindgen-futures = { version = "0.4.18", optional = true } webbrowser = "0.5.5" diff --git a/map_gui/src/load.rs b/map_gui/src/load.rs index b7ab9c93a5..d3072ed93c 100644 --- a/map_gui/src/load.rs +++ b/map_gui/src/load.rs @@ -1,10 +1,18 @@ //! Loading large resources (like maps, scenarios, and prebaked data) requires different strategies //! on native and web. Both cases are wrapped up as a State that runs a callback when done. +use std::future::Future; +use std::pin::Pin; + +use futures_channel::oneshot; +use instant::Instant; use serde::de::DeserializeOwned; +#[cfg(not(target_arch = "wasm32"))] +use tokio::runtime::Runtime; use abstutil::{MapName, Timer}; -use widgetry::{Color, EventCtx, GfxCtx, State, Transition}; +use geom::Duration; +use widgetry::{Color, EventCtx, GfxCtx, Line, Panel, State, Text, Transition, UpdateType}; use crate::tools::PopupMsg; use crate::AppLike; @@ -244,3 +252,118 @@ mod wasm_loader { } } } + +pub struct FutureLoader +where + A: AppLike, +{ + loading_title: String, + started: Instant, + panel: Panel, + receiver: oneshot::Receiver T>>>, + on_load: Option) -> Transition>>, + + // If Runtime is dropped, any active tasks will be canceled, so we retain it here even + // though we never access it. It might make more sense for Runtime to live on App if we're + // going to be doing more background spawning. + #[cfg(not(target_arch = "wasm32"))] + #[allow(dead_code)] + runtime: Runtime, +} + +impl FutureLoader +where + A: 'static + AppLike, + T: 'static, +{ + #[cfg(target_arch = "wasm32")] + pub fn new( + ctx: &mut EventCtx, + future: Pin T>>>>>, + loading_title: &str, + on_load: Box) -> Transition>, + ) -> Box> { + let (tx, receiver) = oneshot::channel(); + wasm_bindgen_futures::spawn_local(async move { + tx.send(future.await).ok().unwrap(); + }); + Box::new(FutureLoader { + loading_title: loading_title.to_string(), + started: Instant::now(), + panel: ctx.make_loading_screen(Text::from(Line(loading_title))), + receiver, + on_load: Some(on_load), + }) + } + + #[cfg(not(target_arch = "wasm32"))] + pub fn new( + ctx: &mut EventCtx, + future: Pin< + Box T>>>>, + >, + loading_title: &str, + on_load: Box) -> Transition>, + ) -> Box> { + let runtime = Runtime::new().unwrap(); + let (tx, receiver) = oneshot::channel(); + runtime.spawn(async move { + tx.send(future.await).ok().unwrap(); + }); + + Box::new(FutureLoader { + loading_title: loading_title.to_string(), + started: Instant::now(), + panel: ctx.make_loading_screen(Text::from(Line(loading_title))), + receiver, + on_load: Some(on_load), + runtime, + }) + } +} + +impl State for FutureLoader +where + A: 'static + AppLike, + T: 'static, +{ + fn event(&mut self, ctx: &mut EventCtx, app: &mut A) -> Transition { + match self.receiver.try_recv() { + Err(e) => { + error!("channel failed: {:?}", e); + let on_load = self.on_load.take().unwrap(); + return on_load(ctx, app, Err(anyhow::anyhow!("channel canceled"))); + } + Ok(None) => { + self.panel = ctx.make_loading_screen(Text::from_multiline(vec![ + Line(&self.loading_title), + Line(format!( + "Time spent: {}", + Duration::realtime_elapsed(self.started) + )), + ])); + + // Until the response is received, just ask winit to regularly call event(), so we + // can keep polling the channel. + ctx.request_update(UpdateType::Game); + return Transition::Keep; + } + Ok(Some(Err(e))) => { + error!("error in fetching data"); + let on_load = self.on_load.take().unwrap(); + return on_load(ctx, app, Err(e)); + } + Ok(Some(Ok(builder))) => { + debug!("future complete"); + let t = builder(app); + let on_load = self.on_load.take().unwrap(); + return on_load(ctx, app, Ok(t)); + } + } + } + + fn draw(&self, g: &mut GfxCtx, _: &A) { + g.clear(Color::BLACK); + self.panel.draw(g); + } +} diff --git a/popdat/Cargo.toml b/popdat/Cargo.toml index 0cf7d0a0ac..b1a9894d22 100644 --- a/popdat/Cargo.toml +++ b/popdat/Cargo.toml @@ -17,7 +17,6 @@ log = "0.4.11" map_model = { path = "../map_model" } rand = "0.7.0" rand_xorshift = "0.2.0" -tokio = "0.2.24" geo-booleanop = "0.3.2" serde_json = "1.0.60" sim = { path = "../sim" } diff --git a/popdat/src/import_census.rs b/popdat/src/import_census.rs index 6008242de9..fcaf0a010b 100644 --- a/popdat/src/import_census.rs +++ b/popdat/src/import_census.rs @@ -1,30 +1,17 @@ use geo::algorithm::intersects::Intersects; -use abstutil::Timer; -use map_model::Map; +use geom::{GPSBounds, Polygon}; use crate::CensusArea; impl CensusArea { - pub fn fetch_all_for_map(map: &Map, timer: &mut Timer) -> anyhow::Result> { - timer.start("processing population areas fgb"); - let areas = tokio::runtime::Runtime::new() - .expect("Failed to create Tokio runtime") - .block_on(Self::fetch_all_for_map_async(map, timer))?; - timer.stop("processing population areas fgb"); - Ok(areas) - } - - async fn fetch_all_for_map_async( - map: &Map, - timer: &mut Timer<'_>, + pub async fn fetch_all_for_map( + map_area: &Polygon, + bounds: &GPSBounds, ) -> anyhow::Result> { use flatgeobuf::HttpFgbReader; use geozero_core::geo_types::Geo; - let map_area = map.get_boundary_polygon(); - let bounds = map.get_gps_bounds(); - use geo::algorithm::{bounding_rect::BoundingRect, map_coords::MapCoordsInplace}; let mut geo_map_area: geo::Polygon<_> = map_area.clone().into(); geo_map_area.map_coords_inplace(|c| { @@ -32,13 +19,11 @@ impl CensusArea { (projected.x(), projected.y()) }); - timer.start("opening FGB reader"); // See the import handbook for how to prepare this file. let mut fgb = - HttpFgbReader::open("https://abstreet.s3.amazonaws.com/population_areas.fgb").await?; - timer.stop("opening FGB reader"); + // HttpFgbReader::open("https://abstreet.s3.amazonaws.com/population_areas.fgb").await?; + HttpFgbReader::open("https://s3.amazonaws.com/mjk_asdf/abs/population_areas.fgb").await?; - timer.start("selecting bounding box"); let bounding_rect = geo_map_area .bounding_rect() .ok_or(anyhow!("missing bound rect"))?; @@ -49,9 +34,7 @@ impl CensusArea { bounding_rect.max().y, ) .await?; - timer.stop("selecting bounding box"); - timer.start("processing features"); let mut results = vec![]; while let Some(feature) = fgb.next().await? { // PERF TODO: how to parse into usize directly? And avoid parsing entire props dict? @@ -105,7 +88,6 @@ impl CensusArea { continue; } } - timer.stop("processing features"); Ok(results) } diff --git a/popdat/src/lib.rs b/popdat/src/lib.rs index a82ad4907f..447d64e099 100644 --- a/popdat/src/lib.rs +++ b/popdat/src/lib.rs @@ -102,17 +102,15 @@ impl Config { /// appropriate census data, and use it to produce a Scenario. pub fn generate_scenario( scenario_name: &str, + areas: Vec, config: Config, map: &Map, rng: &mut XorShiftRng, -) -> Result { +) -> Scenario { + let mut timer = Timer::new("building scenario"); + // find_data_for_map may return an error. If so, just plumb it back to the caller using the ? // operator - let mut timer = Timer::new("generate census scenario"); - timer.start("building population areas for map"); - let areas = CensusArea::fetch_all_for_map(map, &mut timer).map_err(|e| e.to_string())?; - timer.stop("building population areas for map"); - timer.start("assigning people to houses"); let people = distribute_people::assign_people_to_houses(areas, map, rng, &config); timer.stop("assigning people to houses"); @@ -128,5 +126,5 @@ pub fn generate_scenario( scenario = scenario.remove_weird_schedules(); timer.stop("removing weird schedules"); - Ok(scenario) + scenario }