supercluster_rs/
supercluster.rs

1use std::collections::HashMap;
2
3use geo_index::kdtree::KDTreeIndex;
4
5use crate::cluster::{ClusterId, ClusterInfo};
6use crate::error::SuperclusterError;
7use crate::options::SuperclusterOptions;
8use crate::tree::TreeWithData;
9use crate::util::{latitude_to_y, longitude_to_x};
10
11/// Create this via a [SuperclusterBuilder][crate::SuperclusterBuilder].
12#[derive(Debug, Clone)]
13pub struct Supercluster {
14    options: SuperclusterOptions,
15
16    /// Vector of KDBush structures for different zoom levels
17    trees: HashMap<usize, TreeWithData>,
18
19    /// Note: these points are in the user's original coordinate system (usually lon-lat).
20    points: Vec<(f64, f64)>,
21}
22
23impl Supercluster {
24    pub(crate) fn new(
25        points: Vec<(f64, f64)>,
26        trees: HashMap<usize, TreeWithData>,
27        options: SuperclusterOptions,
28    ) -> Self {
29        Self {
30            options,
31            trees,
32            points,
33        }
34    }
35
36    /// Get clusters within a given bounding box and zoom.
37    ///
38    /// Returns a vec of [ClusterInfo] objects which point into indices of the original input data.
39    pub fn get_clusters(
40        &self,
41        min_lng: f64,
42        min_lat: f64,
43        max_lng: f64,
44        max_lat: f64,
45        zoom: usize,
46    ) -> Vec<ClusterInfo> {
47        let mut min_lng = ((min_lng + 180.0) % 360.0 + 360.0) % 360.0 - 180.0;
48        let min_lat = min_lat.clamp(-90.0, 90.0);
49        let mut max_lng = if max_lng == 180.0 {
50            180.0
51        } else {
52            ((max_lng + 180.0) % 360.0 + 360.0) % 360.0 - 180.0
53        };
54        let max_lat = max_lat.clamp(-90.0, 90.0);
55
56        if max_lng - min_lng >= 360.0 {
57            min_lng = -180.0;
58            max_lng = 180.0;
59        } else if min_lng > max_lng {
60            let mut eastern_hem = self.get_clusters(min_lng, min_lat, 180.0, max_lat, zoom);
61            let mut western_hem = self.get_clusters(-180.0, min_lat, max_lng, max_lat, zoom);
62            eastern_hem.append(&mut western_hem);
63            return eastern_hem;
64        }
65
66        let tree_with_data = self.trees.get(&self.clamp_zoom(zoom)).unwrap();
67
68        // NOTE! it is intentional for max_lat to be passed to min_y and for min_lat to be passed
69        // to max_y. Apparently the spherical mercator coord system has a flipped y.
70        let ids = tree_with_data.tree.as_ref().range(
71            longitude_to_x(min_lng),
72            latitude_to_y(max_lat),
73            longitude_to_x(max_lng),
74            latitude_to_y(min_lat),
75        );
76
77        let data = tree_with_data.data();
78
79        let mut clusters = Vec::with_capacity(ids.len());
80        for id in ids {
81            let cluster_data = &data[id];
82
83            // If there's more than one point in this cluster, group them.
84            if cluster_data.num_points > 1 {
85                clusters.push(ClusterInfo::new_cluster(
86                    cluster_data.source_id,
87                    cluster_data.x,
88                    cluster_data.y,
89                    cluster_data.num_points,
90                ));
91            } else {
92                let (x, y) = self.points[id];
93                clusters.push(ClusterInfo::new_leaf(cluster_data.source_id, x, y))
94            }
95        }
96
97        clusters
98    }
99
100    /// Returns the children of a cluster (on the next zoom level) given its id.
101    ///
102    /// You can access a cluster's id via the [`ClusterInfo::id`] method.
103    pub fn get_children(
104        &self,
105        cluster_id: ClusterId,
106    ) -> Result<Vec<ClusterInfo>, SuperclusterError> {
107        let origin_id = self.get_origin_idx(cluster_id);
108        let origin_zoom = self.get_origin_zoom(cluster_id);
109
110        let tree_with_data = match self.trees.get(&origin_zoom) {
111            Some(tree_with_data) => tree_with_data,
112            None => return Err(SuperclusterError::NoClusterFound),
113        };
114
115        let data = tree_with_data.data();
116        let tree = tree_with_data.tree();
117        if origin_id >= data.len() {
118            return Err(SuperclusterError::NoClusterFound);
119        }
120
121        let r = self.options.radius
122            / (self.options.extent * usize::pow(2, (origin_zoom - 1).try_into().unwrap()) as f64);
123        let x = data[origin_id].x;
124        let y = data[origin_id].y;
125        let ids = tree.as_ref().within(x, y, r);
126        let mut children = vec![];
127
128        for id in ids {
129            let cluster_data = &data[id];
130
131            if cluster_data
132                .parent_id
133                .is_some_and(|parent_id| parent_id == cluster_id)
134            {
135                if cluster_data.num_points > 1 {
136                    children.push(ClusterInfo::new_cluster(
137                        cluster_data.source_id,
138                        cluster_data.x,
139                        cluster_data.y,
140                        cluster_data.num_points,
141                    ));
142                } else {
143                    let (x, y) = self.points[id];
144                    children.push(ClusterInfo::new_leaf(cluster_data.source_id, x, y))
145                }
146            }
147        }
148
149        if children.is_empty() {
150            return Err(SuperclusterError::NoClusterFound);
151        }
152
153        Ok(children)
154    }
155
156    /// Returns all the points of a cluster (given its cluster_id), with pagination support: limit
157    /// is the number of points to return (set to Infinity for all points), and offset is the
158    /// amount of points to skip (for pagination).
159    ///
160    /// You can access a cluster's id via the [`ClusterInfo::id`] method.
161    ///
162    /// If not provided, `limit` defaults to `10` and `offset` defaults to `0`.
163    pub fn get_leaves(
164        &self,
165        cluster_id: ClusterId,
166        limit: Option<usize>,
167        offset: Option<usize>,
168    ) -> Result<Vec<ClusterInfo>, SuperclusterError> {
169        let limit = limit.unwrap_or(10);
170        let offset = offset.unwrap_or(0);
171
172        let mut leaves = vec![];
173        self.append_leaves(&mut leaves, cluster_id, limit, offset, 0)?;
174
175        Ok(leaves)
176    }
177
178    // pub fn get_tile(self, z: usize, x: usize, y: usize) {
179    //     let tree = self.trees.get(&self.clamp_zoom(z)).unwrap();
180    //     let z2 = usize::pow(2, z.try_into().unwrap()) as f64;
181    //     let p = self.options.radius / self.options.extent;
182    //     let top = (y as f64 - p) / z2;
183    //     let bottom = (y as f64 + 1.0 + p) / z2;
184
185    //     todo!()
186    // }
187
188    /// Returns the zoom on which the cluster expands into several children (useful for "click to
189    /// zoom" feature) given the cluster's id.
190    ///
191    /// You can access a cluster's id via the [`ClusterInfo::id`] method.
192    pub fn get_cluster_expansion_zoom(
193        &self,
194        cluster_id: ClusterId,
195    ) -> Result<usize, SuperclusterError> {
196        let mut cluster_id = cluster_id;
197        let mut expansion_zoom = cluster_id.get_origin_zoom(self.points.len()) - 1;
198        while expansion_zoom <= self.options.max_zoom {
199            let children = self.get_children(cluster_id)?;
200            expansion_zoom += 1;
201            if children.len() != 1 {
202                break;
203            }
204            cluster_id = children[0].id();
205        }
206
207        Ok(expansion_zoom)
208    }
209
210    fn append_leaves(
211        &self,
212        result: &mut Vec<ClusterInfo>,
213        cluster_id: ClusterId,
214        limit: usize,
215        offset: usize,
216        skipped: usize,
217    ) -> Result<usize, SuperclusterError> {
218        let children = self.get_children(cluster_id)?;
219
220        let mut skipped = skipped;
221
222        for child in children {
223            if child.is_cluster() {
224                if skipped + child.count() <= offset {
225                    // skip the whole cluster
226                    skipped += child.count();
227                } else {
228                    // enter the cluster
229                    skipped = self.append_leaves(result, child.id(), limit, offset, skipped)?;
230                    // exit the cluster
231                }
232                skipped += 1;
233            } else if skipped < offset {
234                // skip a single point
235                skipped += 1;
236            } else {
237                // add a single point
238                result.push(child);
239            }
240
241            if result.len() == limit {
242                break;
243            }
244        }
245
246        Ok(skipped)
247    }
248
249    fn clamp_zoom(&self, zoom: usize) -> usize {
250        zoom.clamp(self.options.min_zoom, self.options.max_zoom + 1)
251    }
252
253    /// get index of the point from which the cluster originated
254    fn get_origin_idx(&self, cluster_id: ClusterId) -> usize {
255        cluster_id.get_origin_idx(self.points.len())
256    }
257
258    /// get zoom of the point from which the cluster originated
259    fn get_origin_zoom(&self, cluster_id: ClusterId) -> usize {
260        cluster_id.get_origin_zoom(self.points.len())
261    }
262}
263
264#[cfg(test)]
265mod test {
266    use crate::test::load_fixture::load_places;
267    use crate::SuperclusterBuilder;
268
269    #[test]
270    fn test_builder() {
271        let coords = load_places();
272        let mut builder = SuperclusterBuilder::new(coords.len());
273        for coord in coords {
274            builder.add(coord[0], coord[1]);
275        }
276        let supercluster = builder.finish();
277        let clusters = supercluster.get_clusters(-50., -50., 50., 50., 0);
278        dbg!(clusters.len());
279        dbg!(&clusters);
280        // dbg!(supercluster);
281    }
282}