Refactor the support for making browsers download files, and use more widely

This commit is contained in:
Dustin Carlino 2022-05-18 14:43:16 +01:00
parent 5e11e6254b
commit ffef0279bf
11 changed files with 89 additions and 92 deletions

6
Cargo.lock generated
View File

@ -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",

View File

@ -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"] }

View File

@ -198,3 +198,10 @@ impl Read for FileWithProgress {
Ok(bytes)
}
}
/// Returns path on success
pub fn write_file(path: String, contents: String) -> Result<String> {
let mut file = File::create(&path)?;
write!(file, "{}", contents)?;
Ok(path)
}

View File

@ -179,3 +179,29 @@ fn list_local_storage_keys() -> Vec<String> {
}
keys
}
/// Returns path on success
pub fn write_file(path: String, contents: String) -> Result<String> {
// 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::<web_sys::HtmlElement>()
.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)
}

View File

@ -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))
}

View File

@ -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<String> {
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<String> {
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<String> {
}
}
Ok(path)
abstio::write_file(path, out)
}

View File

@ -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<String> {
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<String> {
.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<String> {
a.inner_seconds()
)?;
}
Ok(path)
abstio::write_file(path, out)
}

View File

@ -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"]

View File

@ -9,39 +9,7 @@ use crate::{App, Neighborhood};
pub fn write_geojson_file(ctx: &EventCtx, app: &App) -> Result<String> {
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::<web_sys::HtmlElement>()
.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<String> {

View File

@ -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"

View File

@ -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<X: Ord + Clone> TimeSeriesCount<X> {
pts_per_type.into_iter().collect()
}
pub fn export_csv<F: Fn(&X) -> 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<F: Fn(&X) -> 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
}
}