1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Serialize};
4use spatialrust_viewer::{InputAction, ViewerController, ViewerState, ViewportSize};
5use spatialrust_viz::{Camera, LayerId};
6
7use crate::{WebError, WebResult};
8
9pub const WEB_VIEWER_STATE_VERSION: u32 = 1;
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct WebViewerState {
16 pub version: u32,
18 pub revision: u64,
20 pub viewer: ViewerState,
22}
23
24impl WebViewerState {
25 pub fn try_new(viewer: ViewerState) -> WebResult<Self> {
27 let state = Self { version: WEB_VIEWER_STATE_VERSION, revision: 0, viewer };
28 state.validate()?;
29 Ok(state)
30 }
31
32 pub fn to_json(&self) -> WebResult<String> {
34 self.validate()?;
35 serde_json::to_string(self).map_err(|error| WebError::InvalidState(error.to_string()))
36 }
37
38 pub fn from_json(json: &str) -> WebResult<Self> {
40 let state: Self = serde_json::from_str(json)
41 .map_err(|error| WebError::InvalidState(error.to_string()))?;
42 state.validate()?;
43 Ok(state)
44 }
45
46 pub fn apply(&mut self, input: BrowserInput) -> WebResult<()> {
48 self.validate()?;
49 let action = input.into_action()?;
50 let next_revision = self
51 .revision
52 .checked_add(1)
53 .ok_or_else(|| WebError::InvalidState("Web viewer revision overflow".into()))?;
54 let mut next_viewer = self.viewer.clone();
55 ViewerController::default()
56 .apply(&mut next_viewer, action)
57 .map_err(|error| WebError::Viewer(error.to_string()))?;
58 self.viewer = next_viewer;
59 self.revision = next_revision;
60 Ok(())
61 }
62
63 pub fn validate(&self) -> WebResult<()> {
65 if self.version != WEB_VIEWER_STATE_VERSION {
66 return Err(WebError::InvalidState(format!(
67 "unsupported Web viewer state version {}",
68 self.version
69 )));
70 }
71 Camera::try_new(
72 self.viewer.camera.eye,
73 self.viewer.camera.target,
74 self.viewer.camera.up,
75 self.viewer.camera.projection,
76 )
77 .map_err(|error| WebError::InvalidState(error.to_string()))?;
78 ViewportSize::try_new(self.viewer.viewport.width, self.viewer.viewport.height)
79 .map_err(|error| WebError::InvalidState(error.to_string()))?;
80 let mut ids = BTreeSet::new();
81 for layer in &self.viewer.layers {
82 if !ids.insert(layer.id.as_str()) {
83 return Err(WebError::InvalidState(format!(
84 "duplicate layer `{}`",
85 layer.id.as_str()
86 )));
87 }
88 }
89 if let Some(selected) = &self.viewer.selected_layer {
90 if !ids.contains(selected.as_str()) {
91 return Err(WebError::InvalidState(
92 "selected layer is absent from portable layers".into(),
93 ));
94 }
95 }
96 Ok(())
97 }
98}
99
100#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
102#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
103pub enum BrowserInput {
104 Orbit {
106 delta_x: f32,
108 delta_y: f32,
110 },
111 Pan {
113 delta_x: f32,
115 delta_y: f32,
117 },
118 Zoom {
120 delta: f32,
122 },
123 Resize {
125 width: u32,
127 height: u32,
129 },
130 ToggleLayer {
132 layer_id: String,
134 },
135 SelectLayer {
137 layer_id: Option<String>,
139 },
140 DropFile {
142 path: String,
144 },
145}
146
147impl BrowserInput {
148 fn into_action(self) -> WebResult<InputAction> {
149 Ok(match self {
150 Self::Orbit { delta_x, delta_y } => InputAction::Orbit { delta_x, delta_y },
151 Self::Pan { delta_x, delta_y } => InputAction::Pan { delta_x, delta_y },
152 Self::Zoom { delta } => InputAction::Zoom(delta),
153 Self::Resize { width, height } => InputAction::Resize(
154 ViewportSize::try_new(width, height)
155 .map_err(|error| WebError::Viewer(error.to_string()))?,
156 ),
157 Self::ToggleLayer { layer_id } => InputAction::ToggleLayer(
158 LayerId::try_new(layer_id).map_err(|error| WebError::Viewer(error.to_string()))?,
159 ),
160 Self::SelectLayer { layer_id } => InputAction::SelectLayer(
161 layer_id
162 .map(LayerId::try_new)
163 .transpose()
164 .map_err(|error| WebError::Viewer(error.to_string()))?,
165 ),
166 Self::DropFile { path } => InputAction::FileDropped(path),
167 })
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use spatialrust_math::Vec3;
174 use spatialrust_viewer::{ViewerState, ViewportSize};
175 use spatialrust_viz::{Camera, Projection};
176
177 use super::{BrowserInput, WebViewerState};
178
179 fn state() -> WebViewerState {
180 WebViewerState::try_new(
181 ViewerState::try_new(
182 Camera::try_new(
183 Vec3::new(0.0, 0.0, 5.0),
184 Vec3::new(0.0, 0.0, 0.0),
185 Vec3::new(0.0, 1.0, 0.0),
186 Projection::Perspective { vertical_fov_radians: 1.0, near: 0.1, far: 100.0 },
187 )
188 .unwrap(),
189 ViewportSize::try_new(800, 600).unwrap(),
190 )
191 .unwrap(),
192 )
193 .unwrap()
194 }
195
196 #[test]
197 fn state_json_roundtrip_and_input_revision_are_exact() {
198 let mut state = state();
199 let json = state.to_json().unwrap();
200 assert_eq!(WebViewerState::from_json(&json).unwrap(), state);
201 state.apply(BrowserInput::Zoom { delta: 1.0 }).unwrap();
202 state.apply(BrowserInput::Resize { width: 1920, height: 1080 }).unwrap();
203 assert_eq!(state.revision, 2);
204 assert_eq!(state.viewer.viewport.width, 1920);
205 }
206
207 #[test]
208 fn unknown_fields_versions_and_invalid_input_fail_closed() {
209 let mut value: serde_json::Value =
210 serde_json::from_str(&state().to_json().unwrap()).unwrap();
211 value["extra"] = serde_json::json!(true);
212 assert!(WebViewerState::from_json(&value.to_string()).is_err());
213
214 let mut invalid_state = state();
215 invalid_state.version = 99;
216 assert!(invalid_state.validate().is_err());
217 assert!(invalid_state.apply(BrowserInput::Resize { width: 0, height: 1 }).is_err());
218 assert_eq!(invalid_state.revision, 0);
219
220 let mut state = state();
221 state.revision = u64::MAX;
222 let before = state.viewer.clone();
223 assert!(state.apply(BrowserInput::Zoom { delta: 1.0 }).is_err());
224 assert_eq!(state.viewer, before);
225 assert_eq!(state.revision, u64::MAX);
226 }
227}