Skip to content

On the web, modify the current URL as we change maps and scenarios. #509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion game/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ crate-type = ["cdylib", "lib"]

[features]
default = ["built", "map_gui/native", "widgetry/native-backend"]
wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"]
wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "web-sys", "widgetry/wasm-backend"]

[dependencies]
aabb-quadtree = "0.1.0"
Expand Down Expand Up @@ -45,6 +45,7 @@ serde_json = "1.0.61"
svg_face = "0.1.3"
sim = { path = "../sim" }
wasm-bindgen = { version = "0.2.70", optional = true }
web-sys = { version = "0.3.47", optional = true, features=["History", "Location", "Window"] }
widgetry = { path = "../widgetry" }

[build-dependencies]
Expand Down
102 changes: 102 additions & 0 deletions game/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::collections::BTreeSet;

use anyhow::Result;

use geom::{Duration, Polygon};
use map_gui::ID;
use map_model::{IntersectionID, Map, RoadID};
Expand Down Expand Up @@ -407,3 +409,103 @@ pub fn checkbox_per_mode(
}
Widget::custom_row(filters)
}

/// This does nothing on native. On web, it modifies the current URL to change the first free
/// parameter in the HTTP GET params to the specified value, adding it if needed.
#[allow(unused_variables)]
pub fn update_url(free_param: &str) -> Result<()> {
#[cfg(target_arch = "wasm32")]
{
let window = web_sys::window().ok_or(anyhow!("no window?"))?;
let url = window.location().href().map_err(|err| {
anyhow!(err
.as_string()
.unwrap_or("window.location.href failed".to_string()))
})?;
let new_url = change_url_free_query_param(url, free_param);

// Setting window.location.href may seem like the obvious thing to do, but that actually
// refreshes the page. This method just changes the URL and doesn't mess up history. See
// https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API.
let history = window.history().map_err(|err| {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that web-sys Results have a JSValue as their Err case.

Some discussion to change that:
rustwasm/wasm-bindgen#1742

But seems like it's stalled out.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it would be so nice if this went through. In the meantime, I guess we can keep the boilerplate. And maybe figure out a macro/helper to make it a bit more ergonomic.

anyhow!(err
.as_string()
.unwrap_or("window.history failed".to_string()))
})?;
history
.replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url))
.map_err(|err| {
anyhow!(err
.as_string()
.unwrap_or("window.history.replace_state failed".to_string()))
})?;
}
Ok(())
}

#[allow(unused)]
fn change_url_free_query_param(url: String, free_param: &str) -> String {
// The URL parsing crates I checked had lots of dependencies and didn't even expose such a nice
// API for doing this anyway.
let url_parts = url.split("?").collect::<Vec<_>>();
if url_parts.len() == 1 {
return format!("{}?{}", url, free_param);
}
let mut query_params = String::new();
let mut found_free = false;
let mut first = true;
for x in url_parts[1].split("&") {
if !first {
query_params.push('&');
}
first = false;

if x.starts_with("--") {
query_params.push_str(x);
} else if !found_free {
// Replace the first free parameter
query_params.push_str(free_param);
found_free = true;
} else {
query_params.push_str(x);
}
}
if !found_free {
if !first {
query_params.push('&');
}
query_params.push_str(free_param);
}

format!("{}?{}", url_parts[0], query_params)
}

#[cfg(test)]
mod tests {
#[test]
fn test_change_url() {
use super::change_url_free_query_param;

assert_eq!(
"http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin",
change_url_free_query_param(
"http://0.0.0.0:8000/?--dev".to_string(),
"seattle/maps/montlake.bin"
)
);
assert_eq!(
"http://0.0.0.0:8000/?--dev&seattle/maps/qa.bin",
change_url_free_query_param(
"http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin".to_string(),
"seattle/maps/qa.bin"
)
);
assert_eq!(
"http://0.0.0.0:8000?seattle/maps/montlake.bin",
change_url_free_query_param(
"http://0.0.0.0:8000".to_string(),
"seattle/maps/montlake.bin"
)
);
}
}
15 changes: 13 additions & 2 deletions game/src/sandbox/gameplay/freeform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use widgetry::{
};

use crate::app::{App, Transition};
use crate::common::CommonState;
use crate::common::{update_url, CommonState};
use crate::edit::EditMode;
use crate::sandbox::gameplay::{GameplayMode, GameplayState};
use crate::sandbox::{Actions, SandboxControls, SandboxMode};
Expand All @@ -26,7 +26,18 @@ pub struct Freeform {
}

impl Freeform {
pub fn new(ctx: &mut EventCtx) -> Box<dyn GameplayState> {
pub fn new(ctx: &mut EventCtx, app: &App) -> Box<dyn GameplayState> {
if let Err(err) = update_url(
app.primary
.map
.get_name()
.path()
.strip_prefix(&abstio::path(""))
.unwrap(),
) {
warn!("Couldn't update URL: {}", err);
}

Box::new(Freeform {
top_center: Panel::empty(ctx),
})
Expand Down
4 changes: 2 additions & 2 deletions game/src/sandbox/gameplay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,9 @@ impl GameplayMode {
/// after this, so each constructor doesn't need to.
pub fn initialize(&self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn GameplayState> {
match self {
GameplayMode::Freeform(_) => freeform::Freeform::new(ctx),
GameplayMode::Freeform(_) => freeform::Freeform::new(ctx, app),
GameplayMode::PlayScenario(_, ref scenario, ref modifiers) => {
play_scenario::PlayScenario::new(ctx, scenario, modifiers.clone())
play_scenario::PlayScenario::new(ctx, app, scenario, modifiers.clone())
}
GameplayMode::FixTrafficSignals => {
fix_traffic_signals::FixTrafficSignals::new(ctx, app)
Expand Down
14 changes: 13 additions & 1 deletion game/src/sandbox/gameplay/play_scenario.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use widgetry::{
};

use crate::app::{App, Transition};
use crate::common::checkbox_per_mode;
use crate::common::{checkbox_per_mode, update_url};
use crate::edit::EditMode;
use crate::sandbox::gameplay::freeform::ChangeScenario;
use crate::sandbox::gameplay::{GameplayMode, GameplayState};
Expand All @@ -25,9 +25,21 @@ pub struct PlayScenario {
impl PlayScenario {
pub fn new(
ctx: &mut EventCtx,
app: &App,
name: &String,
modifiers: Vec<ScenarioModifier>,
) -> Box<dyn GameplayState> {
if let Err(err) = update_url(
// For dynamiclly generated scenarios like "random" and "home_to_work", this winds up
// making up a filename that doesn't actually exist. But if you pass that in, it winds
// up working, because we call abstio::parse_scenario_path() on the other side.
abstio::path_scenario(app.primary.map.get_name(), name)
.strip_prefix(&abstio::path(""))
.unwrap(),
) {
warn!("Couldn't update URL: {}", err);
}

Box::new(PlayScenario {
top_center: Panel::empty(ctx),
scenario_name: name.to_string(),
Expand Down