worldwide todo list: global map (List/Map toggle, clustered category pins, Global strip, pick on map, location_precision, tasks.geojson)
All checks were successful
Deploy production / deploy (push) Successful in 1m13s

This commit is contained in:
2026-10-26 08:38:51 +00:00
parent 21b10ea894
commit 9d79dfb43a
15 changed files with 42669 additions and 13 deletions

View File

@@ -0,0 +1,45 @@
// 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)}`;