atmos/os_lib/web_engine/
osm_embed.rs1extern crate alloc;
14use alloc::string::String;
15use alloc::vec::Vec;
16
17pub const TILE: i32 = 256;
19pub const MAX_ZOOM: u32 = 19;
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct Embed {
25 pub min_lon: f64,
27 pub min_lat: f64,
28 pub max_lon: f64,
29 pub max_lat: f64,
30 pub marker: Option<(f64, f64)>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub struct Plan {
37 pub zoom: u32,
38 pub tiles: Vec<(String, i32, i32)>,
40 pub marker: Option<(i32, i32)>,
42}
43
44pub fn parse_embed_url(url: &str) -> Option<Embed> {
46 let u = url.trim();
47 let rest = u
48 .strip_prefix("https://")
49 .or_else(|| u.strip_prefix("http://"))
50 .or_else(|| u.strip_prefix("//"))?;
51 let (host_path, query) = rest.split_once('?')?;
52 let host_ok = host_path.starts_with("www.openstreetmap.org/") || host_path.starts_with("openstreetmap.org/");
53 if !host_ok || !host_path.ends_with("/export/embed.html") {
54 return None;
55 }
56 let mut bbox: Option<[f64; 4]> = None;
57 let mut marker = None;
58 for kv in query.split('&') {
59 let kv = kv.strip_prefix("amp;").unwrap_or(kv);
61 let (k, v) = kv.split_once('=').unwrap_or((kv, ""));
62 let v = v.replace("%2C", ",").replace("%2c", ",");
63 match k {
64 "bbox" => {
65 let n: Vec<f64> = v.split(',').filter_map(|x| x.trim().parse::<f64>().ok()).collect();
66 if n.len() == 4 && n.iter().all(|x| x.is_finite()) {
67 bbox = Some([n[0], n[1], n[2], n[3]]);
68 }
69 }
70 "marker" => {
71 let n: Vec<f64> = v.split(',').filter_map(|x| x.trim().parse::<f64>().ok()).collect();
72 if n.len() == 2 && n.iter().all(|x| x.is_finite()) {
73 marker = Some((n[0], n[1]));
74 }
75 }
76 _ => {}
77 }
78 }
79 let [a, b, c, d] = bbox?;
80 Some(Embed {
81 min_lon: a.min(c),
82 min_lat: b.min(d),
83 max_lon: a.max(c),
84 max_lat: b.max(d),
85 marker,
86 })
87}
88
89pub fn project(lon: f64, lat: f64, z: u32) -> (f64, f64) {
91 let world = TILE as f64 * (1u64 << z) as f64;
92 let lat = lat.clamp(-85.051_128_78, 85.051_128_78);
93 let x = (lon + 180.0) / 360.0 * world;
94 let rad = lat * core::f64::consts::PI / 180.0;
95 let y = (1.0 - libm::log(libm::tan(rad) + 1.0 / libm::cos(rad)) / core::f64::consts::PI) / 2.0 * world;
96 (x, y)
97}
98
99pub fn fit_zoom(e: &Embed, w: i32, h: i32) -> u32 {
101 let mut best = 0;
102 for z in 0..=MAX_ZOOM {
103 let (x0, y0) = project(e.min_lon, e.max_lat, z);
104 let (x1, y1) = project(e.max_lon, e.min_lat, z);
105 if (x1 - x0) <= w as f64 && (y1 - y0) <= h as f64 {
106 best = z;
107 } else {
108 break;
109 }
110 }
111 best
112}
113
114pub fn plan(e: &Embed, w: i32, h: i32) -> Option<Plan> {
116 if w <= 0 || h <= 0 {
117 return None;
118 }
119 let z = fit_zoom(e, w, h);
120 let (x0, y0) = project(e.min_lon, e.max_lat, z);
121 let (x1, y1) = project(e.max_lon, e.min_lat, z);
122 let (cx, cy) = ((x0 + x1) / 2.0, (y0 + y1) / 2.0);
123 let left = libm::floor(cx - w as f64 / 2.0) as i64;
124 let top = libm::floor(cy - h as f64 / 2.0) as i64;
125 let n = 1i64 << z;
126 let t = TILE as i64;
127 let tx0 = left.div_euclid(t);
128 let tx1 = (left + w as i64 - 1).div_euclid(t);
129 let ty0 = top.div_euclid(t).max(0);
130 let ty1 = (top + h as i64 - 1).div_euclid(t).min(n - 1);
131 let mut tiles = Vec::new();
132 for ty in ty0..=ty1 {
133 for tx in tx0..=tx1 {
134 let wx = tx.rem_euclid(n);
136 let url = alloc::format!("https://tile.openstreetmap.org/{}/{}/{}.png", z, wx, ty);
137 tiles.push((url, (tx * t - left) as i32, (ty * t - top) as i32));
138 }
139 }
140 let marker = e.marker.map(|(lat, lon)| {
141 let (mx, my) = project(lon, lat, z);
142 ((libm::floor(mx) as i64 - left) as i32, (libm::floor(my) as i64 - top) as i32)
143 });
144 Some(Plan { zoom: z, tiles, marker })
145}