Prefer longer routes when ranking river sources

routing.lengthPreference adds route length to the source score so outlets take their longest legal course.
This commit is contained in:
Brian Neumann-Fopiano
2026-09-01 23:33:15 -04:00
parent 6945cbfd9b
commit 4b497a7d20
13 changed files with 28 additions and 17 deletions
@@ -278,6 +278,7 @@ final class PackHydrologyValidator {
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "uphillPenalty", 0D, 128D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors);
int tileSize = integerValue(routing, "tileSize", 2048);
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
@@ -2113,9 +2113,10 @@ public final class HydrologyPlanner {
addSourceDiagnostic(node, surface, stable, HydrologyCandidateRejection.COURSE_TOO_SHORT, diagnostics);
continue;
}
// Elevation and route length share the same scale: 1000 route blocks count as 100 blocks of height.
double score = (required ? 1.0E15D : 0D)
+ weight * 1.0E9D
+ terrain.naturalHeight() * 1.0E5D
+ (terrain.naturalHeight() + routeLength * 0.1D * settings.routing().lengthPreference()) * 1.0E5D
+ routing.potential()[node.index()]
+ HydrologyHash.unit(stable);
candidates.add(new SourceCandidate(node.index(), stable, score, required));
@@ -35,7 +35,7 @@ public record HydrologyPlannerSettings(
Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512);
return new HydrologyPlannerSettings(
63,
new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D),
new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D),
new Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
new Hydraulics(8),
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
@@ -151,7 +151,8 @@ public record HydrologyPlannerSettings(
double valleyPreference,
double uphillPenalty,
double slopePenalty,
double confluenceAttraction
double confluenceAttraction,
double lengthPreference
) {
public Routing {
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) {
@@ -177,6 +178,7 @@ public record HydrologyPlannerSettings(
requireFiniteNonNegative(uphillPenalty, "uphillPenalty");
requireFiniteNonNegative(slopePenalty, "slopePenalty");
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
requireFiniteNonNegative(lengthPreference, "lengthPreference");
}
// Route refinement is derived from the lattice, never authored.
@@ -671,7 +671,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
routing.getValleyPreference(),
routing.getUphillPenalty(),
routing.getSlopePenalty(),
routing.getConfluenceAttraction()
routing.getConfluenceAttraction(),
routing.getLengthPreference()
);
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
surfaceEnabled,
@@ -70,4 +70,9 @@ public class IrisRiverRoutingConfig {
@MaxNumber(1)
@Desc("How strongly routes are drawn toward existing drainage.")
private double confluenceAttraction = 0.2D;
@MinNumber(0)
@MaxNumber(8)
@Desc("How strongly longer source-to-outlet routes are preferred when choosing river sources; 0 ranks by elevation alone.")
private double lengthPreference = 1D;
}
@@ -252,7 +252,7 @@ public class HydrologyPlannerCrossTileCaveAdmissionIntegrationTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D),
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -668,7 +668,7 @@ public class HydrologyPlannerGlobalInvariantTest {
12D,
0.5D,
0.1D
),
, 1D),
new HydrologyPlannerSettings.Surface(
surfaceEnabled,
surfaceSources,
@@ -79,7 +79,7 @@ public class HydrologyPlannerSettingsTest {
0D,
0D,
0D
);
, 1D);
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
true,
@@ -581,7 +581,7 @@ public class HydrologyPlannerTest {
routing.valleyPreference(),
routing.uphillPenalty(),
routing.slopePenalty(),
routing.confluenceAttraction()
routing.confluenceAttraction(), 1D
),
base.surface(),
base.hydraulics(),
@@ -1733,7 +1733,7 @@ public class HydrologyPlannerTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D),
new HydrologyPlannerSettings.Surface(
surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0,
surfaceSources,
@@ -1800,7 +1800,7 @@ public class HydrologyPlannerTest {
24D,
2D,
0.2D
),
, 1D),
new HydrologyPlannerSettings.Surface(
true,
new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128),
@@ -96,7 +96,8 @@ public class HydrologyStyledGeometryTest {
widestSurfaceWidth = Math.max(widestSurfaceWidth, segment.width());
}
assertTrue(widestSurfaceWidth >= surfaceStart.width());
assertTrue(widestSurfaceWidth <= surfaceSettings.surface().maximumWidth() * 2);
assertTrue(widestSurfaceWidth <= (int) Math.ceil(surfaceSettings.surface().maximumWidth()
* Math.max(2D, surfaceSettings.surface().banks().springWidthRatio())));
RiverCourse underground = courses(undergroundFirst, RiverCourseType.UNDERGROUND).getFirst();
DrainageNode undergroundSource = undergroundFirst.node(underground.sourceNodeId().orElseThrow()).orElseThrow();
@@ -194,7 +195,7 @@ public class HydrologyStyledGeometryTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D),
new HydrologyPlannerSettings.Surface(
surfaceDensity > 0D,
surfaceSources,
@@ -243,7 +243,7 @@ public class HydrologyTileCacheTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D),
new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D, 1D),
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
HydrologyPlannerSettings.Banks.defaults()),
new HydrologyPlannerSettings.Hydraulics(3),
@@ -282,7 +282,7 @@ public class HydrologyTileCacheTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D),
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D),
new HydrologyPlannerSettings.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
new HydrologyPlannerSettings.Hydraulics(3),
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
@@ -1530,7 +1530,7 @@ public class MantleHydrologyComponentTest {
);
return new HydrologyPlannerSettings(
63,
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D),
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -228,7 +228,7 @@ public final class HydrologyPlannerProbe {
12D,
0.5D,
0.1D
),
, 1D),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,
@@ -439,7 +439,7 @@ public final class HydrologyPlannerProbe {
12D,
0.5D,
0.2D
),
, 1D),
new HydrologyPlannerSettings.Surface(
true,
surfaceSources,