import type { LatLon } from './geo.js'; import { triangleAreaKm2 } from './geo.js'; export interface LinkEdge { fromPlaceId: number; toPlaceId: number; team: string; } export interface Field { team: string; placeIds: [number, number, number]; areaKm2: number; } /** * Every set of three mutually-linked, same-team places forms a control field — * mirrors Ingress's triangle-fill rule. Recomputed from scratch on read rather * than persisted, since claims/links can invalidate fields from several call * sites and a derived view can't go stale. */ export function computeFields(links: LinkEdge[], placeCoords: Map): Field[] { const adjacencyByTeam = new Map>>(); for (const link of links) { if (!adjacencyByTeam.has(link.team)) adjacencyByTeam.set(link.team, new Map()); const adj = adjacencyByTeam.get(link.team)!; if (!adj.has(link.fromPlaceId)) adj.set(link.fromPlaceId, new Set()); if (!adj.has(link.toPlaceId)) adj.set(link.toPlaceId, new Set()); adj.get(link.fromPlaceId)!.add(link.toPlaceId); adj.get(link.toPlaceId)!.add(link.fromPlaceId); } const fields: Field[] = []; for (const [team, adj] of adjacencyByTeam) { const nodes = [...adj.keys()].sort((x, y) => x - y); for (const u of nodes) { const uNeighbors = [...adj.get(u)!].filter((v) => v > u); for (const v of uNeighbors) { const vNeighbors = adj.get(v)!; for (const w of uNeighbors) { if (w <= v || !vNeighbors.has(w)) continue; const a = placeCoords.get(u); const b = placeCoords.get(v); const c = placeCoords.get(w); if (!a || !b || !c) continue; fields.push({ team, placeIds: [u, v, w], areaKm2: triangleAreaKm2(a, b, c) }); } } } } return fields; } export interface TeamScore { team: string; claimedPlaces: number; links: number; fields: number; areaKm2: number; } export function summarizeScores( teams: string[], claimCounts: Map, linkCounts: Map, fields: Field[], ): TeamScore[] { return teams.map((team) => { const teamFields = fields.filter((f) => f.team === team); return { team, claimedPlaces: claimCounts.get(team) ?? 0, links: linkCounts.get(team) ?? 0, fields: teamFields.length, areaKm2: teamFields.reduce((sum, f) => sum + f.areaKm2, 0), }; }); }