mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-27 14:30:14 +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:
1 parent
185e2fa4ae
commit
d23d6d5c9b
15 files changed
+360
-37
No files matched your search
@@ -282,6 +282,7 @@ final class PackHydrologyValidator {
|
|||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "slopePenalty", 0D, 16D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "confluenceAttraction", 0D, 1D, errors);
|
||||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors);
|
PackJsonFieldChecks.validateOptionalDoubleRange(path, routing, "lengthPreference", 0D, 8D, errors);
|
||||||
|
PackJsonFieldChecks.validateOptionalIntegerRange(path, routing, "tributaries", 0, 4, errors);
|
||||||
|
|
||||||
int tileSize = integerValue(routing, "tileSize", 2048);
|
int tileSize = integerValue(routing, "tileSize", 2048);
|
||||||
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
|
int sampleSpacing = integerValue(routing, "sampleSpacing", 64);
|
||||||
|
|||||||
@@ -1099,7 +1099,7 @@ public final class HydrologyPlanner {
|
|||||||
graph = mergeGraphs(surfaceGraph, undergroundGraph);
|
graph = mergeGraphs(surfaceGraph, undergroundGraph);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<RiverCourse> normalizedTrunkCourses = normalizeSharedTrunks(courses);
|
List<RiverCourse> normalizedTrunkCourses = normalizeSharedTrunks(courses, diagnostics);
|
||||||
List<RiverCourse> normalizedOutletCourses = normalizeOutletContinuations(normalizedTrunkCourses);
|
List<RiverCourse> normalizedOutletCourses = normalizeOutletContinuations(normalizedTrunkCourses);
|
||||||
courses.clear();
|
courses.clear();
|
||||||
courses.addAll(normalizedOutletCourses);
|
courses.addAll(normalizedOutletCourses);
|
||||||
@@ -1277,7 +1277,7 @@ public final class HydrologyPlanner {
|
|||||||
return false;
|
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<>();
|
HashMap<CourseEdgeKey, RiverCourse> edgeOwners = new HashMap<>();
|
||||||
for (RiverCourse course : courses) {
|
for (RiverCourse course : courses) {
|
||||||
if (course.sourceNodeId().isEmpty()) {
|
if (course.sourceNodeId().isEmpty()) {
|
||||||
@@ -1317,6 +1317,14 @@ public final class HydrologyPlanner {
|
|||||||
.get(ownedEdgeCount - 1)
|
.get(ownedEdgeCount - 1)
|
||||||
.centerline()
|
.centerline()
|
||||||
.getLast();
|
.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);
|
int retainedSegmentCount = retainedSegmentCount(course.segments(), boundary);
|
||||||
if (retainedSegmentCount < 1) {
|
if (retainedSegmentCount < 1) {
|
||||||
normalized.add(course);
|
normalized.add(course);
|
||||||
@@ -1336,6 +1344,137 @@ public final class HydrologyPlanner {
|
|||||||
return List.copyOf(normalized);
|
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) {
|
private RiverCourse preferredTrunkCourse(RiverCourse first, RiverCourse second) {
|
||||||
double firstLength = edgeLength(first.drainageEdges(), first.drainageEdges().size());
|
double firstLength = edgeLength(first.drainageEdges(), first.drainageEdges().size());
|
||||||
double secondLength = edgeLength(second.drainageEdges(), second.drainageEdges().size());
|
double secondLength = edgeLength(second.drainageEdges(), second.drainageEdges().size());
|
||||||
@@ -2161,7 +2300,8 @@ public final class HydrologyPlanner {
|
|||||||
? Math.max(1, sourceSettings.maximumPerTile())
|
? Math.max(1, sourceSettings.maximumPerTile())
|
||||||
: sourceSettings.maximumPerTile();
|
: sourceSettings.maximumPerTile();
|
||||||
target = Math.min(maximum, target);
|
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
|
Comparator<SourceCandidate> candidateOrder = Comparator
|
||||||
.comparing(SourceCandidate::required)
|
.comparing(SourceCandidate::required)
|
||||||
.reversed();
|
.reversed();
|
||||||
@@ -2170,7 +2310,7 @@ public final class HydrologyPlanner {
|
|||||||
.thenComparingLong(SourceCandidate::stableId));
|
.thenComparingLong(SourceCandidate::stableId));
|
||||||
prioritizeInlandSource(candidates, routing);
|
prioritizeInlandSource(candidates, routing);
|
||||||
int guaranteed = Math.min(target, requiredMinimum);
|
int guaranteed = Math.min(target, requiredMinimum);
|
||||||
int maximumCoursesPerOutlet = surface ? 1 : Integer.MAX_VALUE;
|
int maximumCoursesPerOutlet = coursesPerOutlet;
|
||||||
SourceAdmissionSelection admission = selectSourceAdmissionsByOutlet(
|
SourceAdmissionSelection admission = selectSourceAdmissionsByOutlet(
|
||||||
candidates,
|
candidates,
|
||||||
routing,
|
routing,
|
||||||
@@ -2209,10 +2349,15 @@ public final class HydrologyPlanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int effectiveSourceTarget(boolean outletBounded, int requestedTarget, int outletCount) {
|
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.");
|
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(
|
private SourceAdmissionSelection selectSourceAdmissionsByOutlet(
|
||||||
@@ -4144,7 +4289,11 @@ public final class HydrologyPlanner {
|
|||||||
.thenComparingLong(SurfaceCourseDraft::courseId));
|
.thenComparingLong(SurfaceCourseDraft::courseId));
|
||||||
RiverCourse mainCourse = null;
|
RiverCourse mainCourse = null;
|
||||||
HashMap<Long, SurfaceCourseBuild> mainRejections = new HashMap<>();
|
HashMap<Long, SurfaceCourseBuild> mainRejections = new HashMap<>();
|
||||||
|
int tributaries = 0;
|
||||||
for (SurfaceCourseDraft draft : outletDrafts) {
|
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||||
|
if (mainCourse != null && tributaries >= settings.routing().tributaries()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
SurfaceCourseBuild candidate = buildSurfaceCourse(
|
SurfaceCourseBuild candidate = buildSurfaceCourse(
|
||||||
draft.courseId(),
|
draft.courseId(),
|
||||||
draft.profileKey(),
|
draft.profileKey(),
|
||||||
@@ -4152,12 +4301,19 @@ public final class HydrologyPlanner {
|
|||||||
draft.path()
|
draft.path()
|
||||||
);
|
);
|
||||||
if (candidate.course() == null) {
|
if (candidate.course() == null) {
|
||||||
mainRejections.put(draft.courseId(), candidate);
|
if (mainCourse == null) {
|
||||||
|
mainRejections.put(draft.courseId(), candidate);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
mainCourse = candidate.course();
|
if (mainCourse == null) {
|
||||||
courses.add(mainCourse);
|
mainCourse = candidate.course();
|
||||||
break;
|
} 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) {
|
if (mainCourse == null) {
|
||||||
for (SurfaceCourseDraft draft : outletDrafts) {
|
for (SurfaceCourseDraft draft : outletDrafts) {
|
||||||
@@ -4236,8 +4392,11 @@ public final class HydrologyPlanner {
|
|||||||
segments
|
segments
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (path.organicSurfaceRequired() && !surfaceShapeAccepted(path.points())) {
|
if (path.organicSurfaceRequired()) {
|
||||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, 0);
|
int shapeRejection = surfaceShapeRejection(path.points());
|
||||||
|
if (shapeRejection != 0) {
|
||||||
|
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED, shapeRejection);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
int discharge = maximumSurfaceDischarge(path.edges());
|
int discharge = maximumSurfaceDischarge(path.edges());
|
||||||
return SurfaceCourseBuild.accepted(new RiverCourse(
|
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);
|
List<HydrologyPoint> centerline = withoutDuplicateRoutePoints(routedCenterline);
|
||||||
if (centerline.size() < 3) {
|
if (centerline.size() < 3) {
|
||||||
return false;
|
return 1;
|
||||||
}
|
}
|
||||||
double routedLength = centerlineLength(centerline);
|
double routedLength = centerlineLength(centerline);
|
||||||
HydrologyPoint start = centerline.getFirst();
|
HydrologyPoint start = centerline.getFirst();
|
||||||
@@ -4263,7 +4423,7 @@ public final class HydrologyPlanner {
|
|||||||
double directLength = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
double directLength = StrictMath.hypot(end.x() - start.x(), end.z() - start.z());
|
||||||
if (directLength <= 0D
|
if (directLength <= 0D
|
||||||
|| routedLength / directLength > SURFACE_MAXIMUM_SINUOSITY) {
|
|| routedLength / directLength > SURFACE_MAXIMUM_SINUOSITY) {
|
||||||
return false;
|
return 2;
|
||||||
}
|
}
|
||||||
List<HydrologyPoint> sampled = resampleRouteCenterline(
|
List<HydrologyPoint> sampled = resampleRouteCenterline(
|
||||||
centerline,
|
centerline,
|
||||||
@@ -4298,7 +4458,7 @@ public final class HydrologyPlanner {
|
|||||||
if (routedLength >= 96D
|
if (routedLength >= 96D
|
||||||
&& (lockedLength / routedLength > SURFACE_MAXIMUM_GRID_LOCKED_FRACTION
|
&& (lockedLength / routedLength > SURFACE_MAXIMUM_GRID_LOCKED_FRACTION
|
||||||
|| longestLockedRun > SURFACE_MAXIMUM_GRID_LOCKED_RUN)) {
|
|| longestLockedRun > SURFACE_MAXIMUM_GRID_LOCKED_RUN)) {
|
||||||
return false;
|
return 3;
|
||||||
}
|
}
|
||||||
ArrayList<Double> turnAngles = new ArrayList<>();
|
ArrayList<Double> turnAngles = new ArrayList<>();
|
||||||
for (int pointIndex = 1; pointIndex < sampled.size() - 1; pointIndex++) {
|
for (int pointIndex = 1; pointIndex < sampled.size() - 1; pointIndex++) {
|
||||||
@@ -4308,12 +4468,12 @@ public final class HydrologyPlanner {
|
|||||||
sampled.get(pointIndex + 1)
|
sampled.get(pointIndex + 1)
|
||||||
);
|
);
|
||||||
if (turn > SURFACE_MAXIMUM_RENDERED_TURN_DEGREES) {
|
if (turn > SURFACE_MAXIMUM_RENDERED_TURN_DEGREES) {
|
||||||
return false;
|
return 4;
|
||||||
}
|
}
|
||||||
turnAngles.add(turn);
|
turnAngles.add(turn);
|
||||||
}
|
}
|
||||||
if (turnAngles.isEmpty()) {
|
if (turnAngles.isEmpty()) {
|
||||||
return false;
|
return 1;
|
||||||
}
|
}
|
||||||
for (int turnIndex = 0; turnIndex < turnAngles.size(); turnIndex++) {
|
for (int turnIndex = 0; turnIndex < turnAngles.size(); turnIndex++) {
|
||||||
double turn = turnAngles.get(turnIndex);
|
double turn = turnAngles.get(turnIndex);
|
||||||
@@ -4322,7 +4482,7 @@ public final class HydrologyPlanner {
|
|||||||
if (turn > SURFACE_MAXIMUM_ISOLATED_TURN_DEGREES
|
if (turn > SURFACE_MAXIMUM_ISOLATED_TURN_DEGREES
|
||||||
&& previous <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES
|
&& previous <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES
|
||||||
&& next <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES) {
|
&& next <= SURFACE_MAXIMUM_ISOLATED_NEIGHBOR_TURN_DEGREES) {
|
||||||
return false;
|
return 4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
turnAngles.sort(Double::compare);
|
turnAngles.sort(Double::compare);
|
||||||
@@ -4331,9 +4491,9 @@ public final class HydrologyPlanner {
|
|||||||
Math.max(0, Math.min(percentileIndex, turnAngles.size() - 1))
|
Math.max(0, Math.min(percentileIndex, turnAngles.size() - 1))
|
||||||
);
|
);
|
||||||
if (percentile > SURFACE_MAXIMUM_P95_TURN_DEGREES) {
|
if (percentile > SURFACE_MAXIMUM_P95_TURN_DEGREES) {
|
||||||
return false;
|
return 4;
|
||||||
}
|
}
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double centerlineLength(List<HydrologyPoint> points) {
|
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);
|
Source undergroundSources = new Source(true, 0.25D, Integer.MIN_VALUE, 0, 1, 512);
|
||||||
return new HydrologyPlannerSettings(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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 Surface(true, surfaceSources, 4, 8, 2, 4, 10, 1.5D, Banks.defaults()),
|
||||||
new Hydraulics(8),
|
new Hydraulics(8),
|
||||||
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
|
new Underground(true, undergroundSources, -48, 72, 3, 8, 1, 3, 6, 14, true),
|
||||||
@@ -166,7 +166,8 @@ public record HydrologyPlannerSettings(
|
|||||||
double uphillPenalty,
|
double uphillPenalty,
|
||||||
double slopePenalty,
|
double slopePenalty,
|
||||||
double confluenceAttraction,
|
double confluenceAttraction,
|
||||||
double lengthPreference
|
double lengthPreference,
|
||||||
|
int tributaries
|
||||||
) {
|
) {
|
||||||
public Routing {
|
public Routing {
|
||||||
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) {
|
if (tileSize < 32 || sampleSpacing < 4 || tileSize % sampleSpacing != 0) {
|
||||||
@@ -193,6 +194,9 @@ public record HydrologyPlannerSettings(
|
|||||||
requireFiniteNonNegative(slopePenalty, "slopePenalty");
|
requireFiniteNonNegative(slopePenalty, "slopePenalty");
|
||||||
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
|
requireFiniteNonNegative(confluenceAttraction, "confluenceAttraction");
|
||||||
requireFiniteNonNegative(lengthPreference, "lengthPreference");
|
requireFiniteNonNegative(lengthPreference, "lengthPreference");
|
||||||
|
if (tributaries < 0) {
|
||||||
|
throw new IllegalArgumentException("tributaries must not be negative.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Route refinement is derived from the lattice, never authored.
|
// Route refinement is derived from the lattice, never authored.
|
||||||
|
|||||||
+2
-1
@@ -677,7 +677,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
|||||||
routing.getUphillPenalty(),
|
routing.getUphillPenalty(),
|
||||||
routing.getSlopePenalty(),
|
routing.getSlopePenalty(),
|
||||||
routing.getConfluenceAttraction(),
|
routing.getConfluenceAttraction(),
|
||||||
routing.getLengthPreference()
|
routing.getLengthPreference(),
|
||||||
|
routing.getTributaries()
|
||||||
);
|
);
|
||||||
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
|
HydrologyPlannerSettings.Source plannerSurfaceSources = new HydrologyPlannerSettings.Source(
|
||||||
surfaceEnabled,
|
surfaceEnabled,
|
||||||
|
|||||||
@@ -75,4 +75,9 @@ public class IrisRiverRoutingConfig {
|
|||||||
@MaxNumber(8)
|
@MaxNumber(8)
|
||||||
@Desc("How strongly longer source-to-outlet routes are preferred when choosing river sources; 0 ranks by elevation alone.")
|
@Desc("How strongly longer source-to-outlet routes are preferred when choosing river sources; 0 ranks by elevation alone.")
|
||||||
private double lengthPreference = 1D;
|
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(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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(
|
new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
|
|||||||
+1
-1
@@ -668,7 +668,7 @@ public class HydrologyPlannerGlobalInvariantTest {
|
|||||||
12D,
|
12D,
|
||||||
0.5D,
|
0.5D,
|
||||||
0.1D
|
0.1D
|
||||||
, 1D),
|
, 1D, 0),
|
||||||
new HydrologyPlannerSettings.Surface(
|
new HydrologyPlannerSettings.Surface(
|
||||||
surfaceEnabled,
|
surfaceEnabled,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
|
|||||||
+1
-1
@@ -79,7 +79,7 @@ public class HydrologyPlannerSettingsTest {
|
|||||||
0D,
|
0D,
|
||||||
0D,
|
0D,
|
||||||
0D
|
0D
|
||||||
, 1D);
|
, 1D, 0);
|
||||||
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
|
HydrologyPlannerSettings.Source sources = new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0);
|
||||||
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
HydrologyPlannerSettings.Surface surface = new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ public class HydrologyPlannerSurfacePoolTest {
|
|||||||
private static HydrologyPlannerSettings settings(double density, int spacing, int minimumRadius, int maximumRadius, int depth) {
|
private static HydrologyPlannerSettings settings(double density, int spacing, int minimumRadius, int maximumRadius, int depth) {
|
||||||
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
HydrologyPlannerSettings base = HydrologyPlannerSettings.defaults();
|
||||||
HydrologyPlannerSettings.Routing routing = new HydrologyPlannerSettings.Routing(
|
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(
|
return new HydrologyPlannerSettings(
|
||||||
base.seaLevel(),
|
base.seaLevel(),
|
||||||
routing,
|
routing,
|
||||||
|
|||||||
@@ -581,7 +581,7 @@ public class HydrologyPlannerTest {
|
|||||||
routing.valleyPreference(),
|
routing.valleyPreference(),
|
||||||
routing.uphillPenalty(),
|
routing.uphillPenalty(),
|
||||||
routing.slopePenalty(),
|
routing.slopePenalty(),
|
||||||
routing.confluenceAttraction(), 1D
|
routing.confluenceAttraction(), 1D, 0
|
||||||
),
|
),
|
||||||
base.surface(),
|
base.surface(),
|
||||||
base.hydraulics(),
|
base.hydraulics(),
|
||||||
@@ -1733,7 +1733,7 @@ public class HydrologyPlannerTest {
|
|||||||
);
|
);
|
||||||
return new HydrologyPlannerSettings(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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(
|
new HydrologyPlannerSettings.Surface(
|
||||||
surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0,
|
surfaceDensity > 0D || surfaceSources.maximumPerTile() > 0,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
@@ -1800,7 +1800,7 @@ public class HydrologyPlannerTest {
|
|||||||
24D,
|
24D,
|
||||||
2D,
|
2D,
|
||||||
0.2D
|
0.2D
|
||||||
, 1D),
|
, 1D, 0),
|
||||||
new HydrologyPlannerSettings.Surface(
|
new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
new HydrologyPlannerSettings.Source(true, 8D, 80, 0, 8, 128),
|
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(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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(
|
new HydrologyPlannerSettings.Surface(
|
||||||
surfaceDensity > 0D,
|
surfaceDensity > 0D,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ public class HydrologyTileCacheTest {
|
|||||||
);
|
);
|
||||||
return new HydrologyPlannerSettings(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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,
|
new HydrologyPlannerSettings.Surface(false, disabled, 2, 4, 1, 2, 4, 1D,
|
||||||
HydrologyPlannerSettings.Banks.defaults()),
|
HydrologyPlannerSettings.Banks.defaults()),
|
||||||
new HydrologyPlannerSettings.Hydraulics(3),
|
new HydrologyPlannerSettings.Hydraulics(3),
|
||||||
@@ -282,7 +282,7 @@ public class HydrologyTileCacheTest {
|
|||||||
);
|
);
|
||||||
return new HydrologyPlannerSettings(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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.Surface(true, surfaceSources, 4, 8, 2, 3, 20, 1.5D, tileBoundedBanks()),
|
||||||
new HydrologyPlannerSettings.Hydraulics(3),
|
new HydrologyPlannerSettings.Hydraulics(3),
|
||||||
new HydrologyPlannerSettings.Underground(false, disabled, -32, 32, 2, 4, 1, 2, 3, 4, false),
|
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(
|
return new HydrologyPlannerSettings(
|
||||||
63,
|
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(
|
new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ public final class HydrologyPlannerProbe {
|
|||||||
12D,
|
12D,
|
||||||
0.5D,
|
0.5D,
|
||||||
0.1D
|
0.1D
|
||||||
, 1D),
|
, 1D, 0),
|
||||||
new HydrologyPlannerSettings.Surface(
|
new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
@@ -440,7 +440,7 @@ public final class HydrologyPlannerProbe {
|
|||||||
12D,
|
12D,
|
||||||
0.5D,
|
0.5D,
|
||||||
0.2D
|
0.2D
|
||||||
, 1D),
|
, 1D, 0),
|
||||||
new HydrologyPlannerSettings.Surface(
|
new HydrologyPlannerSettings.Surface(
|
||||||
true,
|
true,
|
||||||
surfaceSources,
|
surfaceSources,
|
||||||
|
|||||||
Reference in new issue
Block a user