From 096bfa84db93507297cdc3681982f4f744a5c5d6 Mon Sep 17 00:00:00 2001 From: Derick M <58572875+TurtIeSocks@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:58:45 -0400 Subject: [PATCH] feat: center clusters on points --- client/src/components/drawer/Routing.tsx | 1 + client/src/hooks/usePersist.ts | 2 + client/src/hooks/usePixi.ts | 14 +-- client/src/pages/map/markers/index.tsx | 140 +++++++++++++---------- client/src/services/fetches.ts | 2 + client/src/services/utils.ts | 14 +++ server/algorithms/src/clustering/mod.rs | 9 +- server/algorithms/src/lib.rs | 1 + server/algorithms/src/sec/circle.rs | 85 ++++++++++++++ server/algorithms/src/sec/mod.rs | 88 ++++++++++++++ server/algorithms/src/sec/sec.rs | 103 +++++++++++++++++ server/algorithms/src/sec/state.rs | 10 ++ server/algorithms/src/sec/utils.rs | 21 ++++ server/algorithms/src/stats.rs | 5 + server/api/src/public/v1/calculate.rs | 2 + server/model/src/api/args.rs | 8 ++ 16 files changed, 434 insertions(+), 71 deletions(-) create mode 100644 server/algorithms/src/sec/circle.rs create mode 100644 server/algorithms/src/sec/mod.rs create mode 100644 server/algorithms/src/sec/sec.rs create mode 100644 server/algorithms/src/sec/state.rs create mode 100644 server/algorithms/src/sec/utils.rs diff --git a/client/src/components/drawer/Routing.tsx b/client/src/components/drawer/Routing.tsx index 8a93fe80..4a4ebb7d 100644 --- a/client/src/components/drawer/Routing.tsx +++ b/client/src/components/drawer/Routing.tsx @@ -140,6 +140,7 @@ export default function RoutingTab() { m === cluster_mode)}> + diff --git a/client/src/hooks/usePersist.ts b/client/src/hooks/usePersist.ts index c57ebd4c..38f8ec61 100644 --- a/client/src/hooks/usePersist.ts +++ b/client/src/hooks/usePersist.ts @@ -75,6 +75,7 @@ export interface UsePersist { routing_args: string clustering_args: string bootstrapping_args: string + center_clusters: boolean // generations: number | '' // routing_time: number | '' // devices: number | '' @@ -121,6 +122,7 @@ export const usePersist = create( radius: 70, route_split_level: 0, cluster_split_level: 0, + center_clusters: false, // routing_chunk_size: 0, calculation_mode: 'Radius', s2_level: 15, diff --git a/client/src/hooks/usePixi.ts b/client/src/hooks/usePixi.ts index 63797c99..7d7b2859 100644 --- a/client/src/hooks/usePixi.ts +++ b/client/src/hooks/usePixi.ts @@ -6,7 +6,6 @@ import { useEffect, useState } from 'react' import geohash from 'ngeohash' -import seed from 'seedrandom' import { shallow } from 'zustand/shallow' import 'leaflet-pixi-overlay' @@ -15,26 +14,21 @@ import * as PIXI from 'pixi.js' import { useMap } from 'react-leaflet' import { PixiMarker } from '@assets/types' +import { getDataPointColor } from '@services/utils' import { ICON_SVG } from '../assets/constants' import { usePersist } from './usePersist' -const colorMap: Map = new Map() - PIXI.settings.FAIL_IF_MAJOR_PERFORMANCE_CAVEAT = false PIXI.utils.skipHello() const PIXILoader = PIXI.Loader.shared function getHashSvg(hash: string) { - let color = colorMap.get(hash) - if (!color) { - const rng = seed(hash) - color = `#${rng().toString(16).slice(2, 8)}` - colorMap.set(hash, color) - } return ` - + ` } diff --git a/client/src/pages/map/markers/index.tsx b/client/src/pages/map/markers/index.tsx index 3b55cb9f..7537f2b7 100644 --- a/client/src/pages/map/markers/index.tsx +++ b/client/src/pages/map/markers/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { Circle } from 'react-leaflet' +import { Circle, useMap } from 'react-leaflet' import geohash from 'ngeohash' import { shallow } from 'zustand/shallow' @@ -10,8 +10,12 @@ import { useStatic } from '@hooks/useStatic' import useDeepCompareEffect from 'use-deep-compare-effect' import { getMarkers } from '@services/fetches' import { Category, PixiMarker } from '@assets/types' +import { getDataPointColor } from '@services/utils' + import StyledPopup from '../popups/Styled' -// import { GeohashMarker } from './Geohash' +import { GeohashMarker } from './Geohash' + +const DEBUG_HASHES: string[] = [] export default function Markers({ category }: { category: Category }) { const enabled = usePersist((s) => s[category], shallow) @@ -20,10 +24,13 @@ export default function Markers({ category }: { category: Category }) { const last_seen = usePersist((s) => s.last_seen) const pokestopRange = usePersist((s) => s.pokestopRange) const tth = usePersist((s) => s.tth) + const colorByGeoHash = usePersist((s) => s.colorByGeohash) + const geohashPrecision = usePersist((s) => s.geohashPrecision) const updateButton = useStatic((s) => s.updateButton) const bounds = useStatic((s) => s.bounds) const geojson = useStatic((s) => s.geojson) + const showCenterCircle = useMap().getZoom() > 15 const [markers, setMarkers] = React.useState([]) const [focused, setFocused] = React.useState(true) @@ -84,63 +91,78 @@ export default function Markers({ category }: { category: Category }) { } }, [memoSetFocused]) - return nativeLeaflet ? ( + return ( <> - {/* */} - {markers.map((i) => { - const hash = geohash.encode(...i.p, 12) - return ( - - {pokestopRange && ( - - )} - - -
- Lat: {i.p[0]} -
- Lng: {i.p[1]} -
- Hash: {geohash.encode(...i.p, 9)} -
- Hash: {geohash.encode(...i.p, 12)} -
-
-
- -
- ) - })} + {DEBUG_HASHES.map((hash) => ( + + ))} + {nativeLeaflet ? ( + <> + {markers.map((i) => { + const uniqueHash = geohash.encode(...i.p, 12) + const groupHash = geohash.encode(...i.p, geohashPrecision) + return ( + + {pokestopRange && ( + + )} + + +
+ Lat: {i.p[0]} +
+ Lng: {i.p[1]} +
+ Hash: {groupHash} +
+ Hash: {uniqueHash} +
+
+
+ {showCenterCircle && ( + + )} +
+ ) + })} + + ) : null} - ) : null + ) } diff --git a/client/src/services/fetches.ts b/client/src/services/fetches.ts index b4093f80..eb892f3b 100644 --- a/client/src/services/fetches.ts +++ b/client/src/services/fetches.ts @@ -117,6 +117,7 @@ export async function clusteringRouting({ const { mode, radius, + center_clusters, cluster_mode, category: rawCategory, min_points, @@ -234,6 +235,7 @@ export async function clusteringRouting({ `${area.geometry.type}${area.id ? `-${area.id}` : ''}`, last_seen: Math.floor((last_seen?.getTime?.() || 0) / 1000), radius, + center_clusters, min_points, cluster_mode, parent, diff --git a/client/src/services/utils.ts b/client/src/services/utils.ts index d93d885a..a89772c2 100644 --- a/client/src/services/utils.ts +++ b/client/src/services/utils.ts @@ -3,6 +3,8 @@ import { capitalize } from '@mui/material' import type { MultiPoint, MultiPolygon, Point, Polygon } from 'geojson' import union from '@turf/union' import bbox from '@turf/bbox' +import seed from 'seedrandom' + import { useStatic } from '@hooks/useStatic' import booleanPointInPolygon from '@turf/boolean-point-in-polygon' import { useShapes } from '@hooks/useShapes' @@ -305,3 +307,15 @@ export function getPointColor( ? VECTOR_COLORS.GREEN : VECTOR_COLORS.BLUE } + +const colorMap: Map = new Map() +const rng = seed() + +export function getDataPointColor(hash: string) { + let color = colorMap.get(hash) + if (!color) { + color = `#${rng().toString(16).slice(2, 8)}` + colorMap.set(hash, color) + } + return color +} diff --git a/server/algorithms/src/clustering/mod.rs b/server/algorithms/src/clustering/mod.rs index 35ebf3e8..1b31ed34 100644 --- a/server/algorithms/src/clustering/mod.rs +++ b/server/algorithms/src/clustering/mod.rs @@ -30,6 +30,7 @@ pub fn main( s2_size: u8, collection: FeatureCollection, clustering_args: &str, + center_clusters: bool, ) -> SingleVec { if data_points.is_empty() { return vec![]; @@ -84,9 +85,13 @@ pub fn main( } }, }; - + let clusters = if center_clusters { + sec::with_data(radius, data_points, &clusters) + } else { + clusters + }; stats.set_cluster_time(time); - stats.cluster_stats(radius, &data_points, &clusters); + stats.cluster_stats(radius, data_points, &clusters); stats.set_score(); clusters diff --git a/server/algorithms/src/lib.rs b/server/algorithms/src/lib.rs index d191d07f..dbe5f9d0 100644 --- a/server/algorithms/src/lib.rs +++ b/server/algorithms/src/lib.rs @@ -7,5 +7,6 @@ mod project; pub mod routing; mod rtree; pub mod s2; +mod sec; pub mod stats; pub mod utils; diff --git a/server/algorithms/src/sec/circle.rs b/server/algorithms/src/sec/circle.rs new file mode 100644 index 00000000..f0d0f2dd --- /dev/null +++ b/server/algorithms/src/sec/circle.rs @@ -0,0 +1,85 @@ +use std::fmt::Display; + +use geo::{HaversineDistance, Point}; + +use super::*; + +#[derive(PartialEq, Copy, Clone, Debug)] +pub enum Circle { + None, + One(Point), + Two(Point, Point), + Three(Point, Point, Point), +} + +impl Display for Circle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Circle::None => write!(f, "None"), + Circle::One(_) => write!(f, "One"), + Circle::Two(_, _) => write!(f, "Two"), + Circle::Three(_, _, _) => write!(f, "Three"), + } + } +} + +impl Circle { + pub fn new(points: &Vec) -> Self { + match points.len() { + 0 => Circle::None, + 1 => Circle::One(points[0]), + 2 => Circle::Two(points[0], points[1]), + 3 => { + let [a, b, c] = [points[0], points[1], points[2]]; + let [ab, bc, ca] = [a == b, b == c, c == a]; + match (ab, bc, ca) { + (true, true, true) => Circle::One(a), + (true, true, false) | (true, false, true) | (false, true, true) => { + unreachable!() + } + (true, false, false) => Circle::Two(a, c), + (false, true, false) => Circle::Two(a, b), + (false, false, true) => Circle::Two(b, c), + (false, false, false) => Circle::Three(a, b, c), + } + } + _ => { + panic!() + } + } + } + + pub fn contains(&self, point: Point, radius: f64) -> bool { + match self { + Circle::None => false, + Circle::One(a) => a.x() == point.x() && a.y() == point.y(), + Circle::Two(a, b) => { + let center = utils::midpoint(&a, &b); + let dis = center.haversine_distance(&point); + dis <= radius + } + Circle::Three(a, b, c) => { + let (circle, radius) = utils::smallest_three_point_circle(a, b, c); + circle.haversine_distance(&point) <= radius + } + } + } + + pub fn radius(&self) -> f64 { + match self { + Circle::None => 0., + Circle::One(_) => 0., + Circle::Two(a, b) => a.haversine_distance(b) / 2., + Circle::Three(a, b, c) => utils::smallest_three_point_circle(a, b, c).1, + } + } + + pub fn center(&self) -> Option { + match self { + Circle::None => None, + &Circle::One(a) => Some(a), + Circle::Two(a, b) => Some(utils::midpoint(a, b)), + Circle::Three(a, b, c) => Some(utils::smallest_three_point_circle(a, b, c).0), + } + } +} diff --git a/server/algorithms/src/sec/mod.rs b/server/algorithms/src/sec/mod.rs new file mode 100644 index 00000000..8641dbef --- /dev/null +++ b/server/algorithms/src/sec/mod.rs @@ -0,0 +1,88 @@ +mod circle; +mod sec; +mod state; +mod utils; + +use std::time::Instant; + +use model::api::{single_vec::SingleVec, Precision}; +use rayon::{ + iter::{IntoParallelRefIterator, ParallelIterator}, + slice::ParallelSliceMut, +}; + +use crate::rtree::{self, SortDedupe}; + +pub fn with_data(radius: Precision, points: &SingleVec, clusters: &SingleVec) -> SingleVec { + let time = Instant::now(); + log::info!("centering clusters on their points"); + let tree = rtree::spawn(radius, points); + let clusters: Vec = clusters + .into_iter() + .map(|c| rtree::point::Point::new(radius, 20, *c)) + .collect(); + + let mut clusters = rtree::cluster_info(&tree, &clusters); + clusters.par_sort_by(|a, b| b.all.len().cmp(&a.all.len())); + + let mut seen_points = std::collections::HashSet::new(); + + for c in clusters.iter_mut() { + let mut unique_points = vec![]; + for p in c.all.iter() { + if seen_points.contains(p) { + continue; + } + seen_points.insert(p); + unique_points.push(*p); + } + unique_points.sort_dedupe(); + c.unique = unique_points; + } + + let centered: Vec<_> = clusters + .par_iter() + .map(|c| { + ( + sec::multi_attempt( + c.unique + .iter() + .map(|p| geo::Point::new(p.center[1], p.center[0])), + radius, + 100, + ), + c.point, + ) + }) + .collect(); + + let mut radius_too_big = 0; + let mut points_outside = 0; + let mut successes = 0; + let mut circle_fails = 0; + let mut final_clusters = vec![]; + + for (result, fallback) in centered.into_iter() { + match result { + sec::SmallestEnclosingCircle::RadiusTooBig => { + radius_too_big += 1; + final_clusters.push(fallback.center); + } + sec::SmallestEnclosingCircle::MissingPoints => { + points_outside += 1; + final_clusters.push(fallback.center); + } + sec::SmallestEnclosingCircle::Centered(center) => { + successes += 1; + final_clusters.push([center.y(), center.x()]); + } + sec::SmallestEnclosingCircle::None => { + circle_fails += 1; + final_clusters.push(fallback.center); + } + } + } + log::info!("Success: {successes} | Radius Too Big: {radius_too_big} | Missing Points: {points_outside} | Circle Fails: {circle_fails}"); + log::info!("centered clusters in {:.2}s", time.elapsed().as_secs_f32()); + final_clusters +} diff --git a/server/algorithms/src/sec/sec.rs b/server/algorithms/src/sec/sec.rs new file mode 100644 index 00000000..adcca2c1 --- /dev/null +++ b/server/algorithms/src/sec/sec.rs @@ -0,0 +1,103 @@ +use super::{circle::Circle, state::State, *}; + +use geo::Point; +use rand::seq::SliceRandom; + +#[derive(Debug)] +pub enum SmallestEnclosingCircle { + None, + RadiusTooBig, + MissingPoints, + Centered(Point), +} + +pub fn multi_attempt>( + points: I, + radius: f64, + max_attempts: usize, +) -> SmallestEnclosingCircle { + let points: Vec<_> = points.collect(); + let mut circle = Circle::None; + let mut attempt = 0; + let mut rng = rand::thread_rng(); + + for i in 0..max_attempts { + attempt = i; + + let mut points = points.clone(); + points.shuffle(&mut rng); + circle = smallest_enclosing_circle(points.clone(), radius); + + if let Some(center) = circle.center() { + if !utils::is_missing_points(points, center, radius) && circle.radius() <= radius { + break; + } + } + } + + if attempt > 0 { + log::debug!("Attempt: {}", attempt); + } + eval_result(points, circle, radius) +} + +// pub fn single_attempt>(points: I, radius: f64) -> CircleResult { +// let points: Vec<_> = points.collect(); +// let circle = smallest_enclosing_circle(points.clone(), radius); +// eval_result(points, circle, radius) +// } + +fn smallest_enclosing_circle(points: Vec, radius: f64) -> Circle { + let mut p = points; + let mut circle = Circle::None; + let mut r = Vec::new(); + let mut stack = Vec::from([State::S0]); + + while !stack.is_empty() { + let state = stack.pop().unwrap(); + match state { + State::S0 => { + if p.len() == 0 || r.len() == 3 { + circle = Circle::new(&r); + } else { + stack.push(State::S1); + } + } + State::S1 => { + let element = p.pop().unwrap(); + stack.push(State::S2(element)); + stack.push(State::S0); + } + State::S2(element) => { + stack.push(State::S3(element)); + + if !circle.contains(element, radius) { + r.push(element); + stack.push(State::S4); + stack.push(State::S0); + } + } + State::S3(element) => { + p.push(element); + } + State::S4 => { + r.pop(); + } + } + } + circle +} + +fn eval_result(points: Vec, circle: Circle, radius: f64) -> SmallestEnclosingCircle { + if let Some(center) = circle.center() { + if circle.radius() > radius { + SmallestEnclosingCircle::RadiusTooBig + } else if utils::is_missing_points(points, center, radius) { + SmallestEnclosingCircle::MissingPoints + } else { + SmallestEnclosingCircle::Centered(center) + } + } else { + SmallestEnclosingCircle::None + } +} diff --git a/server/algorithms/src/sec/state.rs b/server/algorithms/src/sec/state.rs new file mode 100644 index 00000000..223e449d --- /dev/null +++ b/server/algorithms/src/sec/state.rs @@ -0,0 +1,10 @@ +use geo::Point; + +#[derive(Debug)] +pub enum State { + S0, + S1, + S2(Point), + S3(Point), + S4, +} diff --git a/server/algorithms/src/sec/utils.rs b/server/algorithms/src/sec/utils.rs new file mode 100644 index 00000000..86455f2a --- /dev/null +++ b/server/algorithms/src/sec/utils.rs @@ -0,0 +1,21 @@ +use geo::{Centroid, HaversineDistance, Point}; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + +pub fn is_missing_points(points: Vec, center: Point, radius: f64) -> bool { + points + .par_iter() + .any(|p| center.haversine_distance(p) > radius) +} + +pub fn midpoint(a: &Point, b: &Point) -> Point { + Point::new((a.x() + b.x()) / 2., (a.y() + b.y()) / 2.) +} + +pub fn smallest_three_point_circle(p1: &Point, p2: &Point, p3: &Point) -> (Point, f64) { + let center = geo::Triangle::new(p1.0, p2.0, p3.0).centroid(); + let radius = center + .haversine_distance(&p1) + .max(center.haversine_distance(&p2)) + .max(center.haversine_distance(&p3)); + (center, radius) +} diff --git a/server/algorithms/src/stats.rs b/server/algorithms/src/stats.rs index 4edeebee..bf61bdc9 100644 --- a/server/algorithms/src/stats.rs +++ b/server/algorithms/src/stats.rs @@ -233,6 +233,11 @@ impl Stats { worst = 0; } + // for point in tree.iter() { + // if !points_covered.contains(&point) { + // log::debug!("point not covered: {}", point); + // } + // } self.best_cluster_point_count = best; self.worst_cluster_point_count = worst; self.worst_cluster_count = worst_count; diff --git a/server/api/src/public/v1/calculate.rs b/server/api/src/public/v1/calculate.rs index e60850d8..6ea8018c 100644 --- a/server/api/src/public/v1/calculate.rs +++ b/server/api/src/public/v1/calculate.rs @@ -175,6 +175,7 @@ async fn cluster( parent, max_clusters, clustering_args, + center_clusters, .. } = payload.into_inner().init(Some(&mode)); @@ -242,6 +243,7 @@ async fn cluster( s2_size, area, &clustering_args, + center_clusters, ); let clusters = routing::main( &data_points, diff --git a/server/model/src/api/args.rs b/server/model/src/api/args.rs index 614a10ff..cdc2e7a5 100644 --- a/server/model/src/api/args.rs +++ b/server/model/src/api/args.rs @@ -372,6 +372,10 @@ pub struct Args { /// /// Default: `All` pub tth: Option, + /// If true, attempts to center clusters based on the points they cover + /// + /// Default: `false` + pub center_clusters: Option, } pub struct ArgsUnwrapped { @@ -403,6 +407,7 @@ pub struct ArgsUnwrapped { pub routing_args: String, pub clustering_args: String, pub bootstrapping_args: String, + pub center_clusters: bool, } fn validate_s2_cell(value_to_check: Option, label: &str) -> u64 { @@ -474,6 +479,7 @@ impl Args { routing_args, clustering_args, bootstrapping_args, + center_clusters, } = self; let enum_type = get_enum_by_geometry_string(geometry_type); let (area, default_return_type) = if let Some(area) = area { @@ -538,6 +544,7 @@ impl Args { } else { usize::MAX }; + let center_clusters = center_clusters.unwrap_or(false); let clusters = resolve_data_points(clusters); let last_seen = last_seen.unwrap_or(0); let save_to_db = save_to_db.unwrap_or(false); @@ -595,6 +602,7 @@ impl Args { routing_args, clustering_args, bootstrapping_args, + center_clusters, } } }