Weltkarte Wald: Outline auf echter Density-Isokontur statt Binärmaske

Vorher: Density → binäre Maske (-1/+1) → MS bei 0.5 → Boundary 75% vom
Zellrand entfernt (falscher Offset, unabhängig von echten Density-Werten).

Jetzt: h = threshold - density → MS direkt auf Density-Werten bei thr=0
→ msLerp interpoliert genau an der echten threshold-Isokontur, identisch
zur Fill-Grenze in computeForestPixels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:33:56 +02:00
parent f0a3c462b6
commit cf11a50a21

View File

@@ -1126,32 +1126,21 @@ public class WorldMapView extends VBox {
/** Extrahiert Wald-Konturen aus der Tree-Density-Map als geglättete Weltkoordinaten-Pfade. */
private static List<float[][]> buildForestOutlinePaths(float[] density, int fdSize, float threshold) {
// Density-Map: mz=0 = Norden (WH-pm.z()=0 → pm.z()=+WH), mz=fdSize-1 = Süden.
// Marching Squares: mz=0 → msW=-WH (Süden). → Z muss gespiegelt werden.
// Bilinear-Interpolation wie in computeForestPixels → Outline liegt exakt auf Fill-Grenze.
final int SIZE = SEA_MASK_SIZE;
float[] mask = new float[SIZE * SIZE];
for (int mz = 0; mz < SIZE; mz++) {
// Z-Flip: mz=0 (Süd im MS) → Süd in Density (dz=fdSize-1)
float fz = (float)(SIZE - 1 - mz) / (SIZE - 1) * (fdSize - 1);
int dz0 = (int) fz, dz1 = Math.min(dz0 + 1, fdSize - 1);
float tz = fz - dz0;
for (int mx = 0; mx < SIZE; mx++) {
float fx = (float) mx / (SIZE - 1) * (fdSize - 1);
int dx0 = (int) fx, dx1 = Math.min(dx0 + 1, fdSize - 1);
float tx = fx - dx0;
float d = (density[dz0 * fdSize + dx0] * (1 - tx) + density[dz0 * fdSize + dx1] * tx) * (1 - tz)
+ (density[dz1 * fdSize + dx0] * (1 - tx) + density[dz1 * fdSize + dx1] * tx) * tz;
mask[mz * SIZE + mx] = d >= threshold ? -1f : 1f;
// Z-Flip: Density mz=0=Nord, MS mz=0=Süd.
// h = threshold - density → forest (density > threshold) ergibt h < 0 = "inside" für MS.
// MS läuft direkt auf echten Density-Werten mit thr=0 → exakt dieselbe Isokontur wie
// die Füllung in computeForestPixels (keine binäre Maske, kein Boundary-Versatz).
float[] h = new float[fdSize * fdSize];
for (int mz = 0; mz < fdSize; mz++) {
int dz = fdSize - 1 - mz;
for (int mx = 0; mx < fdSize; mx++) {
h[mz * fdSize + mx] = threshold - density[dz * fdSize + mx];
}
}
List<float[]> segs = new ArrayList<>();
marchingSquares(mask, SIZE, 0.5f, null, null, segs);
// chaikinIter=0: Density-Boundary ist bereits glatt (kreisförmige Kernels),
// kein Chaikin-Schrumpfen → Outline liegt exakt auf der Fill-Grenze.
marchingSquares(h, fdSize, 0f, null, null, segs);
List<float[][]> paths = assembleClosedPaths(segs.toArray(new float[0][]), 0);
// Kleinstpolygone filtern: weniger als ~60m Umfang = Artefakt (z.B. isolierte Bäume)
final float MIN_PERIMETER_SQ = 60f * 60f;
return paths.stream().filter(p -> pathPerimeterSq(p) >= MIN_PERIMETER_SQ)
.collect(java.util.stream.Collectors.toList());