Skip to main content

sl_map_web/routes/
notecard.rs

1//! Handler that resolves a USB notecard to a grid rectangle (for client-side
2//! preview rendering).
3
4use axum::Json;
5use axum::extract::{Multipart, State};
6use serde::Serialize;
7use sl_map_apis::region::usb_notecard_to_grid_rectangle;
8use sl_types::map::{GridRectangleLike as _, USBNotecard};
9
10use crate::auth::CurrentUser;
11use crate::error::Error;
12use crate::library;
13use crate::state::AppState;
14use crate::usb_notecard::parse_notecard_form;
15
16/// One waypoint with its resolved grid coordinates.
17#[derive(Debug, Clone, Serialize)]
18pub struct ResolvedWaypoint {
19    /// the region the waypoint refers to.
20    pub region_name: String,
21    /// resolved x grid coordinate of the region.
22    pub region_x: u32,
23    /// resolved y grid coordinate of the region.
24    pub region_y: u32,
25    /// in-region x coordinate of the waypoint (metres).
26    pub x: f32,
27    /// in-region y coordinate of the waypoint (metres).
28    pub y: f32,
29    /// in-region z coordinate of the waypoint (metres).
30    pub z: f32,
31}
32
33/// Response shape for `/api/notecard/derive-rectangle`.
34#[derive(Debug, Clone, Serialize)]
35pub struct DeriveResponse {
36    /// lower-left x grid coordinate (after border expansion).
37    pub lower_left_x: u32,
38    /// lower-left y grid coordinate (after border expansion).
39    pub lower_left_y: u32,
40    /// upper-right x grid coordinate (after border expansion).
41    pub upper_right_x: u32,
42    /// upper-right y grid coordinate (after border expansion).
43    pub upper_right_y: u32,
44    /// the resolved waypoints.
45    pub waypoints: Vec<ResolvedWaypoint>,
46}
47
48/// `POST /api/notecard/derive-rectangle` — parse the uploaded/pasted notecard
49/// and return the bounding rectangle (with optional border expansion) plus
50/// the resolved waypoints, suitable for a client-side preview overlay.
51///
52/// # Errors
53///
54/// Returns an error if the multipart form is malformed, the notecard
55/// cannot be parsed, or a referenced region cannot be resolved.
56pub async fn derive_rectangle(
57    user: CurrentUser,
58    State(state): State<AppState>,
59    multipart: Multipart,
60) -> Result<Json<DeriveResponse>, Error> {
61    let form = parse_notecard_form(multipart).await?;
62    let (border_north, border_south, border_east, border_west) = form.borders();
63    let notecard = match (form.notecard, form.notecard_id) {
64        (Some(nc), _) => nc,
65        (None, Some(id)) => {
66            // The same auth gate the library / render endpoints use — a
67            // caller who cannot read the notecard gets the indistinguishable
68            // NotFound, never the body.
69            let row = library::assert_can_read_notecard(&state.db, user.user_id, id).await?;
70            row.body.parse()?
71        }
72        (None, None) => return Err(Error::BadRequest("missing notecard".to_owned())),
73    };
74    let mut region_cache = state.region_cache.lock().await;
75    let rect = usb_notecard_to_grid_rectangle(&mut region_cache, &notecard)
76        .await?
77        .expanded_west(border_west)
78        .expanded_east(border_east)
79        .expanded_south(border_south)
80        .expanded_north(border_north);
81    let waypoints = resolve_waypoints(&mut region_cache, &notecard).await?;
82    drop(region_cache);
83    let lower_left = rect.lower_left_corner();
84    let upper_right = rect.upper_right_corner();
85    Ok(Json(DeriveResponse {
86        lower_left_x: lower_left.x(),
87        lower_left_y: lower_left.y(),
88        upper_right_x: upper_right.x(),
89        upper_right_y: upper_right.y(),
90        waypoints,
91    }))
92}
93
94/// Resolve each waypoint's region to grid coordinates. Returns an error if
95/// any region cannot be resolved.
96async fn resolve_waypoints(
97    region_cache: &mut sl_map_apis::region::RegionNameToGridCoordinatesCache,
98    notecard: &USBNotecard,
99) -> Result<Vec<ResolvedWaypoint>, Error> {
100    let mut out = Vec::with_capacity(notecard.waypoints().len());
101    for waypoint in notecard.waypoints() {
102        let region = waypoint.location().region_name();
103        let grid = region_cache
104            .get_grid_coordinates(region)
105            .await?
106            .ok_or_else(|| {
107                Error::BadRequest(format!(
108                    "could not resolve region `{}` to grid coordinates",
109                    region.to_owned().into_inner()
110                ))
111            })?;
112        let rc = waypoint.region_coordinates();
113        out.push(ResolvedWaypoint {
114            region_name: region.to_owned().into_inner(),
115            region_x: grid.x(),
116            region_y: grid.y(),
117            x: rc.x(),
118            y: rc.y(),
119            z: rc.z(),
120        });
121    }
122    Ok(out)
123}