diff --git a/src/lib.rs b/src/lib.rs index 7a2893e..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)] @@ -116,11 +153,22 @@ impl Supercluster { None => continue, }; - // Longitude - data.push(lng_x(coordinates[0])); + match &self.options.coordinate_system { + CoordinateSystem::Cartesian { data_range } => { + // X Coordinate + data.push(data_range.normalize_x(coordinates[0])); - // Latitude - data.push(lat_y(coordinates[1])); + // 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])); + } + }; // The last zoom the point was processed at data.push(f64::INFINITY); @@ -161,39 +209,55 @@ 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.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 { + 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.options.coordinate_system, + ) } else { self.points[tree.data[k + OFFSET_ID] as usize].clone() }); @@ -244,7 +308,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.options.coordinate_system, + )); } else { let point_id = data[k + OFFSET_ID] as usize; @@ -490,8 +559,16 @@ 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]); + 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; } @@ -679,8 +756,19 @@ 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], + coordinate_system: &CoordinateSystem, +) -> Feature { + 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 { id: Some(Id::String(data[i + OFFSET_ID].to_string())), @@ -797,6 +885,7 @@ mod tests { min_zoom: 0, min_points: 2, node_size: 64, + coordinate_system: CoordinateSystem::LatLng, }) } @@ -836,7 +925,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], &CoordinateSystem::LatLng); assert_eq!(result.id, Some(Id::String("0".to_string()))); @@ -873,7 +962,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, &CoordinateSystem::LatLng); assert_eq!(result.id, Some(Id::String("0".to_string()))); diff --git a/tests/common/cartesian.json b/tests/common/cartesian.json new file mode 100644 index 0000000..c7a6ad7 --- /dev/null +++ b/tests/common/cartesian.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/common/mod.rs b/tests/common/mod.rs index 2fca0e6..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; +use supercluster::{CoordinateSystem, Options}; -pub fn get_options(radius: f64, extent: f64, min_points: u8, max_zoom: u8) -> 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, } } @@ -34,3 +41,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_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("cartesian.json was not parsed") +} diff --git a/tests/supercluster_integration_test.rs b/tests/supercluster_integration_test.rs index 88ed16f..233be5a 100644 --- a/tests/supercluster_integration_test.rs +++ b/tests/supercluster_integration_test.rs @@ -1,15 +1,19 @@ mod common; +mod util; -use common::{get_options, load_places, load_tile_places, load_tile_places_with_min_5}; +use common::{ + 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"); @@ -22,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"); @@ -33,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 @@ -58,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); @@ -70,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); @@ -91,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 @@ -106,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, @@ -148,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!( @@ -192,3 +196,26 @@ fn test_does_not_crash_on_weird_bbox_values() { 61 ); } + +#[test] +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); + + 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); +} 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 + } +}