mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 18:11:24 +01:00
refactor: river generation start
This commit is contained in:
parent
4833a8ab35
commit
4e65616dbc
11 changed files with 285 additions and 265 deletions
|
|
@ -1,8 +1,8 @@
|
|||
// @ts-nocheckd
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {TIME} from "config/logging";
|
||||
import {rn} from "utils/numberUtils";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {getInputNumber, getInputValue} from "utils/nodeUtils";
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
|
|
@ -39,69 +39,6 @@ window.Lakes = (function () {
|
|||
return lakeOutCells;
|
||||
};
|
||||
|
||||
// get array of land cells aroound lake
|
||||
const getShoreline = function (lake: IPackFeatureLake, pack: IPack) {
|
||||
const uniqueCells = new Set();
|
||||
lake.vertices.forEach(v =>
|
||||
pack.vertices.c[v].forEach(c => pack.cells.h[c] >= MIN_LAND_HEIGHT && uniqueCells.add(c))
|
||||
);
|
||||
lake.shoreline = [...uniqueCells];
|
||||
};
|
||||
|
||||
const prepareLakeData = (h: Uint8Array, pack: IPack) => {
|
||||
const cells = pack.cells;
|
||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||
|
||||
pack.features.forEach(feature => {
|
||||
if (!feature || feature.type !== "lake") return;
|
||||
delete feature.flux;
|
||||
delete feature.inlets;
|
||||
delete feature.outlet;
|
||||
delete feature.height;
|
||||
delete feature.closed;
|
||||
!feature.shoreline && getShoreline(feature, pack);
|
||||
|
||||
// lake surface height is as lowest land cells around
|
||||
const min = feature.shoreline.sort((a, b) => h[a] - h[b])[0];
|
||||
feature.height = h[min] - 0.1;
|
||||
|
||||
// check if lake can be open (not in deep depression)
|
||||
if (ELEVATION_LIMIT === 80) {
|
||||
feature.closed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let deep = true;
|
||||
const threshold = feature.height + ELEVATION_LIMIT;
|
||||
const queue = [min];
|
||||
const checked = [];
|
||||
checked[min] = true;
|
||||
|
||||
// check if elevated lake can potentially pour to another water body
|
||||
while (deep && queue.length) {
|
||||
const q = queue.pop();
|
||||
|
||||
for (const n of cells.c[q]) {
|
||||
if (checked[n]) continue;
|
||||
if (h[n] >= threshold) continue;
|
||||
|
||||
if (h[n] < 20) {
|
||||
const nFeature = pack.features[cells.f[n]];
|
||||
if ((nFeature && nFeature.type === "ocean") || feature.height > nFeature.height) {
|
||||
deep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
checked[n] = true;
|
||||
queue.push(n);
|
||||
}
|
||||
}
|
||||
|
||||
feature.closed = deep;
|
||||
});
|
||||
};
|
||||
|
||||
const cleanupLakeData = function (pack: IPack) {
|
||||
for (const feature of pack.features) {
|
||||
if (feature.type !== "lake") continue;
|
||||
|
|
@ -277,11 +214,9 @@ window.Lakes = (function () {
|
|||
return {
|
||||
setClimateData,
|
||||
cleanupLakeData,
|
||||
prepareLakeData,
|
||||
defineGroup,
|
||||
generateName,
|
||||
getName,
|
||||
getShoreline,
|
||||
addLakesInDeepDepressions,
|
||||
openNearSeaLakes
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import {MIN_LAND_HEIGHT, DISTANCE_FIELD} from "config/generation";
|
||||
import * as d3 from "d3";
|
||||
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
import {TIME} from "config/logging";
|
||||
import {INT8_MAX} from "constants";
|
||||
// @ts-expect-error js module
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {createTypedArray} from "utils/arrayUtils";
|
||||
import {dist2} from "utils/functionUtils";
|
||||
import {getFeatureVertices} from "scripts/connectVertices";
|
||||
import {createTypedArray, unique} from "utils/arrayUtils";
|
||||
import {dist2} from "utils/functionUtils";
|
||||
import {clipPoly} from "utils/lineUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
|
||||
const {UNMARKED, LAND_COAST, WATER_COAST, LANDLOCKED, DEEPER_WATER} = DISTANCE_FIELD;
|
||||
|
||||
|
|
@ -132,23 +135,22 @@ export function markupPackFeatures(
|
|||
}
|
||||
|
||||
const featureVertices = getFeatureVertices({firstCell, vertices, cells, featureIds, featureId});
|
||||
|
||||
// let points = clipPoly(vchain.map(v => vertices.p[v]));
|
||||
// const area = d3.polygonArea(points); // area with lakes/islands
|
||||
// if (area > 0 && features[f].type === "lake") {
|
||||
// points = points.reverse();
|
||||
// vchain = vchain.reverse();
|
||||
// }
|
||||
const points = clipPoly(featureVertices.map(vertex => vertices.p[vertex]));
|
||||
const area = d3.polygonArea(points); // feature perimiter area
|
||||
|
||||
const feature = addFeature({
|
||||
vertices,
|
||||
heights: cells.h,
|
||||
features,
|
||||
featureIds,
|
||||
firstCell,
|
||||
land,
|
||||
border,
|
||||
featureVertices,
|
||||
featureId,
|
||||
cellNumber,
|
||||
gridCellsNumber
|
||||
gridCellsNumber,
|
||||
area
|
||||
});
|
||||
features.push(feature);
|
||||
|
||||
|
|
@ -164,16 +166,23 @@ export function markupPackFeatures(
|
|||
}
|
||||
|
||||
function addFeature({
|
||||
vertices,
|
||||
heights,
|
||||
features,
|
||||
featureIds,
|
||||
firstCell,
|
||||
land,
|
||||
border,
|
||||
featureVertices,
|
||||
featureId,
|
||||
cellNumber,
|
||||
gridCellsNumber
|
||||
gridCellsNumber,
|
||||
area
|
||||
}: {
|
||||
vertices: IGraphVertices;
|
||||
heights: Uint8Array;
|
||||
features: TPackFeatures;
|
||||
featureIds: Uint16Array;
|
||||
firstCell: number;
|
||||
land: boolean;
|
||||
border: boolean;
|
||||
|
|
@ -181,12 +190,15 @@ function addFeature({
|
|||
featureId: number;
|
||||
cellNumber: number;
|
||||
gridCellsNumber: number;
|
||||
area: number;
|
||||
}) {
|
||||
const OCEAN_MIN_SIZE = gridCellsNumber / 25;
|
||||
const SEA_MIN_SIZE = gridCellsNumber / 1000;
|
||||
const CONTINENT_MIN_SIZE = gridCellsNumber / 10;
|
||||
const ISLAND_MIN_SIZE = gridCellsNumber / 1000;
|
||||
|
||||
const absArea = Math.abs(rn(area));
|
||||
|
||||
if (land) return addIsland();
|
||||
if (border) return addOcean();
|
||||
return addLake();
|
||||
|
|
@ -201,7 +213,8 @@ function addFeature({
|
|||
border,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: featureVertices,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -216,7 +229,8 @@ function addFeature({
|
|||
border: false,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: featureVertices,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -224,6 +238,25 @@ function addFeature({
|
|||
function addLake() {
|
||||
const group = "freshwater"; // temp, to be defined later
|
||||
const name = ""; // temp, to be defined later
|
||||
|
||||
// ensure lake ring is clockwise (to form a hole)
|
||||
const lakeVertices = area > 0 ? featureVertices.reverse() : featureVertices;
|
||||
|
||||
const shoreline = getShoreline(); // land cells around lake
|
||||
const height = getLakeElevation();
|
||||
|
||||
function getShoreline() {
|
||||
const isLand = (cellId: number) => heights[cellId] >= MIN_LAND_HEIGHT;
|
||||
const cellsAround = lakeVertices.map(vertex => vertices.c[vertex].filter(isLand)).flat();
|
||||
return unique(cellsAround);
|
||||
}
|
||||
|
||||
function getLakeElevation() {
|
||||
const MIN_ELEVATION_DELTA = 0.1;
|
||||
const minShoreHeight = d3.min(shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
|
||||
return minShoreHeight - MIN_ELEVATION_DELTA;
|
||||
}
|
||||
|
||||
const feature: IPackFeatureLake = {
|
||||
i: featureId,
|
||||
type: "lake",
|
||||
|
|
@ -233,7 +266,10 @@ function addFeature({
|
|||
border: false,
|
||||
cells: cellNumber,
|
||||
firstCell,
|
||||
vertices: featureVertices
|
||||
vertices: lakeVertices,
|
||||
shoreline: shoreline,
|
||||
height,
|
||||
area: absArea
|
||||
};
|
||||
return feature;
|
||||
}
|
||||
|
|
@ -245,7 +281,7 @@ function addFeature({
|
|||
}
|
||||
|
||||
function defineIslandGroup() {
|
||||
const prevFeature = features.at(-1);
|
||||
const prevFeature = features[featureIds[firstCell - 1]];
|
||||
|
||||
if (prevFeature && prevFeature.type === "lake") return "lake_island";
|
||||
if (cellNumber > CONTINENT_MIN_SIZE) return "continent";
|
||||
|
|
|
|||
|
|
@ -6,28 +6,36 @@ import {rn} from "utils/numberUtils";
|
|||
import {round} from "utils/stringUtils";
|
||||
import {rw, each} from "utils/probabilityUtils";
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {DISTANCE_FIELD, MIN_LAND_HEIGHT} from "config/generation";
|
||||
import {getInputNumber} from "utils/nodeUtils";
|
||||
|
||||
const {Lakes} = window;
|
||||
const {LAND_COAST} = DISTANCE_FIELD;
|
||||
|
||||
interface IRiverPackData {
|
||||
cells: Pick<IPack["cells"], "i" | "h" | "c" | "t">;
|
||||
features: TPackFeatures;
|
||||
}
|
||||
|
||||
window.Rivers = (function () {
|
||||
const generate = function (pack, grid, allowErosion = true) {
|
||||
const generate = function (grid: IGrid, {cells, features}: IRiverPackData, allowErosion = true) {
|
||||
TIME && console.time("generateRivers");
|
||||
|
||||
Math.random = aleaPRNG(seed);
|
||||
const {cells, features} = pack;
|
||||
|
||||
const riversData = {}; // rivers data
|
||||
const riverParents = {};
|
||||
const addCellToRiver = function (cell, river) {
|
||||
if (!riversData[river]) riversData[river] = [cell];
|
||||
else riversData[river].push(cell);
|
||||
};
|
||||
|
||||
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
||||
cells.r = new Uint16Array(cells.i.length); // rivers array
|
||||
cells.conf = new Uint8Array(cells.i.length); // confluences array
|
||||
let riverNext = 1; // first river id is 1
|
||||
const cellsNumber = cells.i.length;
|
||||
const flux = new Uint16Array(cellsNumber);
|
||||
const riverIds = new Uint16Array(cellsNumber);
|
||||
const confluence = new Uint8Array(cellsNumber);
|
||||
|
||||
const h = alterHeights(pack.cells);
|
||||
Lakes.prepareLakeData(h, pack);
|
||||
resolveDepressions(pack, h);
|
||||
let nextRiverId = 1; // starts with 1
|
||||
|
||||
const alteredHeights = alterHeights({h: cells.h, c: cells.c, t: cells.t});
|
||||
|
||||
resolveDepressions(pack, alteredHeights);
|
||||
drainWater();
|
||||
defineRivers();
|
||||
|
||||
|
|
@ -35,7 +43,7 @@ window.Rivers = (function () {
|
|||
Lakes.cleanupLakeData(pack);
|
||||
|
||||
if (allowErosion) {
|
||||
cells.h = Uint8Array.from(h); // apply gradient
|
||||
cells.h = Uint8Array.from(alteredHeights); // apply gradient
|
||||
downcutRivers(); // downcut river beds
|
||||
}
|
||||
|
||||
|
|
@ -46,36 +54,36 @@ window.Rivers = (function () {
|
|||
const cellsNumberModifier = (pointsInput.dataset.cells / 10000) ** 0.25;
|
||||
|
||||
const prec = grid.cells.prec;
|
||||
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
|
||||
const lakeOutCells = Lakes.setClimateData(h, pack, grid);
|
||||
const land = cells.i.filter(i => alteredHeights[i] >= 20).sort((a, b) => alteredHeights[b] - alteredHeights[a]);
|
||||
const lakeOutCells = Lakes.setClimateData(alteredHeights, pack, grid);
|
||||
|
||||
land.forEach(function (i) {
|
||||
cells.fl[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
flux[i] += prec[cells.g[i]] / cellsNumberModifier; // add flux from precipitation
|
||||
|
||||
// create lake outlet if lake is not in deep depression and flux > evaporation
|
||||
const lakes = lakeOutCells[i]
|
||||
? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation)
|
||||
: [];
|
||||
for (const lake of lakes) {
|
||||
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
|
||||
cells.fl[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
|
||||
const lakeCell = cells.c[i].find(c => alteredHeights[c] < 20 && cells.f[c] === lake.i);
|
||||
flux[lakeCell] += Math.max(lake.flux - lake.evaporation, 0); // not evaporated lake water drains to outlet
|
||||
|
||||
// allow chain lakes to retain identity
|
||||
if (cells.r[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river);
|
||||
if (riverIds[lakeCell] !== lake.river) {
|
||||
const sameRiver = cells.c[lakeCell].some(c => riverIds[c] === lake.river);
|
||||
|
||||
if (sameRiver) {
|
||||
cells.r[lakeCell] = lake.river;
|
||||
riverIds[lakeCell] = lake.river;
|
||||
addCellToRiver(lakeCell, lake.river);
|
||||
} else {
|
||||
cells.r[lakeCell] = riverNext;
|
||||
addCellToRiver(lakeCell, riverNext);
|
||||
riverNext++;
|
||||
riverIds[lakeCell] = nextRiverId;
|
||||
addCellToRiver(lakeCell, nextRiverId);
|
||||
nextRiverId++;
|
||||
}
|
||||
}
|
||||
|
||||
lake.outlet = cells.r[lakeCell];
|
||||
flowDown(i, cells.fl[lakeCell], lake.outlet);
|
||||
lake.outlet = riverIds[lakeCell];
|
||||
flowDown(i, flux[lakeCell], lake.outlet);
|
||||
}
|
||||
|
||||
// assign all tributary rivers to outlet basin
|
||||
|
|
@ -88,21 +96,21 @@ window.Rivers = (function () {
|
|||
}
|
||||
|
||||
// near-border cell: pour water out of the screen
|
||||
if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
|
||||
if (cells.b[i] && riverIds[i]) return addCellToRiver(-1, riverIds[i]);
|
||||
|
||||
// downhill cell (make sure it's not in the source lake)
|
||||
let min = null;
|
||||
if (lakeOutCells[i]) {
|
||||
const filtered = cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c]));
|
||||
min = filtered.sort((a, b) => h[a] - h[b])[0];
|
||||
min = filtered.sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
|
||||
} else if (cells.haven[i]) {
|
||||
min = cells.haven[i];
|
||||
} else {
|
||||
min = cells.c[i].sort((a, b) => h[a] - h[b])[0];
|
||||
min = cells.c[i].sort((a, b) => alteredHeights[a] - alteredHeights[b])[0];
|
||||
}
|
||||
|
||||
// cells is depressed
|
||||
if (h[i] <= h[min]) return;
|
||||
if (alteredHeights[i] <= alteredHeights[min]) return;
|
||||
|
||||
// debug
|
||||
// .append("line")
|
||||
|
|
@ -113,40 +121,45 @@ window.Rivers = (function () {
|
|||
// .attr("stroke", "#333")
|
||||
// .attr("stroke-width", 0.2);
|
||||
|
||||
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||
if (flux[i] < MIN_FLUX_TO_FORM_RIVER) {
|
||||
// flux is too small to operate as a river
|
||||
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||
if (alteredHeights[min] >= 20) flux[min] += flux[i];
|
||||
return;
|
||||
}
|
||||
|
||||
// proclaim a new river
|
||||
if (!cells.r[i]) {
|
||||
cells.r[i] = riverNext;
|
||||
addCellToRiver(i, riverNext);
|
||||
riverNext++;
|
||||
if (!riverIds[i]) {
|
||||
riverIds[i] = nextRiverId;
|
||||
addCellToRiver(i, nextRiverId);
|
||||
nextRiverId++;
|
||||
}
|
||||
|
||||
flowDown(min, cells.fl[i], cells.r[i]);
|
||||
flowDown(min, flux[i], riverIds[i]);
|
||||
});
|
||||
}
|
||||
|
||||
function addCellToRiver(cellId: number, riverId: number) {
|
||||
if (!riversData[riverId]) riversData[riverId] = [cellId];
|
||||
else riversData[riverId].push(cellId);
|
||||
}
|
||||
|
||||
function flowDown(toCell, fromFlux, river) {
|
||||
const toFlux = cells.fl[toCell] - cells.conf[toCell];
|
||||
const toRiver = cells.r[toCell];
|
||||
const toFlux = flux[toCell] - confluence[toCell];
|
||||
const toRiver = riverIds[toCell];
|
||||
|
||||
if (toRiver) {
|
||||
// downhill cell already has river assigned
|
||||
if (fromFlux > toFlux) {
|
||||
cells.conf[toCell] += cells.fl[toCell]; // mark confluence
|
||||
if (h[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
|
||||
cells.r[toCell] = river; // re-assign river if downhill part has less flux
|
||||
confluence[toCell] += flux[toCell]; // mark confluence
|
||||
if (alteredHeights[toCell] >= 20) riverParents[toRiver] = river; // min river is a tributary of current river
|
||||
riverIds[toCell] = river; // re-assign river if downhill part has less flux
|
||||
} else {
|
||||
cells.conf[toCell] += fromFlux; // mark confluence
|
||||
if (h[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
|
||||
confluence[toCell] += fromFlux; // mark confluence
|
||||
if (alteredHeights[toCell] >= 20) riverParents[river] = toRiver; // current river is a tributary of min river
|
||||
}
|
||||
} else cells.r[toCell] = river; // assign the river to the downhill cell
|
||||
} else riverIds[toCell] = river; // assign the river to the downhill cell
|
||||
|
||||
if (h[toCell] < 20) {
|
||||
if (alteredHeights[toCell] < 20) {
|
||||
// pour water to the water body
|
||||
const waterBody = features[cells.f[toCell]];
|
||||
if (waterBody.type === "lake") {
|
||||
|
|
@ -160,7 +173,7 @@ window.Rivers = (function () {
|
|||
}
|
||||
} else {
|
||||
// propagate flux and add next river segment
|
||||
cells.fl[toCell] += fromFlux;
|
||||
flux[toCell] += fromFlux;
|
||||
}
|
||||
|
||||
addCellToRiver(toCell, river);
|
||||
|
|
@ -168,8 +181,8 @@ window.Rivers = (function () {
|
|||
|
||||
function defineRivers() {
|
||||
// re-initialize rivers and confluence arrays
|
||||
cells.r = new Uint16Array(cells.i.length);
|
||||
cells.conf = new Uint16Array(cells.i.length);
|
||||
riverIds = new Uint16Array(cellsNumber);
|
||||
confluence = new Uint16Array(cellsNumber);
|
||||
pack.rivers = [];
|
||||
|
||||
const defaultWidthFactor = rn(1 / (pointsInput.dataset.cells / 10000) ** 0.25, 2);
|
||||
|
|
@ -184,8 +197,8 @@ window.Rivers = (function () {
|
|||
if (cell < 0 || cells.h[cell] < 20) continue;
|
||||
|
||||
// mark real confluences and assign river to cells
|
||||
if (cells.r[cell]) cells.conf[cell] = 1;
|
||||
else cells.r[cell] = riverId;
|
||||
if (riverIds[cell]) confluence[cell] = 1;
|
||||
else riverIds[cell] = riverId;
|
||||
}
|
||||
|
||||
const source = riverCells[0];
|
||||
|
|
@ -194,7 +207,7 @@ window.Rivers = (function () {
|
|||
|
||||
const widthFactor = !parent || parent === riverId ? mainStemWidthFactor : defaultWidthFactor;
|
||||
const meanderedPoints = addMeandering(pack, riverCells);
|
||||
const discharge = cells.fl[mouth]; // m3 in second
|
||||
const discharge = flux[mouth]; // m3 in second
|
||||
const length = getApproximateLength(meanderedPoints);
|
||||
const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
|
||||
|
||||
|
|
@ -218,48 +231,94 @@ window.Rivers = (function () {
|
|||
|
||||
for (const i of pack.cells.i) {
|
||||
if (cells.h[i] < 35) continue; // don't donwcut lowlands
|
||||
if (!cells.fl[i]) continue;
|
||||
if (!flux[i]) continue;
|
||||
|
||||
const higherCells = cells.c[i].filter(c => cells.h[c] > cells.h[i]);
|
||||
const higherFlux = higherCells.reduce((acc, c) => acc + cells.fl[c], 0) / higherCells.length;
|
||||
const higherFlux = higherCells.reduce((acc, c) => acc + flux[c], 0) / higherCells.length;
|
||||
if (!higherFlux) continue;
|
||||
|
||||
const downcut = Math.floor(cells.fl[i] / higherFlux);
|
||||
const downcut = Math.floor(flux[i] / higherFlux);
|
||||
if (downcut) cells.h[i] -= Math.min(downcut, MAX_DOWNCUT);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateConfluenceFlux() {
|
||||
for (const i of cells.i) {
|
||||
if (!cells.conf[i]) continue;
|
||||
if (!confluence[i]) continue;
|
||||
|
||||
const sortedInflux = cells.c[i]
|
||||
.filter(c => cells.r[c] && h[c] > h[i])
|
||||
.map(c => cells.fl[c])
|
||||
.filter(c => riverIds[c] && alteredHeights[c] > alteredHeights[i])
|
||||
.map(c => flux[c])
|
||||
.sort((a, b) => b - a);
|
||||
cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
|
||||
confluence[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// add distance to water value to land cells to make map less depressed
|
||||
const alterHeights = ({h, c, t}) => {
|
||||
return Array.from(h).map((h, i) => {
|
||||
if (h < 20 || t[i] < 1) return h;
|
||||
return h + t[i] / 100 + d3.mean(c[i].map(c => t[c])) / 10000;
|
||||
const alterHeights = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => {
|
||||
return Array.from(h).map((height, index) => {
|
||||
if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height;
|
||||
const mean = d3.mean(c[index].map(c => t[c])) || 0;
|
||||
return height + t[index] / 100 + mean / 10000;
|
||||
});
|
||||
};
|
||||
|
||||
// depression filling algorithm (for a correct water flux modeling)
|
||||
const resolveDepressions = function (pack, h) {
|
||||
const {cells, features} = pack;
|
||||
const maxIterations = +document.getElementById("resolveDepressionsStepsOutput").value;
|
||||
const maxIterations = getInputNumber("resolveDepressionsStepsOutput");
|
||||
const checkLakeMaxIteration = maxIterations * 0.85;
|
||||
const elevateLakeMaxIteration = maxIterations * 0.75;
|
||||
|
||||
const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
|
||||
|
||||
const lakes = features.filter(f => f.type === "lake");
|
||||
const lakes = features.filter(feature => feature.type === "lake");
|
||||
const canBePoured = () => {
|
||||
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
|
||||
|
||||
const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
|
||||
const lakeData = lakes.map(feature => {
|
||||
const minShoreHeight = d3.min(feature.shoreline.map(cellId => heights[cellId])) || MIN_LAND_HEIGHT;
|
||||
const minHeightCell =
|
||||
feature.shoreline.find(cellId => heights[cellId] === minShoreHeight) || feature.shoreline[0];
|
||||
|
||||
if (ELEVATION_LIMIT === 80) return {...feature, closed: false};
|
||||
|
||||
// check if lake can be open (not in deep depression)
|
||||
let deep = true;
|
||||
|
||||
const threshold = feature.height + ELEVATION_LIMIT;
|
||||
const queue = [minHeightCell];
|
||||
const checked = [];
|
||||
checked[minHeightCell] = true;
|
||||
|
||||
// check if elevated lake can potentially pour to another water body
|
||||
while (deep && queue.length) {
|
||||
const cellId = queue.pop()!;
|
||||
|
||||
for (const neibCellId of cells.c[cellId]) {
|
||||
if (checked[neibCellId]) continue;
|
||||
if (heights[neibCellId] >= threshold) continue;
|
||||
|
||||
if (heights[neibCellId] < MIN_LAND_HEIGHT) {
|
||||
const waterFeatureMet = features[cells.f[neibCellId]];
|
||||
|
||||
if ((waterFeatureMet && waterFeatureMet.type === "ocean") || feature.height > waterFeatureMet.height) {
|
||||
deep = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
checked[neibCellId] = true;
|
||||
queue.push(neibCellId);
|
||||
}
|
||||
}
|
||||
|
||||
return {...feature, closed: deep};
|
||||
});
|
||||
};
|
||||
|
||||
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
|
||||
land.sort((a, b) => h[a] - h[b]); // lowest cells go first
|
||||
|
||||
|
|
@ -476,10 +535,10 @@ window.Rivers = (function () {
|
|||
|
||||
// Real mouth width examples: Amazon 6000m, Volga 6000m, Dniepr 3000m, Mississippi 1300m, Themes 900m,
|
||||
// Danube 800m, Daugava 600m, Neva 500m, Nile 450m, Don 400m, Wisla 300m, Pripyat 150m, Bug 140m, Muchavets 40m
|
||||
const getWidth = offset => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
|
||||
const getWidth = (offset: number) => rn((offset / 1.5) ** 1.8, 2); // mouth width in km
|
||||
|
||||
// remove river and all its tributaries
|
||||
const remove = function (id) {
|
||||
const remove = function (id: number) {
|
||||
const cells = pack.cells;
|
||||
const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
|
||||
riversToRemove.forEach(r => rivers.select("#river" + r).remove());
|
||||
|
|
@ -492,9 +551,9 @@ window.Rivers = (function () {
|
|||
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
|
||||
};
|
||||
|
||||
const getBasin = function (r) {
|
||||
const parent = pack.rivers.find(river => river.i === r)?.parent;
|
||||
if (!parent || r === parent) return r;
|
||||
const getBasin = function (riverId: number) {
|
||||
const parent = pack.rivers.find(river => river.i === riverId)?.parent;
|
||||
if (!parent || riverId === parent) return riverId;
|
||||
return getBasin(parent);
|
||||
};
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue