From ffef0279bfc2e15969e55d2e45cb834024272cdc Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Wed, 18 May 2022 14:43:16 +0100 Subject: [PATCH] Refactor the support for making browsers download files, and use more widely --- Cargo.lock | 6 ++-- abstio/Cargo.toml | 4 ++- abstio/src/io_native.rs | 7 ++++ abstio/src/io_web.rs | 26 ++++++++++++++ apps/game/src/layer/traffic.rs | 32 +++++++++-------- apps/game/src/sandbox/dashboards/risks.rs | 11 +++--- .../src/sandbox/dashboards/travel_times.rs | 28 +++++++-------- apps/ltn/Cargo.toml | 9 +---- apps/ltn/src/export.rs | 34 +------------------ sim/Cargo.toml | 1 - sim/src/analytics.rs | 23 ++++++++----- 11 files changed, 89 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce23269581..84fbeddb3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,12 +23,14 @@ dependencies = [ "futures-channel", "include_dir", "instant", + "js-sys", "lazy_static", "log", "reqwest", "serde", "serde_json", "tokio", + "wasm-bindgen", "web-sys", ] @@ -2101,12 +2103,10 @@ dependencies = [ "anyhow", "contour", "flate2", - "fs-err", "geo", "geojson", "geom", "getrandom", - "js-sys", "lazy_static", "log", "map_gui", @@ -2119,7 +2119,6 @@ dependencies = [ "structopt", "synthpop", "wasm-bindgen", - "web-sys", "widgetry", ] @@ -3508,7 +3507,6 @@ dependencies = [ "ctrlc", "downcast-rs", "enum_dispatch", - "fs-err", "geom", "instant", "libm 0.2.1", diff --git a/abstio/Cargo.toml b/abstio/Cargo.toml index 9851fd6380..62f5e3238b 100644 --- a/abstio/Cargo.toml +++ b/abstio/Cargo.toml @@ -23,4 +23,6 @@ tokio = "1.1.1" [target.'cfg(target_arch = "wasm32")'.dependencies] include_dir = { git = "https://github.com/dabreegster/include_dir", branch = "union" } -web-sys = { version = "0.3.47", features=["Storage", "Window"] } +js-sys = "0.3.47" +wasm-bindgen = "0.2.70" +web-sys = { version = "0.3.47", features=["HtmlElement", "Storage", "Window"] } diff --git a/abstio/src/io_native.rs b/abstio/src/io_native.rs index 7cb7921481..d4f42cd82e 100644 --- a/abstio/src/io_native.rs +++ b/abstio/src/io_native.rs @@ -198,3 +198,10 @@ impl Read for FileWithProgress { Ok(bytes) } } + +/// Returns path on success +pub fn write_file(path: String, contents: String) -> Result { + let mut file = File::create(&path)?; + write!(file, "{}", contents)?; + Ok(path) +} diff --git a/abstio/src/io_web.rs b/abstio/src/io_web.rs index 19b181a4b3..e15cc82ae0 100644 --- a/abstio/src/io_web.rs +++ b/abstio/src/io_web.rs @@ -179,3 +179,29 @@ fn list_local_storage_keys() -> Vec { } keys } + +/// Returns path on success +pub fn write_file(path: String, contents: String) -> Result { + // Make the browser prompt the user to save a local file with arbitrary contents. + use wasm_bindgen::JsCast; + + let data: String = js_sys::JsString::from("data:text/json;charset=utf-8,") + .concat(&js_sys::encode_uri_component(&contents)) + .into(); + + // TODO Proper error handling + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let node = document + .create_element("a") + .unwrap() + .dyn_into::() + .unwrap(); + node.set_attribute("href", &data).unwrap(); + node.set_attribute("download", &path).unwrap(); + document.body().unwrap().append_child(&node).unwrap(); + node.click(); + node.remove(); + + Ok(path) +} diff --git a/apps/game/src/layer/traffic.rs b/apps/game/src/layer/traffic.rs index 253db12f78..149904c6c0 100644 --- a/apps/game/src/layer/traffic.rs +++ b/apps/game/src/layer/traffic.rs @@ -234,11 +234,7 @@ impl Throughput { ) .flex_wrap(ctx, Percent::int(20)), ColorLegend::gradient(ctx, &app.cs.good_to_bad_red, vec!["0", "highest"]), - if cfg!(not(target_arch = "wasm32")) { - ctx.style().btn_plain.text("Export to CSV").build_def(ctx) - } else { - Widget::nothing() - }, + ctx.style().btn_plain.text("Export to CSV").build_def(ctx), ])) .aligned_pair(PANEL_PLACEMENT) .build(ctx); @@ -758,22 +754,28 @@ fn export_throughput(app: &App) -> Result<(String, String)> { app.primary.map.get_name().as_filename(), app.primary.sim.time().as_filename() ); - app.primary - .sim - .get_analytics() - .road_thruput - .export_csv(&path1, |id| id.0)?; + let path1 = abstio::write_file( + path1, + app.primary + .sim + .get_analytics() + .road_thruput + .export_csv(|id| id.0), + )?; let path2 = format!( "intersection_throughput_{}_{}.csv", app.primary.map.get_name().as_filename(), app.primary.sim.time().as_filename() ); - app.primary - .sim - .get_analytics() - .intersection_thruput - .export_csv(&path2, |id| id.0)?; + let path2 = abstio::write_file( + path2, + app.primary + .sim + .get_analytics() + .intersection_thruput + .export_csv(|id| id.0), + )?; Ok((path1, path2)) } diff --git a/apps/game/src/sandbox/dashboards/risks.rs b/apps/game/src/sandbox/dashboards/risks.rs index 562f050ae9..0c8c7b0584 100644 --- a/apps/game/src/sandbox/dashboards/risks.rs +++ b/apps/game/src/sandbox/dashboards/risks.rs @@ -1,8 +1,7 @@ use std::collections::BTreeSet; -use std::io::Write; +use std::fmt::Write; use anyhow::Result; -use fs_err::File; use abstutil::prettyprint_usize; use sim::{ProblemType, TripID}; @@ -207,9 +206,9 @@ fn export_problems(app: &App) -> Result { app.primary.map.get_name().as_filename(), app.primary.sim.time().as_filename() ); - let mut f = File::create(&path)?; + let mut out = String::new(); writeln!( - f, + out, "id,mode,seconds_after,problem_type,problems_before,problems_after" )?; @@ -225,7 +224,7 @@ fn export_problems(app: &App) -> Result { problem_type.count(after.problems_per_trip.get(&id).unwrap_or(&empty)); if count_before != 0 || count_after != 0 { writeln!( - f, + out, "{},{:?},{},{:?},{},{}", id.0, mode, @@ -238,5 +237,5 @@ fn export_problems(app: &App) -> Result { } } - Ok(path) + abstio::write_file(path, out) } diff --git a/apps/game/src/sandbox/dashboards/travel_times.rs b/apps/game/src/sandbox/dashboards/travel_times.rs index 208e0b2e4c..287a7d210a 100644 --- a/apps/game/src/sandbox/dashboards/travel_times.rs +++ b/apps/game/src/sandbox/dashboards/travel_times.rs @@ -1,8 +1,7 @@ use std::collections::BTreeSet; -use std::io::Write; +use std::fmt::Write; use anyhow::Result; -use fs_err::File; use abstutil::prettyprint_usize; use geom::{Distance, Duration, Polygon, Pt2D}; @@ -42,16 +41,13 @@ impl TravelTimes { )); } - // TODO We can make file downloads of dynamically generated data work on the browser too... - if cfg!(not(target_arch = "wasm32")) { - filters.push( - ctx.style() - .btn_plain - .text("Export to CSV") - .build_def(ctx) - .align_bottom(), - ); - } + filters.push( + ctx.style() + .btn_plain + .text("Export to CSV") + .build_def(ctx) + .align_bottom(), + ); Panel::new_builder(Widget::col(vec![ DashTab::TravelTimes.picker(ctx, app), @@ -645,8 +641,8 @@ fn export_times(app: &App) -> Result { app.primary.map.get_name().as_filename(), app.primary.sim.time().as_filename() ); - let mut f = File::create(&path)?; - writeln!(f, "id,mode,seconds_before,seconds_after")?; + let mut out = String::new(); + writeln!(out, "id,mode,seconds_before,seconds_after")?; for (id, b, a, mode) in app .primary .sim @@ -654,7 +650,7 @@ fn export_times(app: &App) -> Result { .both_finished_trips(app.primary.sim.time(), app.prebaked()) { writeln!( - f, + out, "{},{:?},{},{}", id.0, mode, @@ -662,5 +658,5 @@ fn export_times(app: &App) -> Result { a.inner_seconds() )?; } - Ok(path) + abstio::write_file(path, out) } diff --git a/apps/ltn/Cargo.toml b/apps/ltn/Cargo.toml index 40e75a4b5d..88c842bd08 100644 --- a/apps/ltn/Cargo.toml +++ b/apps/ltn/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "lib"] [features] default = ["map_gui/native", "widgetry/native-backend"] -wasm = ["getrandom/js", "js-sys", "map_gui/wasm", "wasm-bindgen", "web-sys", "widgetry/wasm-backend"] +wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"] [dependencies] abstio = { path = "../../abstio" } @@ -17,12 +17,10 @@ abstutil = { path = "../../abstutil" } anyhow = "1.0.38" contour = "0.4.0" flate2 = "1.0.20" -fs-err = "2.6.0" geo = "0.20.1" geojson = { version = "0.22.2", features = ["geo-types"] } geom = { path = "../../geom" } getrandom = { version = "0.2.3", optional = true } -js-sys = { version = "0.3.47", optional = true } lazy_static = "1.4.0" log = "0.4" maplit = "1.0.2" @@ -36,8 +34,3 @@ synthpop = { path = "../../synthpop" } wasm-bindgen = { version = "0.2.70", optional = true } widgetry = { path = "../../widgetry" } structopt = "0.3.23" - -[dependencies.web-sys] -version = "0.3.47" -optional = true -features = ["HtmlElement"] diff --git a/apps/ltn/src/export.rs b/apps/ltn/src/export.rs index db843d3c25..098d1b277c 100644 --- a/apps/ltn/src/export.rs +++ b/apps/ltn/src/export.rs @@ -9,39 +9,7 @@ use crate::{App, Neighborhood}; pub fn write_geojson_file(ctx: &EventCtx, app: &App) -> Result { let contents = geojson_string(ctx, app)?; let path = format!("ltn_{}.geojson", app.map.get_name().map); - - // TODO Refactor into map_gui or abstio and handle errors better - #[cfg(target_arch = "wasm32")] - { - use wasm_bindgen::JsCast; - - let data: String = js_sys::JsString::from("data:text/json;charset=utf-8,") - .concat(&js_sys::encode_uri_component(&contents)) - .into(); - - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let node = document - .create_element("a") - .unwrap() - .dyn_into::() - .unwrap(); - node.set_attribute("href", &data).unwrap(); - node.set_attribute("download", &path).unwrap(); - document.body().unwrap().append_child(&node).unwrap(); - node.click(); - node.remove(); - } - - #[cfg(not(target_arch = "wasm32"))] - { - use std::io::Write; - - let mut file = fs_err::File::create(&path)?; - write!(file, "{}", contents)?; - } - - Ok(path) + abstio::write_file(path, contents) } fn geojson_string(ctx: &EventCtx, app: &App) -> Result { diff --git a/sim/Cargo.toml b/sim/Cargo.toml index ee03c2eba8..af619c41fb 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -11,7 +11,6 @@ anyhow = "1.0.38" ctrlc = { version = "3.1.7", optional = true } downcast-rs = "1.2.0" enum_dispatch = "0.3.5" -fs-err = "2.6.0" geom = { path = "../geom" } instant = "0.1.7" libm = "0.2.1" diff --git a/sim/src/analytics.rs b/sim/src/analytics.rs index 0a346b8c00..bb642a1551 100644 --- a/sim/src/analytics.rs +++ b/sim/src/analytics.rs @@ -1,8 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; -use std::io::Write; +use std::fmt::Write; -use anyhow::Result; -use fs_err::File; use serde::{Deserialize, Serialize}; use abstutil::Counter; @@ -834,13 +832,22 @@ impl TimeSeriesCount { pts_per_type.into_iter().collect() } - pub fn export_csv usize>(&self, path: &str, extract_id: F) -> Result<()> { - let mut f = File::create(path)?; - writeln!(f, "id,agent_type,hour,count")?; + /// Returns the contents of a CSV file + pub fn export_csv usize>(&self, extract_id: F) -> String { + let mut out = String::new(); + writeln!(out, "id,agent_type,hour,count").unwrap(); for ((id, agent_type, hour), count) in &self.counts { - writeln!(f, "{},{:?},{},{}", extract_id(id), agent_type, hour, count)?; + writeln!( + out, + "{},{:?},{},{}", + extract_id(id), + agent_type, + hour, + count + ) + .unwrap(); } - Ok(()) + out } }