mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Tributaries join the main stem at a confluence
- routing.tributaries lets an outlet keep extra surface courses; each is cut where its centerline first meets the stem and joined with a short connector - A tributary must keep half the minimum course length and arrive at or above the stem's water - Surface shape rejections carry the failing rule as their detail
This commit is contained in:
@@ -282,6 +282,7 @@ final class PackHydrologyValidator {
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "tributaries", 0, 4, errors);
|
||||
|
||||
int tileSize = integerValue(routing, "tileSize", 2048);
|
||||
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
|
||||
|
||||
@@ -1099,7 +1099,7 @@ public final class HydrologyPlanner {
|
||||
graph = mergeGraphs(surfaceGraph, undergroundGraph);
|
||||
}
|
||||
}
|
||||
List<RiverCourse> normalizedTrunkCourses = normalizeSharedTrunks(courses);
|
||||
List<RiverCourse> normalizedTrunkCourses = normalizeSharedTrunks(courses, diagnostics);
|
||||
List<RiverCourse> normalizedOutletCourses = normalizeOutletContinuations(normalizedTrunkCourses);
|
||||
courses.clear();
|
||||
courses.addAll(normalizedOutletCourses);
|
||||
@@ -1277,7 +1277,7 @@ public final class HydrologyPlanner {
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<RiverCourse> normalizeSharedTrunks(List<RiverCourse> courses) {
|
||||
private List<RiverCourse> normalizeSharedTrunks(List<RiverCourse> courses, List<HydrologyDiagnosticCandidate> diagnostics) {
|
||||
HashMap<CourseEdgeKey, RiverCourse> edgeOwners = new HashMap<>();
|
||||
for (RiverCourse course : courses) {
|
||||
if (course.sourceNodeId().isEmpty()) {
|
||||
@@ -1317,6 +1317,14 @@ public final class HydrologyPlanner {
|
||||
.get(ownedEdgeCount - 1)
|
||||
.centerline()
|
||||
.getLast();
|
||||
if (course.type() == RiverCourseType.SURFACE) {
|
||||
RiverCourse owner = edgeOwners.get(new CourseEdgeKey(course.type(), course.drainageEdges().get(ownedEdgeCount).id()));
|
||||
RiverCourse tributary = surfaceTributary(course, owner, boundary, ownedEdgeCount, diagnostics);
|
||||
if (tributary != null) {
|
||||
normalized.add(tributary);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
int retainedSegmentCount = retainedSegmentCount(course.segments(), boundary);
|
||||
if (retainedSegmentCount < 1) {
|
||||
normalized.add(course);
|
||||
@@ -1336,6 +1344,137 @@ public final class HydrologyPlanner {
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a surface course that lost its shared drainage to a longer trunk into a tributary: it is cut
|
||||
* where its own centerline first comes within a channel width of the stem and joined to the nearest
|
||||
* stem station with a short connector. The tributary must still be a real reach and must arrive at
|
||||
* or above the stem's water, so it steps down into the river instead of pooling below it.
|
||||
*/
|
||||
private RiverCourse surfaceTributary(
|
||||
RiverCourse course,
|
||||
RiverCourse owner,
|
||||
HydrologyPoint boundary,
|
||||
int ownedEdgeCount,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics
|
||||
) {
|
||||
ArrayList<HydrologyPoint> stemStations = new ArrayList<>();
|
||||
ArrayList<Integer> stemWidths = new ArrayList<>();
|
||||
for (HydraulicSegment segment : owner.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
stemStations.add(point);
|
||||
stemWidths.add(segment.width());
|
||||
}
|
||||
}
|
||||
ArrayList<HydraulicSegment> retained = new ArrayList<>();
|
||||
int stations = 0;
|
||||
HydrologyPoint joinStation = null;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
List<HydrologyPoint> centerline = segment.centerline();
|
||||
int joinIndex = -1;
|
||||
for (int index = 0; index < centerline.size() && joinIndex < 0; index++) {
|
||||
HydrologyPoint point = centerline.get(index);
|
||||
for (int stemIndex = 0; stemIndex < stemStations.size(); stemIndex++) {
|
||||
HydrologyPoint station = stemStations.get(stemIndex);
|
||||
double reach = (segment.width() + stemWidths.get(stemIndex)) / 2D + 1D;
|
||||
if (point.distanceSquared2D(station) <= reach * reach) {
|
||||
joinIndex = index;
|
||||
joinStation = station;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (joinIndex < 0) {
|
||||
retained.add(segment);
|
||||
stations += centerline.size();
|
||||
continue;
|
||||
}
|
||||
if (joinIndex > 0 || retained.isEmpty()) {
|
||||
List<HydrologyPoint> prefix = centerline.subList(0, joinIndex + 1);
|
||||
int downstreamHead = prefix.getLast().y();
|
||||
int drop = segment.upstreamHeadY() - downstreamHead;
|
||||
HydrologyFeatureType type = drop == 0 && segment.type().isDrop()
|
||||
? HydrologyFeatureType.SURFACE_POOL
|
||||
: segment.type();
|
||||
retained.add(new HydraulicSegment(
|
||||
segment.id(),
|
||||
segment.courseId(),
|
||||
type,
|
||||
segment.upstreamHeadY(),
|
||||
downstreamHead,
|
||||
segment.width(),
|
||||
segment.depth(),
|
||||
false,
|
||||
false,
|
||||
prefix
|
||||
));
|
||||
stations += prefix.size();
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (joinStation == null) {
|
||||
addTributaryDiagnostic(course, HydrologyCandidateRejection.NO_DRAINAGE_PATH, 0, diagnostics);
|
||||
return null;
|
||||
}
|
||||
if (retained.isEmpty() || stations < settings.routing().minimumSurfaceCourseLength() / 2) {
|
||||
addTributaryDiagnostic(course, HydrologyCandidateRejection.COURSE_TOO_SHORT, stations, diagnostics);
|
||||
return null;
|
||||
}
|
||||
HydraulicSegment last = retained.getLast();
|
||||
HydrologyPoint end = last.end();
|
||||
int stemHead = joinStation.y();
|
||||
if (last.downstreamHeadY() < stemHead) {
|
||||
addTributaryDiagnostic(course, HydrologyCandidateRejection.SURFACE_HEAD_RANGE,
|
||||
stemHead - last.downstreamHeadY(), diagnostics);
|
||||
return null;
|
||||
}
|
||||
if (end.x() != joinStation.x() || end.z() != joinStation.z()) {
|
||||
int drop = last.downstreamHeadY() - stemHead;
|
||||
HydrologyFeatureType type = drop == 0
|
||||
? HydrologyFeatureType.SURFACE_POOL
|
||||
: drop >= settings.hydraulics().waterfallMinimumDrop()
|
||||
? HydrologyFeatureType.WATERFALL
|
||||
: drop == 1 ? HydrologyFeatureType.RIFFLE : HydrologyFeatureType.CASCADE;
|
||||
retained.add(new HydraulicSegment(
|
||||
HydrologyHash.mix(course.id(), SEGMENT_SALT, 0x4a4f494eL),
|
||||
course.id(),
|
||||
type,
|
||||
last.downstreamHeadY(),
|
||||
stemHead,
|
||||
last.width(),
|
||||
last.depth(),
|
||||
false,
|
||||
false,
|
||||
List.of(end, new HydrologyPoint(joinStation.x(), stemHead, joinStation.z()))
|
||||
));
|
||||
}
|
||||
return new RiverCourse(
|
||||
course.id(),
|
||||
course.type(),
|
||||
course.sourceNodeId(),
|
||||
course.outletId(),
|
||||
course.profileKey(),
|
||||
course.discharge(),
|
||||
course.drainageEdges().subList(0, ownedEdgeCount),
|
||||
retained
|
||||
);
|
||||
}
|
||||
|
||||
private void addTributaryDiagnostic(
|
||||
RiverCourse course,
|
||||
HydrologyCandidateRejection rejection,
|
||||
int detail,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics
|
||||
) {
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||
HydrologyHash.mix(course.id(), DIAGNOSTIC_SALT, rejection.ordinal(), 0x5452494255544152L),
|
||||
HydrologyCandidateKind.SOURCE,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
course.segments().getFirst().start(),
|
||||
rejection,
|
||||
detail
|
||||
));
|
||||
}
|
||||
|
||||
private RiverCourse preferredTrunkCourse(RiverCourse first, RiverCourse second) {
|
||||
double firstLength = edgeLength(first.drainageEdges(), first.drainageEdges().size());
|
||||
double secondLength = edgeLength(second.drainageEdges(), second.drainageEdges().size());
|
||||
@@ -2161,7 +2300,8 @@ public final class HydrologyPlanner {
|
||||
? Math.max(1, sourceSettings.maximumPerTile())
|
||||
: sourceSettings.maximumPerTile();
|
||||
target = Math.min(maximum, target);
|
||||
target = effectiveSourceTarget(surface && !enforceGlobalSpacing, target, routing.outlets().size());
|
||||
int coursesPerOutlet = surface ? 1 + settings.routing().tributaries() : Integer.MAX_VALUE;
|
||||
target = effectiveSourceTarget(surface && !enforceGlobalSpacing, target, routing.outlets().size(), coursesPerOutlet);
|
||||
Comparator<SourceCandidate> candidateOrder = Comparator
|
||||
.comparing(SourceCandidate::required)
|
||||
.reversed();
|
||||
@@ -2170,7 +2310,7 @@ public final class HydrologyPlanner {
|
||||
.thenComparingLong(SourceCandidate::stableId));
|
||||
prioritizeInlandSource(candidates, routing);
|
||||
int guaranteed = Math.min(target, requiredMinimum);
|
||||
int maximumCoursesPerOutlet = surface ? 1 : Integer.MAX_VALUE;
|
||||
int maximumCoursesPerOutlet = coursesPerOutlet;
|
||||
SourceAdmissionSelection admission = selectSourceAdmissionsByOutlet(
|
||||
candidates,
|
||||
routing,
|
||||
@@ -2209,10 +2349,15 @@ public final class HydrologyPlanner {
|
||||
}
|
||||
|
||||
static int effectiveSourceTarget(boolean outletBounded, int requestedTarget, int outletCount) {
|
||||
if (requestedTarget < 0 || outletCount < 0) {
|
||||
return effectiveSourceTarget(outletBounded, requestedTarget, outletCount, 1);
|
||||
}
|
||||
|
||||
static int effectiveSourceTarget(boolean outletBounded, int requestedTarget, int outletCount, int coursesPerOutlet) {
|
||||
if (requestedTarget < 0 || outletCount < 0 || coursesPerOutlet < 1) {
|
||||
throw new IllegalArgumentException("Source target bounds cannot be negative.");
|
||||
}
|
||||
return outletBounded ? Math.min(requestedTarget, outletCount) : requestedTarget;
|
||||
long bound = (long) outletCount * coursesPerOutlet;
|
||||
return outletBounded ? (int) Math.min(requestedTarget, bound) : requestedTarget;
|
||||
}
|
||||
|
||||
private SourceAdmissionSelection selectSourceAdmissionsByOutlet(
|
||||
@@ -4144,7 +4289,11 @@ public final class HydrologyPlanner {
|
||||
.thenComparingLong(SurfaceCourseDraft::courseId));
|
||||
RiverCourse mainCourse = null;
|
||||
HashMap<Long, SurfaceCourseBuild> mainRejections = new HashMap<>();
|
||||
int tributaries = 0;
|
||||
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||
if (mainCourse != null && tributaries >= settings.routing().tributaries()) {
|
||||
break;
|
||||
}
|
||||
SurfaceCourseBuild candidate = buildSurfaceCourse(
|
||||
draft.courseId(),
|
||||
draft.profileKey(),
|
||||
@@ -4152,12 +4301,19 @@ public final class HydrologyPlanner {
|
||||
draft.path()
|
||||
);
|
||||
if (candidate.course() == null) {
|
||||
mainRejections.put(draft.courseId(), candidate);
|
||||
if (mainCourse == null) {
|
||||
mainRejections.put(draft.courseId(), candidate);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
mainCourse = candidate.course();
|
||||
courses.add(mainCourse);
|
||||
break;
|
||||
if (mainCourse == null) {
|
||||
mainCourse = candidate.course();
|
||||
} else {
|
||||
// A later draft to the same outlet shares the stem's tail; shared-trunk
|
||||
// normalisation cuts it at the confluence into a tributary.
|
||||
tributaries++;
|
||||
}
|
||||
courses.add(candidate.course());
|
||||
}
|
||||
if (mainCourse == null) {
|
||||
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||
@@ -4236,8 +4392,11 @@ public final class HydrologyPlanner {
|
||||
segments
|
||||
);
|
||||
}
|
||||
if (path.organicSurfaceRequired() && !surfaceShapeAccepted(path.points())) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, 0);
|
||||
if (path.organicSurfaceRequired()) {
|
||||
int shapeRejection = surfaceShapeRejection(path.points());
|
||||
if (shapeRejection != 0) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, shapeRejection);
|
||||
}
|
||||
}
|
||||
int discharge = maximumSurfaceDischarge(path.edges());
|
||||
return SurfaceCourseBuild.accepted(new RiverCourse(
|
||||
@@ -4252,10 +4411,11 @@ public final class HydrologyPlanner {
|
||||
));
|
||||
}
|
||||
|
||||
private boolean surfaceShapeAccepted(List<HydrologyPoint> routedCenterline) {
|
||||
/** 0 when the shape is organic; otherwise 1 too short, 2 too sinuous, 3 grid-locked, 4 sharp turns. */
|
||||
private int surfaceShapeRejection(List<HydrologyPoint> routedCenterline) {
|
||||
List<HydrologyPoint> centerline = withoutDuplicateRoutePoints(routedCenterline);
|
||||
if (centerline.size() < 3) {
|
||||
return false;
|
||||
return 1;
|
||||
}
|
||||
double routedLength = centerlineLength(centerline);
|
||||
HydrologyPoint start = centerline.getFirst();
|
||||
@@ -4263,7 +4423,7 @@ public final class HydrologyPlanner {
|
||||
double directLength = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
||||
if (directLength <= 0D
|
||||
|| routedLength / directLength > SURFACE_MAXIMUM_SINUOSITY) {
|
||||
return false;
|
||||
return 2;
|
||||
}
|
||||
List<HydrologyPoint> sampled = resampleRouteCenterline(
|
||||
centerline,
|
||||
@@ -4298,7 +4458,7 @@ public final class HydrologyPlanner {
|
||||
if (routedLength >= 96D
|
||||
&& (lockedLength / routedLength > SURFACE_MAXIMUM_GRID_LOCKED_FRACTION
|
||||
|| longestLockedRun > SURFACE_MAXIMUM_GRID_LOCKED_RUN)) {
|
||||
return false;
|
||||
return 3;
|
||||
}
|
||||
ArrayList<Double> turnAngles = new ArrayList<>();
|
||||
for (int pointIndex = 1; pointIndex < sampled.size() - 1; pointIndex++) {
|
||||
@@ -4308,12 +4468,12 @@ public final class HydrologyPlanner {
|
||||
sampled.get(pointIndex + 1)
|
||||
);
|
||||
if (turn > SURFACE_MAXIMUM_RENDERED_TURN_DEGREES) {
|
||||
return false;
|
||||
return 4;
|
||||
}
|
||||
turnAngles.add(turn);
|
||||
}
|
||||
if (turnAngles.isEmpty()) {
|
||||
return false;
|
||||
return 1;
|
||||
}
|
||||
for (int turnIndex = 0; turnIndex < turnAngles.size(); turnIndex++) {
|
||||
double turn = turnAngles.get(turnIndex);
|
||||
@@ -4322,7 +4482,7 @@ public final class HydrologyPlanner {
|
||||
if (turn > SURFACE_MAXIMUM_ISOLATED_TURN_DEGREES
|
||||
&& previous <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES
|
||||
&& next <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES) {
|
||||
return false;
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
turnAngles.sort(Double::compare);
|
||||
@@ -4331,9 +4491,9 @@ public final class HydrologyPlanner {
|
||||
Math.max(0, Math.min(percentileIndex, turnAngles.size() - 1))
|
||||
);
|
||||
if (percentile > SURFACE_MAXIMUM_P95_TURN_DEGREES) {
|
||||
return false;
|
||||
return 4;
|
||||
}
|
||||
return true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private double centerlineLength(List<HydrologyPoint> points) {
|
||||
|
||||
@@ -37,7 +37,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, 1D),
|
||||
new Routing(2048, 64, 8192, 8192, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0),
|
||||
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),
|
||||
@@ -166,7 +166,8 @@ public record HydrologyPlannerSettings(
|
||||
double uphillPenalty,
|
||||
double slopePenalty,
|
||||
double confluenceAttraction,
|
||||
double lengthPreference
|
||||
double lengthPreference,
|
||||
int tributaries
|
||||
) {
|
||||
public Routing {
|
||||
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) {
|
||||
@@ -193,6 +194,9 @@ public record HydrologyPlannerSettings(
|
||||
requireFiniteNonNegative(slopePenalty, "slopePenalty");
|
||||
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
|
||||
requireFiniteNonNegative(lengthPreference, "lengthPreference");
|
||||
if (tributaries < 0) {
|
||||
throw new IllegalArgumentException("tributaries must not be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
// Route refinement is derived from the lattice, never authored.
|
||||
|
||||
+2
-1
@@ -677,7 +677,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
routing.getUphillPenalty(),
|
||||
routing.getSlopePenalty(),
|
||||
routing.getConfluenceAttraction(),
|
||||
routing.getLengthPreference()
|
||||
routing.getLengthPreference(),
|
||||
routing.getTributaries()
|
||||
);
|
||||
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
|
||||
surfaceEnabled,
|
||||
|
||||
@@ -75,4 +75,9 @@ public class IrisRiverRoutingConfig {
|
||||
@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;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(4)
|
||||
@Desc("Extra surface courses an outlet may accept as tributaries joining its main river; 0 keeps one river per outlet.")
|
||||
private int tributaries = 1;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(256, 16, 1_089, 256, 16, 8, 0.5D, 12D, 0.5D, 0.2D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
|
||||
+1
-1
@@ -668,7 +668,7 @@ public class HydrologyPlannerGlobalInvariantTest {
|
||||
12D,
|
||||
0.5D,
|
||||
0.1D
|
||||
, 1D),
|
||||
, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
surfaceEnabled,
|
||||
surfaceSources,
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public class HydrologyPlannerSettingsTest {
|
||||
0D,
|
||||
0D,
|
||||
0D
|
||||
, 1D);
|
||||
, 1D, 0);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ public class HydrologyPlannerSurfacePoolTest {
|
||||
private static HydrologyPlannerSettings settings(double density, int spacing, int minimumRadius, int maximumRadius, int depth) {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
|
||||
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D);
|
||||
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, 0);
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
routing,
|
||||
|
||||
@@ -581,7 +581,7 @@ public class HydrologyPlannerTest {
|
||||
routing.valleyPreference(),
|
||||
routing.uphillPenalty(),
|
||||
routing.slopePenalty(),
|
||||
routing.confluenceAttraction(), 1D
|
||||
routing.confluenceAttraction(), 1D, 0
|
||||
),
|
||||
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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 0, 0, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0,
|
||||
surfaceSources,
|
||||
@@ -1800,7 +1800,7 @@ public class HydrologyPlannerTest {
|
||||
24D,
|
||||
2D,
|
||||
0.2D
|
||||
, 1D),
|
||||
, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128),
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class HydrologyPlannerTributaryTest {
|
||||
private static final HydrologyTileKey TILE = new HydrologyTileKey(0, 0);
|
||||
|
||||
@Test
|
||||
public void aSecondSourceJoinsTheMainStemAsATributaryWhenAllowed() {
|
||||
HydrologyTerrainSampler terrain = HydrologyPlannerTributaryTest::valley;
|
||||
|
||||
HydrologyTile single = new HydrologyPlanner(19L, settings(0), terrain).plan(TILE);
|
||||
HydrologyTile split = new HydrologyPlanner(19L, settings(1), terrain).plan(TILE);
|
||||
|
||||
List<RiverCourse> singleCourses = surface(single);
|
||||
List<RiverCourse> splitCourses = surface(split);
|
||||
assertEquals(single.diagnosticCandidates().toString(), 1, singleCourses.size());
|
||||
assertEquals(split.diagnosticCandidates().toString(), 2, splitCourses.size());
|
||||
RiverCourse stem = null;
|
||||
RiverCourse tributary = null;
|
||||
for (RiverCourse course : splitCourses) {
|
||||
if (stem == null || stations(course) > stations(stem)) {
|
||||
tributary = stem;
|
||||
stem = course;
|
||||
} else {
|
||||
tributary = course;
|
||||
}
|
||||
}
|
||||
assertNotNull(stem);
|
||||
assertNotNull(tributary);
|
||||
assertEquals(stem.outletId(), tributary.outletId());
|
||||
assertTrue(stem.sourceNodeId().getAsLong() != tributary.sourceNodeId().getAsLong());
|
||||
|
||||
HydrologyPoint confluence = tributary.segments().getLast().end();
|
||||
Integer stemHead = null;
|
||||
for (HydraulicSegment segment : stem.segments()) {
|
||||
for (HydrologyPoint point : segment.centerline()) {
|
||||
if (point.x() == confluence.x() && point.z() == confluence.z()) {
|
||||
stemHead = point.y();
|
||||
}
|
||||
}
|
||||
}
|
||||
assertNotNull("tributary ends on the stem", stemHead);
|
||||
assertTrue(confluence.y() >= stemHead);
|
||||
assertTrue(stations(tributary) >= 192);
|
||||
assertTrue(stations(tributary) < stations(stem));
|
||||
for (HydraulicSegment segment : tributary.segments()) {
|
||||
assertTrue(segment.upstreamHeadY() >= segment.downstreamHeadY());
|
||||
}
|
||||
}
|
||||
|
||||
private static int stations(RiverCourse course) {
|
||||
int stations = 0;
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
stations += segment.centerline().size();
|
||||
}
|
||||
return stations;
|
||||
}
|
||||
|
||||
private static List<RiverCourse> surface(HydrologyTile tile) {
|
||||
ArrayList<RiverCourse> courses = new ArrayList<>();
|
||||
for (RiverCourse course : tile.courses()) {
|
||||
if (course.type() == RiverCourseType.SURFACE) {
|
||||
courses.add(course);
|
||||
}
|
||||
}
|
||||
return courses;
|
||||
}
|
||||
|
||||
/** A plain sloping east into the sea with a valley along z = 0 that both flanks drain into. */
|
||||
private static HydrologyTerrainSample valley(int x, int z) {
|
||||
if (x >= 900) {
|
||||
return HydrologyTerrainSample.ocean(50, "sea");
|
||||
}
|
||||
// The valley floor wanders so the stem never runs straight along the lattice, and the eastward
|
||||
// fall is steeper than the valley flanks so routes drift into the valley at a shallow angle.
|
||||
int valleyCenter = (int) Math.round(Math.sin(x / 150D) * 40D);
|
||||
int valley = Math.max(0, 12 - Math.abs(z - valleyCenter) / 20);
|
||||
int wave = (int) Math.round(Math.sin(x / 29D + z / 43D) * 3D + Math.sin(z / 19D - x / 61D + 1D) * 2D);
|
||||
int height = 240 - x / 5 - valley + wave;
|
||||
boolean source = x >= 32 && x <= 96 && (Math.abs(z - 300) <= 40 || Math.abs(z - 150) <= 40);
|
||||
return new HydrologyTerrainSample(
|
||||
height,
|
||||
0D,
|
||||
false,
|
||||
false,
|
||||
height - 40,
|
||||
height - 38,
|
||||
true,
|
||||
true,
|
||||
source,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
0D,
|
||||
source ? 1D : 0D,
|
||||
0D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
1D,
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
"land",
|
||||
List.of("default"),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
private static HydrologyPlannerSettings settings(int tributaries) {
|
||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
|
||||
1024, 64, base.routing().maximumRouteNodes(), 4096, 384, 192, 1.5D, 24D, 2D, 0.2D, 1D, tributaries);
|
||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 4D, 0, 0, 4, 128);
|
||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||
true, sources, 4, 8, 2, 4, 40, 1.5D, HydrologyPlannerSettings.Banks.defaults());
|
||||
HydrologyPlannerSettings.Underground underground = new HydrologyPlannerSettings.Underground(
|
||||
false, base.underground().sources(), -48, 72, 3, 8, 1, 3, 6, 14, true);
|
||||
HydrologyPlannerSettings.Outlets outlets = new HydrologyPlannerSettings.Outlets(
|
||||
true,
|
||||
base.outlets().coastalGrotto(),
|
||||
base.outlets().inlandGrotto(),
|
||||
false,
|
||||
base.outlets().coastalCliffMinimumHeight(),
|
||||
base.outlets().mouthLevelingDistance(),
|
||||
base.outlets().maximumOceanApron(),
|
||||
1
|
||||
);
|
||||
return new HydrologyPlannerSettings(
|
||||
base.seaLevel(),
|
||||
routing,
|
||||
surface,
|
||||
base.hydraulics(),
|
||||
underground,
|
||||
outlets,
|
||||
base.geometry(),
|
||||
List.of(),
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -195,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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(64, 16, 128, 64, 0, 0, 0D, 0D, 0D, 0D, 1D, 0),
|
||||
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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(64, 16, 128, 96, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
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),
|
||||
|
||||
+1
-1
@@ -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, 1D),
|
||||
new HydrologyPlannerSettings.Routing(128, 16, 512, 256, 16, 8, 0.5D, 12D, 0.5D, 0.1D, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
|
||||
@@ -229,7 +229,7 @@ public final class HydrologyPlannerProbe {
|
||||
12D,
|
||||
0.5D,
|
||||
0.1D
|
||||
, 1D),
|
||||
, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
@@ -440,7 +440,7 @@ public final class HydrologyPlannerProbe {
|
||||
12D,
|
||||
0.5D,
|
||||
0.2D
|
||||
, 1D),
|
||||
, 1D, 0),
|
||||
new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
surfaceSources,
|
||||
|
||||
Reference in New Issue
Block a user