Files
website/src/lib/worldtodo/cluster.ts
2026-10-26 08:38:51 +00:00

46 lines
1.6 KiB
TypeScript

// Tiny screen-space clustering for the task map (a few hundred pins at most, so O(n²) is fine
// and avoids a plugin). Points are in projected pixels at the current zoom.
export type ScreenPoint = { x: number; y: number; weight: number };
/**
* Greedy clustering: the heaviest unassigned point (largest scale) takes every unassigned
* point within `radius` px. Returns groups in that order; radius 0 keeps every point alone.
*/
export function clusterPoints<T extends ScreenPoint>(points: T[], radius: number): T[][] {
const order = [...points].sort((a, b) => b.weight - a.weight);
const used = new Set<T>();
const groups: T[][] = [];
const r2 = radius * radius;
for (const p of order) {
if (used.has(p)) continue;
used.add(p);
const group = [p];
if (radius > 0)
for (const q of order) {
if (used.has(q)) continue;
const dx = q.x - p.x;
const dy = q.y - p.y;
if (dx * dx + dy * dy <= r2) {
used.add(q);
group.push(q);
}
}
groups.push(group);
}
return groups;
}
/** Offsets (px) that fan out `n` pins sharing one coordinate in a small ring. */
export function ringOffsets(n: number): { dx: number; dy: number }[] {
if (n <= 1) return [{ dx: 0, dy: 0 }];
const r = 16 + n * 3;
return Array.from({ length: n }, (_, i) => {
const a = (2 * Math.PI * i) / n - Math.PI / 2;
return { dx: Math.round(Math.cos(a) * r), dy: Math.round(Math.sin(a) * r) };
});
}
/** Rounded lat/lon key, so tasks on the same centroid can be fanned out. */
export const coordKey = (lat: number, lon: number) => `${lat.toFixed(4)},${lon.toFixed(4)}`;