From a541ba1d88a4c0bfef9ab7fc3138dd5b6f804eb4 Mon Sep 17 00:00:00 2001 From: Chaim Peck Date: Wed, 25 Dec 2024 15:23:24 -0500 Subject: [PATCH 1/2] adds non-geospatial option --- src/lib.rs | 139 ++++++++++++++++++------- tests/common/mod.rs | 7 ++ tests/common/non-geospatial.json | 83 +++++++++++++++ tests/supercluster_integration_test.rs | 18 +++- 4 files changed, 211 insertions(+), 36 deletions(-) create mode 100644 tests/common/non-geospatial.json 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); +} From 4db7e402b285dc7e0d4016b7a60db6e923b0abb7 Mon Sep 17 00:00:00 2001 From: Chaim Peck Date: Mon, 30 Dec 2024 20:49:37 -0500 Subject: [PATCH 2/2] updates from PR comments and adds DataRange for cartesian coordinates --- src/lib.rs | 140 ++++++++++-------- .../{non-geospatial.json => cartesian.json} | 0 tests/common/mod.rs | 21 ++- tests/supercluster_integration_test.rs | 37 +++-- tests/util.rs | 33 +++++ 5 files changed, 151 insertions(+), 80 deletions(-) rename tests/common/{non-geospatial.json => cartesian.json} (100%) create mode 100644 tests/util.rs diff --git a/src/lib.rs b/src/lib.rs index a374b02..e80eb26 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,40 @@ const OFFSET_NUM: usize = 5; /// An offset index used to access the properties associated with a cluster in the data arrays. const OFFSET_PROP: usize = 6; +/// The range of the incoming data if choosing the cartesian coordinate system +#[derive(Clone, Debug)] +pub struct DataRange { + pub min_x: f64, + pub min_y: f64, + pub max_x: f64, + pub max_y: f64, +} + +impl DataRange { + fn normalize_x(&self, x: f64) -> f64 { + (x - self.min_x) / (self.max_x - self.min_x) + } + + fn normalize_y(&self, y: f64) -> f64 { + (y - self.min_y) / (self.max_y - self.min_y) + } + + fn denormalize_x(&self, x_scaled: f64) -> f64 { + x_scaled * (self.max_x - self.min_x) + self.min_x + } + + fn denormalize_y(&self, y_scaled: f64) -> f64 { + y_scaled * (self.max_y - self.min_y) + self.min_y + } +} + +/// Coordinate system for clustering. +#[derive(Clone, Debug)] +pub enum CoordinateSystem { + LatLng, // Choose this for geo-spatial data + Cartesian { data_range: DataRange }, // Chose this for non-geospatial (i.e. microscopy, etc.) data +} + /// Supercluster configuration options. #[derive(Clone, Debug)] pub struct Options { @@ -42,6 +76,9 @@ pub struct Options { /// Size of the KD-tree leaf node, affects performance. pub node_size: usize, + + /// The type of coordinate system for clustering: lat/lng or cartesian. + pub coordinate_system: CoordinateSystem, } #[derive(Clone, Debug)] @@ -61,9 +98,6 @@ pub struct Supercluster { /// Clusters metadata. cluster_props: Vec, - - /// Use non-geospatial coordinates? - use_non_geospatial_coords: bool, } impl Supercluster { @@ -88,35 +122,6 @@ 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, } } @@ -148,19 +153,22 @@ impl Supercluster { None => continue, }; - if self.use_non_geospatial_coords { - // X Coordinate - data.push(coordinates[0]); + match &self.options.coordinate_system { + CoordinateSystem::Cartesian { data_range } => { + // X Coordinate + data.push(data_range.normalize_x(coordinates[0])); - // Y Coordinate - data.push(coordinates[1]); - } else { - // Longitude - data.push(lng_x(coordinates[0])); + // Y Coordinate + data.push(data_range.normalize_y(coordinates[1])); + } + CoordinateSystem::LatLng => { + // 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); @@ -202,9 +210,14 @@ 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 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 ids = match &self.options.coordinate_system { + CoordinateSystem::Cartesian { data_range } => tree.range( + data_range.normalize_x(bbox[0]), + data_range.normalize_y(bbox[1]), + data_range.normalize_x(bbox[2]), + data_range.normalize_y(bbox[3]), + ), + CoordinateSystem::LatLng => { 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 { @@ -243,7 +256,7 @@ impl Supercluster { &tree.data, k, &self.cluster_props, - self.use_non_geospatial_coords, + &self.options.coordinate_system, ) } else { self.points[tree.data[k + OFFSET_ID] as usize].clone() @@ -299,7 +312,7 @@ impl Supercluster { data, k, &self.cluster_props, - self.use_non_geospatial_coords, + &self.options.coordinate_system, )); } else { let point_id = data[k + OFFSET_ID] as usize; @@ -546,12 +559,15 @@ impl Supercluster { match p.geometry.as_ref() { Some(geometry) => { if let Point(coordinates) = &geometry.value { - if self.use_non_geospatial_coords { - px = coordinates[0]; - py = coordinates[1]; - } else { - px = lng_x(coordinates[0]); - py = lat_y(coordinates[1]); + match &self.options.coordinate_system { + CoordinateSystem::Cartesian { data_range } => { + px = data_range.normalize_x(coordinates[0]); + py = data_range.normalize_y(coordinates[1]); + } + CoordinateSystem::LatLng => { + px = lng_x(coordinates[0]); + py = lat_y(coordinates[1]); + } } } else { continue; @@ -744,11 +760,14 @@ fn get_cluster_json( data: &[f64], i: usize, cluster_props: &[JsonObject], - use_non_geospatial_coords: bool, + coordinate_system: &CoordinateSystem, ) -> 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])])), + let geometry = match coordinate_system { + CoordinateSystem::Cartesian { data_range } => Geometry::new(Point(vec![ + data_range.denormalize_x(data[i]), + data_range.denormalize_y(data[i + 1]), + ])), + CoordinateSystem::LatLng => Geometry::new(Point(vec![x_lng(data[i]), y_lat(data[i + 1])])), }; Feature { @@ -866,6 +885,7 @@ mod tests { min_zoom: 0, min_points: 2, node_size: 64, + coordinate_system: CoordinateSystem::LatLng, }) } @@ -905,7 +925,7 @@ mod tests { json!("0".to_string()), ); - let result = get_cluster_json(&data, i, &[cluster_props], false); + let result = get_cluster_json(&data, i, &[cluster_props], &CoordinateSystem::LatLng); assert_eq!(result.id, Some(Id::String("0".to_string()))); @@ -942,7 +962,7 @@ mod tests { let i = 0; let cluster_props = vec![]; - let result = get_cluster_json(&data, i, &cluster_props, false); + let result = get_cluster_json(&data, i, &cluster_props, &CoordinateSystem::LatLng); assert_eq!(result.id, Some(Id::String("0".to_string()))); diff --git a/tests/common/non-geospatial.json b/tests/common/cartesian.json similarity index 100% rename from tests/common/non-geospatial.json rename to tests/common/cartesian.json diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a23c68f..e99a784 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,8 +1,14 @@ use geojson::{Feature, FeatureCollection}; use std::{fs, path::Path}; -use supercluster::Options; - -pub fn get_options(radius: f64, extent: f64, min_points: u8, max_zoom: u8) -> Options { +use supercluster::{CoordinateSystem, Options}; + +pub fn get_options( + radius: f64, + extent: f64, + min_points: u8, + max_zoom: u8, + coordinate_system: CoordinateSystem, +) -> Options { Options { radius, extent, @@ -10,6 +16,7 @@ pub fn get_options(radius: f64, extent: f64, min_points: u8, max_zoom: u8) -> Op min_zoom: 0, min_points, node_size: 64, + coordinate_system, } } @@ -35,9 +42,9 @@ 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"); +pub fn load_cartesian() -> Vec { + let file_path = Path::new("./tests/common/cartesian.json"); + let json_string = fs::read_to_string(file_path).expect("cartesian.json was not found"); - serde_json::from_str(&json_string).expect("non-geospatial.json was not parsed") + serde_json::from_str(&json_string).expect("cartesian.json was not parsed") } diff --git a/tests/supercluster_integration_test.rs b/tests/supercluster_integration_test.rs index 2a1a49c..233be5a 100644 --- a/tests/supercluster_integration_test.rs +++ b/tests/supercluster_integration_test.rs @@ -1,17 +1,19 @@ mod common; +mod util; use common::{ - get_options, load_non_geospatial, load_places, load_tile_places, load_tile_places_with_min_5, + get_options, load_cartesian, 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; +use supercluster::{CoordinateSystem, Supercluster}; +use util::get_data_range; #[test] fn test_generate_clusters() { let places_tile = load_tile_places(); - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); let tile = index.get_tile(0, 0.0, 0.0).expect("cannot get a tile"); @@ -24,7 +26,7 @@ fn test_generate_clusters() { fn test_generate_clusters_with_min_points() { let places_tile = load_tile_places_with_min_5(); - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 5, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 5, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); let tile = index.get_tile(0, 0.0, 0.0).expect("cannot get a tile"); @@ -35,7 +37,7 @@ fn test_generate_clusters_with_min_points() { #[test] fn test_get_cluster() { - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); let cluster_counts: Vec = index @@ -60,7 +62,7 @@ fn test_get_cluster() { #[test] fn test_cluster_expansion_zoom() { - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); assert_eq!(index.get_cluster_expansion_zoom(164), 1); @@ -72,7 +74,7 @@ fn test_cluster_expansion_zoom() { #[test] fn test_cluster_expansion_zoom_for_max_zoom() { - let mut cluster = Supercluster::new(get_options(60.0, 256.0, 2, 4)); + let mut cluster = Supercluster::new(get_options(60.0, 256.0, 2, 4, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); assert_eq!(index.get_cluster_expansion_zoom(2504), 5); @@ -93,7 +95,7 @@ fn test_get_cluster_leaves() { "Cape Bauld", ]; - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); let leaf_names: Vec = index @@ -108,7 +110,7 @@ fn test_get_cluster_leaves() { #[test] fn test_clusters_when_query_crosses_international_dateline() { - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(vec![ Feature { id: None, @@ -150,7 +152,7 @@ fn test_clusters_when_query_crosses_international_dateline() { #[test] fn test_does_not_crash_on_weird_bbox_values() { - let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16)); + let mut cluster = Supercluster::new(get_options(40.0, 512.0, 2, 16, CoordinateSystem::LatLng)); let index = cluster.load(load_places()); assert_eq!( @@ -196,9 +198,18 @@ fn test_does_not_crash_on_weird_bbox_values() { } #[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()); +fn test_cartesian_coordinates() { + let data = load_cartesian(); + let data_range = get_data_range(&data).unwrap(); + + let mut cluster = Supercluster::new(get_options( + 20.0, + 512.0, + 2, + 16, + CoordinateSystem::Cartesian { data_range }, + )); + let index = cluster.load(data); let clusters = index.get_clusters([0.0, 0.0, 1000.0, 1000.0], 0); diff --git a/tests/util.rs b/tests/util.rs new file mode 100644 index 0000000..b0c01f7 --- /dev/null +++ b/tests/util.rs @@ -0,0 +1,33 @@ +use geojson::{Feature, Value}; +use supercluster::DataRange; + +pub fn get_data_range(data: &Vec) -> Option { + let mut min_x = f64::INFINITY; + let mut min_y = f64::INFINITY; + let mut max_x = f64::NEG_INFINITY; + let mut max_y = f64::NEG_INFINITY; + + for feature in data { + if let Some(geometry) = &feature.geometry { + if let Value::Point(ref coords) = geometry.value { + let x = coords[0]; + let y = coords[1]; + min_x = min_x.min(x); + min_y = min_y.min(y); + max_x = max_x.max(x); + max_y = max_y.max(y); + } + } + } + + if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() { + Some(DataRange { + min_x, + max_x, + min_y, + max_y, + }) + } else { + None + } +}