leptos_leaflet/components/
quad_tile_layer.rs

1use leptos::logging::{warn, error};
2use leptos::prelude::*;
3use wasm_bindgen::prelude::*;
4
5use crate::core::JsStoredValue;
6
7use super::LeafletMapContext;
8
9/// Converts tile coordinates (x, y, z) to a quadkey string.
10/// Based on Microsoft's QuadKey algorithm.
11/// 
12/// Clamps zoom level to 31 if it's too high (>= 32) to prevent overflow.
13fn tile_to_quadkey(x: u32, y: u32, z: u32) -> String {
14    let mut quadkey = String::new();
15    
16    // Clamp zoom level to prevent overflow for very high zoom levels
17    let safe_z = if z >= 32 {
18        warn!("Zoom level {} is too high for quadkey calculation, clamping to 31", z);
19        31
20    } else {
21        z
22    };
23    
24    for i in (1..=safe_z).rev() {
25        let mut digit = 0;
26        let mask = 1 << (i - 1);
27        
28        if (x & mask) != 0 {
29            digit += 1;
30        }
31        if (y & mask) != 0 {
32            digit += 2;
33        }
34        
35        // digit can only be 0, 1, 2, or 3, so char::from_digit will always succeed
36        quadkey.push(char::from_digit(digit, 10).unwrap_or('0'));
37    }
38    
39    quadkey
40}
41
42/// A quad tile layer component that uses quadkey-based URLs.
43/// Instead of the standard {z}/{x}/{y} pattern, this component
44/// expects URLs with a {q} placeholder for the quadkey.
45/// 
46/// # Example
47/// 
48/// ```rust
49/// use leptos::prelude::*;
50/// use leptos_leaflet::prelude::*;
51/// 
52/// #[component]
53/// pub fn MapWithQuadTiles() -> impl IntoView {
54///     view! {
55///         <MapContainer style="height: 400px" center=Position::new(51.505, -0.09) zoom=13.0>
56///             <QuadTileLayer 
57///                 url="https://example.com/tiles/{q}.png" 
58///                 attribution="&copy; Example Tile Provider"/>
59///         </MapContainer>
60///     }
61/// }
62/// ```
63/// 
64/// The quadkey format is used by Microsoft Bing Maps and some other tile providers.
65/// Each tile is identified by a quadkey string that represents the tile's location
66/// in the quad tree, rather than separate x/y/z coordinates.
67#[component(transparent)]
68pub fn QuadTileLayer(
69    #[prop(into)] url: String,
70    #[prop(into, optional)] attribution: String,
71    #[prop(optional)] bring_to_front: bool,
72    #[prop(optional)] bring_to_back: bool,
73    #[prop(default = 0.0)] min_zoom: f64,
74    #[prop(default = 18.0)] max_zoom: f64,
75) -> impl IntoView {
76    let map_context = use_context::<LeafletMapContext>().expect("map context not found");
77    
78    // Store the closure to prevent memory leaks
79    let get_tile_url_closure: JsStoredValue<Option<Closure<dyn Fn(JsValue) -> String>>> = 
80        JsStoredValue::new_local(None);
81
82    Effect::new(move |_| {
83        if let Some(map) = map_context.map() {
84            // Create tile layer options
85            let options = leaflet::TileLayerOptions::default();
86            if !attribution.is_empty() {
87                options.set_attribution(attribution.to_string());
88            }
89            options.set_min_zoom(min_zoom);
90            options.set_max_zoom(max_zoom);
91            
92            // Create a standard tile layer first
93            let map_layer = leaflet::TileLayer::new_options(&url, &options);
94            
95            // Override the getTileUrl method to use quadkey
96            let url_pattern = url.clone();
97            let closure = Closure::wrap(Box::new(move |coords: JsValue| -> String {
98                // Extract x, y, z from coords object
99                let x = js_sys::Reflect::get(&coords, &JsValue::from_str("x"))
100                    .unwrap_or(JsValue::from(0))
101                    .as_f64()
102                    .unwrap_or(0.0) as u32;
103                let y = js_sys::Reflect::get(&coords, &JsValue::from_str("y"))
104                    .unwrap_or(JsValue::from(0))
105                    .as_f64()
106                    .unwrap_or(0.0) as u32;
107                let z = js_sys::Reflect::get(&coords, &JsValue::from_str("z"))
108                    .unwrap_or(JsValue::from(0))
109                    .as_f64()
110                    .unwrap_or(0.0) as u32;
111                
112                let quadkey = tile_to_quadkey(x, y, z);
113                url_pattern.replace("{q}", &quadkey)
114            }) as Box<dyn Fn(JsValue) -> String>);
115            
116            // Override the getTileUrl method on the layer instance
117            if let Err(e) = js_sys::Reflect::set(
118                &map_layer,
119                &JsValue::from_str("getTileUrl"),
120                closure.as_ref().unchecked_ref(),
121            ) {
122                error!("Failed to set getTileUrl method: {:?}", e);
123                return;
124            }
125            
126            // Store the closure to prevent it from being dropped
127            get_tile_url_closure.set_value(Some(closure));
128            
129            map_layer.add_to(&map);
130
131            match (bring_to_front, bring_to_back) {
132                (true, true) => warn!("The parameters are set to bring the layer to front and back at the same time. Ignoring these parameters..."),
133                (true, false) => {map_layer.bring_to_front();}
134                (false, true) => {map_layer.bring_to_back();}
135                (false, false) => (),
136            }
137
138            let map_layer = JsStoredValue::new_local(map_layer);
139
140            on_cleanup(move || {
141                map_layer.with_value(|v| v.remove());
142            });
143        }
144    });
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_tile_to_quadkey() {
153        // Test cases based on Microsoft's QuadKey documentation
154        // https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system
155        
156        // Level 1 tests
157        assert_eq!(tile_to_quadkey(0, 0, 1), "0");
158        assert_eq!(tile_to_quadkey(1, 0, 1), "1");
159        assert_eq!(tile_to_quadkey(0, 1, 1), "2");
160        assert_eq!(tile_to_quadkey(1, 1, 1), "3");
161        
162        // Level 2 tests  
163        assert_eq!(tile_to_quadkey(2, 1, 2), "12");
164        assert_eq!(tile_to_quadkey(0, 2, 2), "20");
165        
166        // Level 3 test - example from Microsoft docs
167        assert_eq!(tile_to_quadkey(3, 5, 3), "213");
168        
169        // Level 0 should return empty string
170        assert_eq!(tile_to_quadkey(0, 0, 0), "");
171        
172        // Test clamping for very high zoom levels - should return quadkey for level 31
173        let result_32 = tile_to_quadkey(0, 0, 32);
174        let result_31 = tile_to_quadkey(0, 0, 31);
175        assert_eq!(result_32, result_31);
176        assert!(!result_32.is_empty()); // Should not be empty anymore
177        
178        let result_50 = tile_to_quadkey(1, 1, 50);
179        let result_31_same_coords = tile_to_quadkey(1, 1, 31);
180        assert_eq!(result_50, result_31_same_coords);
181        assert!(!result_50.is_empty()); // Should not be empty anymore
182    }
183}