Skip to content

Commit

Permalink
feat: download png of the map as currently seen
Browse files Browse the repository at this point in the history
  • Loading branch information
CalliEve committed Jan 4, 2025
1 parent f87c657 commit 7abd442
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 38 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ web-sys = { version = "0.3.76", features = [
"Element",
"Window",
"FocusEvent",
"OffscreenCanvasRenderingContext2d",
"OffscreenCanvas",
"ImageEncodeOptions",
] }
js-sys = "0.3.76"
wasm-bindgen = { version = "0.2.99" }
Expand Down
118 changes: 94 additions & 24 deletions src/algorithm/drawing/canvas_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
//! to the html canvas.
#[cfg(all(not(test), not(feature = "benchmarking")))]
use std::{
borrow::Cow,
ops::Deref,
};
use std::borrow::Cow;
#[cfg(any(test, feature = "benchmarking"))]
use std::{
cell::RefCell,
Expand All @@ -22,15 +19,25 @@ use wasm_bindgen::{
};
#[cfg(all(not(test), not(feature = "benchmarking")))]
use web_sys::js_sys::Uint8Array;
use web_sys::HtmlCanvasElement;
use web_sys::{
HtmlCanvasElement,
OffscreenCanvas,
};

#[cfg(all(not(test), not(feature = "benchmarking")))]
enum InnerCanvasContext<'a> {
OffScreen(Cow<'a, web_sys::OffscreenCanvasRenderingContext2d>),
OnScreen(Cow<'a, web_sys::CanvasRenderingContext2d>),
}

/// A wrapper around the [`web_sys::CanvasRenderingContext2d`]. This struct
/// provides the ability to mock and unit-test the drawing functions.
/// A wrapper around the [`web_sys::CanvasRenderingContext2d`] or [`web_sys::].
/// This struct provides the ability to mock and unit-test the drawing
/// functions.
pub struct CanvasContext<'a> {
/// The inner [`web_sys::CanvasRenderingContext2d`] object wrapped by this
/// struct.
#[cfg(all(not(test), not(feature = "benchmarking")))]
inner: Cow<'a, web_sys::CanvasRenderingContext2d>,
inner: InnerCanvasContext<'a>,
#[cfg(any(test, feature = "benchmarking"))]
inner: PhantomData<&'a ()>,
#[cfg(any(test, feature = "benchmarking"))]
Expand All @@ -41,7 +48,7 @@ pub struct CanvasContext<'a> {
impl From<web_sys::CanvasRenderingContext2d> for CanvasContext<'static> {
fn from(context: web_sys::CanvasRenderingContext2d) -> Self {
Self {
inner: Cow::Owned(context),
inner: InnerCanvasContext::OnScreen(Cow::Owned(context)),
}
}
}
Expand All @@ -67,7 +74,23 @@ impl<'a> From<&'a HtmlCanvasElement> for CanvasContext<'a> {
.expect("Failed to convert to CanvasRenderingContext2d");

Self {
inner: Cow::Owned(context),
inner: InnerCanvasContext::OnScreen(Cow::Owned(context)),
}
}
}

#[cfg(all(not(test), not(feature = "benchmarking")))]
impl<'a> From<&'a OffscreenCanvas> for CanvasContext<'a> {
fn from(canvas: &'a OffscreenCanvas) -> Self {
let context = canvas
.get_context("2d")
.expect("Failed to get 2d context")
.expect("offscreen 2d context is null")
.dyn_into::<web_sys::OffscreenCanvasRenderingContext2d>()
.expect("Failed to convert to OffscreenCanvasRenderingContext2d");

Self {
inner: InnerCanvasContext::OffScreen(Cow::Owned(context)),
}
}
}
Expand All @@ -82,11 +105,21 @@ impl<'a> From<&'a HtmlCanvasElement> for CanvasContext<'a> {
}
}

#[cfg(any(test, feature = "benchmarking"))]
impl<'a> From<&'a OffscreenCanvas> for CanvasContext<'a> {
fn from(_: &'a OffscreenCanvas) -> Self {
Self {
inner: PhantomData,
recorder: RefCell::new(HashMap::new()),
}
}
}

#[cfg(all(not(test), not(feature = "benchmarking")))]
impl<'a> From<&'a web_sys::CanvasRenderingContext2d> for CanvasContext<'a> {
fn from(context: &'a web_sys::CanvasRenderingContext2d) -> Self {
Self {
inner: Cow::Borrowed(context),
inner: InnerCanvasContext::OnScreen(Cow::Borrowed(context)),
}
}
}
Expand All @@ -102,26 +135,59 @@ impl<'a> From<&'a web_sys::CanvasRenderingContext2d> for CanvasContext<'a> {
}

#[cfg(all(not(test), not(feature = "benchmarking")))]
impl AsRef<web_sys::CanvasRenderingContext2d> for CanvasContext<'_> {
fn as_ref(&self) -> &web_sys::CanvasRenderingContext2d {
&self.inner
}
}

#[cfg(all(not(test), not(feature = "benchmarking")))]
impl Deref for CanvasContext<'_> {
type Target = web_sys::CanvasRenderingContext2d;
macro_rules! impl_canvas_context_method {
($method:ident($($arg:ident: $arg_ty:ty),*) -> $res:ty) => {
pub fn $method(&self, $($arg: $arg_ty),*) -> $res {
match &self.inner {
InnerCanvasContext::OffScreen(context) => {
context.$method($($arg),*)
},
InnerCanvasContext::OnScreen(context) => {
context.$method($($arg),*)
},
}
}

fn deref(&self) -> &Self::Target {
&self.inner
}
}

#[cfg(all(not(test), not(feature = "benchmarking")))]
impl CanvasContext<'_> {
impl_canvas_context_method!(move_to(x: f64, y: f64) -> ());

impl_canvas_context_method!(line_to(x: f64, y: f64) -> ());

impl_canvas_context_method!(stroke() -> ());

impl_canvas_context_method!(begin_path() -> ());

impl_canvas_context_method!(set_line_width(f: f64) -> ());

impl_canvas_context_method!(set_stroke_style_str(s: &str) -> ());

impl_canvas_context_method!(set_global_alpha(a: f64) -> ());

impl_canvas_context_method!(rect(x: f64, y: f64, width: f64, height: f64) -> ());

impl_canvas_context_method!(arc(x: f64, y: f64, radius: f64, start_angle: f64, end_angle: f64) -> Result<(), JsValue>);

impl_canvas_context_method!(fill() -> ());

impl_canvas_context_method!(set_fill_style_str(style: &str) -> ());

pub fn set_line_dash(&self, segments: &[u8]) -> Result<(), JsValue> {
self.inner
.set_line_dash(&Uint8Array::from(segments))
let array = Uint8Array::from(segments);
match &self.inner {
InnerCanvasContext::OffScreen(context) => context.set_line_dash(&array),
InnerCanvasContext::OnScreen(context) => context.set_line_dash(&array),
}
}

pub fn is_onscreen(&self) -> bool {
match &self.inner {
InnerCanvasContext::OffScreen(_) => false,
InnerCanvasContext::OnScreen(_) => true,
}
}
}

Expand All @@ -134,6 +200,10 @@ impl<'a> CanvasContext<'a> {
}
}

pub fn is_onscreen(&self) -> bool {
true
}

pub fn move_to(&self, x: f64, y: f64) {
self.record(
"move_to",
Expand Down
14 changes: 13 additions & 1 deletion src/algorithm/drawing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,21 @@ pub fn redraw_canvas<'a, C>(canvas: C, state: &MapState)
where
C: Into<CanvasContext<'a>>,
{
// Get a 2d canvas rendering context
// Get a canvas rendering context
let context: CanvasContext = canvas.into();

// If we're offscreen, then this is for an image of the map to get downloaded,
// so we only need to draw the map and no grid.
if !context.is_onscreen() {
let map = state
.get_map()
.without_checkpoints();

map.draw(&context, state.get_canvas_state(), 1.0);

return;
}

draw_grid(&context, state.get_canvas_state());

if state.is_original_overlay_enabled() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/canvas/mouse_up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,8 @@ pub fn on_mouse_up(
}

if added_station {
map_state.clear_all_selections();
map_state.set_map(map);
map_state.clear_selected_lines();
return;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/file_downloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use crate::{
utils::json::encode_map,
};

/// A modal that lets the user download a file representing the map.
/// A button that lets the user download a file representing the map.
#[component]
pub fn FileDownloader() -> impl IntoView {
let map_state =
Expand Down
73 changes: 73 additions & 0 deletions src/components/molecules/map_exporter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Contains the [`FileDownloader`] component.
use leptos::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{
ImageEncodeOptions,
OffscreenCanvas,
Url,
};

use crate::{
algorithm::drawing::redraw_canvas,
components::{
atoms::Button,
MapState,
},
};

/// A button that lets the user export and download the map as a png file.
#[component]
pub fn MapExporter() -> impl IntoView {
let map_state =
use_context::<RwSignal<MapState>>().expect("to have found the global map state");

let export_map = Action::new_local(move |_| {
async move {
let blob_promise = {
let state = map_state.get_untracked();
let (y_size, x_size) = state
.get_canvas_state()
.get_size();

let canvas = OffscreenCanvas::new(x_size as u32, y_size as u32) // Full HD size (common resolution and not too big)
.expect("to create an offscreen canvas");

redraw_canvas(&canvas, &state);

let blob_options = ImageEncodeOptions::new();
blob_options.set_type("image/png");

canvas
.convert_to_blob_with_options(&blob_options)
.expect("to convert the canvas to a blob promise")
};

let blob = JsFuture::from(blob_promise)
.await
.expect("to await the promise")
.dyn_into::<web_sys::Blob>()
.expect("to convert the promise to a blob");

let url = Url::create_object_url_with_blob(&blob)
.expect("to create an object URL from the blob");

let elem = document()
.create_element("a")
.expect("to create an anchor element")
.dyn_into::<web_sys::HtmlAnchorElement>()
.expect("to convert the element to an anchor element");

elem.set_href(&url);
elem.set_download("metro-map.png");
elem.click();

Url::revoke_object_url(&url).unwrap();
}
});

view! {
<Button text="To PNG" outlined=true can_focus=false on_click=Box::new(move |_| {export_map.dispatch(());})/>
}
}
2 changes: 2 additions & 0 deletions src/components/molecules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod edge_info_box;
mod error_box;
mod file_downloader;
mod file_modal;
mod map_exporter;
mod settings_modal;
mod station_info_box;

Expand All @@ -15,5 +16,6 @@ pub use file_modal::{
FileModal,
FileType,
};
pub use map_exporter::MapExporter;
pub use settings_modal::SettingsModal;
pub use station_info_box::StationInfoBox;
2 changes: 2 additions & 0 deletions src/components/organisms/navbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
FileDownloader,
FileModal,
FileType,
MapExporter,
SettingsModal,
},
ErrorState,
Expand Down Expand Up @@ -61,6 +62,7 @@ pub fn Navbar() -> impl IntoView {
</div>
<div class="flex flex-row items-end space-x-3" >
<Button text="Advanced Settings" outlined=true can_focus=true on_click=Box::new(move |_| set_show_settings_modal(true))/>
<MapExporter/>
<FileDownloader/>
<Button text="Upload File" outlined=true can_focus=true on_click=Box::new(move |_| set_show_file_modal(true))/>
</div>
Expand Down
11 changes: 11 additions & 0 deletions src/models/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,17 @@ impl Map {
}
}

/// Return the map with all checkpoints removed.
pub fn without_checkpoints(&self) -> Self {
let mut map = self.clone();
for station in self.get_stations() {
if station.is_checkpoint() {
map.remove_station(station.get_id());
}
}
map
}

/// Use the A* algorithm to calculate the edges between all stations
/// quickly.
pub fn quickcalc_edges(&mut self) {
Expand Down
12 changes: 1 addition & 11 deletions src/utils/json/encode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,7 @@ pub fn map_to_json(graph: &Map, state: CanvasState) -> JSONMap {
edges: Vec::new(),
};

let mut graph = graph.clone();
let all_stations = graph
.get_stations()
.into_iter()
.cloned()
.collect::<Vec<_>>();
for station in all_stations {
if station.is_checkpoint() {
graph.remove_station(station.get_id());
}
}
let graph = graph.without_checkpoints();

// Add stations
json_map.stations = graph
Expand Down

0 comments on commit 7abd442

Please sign in to comment.