using System;
using System.Collections.Generic;
using Godot;
using IslaApocalypse.Core;
namespace IslaApocalypse.Tools
{
///
/// THE DRAINAGE MAPS (chat2/12) — presentation only, for eyeballing that the flow is sane:
///
/// • the LOG-SCALED ACCUMULATION map — drainage spans orders of magnitude, so log(1+acc) over land;
/// the dendritic uplands and the trunks read as bright channels on dark hillslopes; the ocean is a
/// flat dark blue and enclosed (non-ocean) water a dark teal, so the ocean identity is visible too;
/// • the PROMOTED-CANDIDATES overlay — a faint grey terrain, the sea-reaching trunks in cyan (outlet
/// square, mountain-exit white ring, lean tributaries thin), the endorheic giants in orange (pooling
/// terminal disc, lean tributaries thin), the lean endorheic terminals as red rings. Provisional
/// routes are NOT drawn (routing is a later task). Nothing here touches data.
///
public static class DrainageRenderer
{
private static readonly Color Ocean = new(0.055f, 0.110f, 0.235f);
private static readonly Color Enclosed = new(0.060f, 0.220f, 0.230f);
private static readonly Color Trunk = new(0.250f, 0.900f, 1.000f);
private static readonly Color Giant = new(1.000f, 0.600f, 0.150f);
private static readonly Color Endo = new(1.000f, 0.250f, 0.250f);
private static readonly Color Exit = new(1.000f, 1.000f, 1.000f);
private static readonly Color Ink = new(0.941f, 0.949f, 0.961f);
/// log(1 + acc) / log(1 + max) over land; ocean / enclosed water flat.
public static Image Accumulation(int[] acc, bool[] isOcean, float[,] render, int n, float sea)
{
long max = 1;
for (int i = 0; i < acc.Length; i++) if (acc[i] > max) max = acc[i];
double lmax = Math.Log(1.0 + max);
var img = Image.CreateEmpty(n, n, false, Image.Format.Rgb8);
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
int i = x * n + y;
if (isOcean[i]) { img.SetPixel(x, y, Ocean); continue; }
if (render[x, y] < sea) { img.SetPixel(x, y, Enclosed); continue; }
float v = (float)(Math.Log(1.0 + acc[i]) / lmax);
// a dark-to-bright ramp with a cool tint in the channels
float r = 0.06f + 0.94f * v * v, g = 0.08f + 0.92f * v, b = 0.12f + 0.88f * MathF.Sqrt(v);
img.SetPixel(x, y, new Color(MathF.Min(1f, r), MathF.Min(1f, g), MathF.Min(1f, b)));
}
return img;
}
///
/// The faint grey terrain every overlay map is drawn on — ocean flat dark blue, enclosed
/// (non-ocean) water dark teal, land a shallow sqrt ramp. Factored out at rivers/02 so the
/// promotion maps sit on the SAME base as the chat2/12 candidates map and can be compared
/// without the eye correcting for two different backgrounds.
///
public static Image TerrainBase(bool[] isOcean, float[,] render, int n, float sea, float hMax)
{
var img = Image.CreateEmpty(n, n, false, Image.Format.Rgb8);
float span = MathF.Max(1e-6f, hMax - sea);
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
{
int i = x * n + y;
if (isOcean[i]) { img.SetPixel(x, y, Ocean); continue; }
if (render[x, y] < sea) { img.SetPixel(x, y, Enclosed); continue; }
float t = MathF.Min(1f, (render[x, y] - sea) / span);
float g = 0.30f + 0.45f * MathF.Sqrt(t);
img.SetPixel(x, y, new Color(g, g, g * 0.96f));
}
return img;
}
/// The candidates over a faint terrain.
public static Image Candidates(DrainageAnalysis.Plan plan, bool[] isOcean, float[,] render, int n, float sea, float hMax, string title)
{
var img = TerrainBase(isOcean, render, n, sea, hMax);
int thick = n >= 4096 ? 5 : 3, thin = n >= 4096 ? 3 : 2, mark = n >= 4096 ? 18 : 10;
foreach (var g in plan.Giants)
{
foreach (var tr in g.Tributaries) Polyline(img, tr.Course, n, Giant, thin);
Polyline(img, g.Course, n, Giant, thick);
Disc(img, (int)g.Terminal.x, (int)g.Terminal.y, mark, n, Giant);
Ring(img, (int)g.Terminal.x, (int)g.Terminal.y, mark + 8, n, Ink, 3);
if (g.ExitFound) Ring(img, (int)g.MountainExit.x, (int)g.MountainExit.y, mark, n, Exit, 4);
}
foreach (var t in plan.Trunks)
{
foreach (var tr in t.Tributaries) Polyline(img, tr.Course, n, Trunk, thin);
Polyline(img, t.Course, n, Trunk, thick);
Square(img, (int)t.Outlet.x, (int)t.Outlet.y, mark, n, Trunk);
if (t.ExitFound) Ring(img, (int)t.MountainExit.x, (int)t.MountainExit.y, mark, n, Exit, 4);
}
foreach (var e in plan.Endorheics)
Ring(img, (int)e.Terminal.x, (int)e.Terminal.y, mark + 4, n, Endo, 4);
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
TinyFont.Draw(img, title, 12, 12, s, Ink);
TinyFont.Draw(img, $"CYAN: SEA-REACHING TRUNKS ({plan.Trunks.Count}) - SQUARE = OUTLET WHITE RING = MOUNTAIN EXIT", 12, 12 + lh, s, Ink);
TinyFont.Draw(img, $"ORANGE: ENDORHEIC GIANTS ({plan.Giants.Count}) - DISC = POOLING TERMINAL (EXPECTED, NOT AN ERROR)", 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, $"RED RING: LEAN ENDORHEIC TERMINALS ({plan.Endorheics.Count}) THIN LINES: LEAN TRIBUTARIES NOTHING CARVED - ANALYSIS ONLY", 12, 12 + lh * 3, s, Ink);
return img;
}
// ═══ ⭐ THE PROMOTION MAPS (rivers/02) — the count decision, on the map ═══════════════════
//
// Two views, same base, same colour law:
// SEA-REACHING cyan (as chat2/12's trunks)
// ENDORHEIC orange (as chat2/12's giants)
// so a reader carrying chat2/12 in their eye reads these without relearning anything.
//
// ⚠⚠ NEITHER MAP DRAWS `Giant.ProvisionalRoute`. That steepest-descent placeholder — the
// visible "comb" of parallel threads on the flats — is rivers/03's job to replace, and drawing
// it here would make a count look like a river network it is not. What IS drawn is the REAL
// upland stem: the max-accumulation course traced through erosion-carved valleys.
///
/// ⭐ THE DIAGNOSTIC MAP — every candidate above the floor, marker AREA ∝ drainage area,
/// colour by terminus. Answers "where are the big drainages, and is the spread north/south?"
/// before any count is chosen.
///
/// ⚠ Marker radius scales as √area so the MARKER'S AREA is proportional to the drainage area —
/// scaling the radius linearly would exaggerate the big ones quadratically and make a knee look
/// like a cliff.
///
public static Image PromotionCandidates(List ranked, Image img, int n, string title, int[] ladder)
{
if (ranked.Count == 0) return img;
long maxArea = 1;
foreach (var c in ranked) if (c.DrainagePx > maxArea) maxArea = c.DrainagePx;
float rMax = n >= 4096 ? 46f : 22f, rMin = n >= 4096 ? 6f : 3f;
int ringW = n >= 4096 ? 4 : 2;
// Draw smallest-first so a big marker never hides behind a small one.
for (int i = ranked.Count - 1; i >= 0; i--)
{
var c = ranked[i];
float f = MathF.Sqrt((float)c.DrainagePx / maxArea); // area ∝ drainage
int r = (int)MathF.Round(rMin + (rMax - rMin) * f);
Color col = c.IsSea ? Trunk : Giant;
Disc(img, c.X, c.Y, r, n, col);
Ring(img, c.X, c.Y, r + ringW + 1, n, Ink, ringW); // ink halo: legible on any ground
}
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
int nSea = 0; foreach (var c in ranked) if (c.IsSea) nSea++;
TinyFont.Draw(img, title, 12, 12, s, Ink);
TinyFont.Draw(img, $"ALL {ranked.Count} CANDIDATES ABOVE THE FLOOR - MARKER AREA IS PROPORTIONAL TO DRAINAGE AREA", 12, 12 + lh, s, Ink);
TinyFont.Draw(img, $"CYAN: SEA-REACHING ({nSea}) ORANGE: ENDORHEIC ({ranked.Count - nSea}) - AN INLAND TERMINUS IS A PASS, NOT A FALLBACK", 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, $"NOTHING IS PROMOTED HERE - THIS IS THE DISTRIBUTION THE COUNT ({Join(ladder)}) IS CHOSEN FROM", 12, 12 + lh * 3, s, Ink);
return img;
}
///
/// ⭐ THE A/B PLATE — the unified top-N promoted, real upland stems, width ∝ drainage area,
/// terminus markers coloured by type. One plate per N; the developer picks by comparing them.
///
public static Image PromotedRivers(List promoted, Image img, int n,
int nPromoted, long floorPx, string title)
{
if (promoted.Count == 0) return img;
long maxArea = 1;
foreach (var c in promoted) if (c.DrainagePx > maxArea) maxArea = c.DrainagePx;
float wMax = n >= 4096 ? 11f : 6f, wMin = n >= 4096 ? 3f : 2f;
int mark = n >= 4096 ? 18 : 10;
// Smallest first, so the biggest rivers finish on top.
for (int i = promoted.Count - 1; i >= 0; i--)
{
var c = promoted[i];
if (c.Course == null || c.Course.Count < 2) continue;
float f = MathF.Sqrt((float)c.DrainagePx / maxArea);
int w = (int)MathF.Round(wMin + (wMax - wMin) * f);
Polyline(img, c.Course, n, c.IsSea ? Trunk : Giant, w);
}
// ⚠ Marked at the RIVER's terminus (where its stem pools), NOT at the basin's deepest cell —
// on a flat basin floor those differ, and marking the deepest cell draws the stem visibly
// detached from its own endpoint. → RiverCandidate.TermX.
foreach (var c in promoted)
{
if (c.IsSea) Square(img, c.TermX, c.TermY, mark, n, Trunk);
else { Disc(img, c.TermX, c.TermY, mark, n, Giant); Ring(img, c.TermX, c.TermY, mark + 8, n, Ink, 3); }
}
int s = n >= 4096 ? 4 : 3; int lh = TinyFont.Height(s) + 6;
int nSea = 0; foreach (var c in promoted) if (c.IsSea) nSea++;
TinyFont.Draw(img, title, 12, 12, s, Ink);
TinyFont.Draw(img, $"UNIFIED TOP {nPromoted} BY DRAINAGE AREA - THE SEA/ENDORHEIC SPLIT FELL OUT, IT WAS NOT QUOTA'D", 12, 12 + lh, s, Ink);
TinyFont.Draw(img, $"CYAN SQUARE: SEA OUTLET ({nSea}) ORANGE DISC: ENDORHEIC TERMINUS ({promoted.Count - nSea}) STEM WIDTH IS PROPORTIONAL TO DRAINAGE", 12, 12 + lh * 2, s, Ink);
TinyFont.Draw(img, $"REAL UPLAND STEMS ONLY - NO LOWLAND ROUTING, NO WATER, NOTHING CARVED (FLOOR {floorPx:N0} PX)", 12, 12 + lh * 3, s, Ink);
return img;
}
///
/// ⭐ THE DISTRIBUTION PLOT — drainage area (log y) against rank (linear x), with the ladder
/// counts marked vertically and the analysis's own thresholds marked horizontally.
///
/// **Log y is not a presentation choice, it is the only honest one:** drainage areas span three
/// or more orders of magnitude, so on a linear axis every candidate but the top two or three
/// collapses onto the floor and the knee — the thing this plot exists to show — is invisible.
///
public static Image Distribution(List ranked, int[] ladder,
long endorheicMinInflowPx, long stemMinAccPx, long floorPx, string title)
{
const int W = 1600, H = 1000, L = 150, R = 40, T = 120, B = 90;
var img = Image.CreateEmpty(W, H, false, Image.Format.Rgb8);
var bg = new Color(0.07f, 0.08f, 0.10f);
for (int x = 0; x < W; x++) for (int y = 0; y < H; y++) img.SetPixel(x, y, bg);
if (ranked.Count == 0) return img;
double loMin = Math.Log10(Math.Max(1.0, Math.Min(floorPx, ranked[ranked.Count - 1].DrainagePx)));
double hiMax = Math.Log10(Math.Max(10.0, ranked[0].DrainagePx));
loMin = Math.Floor(loMin); hiMax = Math.Ceiling(hiMax);
int plotW = W - L - R, plotH = H - T - B;
int XOf(int rank) => L + (int)((rank - 1) / (double)Math.Max(1, ranked.Count - 1) * plotW);
int YOf(double area) => T + plotH - (int)((Math.Log10(Math.Max(1.0, area)) - loMin) / Math.Max(1e-9, hiMax - loMin) * plotH);
var grid = new Color(0.16f, 0.18f, 0.22f);
for (int d = (int)loMin; d <= (int)hiMax; d++) // decade gridlines
{
int y = YOf(Math.Pow(10, d));
for (int x = L; x < L + plotW; x++) if (y >= 0 && y < H) img.SetPixel(x, y, grid);
TinyFont.Draw(img, $"1E{d}", 12, Math.Max(0, y - 6), 2, new Color(0.60f, 0.64f, 0.70f));
}
// the analysis's own thresholds — so the ladder is read RELATIVE to them, not in a vacuum
DashH(img, YOf(endorheicMinInflowPx), L, L + plotW, new Color(1f, 0.45f, 0.45f));
TinyFont.Draw(img, $"ENDORHEIC MIN INFLOW {endorheicMinInflowPx:N0}", L + 8, YOf(endorheicMinInflowPx) - 22, 2, new Color(1f, 0.45f, 0.45f));
DashH(img, YOf(stemMinAccPx), L, L + plotW, new Color(0.55f, 0.85f, 0.55f));
TinyFont.Draw(img, $"STEM MIN ACC {stemMinAccPx:N0}", L + 8, YOf(stemMinAccPx) - 22, 2, new Color(0.55f, 0.85f, 0.55f));
foreach (int nn in ladder) // the ladder counts
{
if (nn < 1 || nn > ranked.Count) continue;
int x = XOf(nn);
for (int y = T; y < T + plotH; y += 6)
for (int k = 0; k < 3 && y + k < T + plotH; k++) img.SetPixel(x, y + k, new Color(0.95f, 0.90f, 0.35f));
TinyFont.Draw(img, $"N={nn}", x + 6, T + 6, 3, new Color(0.95f, 0.90f, 0.35f));
TinyFont.Draw(img, $"{ranked[nn - 1].DrainagePx:N0}", x + 6, T + 6 + TinyFont.Height(3) + 4, 2, new Color(0.95f, 0.90f, 0.35f));
}
for (int i = 0; i < ranked.Count; i++) // the candidates
{
var c = ranked[i];
int x = XOf(i + 1), y = YOf(c.DrainagePx);
Color col = c.IsSea ? Trunk : Giant;
for (int ox = -3; ox <= 3; ox++)
for (int oy = -3; oy <= 3; oy++)
{
if (ox * ox + oy * oy > 9) continue;
int px = x + ox, py = y + oy;
if (px >= 0 && py >= 0 && px < W && py < H) img.SetPixel(px, py, col);
}
}
TinyFont.Draw(img, title, 12, 12, 3, Ink);
TinyFont.Draw(img, "DRAINAGE AREA (PX, LOG) VS UNIFIED RANK - CYAN SEA-REACHING, ORANGE ENDORHEIC", 12, 12 + TinyFont.Height(3) + 8, 2, Ink);
TinyFont.Draw(img, $"{ranked.Count} CANDIDATES ABOVE THE {floorPx:N0} PX FLOOR - A KNEE IS A SHARP DROP; A SMOOTH CURVE MEANS THE TERRAIN HAS NO NATURAL COUNT", 12, H - 34, 2, new Color(0.70f, 0.74f, 0.80f));
return img;
}
private static string Join(int[] v)
{
var sb = new System.Text.StringBuilder();
for (int i = 0; i < v.Length; i++) { if (i > 0) sb.Append('/'); sb.Append(v[i]); }
return sb.ToString();
}
private static void DashH(Image img, int y, int x0, int x1, Color c)
{
if (y < 0 || y >= img.GetHeight()) return;
for (int x = x0; x < x1; x += 14)
for (int k = 0; k < 8 && x + k < x1; k++) img.SetPixel(x + k, y, c);
}
private static void Polyline(Image img, List<(float x, float y)> pts, int n, Color c, int thick)
{
for (int i = 1; i < pts.Count; i++)
Line(img, (int)pts[i - 1].x, (int)pts[i - 1].y, (int)pts[i].x, (int)pts[i].y, n, c, thick);
}
private static void Line(Image img, int x0, int y0, int x1, int y1, int n, Color c, int thick)
{
int dx = Math.Abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = -Math.Abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = dx + dy; int r = thick / 2;
int guard = 0;
while (true)
{
for (int ox = -r; ox <= r; ox++)
for (int oy = -r; oy <= r; oy++)
{
int px = x0 + ox, py = y0 + oy;
if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, c);
}
if (x0 == x1 && y0 == y1) break;
if (++guard > 4 * n) break;
int e2 = 2 * err;
if (e2 >= dy) { err += dy; x0 += sx; }
if (e2 <= dx) { err += dx; y0 += sy; }
}
}
private static void Disc(Image img, int cx, int cy, int r, int n, Color c)
{
for (int ox = -r; ox <= r; ox++)
for (int oy = -r; oy <= r; oy++)
{
if (ox * ox + oy * oy > r * r) continue;
int px = cx + ox, py = cy + oy;
if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, c);
}
}
private static void Ring(Image img, int cx, int cy, int r, int n, Color c, int w)
{
for (int ox = -r; ox <= r; ox++)
for (int oy = -r; oy <= r; oy++)
{
int d2 = ox * ox + oy * oy;
if (d2 > r * r || d2 < (r - w) * (r - w)) continue;
int px = cx + ox, py = cy + oy;
if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, c);
}
}
private static void Square(Image img, int cx, int cy, int r, int n, Color c)
{
for (int ox = -r; ox <= r; ox++)
for (int oy = -r; oy <= r; oy++)
{
int px = cx + ox, py = cy + oy;
if (px >= 0 && py >= 0 && px < n && py < n) img.SetPixel(px, py, c);
}
}
}
}