diff --git a/src/lib.rs b/src/lib.rs index 7a2893e..a374b02 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,9 @@ pub struct Supercluster { /// Clusters metadata. cluster_props: Vec, + + /// Use non-geospatial coordinates? + use_non_geospatial_coords: bool, } impl Supercluster { @@ -85,6 +88,35 @@ impl Supercluster { stride: 6, points: vec![], cluster_props: vec![], + use_non_geospatial_coords: false, + } + } + + /// Create a new instance of `Supercluster` with the specified configuration settings. + /// + /// This should work with non-geospatial points and will not perform any mercator transform on + /// the data. + /// + /// # Arguments + /// + /// - `options`: The configuration options for Supercluster. + /// + /// # Returns + /// + /// A new `Supercluster` instance with the given configuration. + pub fn new_non_geospatial(options: Options) -> Self { + let capacity = options.max_zoom + 1; + let trees: Vec = (0..capacity + 1) + .map(|_| KDBush::new(0, options.node_size)) + .collect(); + + Supercluster { + trees, + options, + stride: 6, + points: vec![], + cluster_props: vec![], + use_non_geospatial_coords: true, } } @@ -116,11 +148,19 @@ impl Supercluster { None => continue, }; - // Longitude - data.push(lng_x(coordinates[0])); + if self.use_non_geospatial_coords { + // X Coordinate + data.push(coordinates[0]); + + // Y Coordinate + data.push(coordinates[1]); + } else { + // Longitude + data.push(lng_x(coordinates[0])); - // Latitude - data.push(lat_y(coordinates[1])); + // Latitude + data.push(lat_y(coordinates[1])); + } // The last zoom the point was processed at data.push(f64::INFINITY); @@ -161,39 +201,50 @@ impl Supercluster { /// /// A vector of GeoJSON features representing the clusters within the specified bounding box and zoom level. pub fn get_clusters(&self, bbox: [f64; 4], zoom: u8) -> Vec { - let mut min_lng = ((((bbox[0] + 180.0) % 360.0) + 360.0) % 360.0) - 180.0; - let min_lat = bbox[1].clamp(-90.0, 90.0); - let mut max_lng = if bbox[2] == 180.0 { - 180.0 - } else { - ((((bbox[2] + 180.0) % 360.0) + 360.0) % 360.0) - 180.0 - }; - let max_lat = bbox[3].clamp(-90.0, 90.0); + let tree = &self.trees[self.limit_zoom(zoom)]; + let ids = match self.use_non_geospatial_coords { + true => tree.range(bbox[0], bbox[1], bbox[2], bbox[3]), + false => { + let mut min_lng = ((((bbox[0] + 180.0) % 360.0) + 360.0) % 360.0) - 180.0; + let min_lat = bbox[1].clamp(-90.0, 90.0); + let mut max_lng = if bbox[2] == 180.0 { + 180.0 + } else { + ((((bbox[2] + 180.0) % 360.0) + 360.0) % 360.0) - 180.0 + }; + let max_lat = bbox[3].clamp(-90.0, 90.0); - if bbox[2] - bbox[0] >= 360.0 { - min_lng = -180.0; - max_lng = 180.0; - } else if min_lng > max_lng { - let eastern_hem = self.get_clusters([min_lng, min_lat, 180.0, max_lat], zoom); - let western_hem = self.get_clusters([-180.0, min_lat, max_lng, max_lat], zoom); + if bbox[2] - bbox[0] >= 360.0 { + min_lng = -180.0; + max_lng = 180.0; + } else if min_lng > max_lng { + let eastern_hem = self.get_clusters([min_lng, min_lat, 180.0, max_lat], zoom); + let western_hem = self.get_clusters([-180.0, min_lat, max_lng, max_lat], zoom); - return eastern_hem.into_iter().chain(western_hem).collect(); - } + return eastern_hem.into_iter().chain(western_hem).collect(); + } + + tree.range( + lng_x(min_lng), + lat_y(max_lat), + lng_x(max_lng), + lat_y(min_lat), + ) + } + }; - let tree = &self.trees[self.limit_zoom(zoom)]; - let ids = tree.range( - lng_x(min_lng), - lat_y(max_lat), - lng_x(max_lng), - lat_y(min_lat), - ); let mut clusters = Vec::new(); for id in ids { let k = self.stride * id; clusters.push(if tree.data[k + OFFSET_NUM] > 1.0 { - get_cluster_json(&tree.data, k, &self.cluster_props) + get_cluster_json( + &tree.data, + k, + &self.cluster_props, + self.use_non_geospatial_coords, + ) } else { self.points[tree.data[k + OFFSET_ID] as usize].clone() }); @@ -244,7 +295,12 @@ impl Supercluster { if data[k + OFFSET_PARENT] == (cluster_id as f64) { if data[k + OFFSET_NUM] > 1.0 { - children.push(get_cluster_json(data, k, &self.cluster_props)); + children.push(get_cluster_json( + data, + k, + &self.cluster_props, + self.use_non_geospatial_coords, + )); } else { let point_id = data[k + OFFSET_ID] as usize; @@ -490,8 +546,13 @@ impl Supercluster { match p.geometry.as_ref() { Some(geometry) => { if let Point(coordinates) = &geometry.value { - px = lng_x(coordinates[0]); - py = lat_y(coordinates[1]); + if self.use_non_geospatial_coords { + px = coordinates[0]; + py = coordinates[1]; + } else { + px = lng_x(coordinates[0]); + py = lat_y(coordinates[1]); + } } else { continue; } @@ -679,8 +740,16 @@ impl Supercluster { /// # Returns /// /// A GeoJSON feature representing a cluster. -fn get_cluster_json(data: &[f64], i: usize, cluster_props: &[JsonObject]) -> Feature { - let geometry = Geometry::new(Point(vec![x_lng(data[i]), y_lat(data[i + 1])])); +fn get_cluster_json( + data: &[f64], + i: usize, + cluster_props: &[JsonObject], + use_non_geospatial_coords: bool, +) -> Feature { + let geometry = match use_non_geospatial_coords { + true => Geometry::new(Point(vec![data[i], data[i + 1]])), + false => Geometry::new(Point(vec![x_lng(data[i]), y_lat(data[i + 1])])), + }; Feature { id: Some(Id::String(data[i + OFFSET_ID].to_string())), @@ -836,7 +905,7 @@ mod tests { json!("0".to_string()), ); - let result = get_cluster_json(&data, i, &[cluster_props]); + let result = get_cluster_json(&data, i, &[cluster_props], false); assert_eq!(result.id, Some(Id::String("0".to_string()))); @@ -873,7 +942,7 @@ mod tests { let i = 0; let cluster_props = vec![]; - let result = get_cluster_json(&data, i, &cluster_props); + let result = get_cluster_json(&data, i, &cluster_props, false); assert_eq!(result.id, Some(Id::String("0".to_string()))); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2fca0e6..a23c68f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -34,3 +34,10 @@ pub fn load_tile_places_with_min_5() -> FeatureCollection { serde_json::from_str(&json_string).expect("places-z0-0-0-min5.json was not parsed") } + +pub fn load_non_geospatial() -> Vec { + let file_path = Path::new("./tests/common/non-geospatial.json"); + let json_string = fs::read_to_string(file_path).expect("non-geospatial.json was not found"); + + serde_json::from_str(&json_string).expect("non-geospatial.json was not parsed") +} diff --git a/tests/common/non-geospatial.json b/tests/common/non-geospatial.json new file mode 100644 index 0000000..c7a6ad7 --- /dev/null +++ b/tests/common/non-geospatial.json @@ -0,0 +1,83 @@ +[ + { + "type": "Feature", + "properties": { + "name": "Feature A" + }, + "geometry": { + "type": "Point", + "coordinates": [10.0, 10.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature B" + }, + "geometry": { + "type": "Point", + "coordinates": [15.0, 15.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature C" + }, + "geometry": { + "type": "Point", + "coordinates": [20.0, 20.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature D" + }, + "geometry": { + "type": "Point", + "coordinates": [50.0, 50.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature E" + }, + "geometry": { + "type": "Point", + "coordinates": [181.0, 541.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature F" + }, + "geometry": { + "type": "Point", + "coordinates": [997.0, 800.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature G" + }, + "geometry": { + "type": "Point", + "coordinates": [998.0, 800.0] + } + }, + { + "type": "Feature", + "properties": { + "name": "Feature H" + }, + "geometry": { + "type": "Point", + "coordinates": [999.0, 800.0] + } + } +] + diff --git a/tests/supercluster_integration_test.rs b/tests/supercluster_integration_test.rs index 88ed16f..2a1a49c 100644 --- a/tests/supercluster_integration_test.rs +++ b/tests/supercluster_integration_test.rs @@ -1,6 +1,8 @@ mod common; -use common::{get_options, load_places, load_tile_places, load_tile_places_with_min_5}; +use common::{ + get_options, load_non_geospatial, load_places, load_tile_places, load_tile_places_with_min_5, +}; use geojson::{Feature, Geometry, JsonObject, Value::Point}; use serde_json::json; use supercluster::Supercluster; @@ -192,3 +194,17 @@ fn test_does_not_crash_on_weird_bbox_values() { 61 ); } + +#[test] +fn test_non_geospatial() { + let mut cluster = Supercluster::new_non_geospatial(get_options(500.0, 32.0, 2, 16)); + let index = cluster.load(load_non_geospatial()); + + let clusters = index.get_clusters([0.0, 0.0, 1000.0, 1000.0], 0); + + assert_eq!(clusters.len(), 4); + assert_eq!(clusters[0].property("point_count").unwrap(), 3); + assert_eq!(clusters[1].property("point_count"), None); + assert_eq!(clusters[2].property("point_count"), None); + assert_eq!(clusters[3].property("point_count").unwrap(), 3); +}