widgetry/tools/
url.rs
1use anyhow::Result;
2
3use crate::EventCtx;
4use geom::{GPSBounds, LonLat, Pt2D};
5
6pub struct URLManager;
9
10impl URLManager {
11 pub fn update_url_free_param(free_param: String) {
14 must_update_url(Box::new(move |url| change_url_free_param(url, &free_param)))
15 }
16
17 pub fn update_url_param(key: String, value: String) {
20 must_update_url(Box::new(move |url| change_url_param(url, &key, &value)))
21 }
22
23 pub fn get_cam_param(ctx: &EventCtx, gps_bounds: &GPSBounds) -> String {
27 let center = ctx.canvas.center_to_map_pt().to_gps(gps_bounds);
28
29 let earth_circumference_equator = 40_075_016.686;
31 let log_arg =
32 earth_circumference_equator * center.y().to_radians().cos() * ctx.canvas.cam_zoom;
33 let zoom_lvl = log_arg.log2() - 8.0;
34
35 format!("{:.2}/{:.5}/{:.5}", zoom_lvl, center.y(), center.x())
37 }
38
39 pub fn update_url_cam(ctx: &EventCtx, gps_bounds: &GPSBounds) {
43 let cam = URLManager::get_cam_param(ctx, gps_bounds);
44 must_update_url(Box::new(move |url| change_url_param(url, "--cam", &cam)))
45 }
46
47 pub fn change_camera(ctx: &mut EventCtx, raw: Option<&String>, gps_bounds: &GPSBounds) -> bool {
51 if let Some((pt, zoom)) =
52 raw.and_then(|raw| URLManager::parse_center_camera(raw, gps_bounds))
53 {
54 ctx.canvas.cam_zoom = zoom;
55 ctx.canvas.center_on_map_pt(pt);
56 true
57 } else {
58 false
59 }
60 }
61
62 fn parse_center_camera(raw: &str, gps_bounds: &GPSBounds) -> Option<(Pt2D, f64)> {
66 let parts: Vec<&str> = raw.split('/').collect();
67 if parts.len() != 3 {
68 return None;
69 }
70 let zoom_lvl = parts[0].parse::<f64>().ok()?;
71 let lat = parts[1].parse::<f64>().ok()?;
72 let lon = parts[2].parse::<f64>().ok()?;
73 let gps = LonLat::new(lon, lat);
74 if !gps_bounds.contains(gps) {
75 return None;
76 }
77 let pt = gps.to_pt(gps_bounds);
78
79 let earth_circumference_equator = 40_075_016.686;
82 let horiz_meters_per_pixel =
83 earth_circumference_equator * gps.y().to_radians().cos() / 2.0_f64.powf(zoom_lvl + 8.0);
84
85 let cam_zoom = 1.0 / horiz_meters_per_pixel;
93
94 Some((pt, cam_zoom))
95 }
96}
97
98fn must_update_url(transform: Box<dyn Fn(String) -> String>) {
99 if let Err(err) = update_url(transform) {
100 warn!("Couldn't update URL: {}", err);
101 }
102}
103
104#[allow(unused_variables)]
105fn update_url(transform: Box<dyn Fn(String) -> String>) -> Result<()> {
106 #[cfg(target_arch = "wasm32")]
107 {
108 let window = web_sys::window().ok_or(anyhow!("no window?"))?;
109 let url = window.location().href().map_err(|err| {
110 anyhow!(err
111 .as_string()
112 .unwrap_or("window.location.href failed".to_string()))
113 })?;
114 let new_url = (transform)(url);
115
116 let history = window.history().map_err(|err| {
120 anyhow!(err
121 .as_string()
122 .unwrap_or("window.history failed".to_string()))
123 })?;
124 history
125 .replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url))
126 .map_err(|err| {
127 anyhow!(err
128 .as_string()
129 .unwrap_or("window.history.replace_state failed".to_string()))
130 })?;
131 }
132 Ok(())
133}
134
135fn change_url_free_param(url: String, free_param: &str) -> String {
136 let url_parts = url.split('?').collect::<Vec<_>>();
139 if url_parts.len() == 1 {
140 return format!("{}?{}", url, free_param);
141 }
142 let mut query_params = String::new();
143 let mut found_free = false;
144 let mut first = true;
145 for x in url_parts[1].split('&') {
146 if !first {
147 query_params.push('&');
148 }
149 first = false;
150
151 if x.starts_with("--") {
152 query_params.push_str(x);
153 } else if !found_free {
154 query_params.push_str(free_param);
156 found_free = true;
157 } else {
158 query_params.push_str(x);
159 }
160 }
161 if !found_free {
162 if !first {
163 query_params.push('&');
164 }
165 query_params.push_str(free_param);
166 }
167
168 format!("{}?{}", url_parts[0], query_params)
169}
170
171fn change_url_param(url: String, key: &str, value: &str) -> String {
172 let url_parts = url.split('?').collect::<Vec<_>>();
175 if url_parts.len() == 1 {
176 return format!("{}?{}={}", url, key, value);
177 }
178 let mut query_params = String::new();
179 let mut found_key = false;
180 let mut first = true;
181 for x in url_parts[1].split('&') {
182 if !first {
183 query_params.push('&');
184 }
185 first = false;
186
187 if x.starts_with(key) {
188 query_params.push_str(&format!("{}={}", key, value));
189 found_key = true;
190 } else {
191 query_params.push_str(x);
192 }
193 }
194 if !found_key {
195 if !first {
196 query_params.push('&');
197 }
198 query_params.push_str(&format!("{}={}", key, value));
199 }
200
201 format!("{}?{}", url_parts[0], query_params)
202}
203
204#[cfg(test)]
205mod tests {
206 #[test]
207 fn test_change_url_free_param() {
208 use super::change_url_free_param;
209
210 assert_eq!(
211 "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin",
212 change_url_free_param(
213 "http://0.0.0.0:8000/?--dev".to_string(),
214 "seattle/maps/montlake.bin"
215 )
216 );
217 assert_eq!(
218 "http://0.0.0.0:8000/?--dev&seattle/maps/qa.bin",
219 change_url_free_param(
220 "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin".to_string(),
221 "seattle/maps/qa.bin"
222 )
223 );
224 assert_eq!(
225 "http://0.0.0.0:8000?seattle/maps/montlake.bin",
226 change_url_free_param(
227 "http://0.0.0.0:8000".to_string(),
228 "seattle/maps/montlake.bin"
229 )
230 );
231 }
232
233 #[test]
234 fn test_change_url_param() {
235 use super::change_url_param;
236
237 assert_eq!(
238 "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin&--cam=16.6/53.78449/-1.70701",
239 change_url_param(
240 "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin".to_string(),
241 "--cam",
242 "16.6/53.78449/-1.70701"
243 )
244 );
245 assert_eq!(
246 "http://0.0.0.0:8000?--cam=16.6/53.78449/-1.70701",
247 change_url_param(
248 "http://0.0.0.0:8000".to_string(),
249 "--cam",
250 "16.6/53.78449/-1.70701"
251 )
252 );
253 }
254}