Skip to content

Commit

Permalink
feat: center clusters on points
Browse files Browse the repository at this point in the history
  • Loading branch information
TurtIeSocks committed Jul 24, 2024
1 parent 8c762f5 commit 096bfa8
Show file tree
Hide file tree
Showing 16 changed files with 434 additions and 71 deletions.
1 change: 1 addition & 0 deletions client/src/components/drawer/Routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export default function RoutingTab() {
<Collapse in={!CLUSTERING_MODES.some((m) => m === cluster_mode)}>
<UserTextInput field="clustering_args" helperText="--x 1 --y abc" />
</Collapse>
<Toggle field="center_clusters" label="Center Clusters" />
</Collapse>

<Divider sx={{ my: 2 }} />
Expand Down
2 changes: 2 additions & 0 deletions client/src/hooks/usePersist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 | ''
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 4 additions & 10 deletions client/src/hooks/usePixi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<string, string> = 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 `<svg xmlns="http://www.w3.org/2000/svg" id="${hash}" width="15" height="15" viewBox="-2 -2 24 24">
<circle cx="10" cy="10" r="10" fill="${color}" fill-opacity="0.8" stroke="black" stroke-width="1" />
<circle cx="10" cy="10" r="10" fill="${getDataPointColor(
hash,
)}" fill-opacity="0.8" stroke="black" stroke-width="1" />
<circle cx="10" cy="10" r="1" fill="black" />
</svg>`
}
Expand Down
140 changes: 81 additions & 59 deletions client/src/pages/map/markers/index.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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)
Expand All @@ -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<PixiMarker[]>([])
const [focused, setFocused] = React.useState(true)
Expand Down Expand Up @@ -84,63 +91,78 @@ export default function Markers({ category }: { category: Category }) {
}
}, [memoSetFocused])

return nativeLeaflet ? (
return (
<>
{/* <GeohashMarker hash="u14cu1dtdx2s" /> */}
{markers.map((i) => {
const hash = geohash.encode(...i.p, 12)
return (
<React.Fragment key={hash}>
{pokestopRange && (
<Circle
center={i.p}
radius={70}
opacity={0.2}
fillOpacity={0.2}
fillColor="darkgreen"
color="green"
pane="dev_markers"
pmIgnore
snapIgnore
/>
)}
<Circle
center={i.p}
radius={ICON_RADIUS[i.i[0]]}
fillOpacity={0.8}
opacity={0.8}
fillColor={hash === 'u14cu1d9f6s1' ? 'pink' : ICON_COLOR[i.i[0]]}
color="black"
pane="dev_markers"
pmIgnore
snapIgnore
>
<StyledPopup>
<div>
Lat: {i.p[0]}
<br />
Lng: {i.p[1]}
<br />
Hash: {geohash.encode(...i.p, 9)}
<br />
Hash: {geohash.encode(...i.p, 12)}
</div>
</StyledPopup>
</Circle>
<Circle
center={i.p}
radius={1}
pathOptions={{
fillColor: 'black',
color: 'black',
}}
pane="dev_markers"
pmIgnore
snapIgnore
/>
</React.Fragment>
)
})}
{DEBUG_HASHES.map((hash) => (
<GeohashMarker key={hash} hash={hash} />
))}
{nativeLeaflet ? (
<>
{markers.map((i) => {
const uniqueHash = geohash.encode(...i.p, 12)
const groupHash = geohash.encode(...i.p, geohashPrecision)
return (
<React.Fragment
key={`${uniqueHash}${geohashPrecision}${colorByGeoHash}`}
>
{pokestopRange && (
<Circle
center={i.p}
radius={70}
opacity={0.2}
fillOpacity={0.2}
fillColor="darkgreen"
color="green"
pane="dev_markers"
pmIgnore
snapIgnore
/>
)}
<Circle
center={i.p}
radius={ICON_RADIUS[i.i[0]]}
fillOpacity={0.8}
opacity={0.8}
fillColor={
colorByGeoHash
? getDataPointColor(groupHash)
: ICON_COLOR[i.i[0]]
}
color="black"
pane="dev_markers"
pmIgnore
snapIgnore
>
<StyledPopup>
<div>
Lat: {i.p[0]}
<br />
Lng: {i.p[1]}
<br />
Hash: {groupHash}
<br />
Hash: {uniqueHash}
</div>
</StyledPopup>
</Circle>
{showCenterCircle && (
<Circle
center={i.p}
radius={1}
pathOptions={{
fillColor: 'black',
color: 'black',
}}
pane="dev_markers"
pmIgnore
snapIgnore
/>
)}
</React.Fragment>
)
})}
</>
) : null}
</>
) : null
)
}
2 changes: 2 additions & 0 deletions client/src/services/fetches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export async function clusteringRouting({
const {
mode,
radius,
center_clusters,
cluster_mode,
category: rawCategory,
min_points,
Expand Down Expand Up @@ -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,
Expand Down
14 changes: 14 additions & 0 deletions client/src/services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -305,3 +307,15 @@ export function getPointColor(
? VECTOR_COLORS.GREEN
: VECTOR_COLORS.BLUE
}

const colorMap: Map<string, string> = 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
}
9 changes: 7 additions & 2 deletions server/algorithms/src/clustering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions server/algorithms/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ mod project;
pub mod routing;
mod rtree;
pub mod s2;
mod sec;
pub mod stats;
pub mod utils;
85 changes: 85 additions & 0 deletions server/algorithms/src/sec/circle.rs
Original file line number Diff line number Diff line change
@@ -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<Point>) -> 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<Point> {
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),
}
}
}
Loading

0 comments on commit 096bfa8

Please sign in to comment.