From aed9d9d76804e35eb04d5a96237a2fc6dd688b93 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Fri, 23 Jul 2021 18:40:39 +0300 Subject: [PATCH] confluence to reflect real value --- index.css | 2 +- modules/river-generator.js | 146 ++++++++++++++++++++---------------- modules/ui/rivers-editor.js | 67 ++++++++++------- modules/ui/tools.js | 5 +- 4 files changed, 127 insertions(+), 93 deletions(-) diff --git a/index.css b/index.css index 91fcde1a..4012c15e 100644 --- a/index.css +++ b/index.css @@ -1122,7 +1122,7 @@ div#regimentSelectorBody > div > div { } #debug > text { - font-size: 3px; + font-size: 2px; text-anchor: middle; dominant-baseline: central; } diff --git a/modules/river-generator.js b/modules/river-generator.js index 3bb48db2..719d68d4 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -19,7 +19,9 @@ Lakes.prepareLakeData(h); resolveDepressions(h); drainWater(); + lineGen.curve(d3.curveCatmullRom.alpha(0.1)); defineRivers(); + calculateConfluenceFlux(); Lakes.cleanupLakeData(); if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one @@ -28,32 +30,29 @@ function drainWater() { const MIN_FLUX_TO_FORM_RIVER = 30; + 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); land.forEach(function (i) { - cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation - const [x, y] = p[i]; + cells.fl[i] += prec[cells.g[i]]; // 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 // allow chain lakes to retain identity if (cells.r[lakeCell] !== lake.river) { const sameRiver = cells.c[lakeCell].some(c => cells.r[c] === lake.river); - const [x, y] = p[lakeCell]; - const flux = cells.fl[lakeCell]; if (sameRiver) { cells.r[lakeCell] = lake.river; - riversData.push({river: lake.river, cell: lakeCell, x, y, flux}); + riversData.push({river: lake.river, cell: lakeCell}); } else { cells.r[lakeCell] = riverNext; - riversData.push({river: riverNext, cell: lakeCell, x, y, flux}); + riversData.push({river: riverNext, cell: lakeCell}); riverNext++; } } @@ -69,8 +68,7 @@ // near-border cell: pour water out of the screen if (cells.b[i] && cells.r[i]) { - const [x, y] = getBorderPoint(i); - riversData.push({river: cells.r[i], cell: -1, x, y, flux: cells.fl[i]}); + riversData.push({river: cells.r[i], cell: -1}); return; } @@ -89,14 +87,15 @@ if (h[i] <= h[min]) return; if (cells.fl[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]; - return; // flux is too small to operate as river + return; } // proclaim a new river if (!cells.r[i]) { cells.r[i] = riverNext; - riversData.push({river: riverNext, cell: i, x, y, flux: cells.fl[i]}); + riversData.push({river: riverNext, cell: i}); riverNext++; } @@ -111,11 +110,19 @@ // downhill cell already has river assigned if (fromFlux > toFlux) { cells.conf[toCell] += cells.fl[toCell]; // mark confluence - if (h[toCell] >= 20) riversData.find(r => r.river === cells.r[toCell]).parent = river; // min river is a tributary of current river + if (h[toCell] >= 20) { + // min river is a tributary of current river + const toRiver = riversData.find(r => r.river === cells.r[toCell]); + if (toRiver) toRiver.parent = river; + } cells.r[toCell] = river; // re-assign river if downhill part has less flux } else { cells.conf[toCell] += fromFlux; // mark confluence - if (h[toCell] >= 20) riversData.find(r => r.river === river).parent = cells.r[toCell]; // current river is a tributary of min river + if (h[toCell] >= 20) { + // current river is a tributary of min river + const thisRiver = riversData.find(r => r.river === river); + if (thisRiver) thisRiver.parent = cells.r[toCell]; + } } } else cells.r[toCell] = river; // assign the river to the downhill cell @@ -128,46 +135,52 @@ waterBody.enteringFlux = fromFlux; } waterBody.flux = waterBody.flux + fromFlux; - waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]); + if (!waterBody.inlets) waterBody.inlets = [river]; + else waterBody.inlets.push(river); } } else { // propagate flux and add next river segment cells.fl[toCell] += fromFlux; } - const [x, y] = p[toCell]; - riversData.push({river, cell: toCell, x, y, flux: fromFlux}); + riversData.push({river, cell: toCell}); } function defineRivers() { - cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array - pack.rivers = []; // rivers data + // re-initialize rivers and confluence arrays + cells.r = new Uint16Array(cells.i.length); + cells.conf = new Uint16Array(cells.i.length); + pack.rivers = []; const riverPaths = []; for (let r = 1; r <= riverNext; r++) { - const riverPoints = riversData.filter(d => d.river === r); - if (riverPoints.length < 3) continue; + const riverData = riversData.filter(d => d.river === r); + if (riverData.length < 3) continue; // exclude tiny rivers - for (const segment of riverPoints) { + for (const segment of riverData) { const i = segment.cell; - if (cells.r[i]) continue; - if (cells.h[i] < 20) continue; - cells.r[i] = r; + if (i < 0 || cells.h[i] < 20) continue; + + // mark real confluences and assign river to cells + if (cells.r[i]) cells.conf[i] = 1; + else cells.r[i] = r; } - const source = riverPoints[0].cell; - const mouth = riverPoints[riverPoints.length - 2].cell; - const parent = riverPoints[0].parent || 0; + const source = riverData[0].cell; + const mouth = riverData[riverData.length - 2].cell; + const parent = riverData[0].parent || 0; - const riverCells = riverPoints.map(point => point.cell); - const widthFactor = parent ? 1 : 1.4; + const riverCells = riverData.map(point => point.cell); + const widthFactor = !parent || parent === r ? 1.2 : 1; const initStep = cells.h[source] >= 20 ? 1 : 10; const riverMeandered = addMeandering(riverCells, initStep, 0.5); - const [path, length, offset] = getPath(riverMeandered, widthFactor); + const [path, length, offset] = getRiverPath(riverMeandered, widthFactor); riverPaths.push([path, r]); - const width = rn(offset ** 2, 2); // mounth width in km - const discharge = last(riverPoints).flux; // in m3/s + // Real mounth 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 width = rn((offset / 1.4) ** 2, 2); // mounth width in km + const discharge = last(riverData).flux; // in m3/s pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells}); } @@ -175,6 +188,18 @@ // draw rivers rivers.html(riverPaths.map(d => ``).join("")); } + + function calculateConfluenceFlux() { + for (const i of cells.i) { + if (!cells.conf[i]) continue; + + const sortedInflux = cells.c[i] + .filter(c => cells.r[c] && h[c] > h[i]) + .map(c => cells.fl[c]) + .sort((a, b) => b - a); + cells.conf[i] = sortedInflux.reduce((acc, flux, index) => (index ? acc + flux : acc), 0); + } + } }; // add distance to water value to land cells to make map less depressed @@ -248,7 +273,7 @@ // add points at 1/3 and 2/3 of a line between adjacents river cells const addMeandering = function (riverCells, step = 1, meandering = 0.5) { const meandered = []; - const {p, fl} = pack.cells; + const {p, fl, conf} = pack.cells; const lastStep = riverCells.length - 1; let fluxPrev = 0; @@ -259,7 +284,8 @@ const isLastCell = i === lastStep; const [x1, y1] = p[cell]; - const flux1 = (fluxPrev = getFlux(i, fl[cell])); + const flux1 = getFlux(i, fl[cell]); + fluxPrev = flux1; meandered.push([x1, y1, flux1]); @@ -268,34 +294,36 @@ const nextCell = riverCells[i + 1]; if (nextCell === -1) { const [x, y] = getBorderPoint(cell); - meandered.push([x, y, flux1]); + meandered.push([x, y, fluxPrev]); break; } const [x2, y2] = p[nextCell]; + const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells + if (dist2 <= 25 && riverCells.length >= 6) continue; + const flux2 = getFlux(i + 1, fl[nextCell]); - const angle = Math.atan2(y2 - y1, x2 - x1); - const sin = Math.sin(angle); - const cos = Math.cos(angle); + const keepInitialFlux = conf[nextCell] || flux1 === flux2; const meander = meandering + 1 / step + Math.random() * Math.max(meandering - step / 100, 0); - const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells + const angle = Math.atan2(y2 - y1, x2 - x1); + const sinMeander = Math.sin(angle) * meander; + const cosMeander = Math.cos(angle) * meander; if (step < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) { // if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment - const p1x = (x1 * 2 + x2) / 3 + -sin * meander; - const p1y = (y1 * 2 + y2) / 3 + cos * meander; - const fluxThird1 = (flux1 * 2 + flux2) / 3; - const p2x = (x1 + x2 * 2) / 3 + sin * meander; - const p2y = (y1 + y2 * 2) / 3 + cos * meander; - const fluxThird2 = (flux1 + flux2 * 2) / 3; - meandered.push([p1x, p1y, fluxThird1], [p2x, p2y, fluxThird2]); + const p1x = (x1 * 2 + x2) / 3 + -sinMeander; + const p1y = (y1 * 2 + y2) / 3 + cosMeander; + const p2x = (x1 + x2 * 2) / 3 + sinMeander; + const p2y = (y1 + y2 * 2) / 3 + cosMeander; + const [p1fl, p2fl] = keepInitialFlux ? [flux1, flux1] : [(flux1 * 2 + flux2) / 3, (flux1 + flux2 * 2) / 3]; + meandered.push([p1x, p1y, p1fl], [p2x, p2y, p2fl]); } else if (dist2 > 25 || riverCells.length < 6) { // if dist is medium or river is small add 1 extra middlepoint - const p1x = (x1 + x2) / 2 + -sin * meander; - const p1y = (y1 + y2) / 2 + cos * meander; - const fluxMid = (flux1 + flux2) / 2; - meandered.push([p1x, p1y, fluxMid]); + const p1x = (x1 + x2) / 2 + -sinMeander; + const p1y = (y1 + y2) / 2 + cosMeander; + const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2; + meandered.push([p1x, p1y, p1fl]); } } @@ -303,18 +331,17 @@ }; const fluxFactor = 500; - const maxFluxWidth = 1; + const maxFluxWidth = 2; const widthFactor = 200; const stepWidth = 1 / widthFactor; const lengthProgression = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / widthFactor); const maxProgression = last(lengthProgression); - const getPath = function (points, widthFactor = 1, startingWidth = 0) { + // build polygon from a list of points and calculated offset (width) + const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) { const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0); let width = 0; - console.log("---------"); - // store points on both sides to build a polygon const riverPointsLeft = []; const riverPointsRight = []; @@ -323,7 +350,7 @@ const [x1, y1, flux] = points[p]; const [x2, y2] = points[p + 1] || points[p]; - const fluxWidth = Math.min(flux ** 0.9 / fluxFactor, 1); + const fluxWidth = Math.min(flux ** 0.9 / fluxFactor, maxFluxWidth); const lengthWidth = p * stepWidth + (lengthProgression[p] || maxProgression); width = widthFactor * (lengthWidth + fluxWidth) + startingWidth; @@ -331,17 +358,10 @@ const sinOffset = Math.sin(angle) * width; const cosOffset = Math.cos(angle) * width; - //const text = `${p}: ${rn(flux, 1)} - ${rn(width, 1)}`; - //debug.append("text").attr("x", x1).attr("y", y1).text(text); - - console.log({fluxWidth, lengthWidth, width}); - riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]); riverPointsRight.push([x1 + sinOffset, y1 - cosOffset]); } - // generate polygon path and return - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const right = lineGen(riverPointsRight.reverse()); let left = lineGen(riverPointsLeft); left = left.substring(left.indexOf("C")); @@ -398,5 +418,5 @@ return [graphWidth, y]; }; - return {generate, alterHeights, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove}; + return {generate, alterHeights, resolveDepressions, addMeandering, getPath: getRiverPath, specify, getName, getBasin, remove}; }); diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js index bc8181ee..b0159f19 100644 --- a/modules/ui/rivers-editor.js +++ b/modules/ui/rivers-editor.js @@ -13,7 +13,8 @@ function editRiver(id) { drawControlPoints(node); $("#riverEditor").dialog({ - title: "Edit River", resizable: false, + title: "Edit River", + resizable: false, position: {my: "center top+80", at: "top", of: node, collision: "fit"}, close: closeRiverEditor }); @@ -39,8 +40,8 @@ function editRiver(id) { function showEditorTips() { showMainTip(); - if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); else - if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point"); + if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); + else if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point"); } function getRiver() { @@ -58,7 +59,7 @@ function editRiver(id) { const parentSelect = document.getElementById("riverMainstem"); parentSelect.options.length = 0; const parent = r.parent || r.i; - const sortedRivers = pack.rivers.slice().sort((a, b) => a.name > b.name ? 1 : -1); + const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1)); sortedRivers.forEach(river => { const opt = new Option(river.name, river.i, false, river.i === parent); parentSelect.options.add(opt); @@ -79,7 +80,7 @@ function editRiver(id) { function drawControlPoints(node) { const length = getRiver().length; const segments = Math.ceil(length / 4); - const increment = rn(length / segments * 1e5); + const increment = rn((length / segments) * 1e5); for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) { const p1 = node.getPointAtLength(i / 1e5); const p2 = node.getPointAtLength(c / 1e5); @@ -88,10 +89,7 @@ function editRiver(id) { } function addControlPoint(point, before = null) { - debug.select("#controlPoints").insert("circle", before) - .attr("cx", point[0]).attr("cy", point[1]).attr("r", .6) - .call(d3.drag().on("drag", dragControlPoint)) - .on("click", clickControlPoint); + debug.select("#controlPoints").insert("circle", before).attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.6).call(d3.drag().on("drag", dragControlPoint)).on("click", clickControlPoint); } function dragControlPoint() { @@ -102,22 +100,28 @@ function editRiver(id) { function redrawRiver() { const points = []; - debug.select("#controlPoints").selectAll("circle").each(function() { - points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]); - }); + debug + .select("#controlPoints") + .selectAll("circle") + .each(function () { + points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]); + }); if (points.length < 2) return; if (points.length === 2) { - const p0 = points[0], p1 = points[1]; + const p0 = points[0], + p1 = points[1]; const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]); - const sin = Math.sin(angle), cos = Math.cos(angle); - elSelected.attr("d", `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin/2},${cos/2} Z`); + const sin = Math.sin(angle), + cos = Math.cos(angle); + elSelected.attr("d", `M${p0[0]},${p0[1]} L${p1[0]},${p1[1]} l${-sin / 2},${cos / 2} Z`); return; } const widthFactor = +document.getElementById("riverWidthFactor").value; const sourceWidth = +document.getElementById("riverSourceWidth").value; - const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth); + lineGen.curve(d3.curveCatmullRom.alpha(0.1)); + const [path, length, offset] = Rivers.getRiverPath(points, widthFactor, sourceWidth); elSelected.attr("d", path); const r = getRiver(); @@ -140,7 +144,7 @@ function editRiver(id) { const controls = document.getElementById("controlPoints").querySelectorAll("circle"); const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]); const index = getSegmentId(points, point, 2); - addControlPoint(point, ":nth-child(" + (index+1) + ")"); + addControlPoint(point, ":nth-child(" + (index + 1) + ")"); redrawRiver(); } @@ -160,7 +164,7 @@ function editRiver(id) { function generateNameRandom() { const r = getRiver(); - if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length-1)); + if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length - 1)); } function changeParent() { @@ -225,10 +229,13 @@ function editRiver(id) { // add a river const r = +elSelected.attr("id").slice(5); - const node = elSelected.node(), length = node.getTotalLength() / 2; + const node = elSelected.node(); + const length = node.getTotalLength() / 2; const cells = []; - const segments = Math.ceil(length / 4), increment = rn(length / segments * 1e5); + + const segments = Math.ceil(length / 4); + const increment = rn((length / segments) * 1e5); for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) { const p = node.getPointAtLength(i / 1e5); const cell = findCell(p.x, p.y); @@ -236,30 +243,36 @@ function editRiver(id) { cells.push(cell); } - const source = cells[0], mouth = last(cells); + const source = cells[0]; + const mouth = last(cells); const name = Rivers.getName(mouth); - const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)]; - const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River"; + const smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)]; + const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River"; const discharge = rn(cells.length * 20 * Math.random()); const widthFactor = +document.getElementById("riverWidthFactor").value; const sourceWidth = +document.getElementById("riverSourceWidth").value; - pack.rivers.push({i:r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent:0, name, type, basin:r}); + pack.rivers.push({i: r, source, mouth, discharge, length, width: sourceWidth, widthFactor, sourceWidth, parent: 0, name, type, basin: r}); } function removeRiver() { alertMessage.innerHTML = "Are you sure you want to remove the river? All tributaries will be auto-removed"; - $("#alert").dialog({resizable: false, width: "22em", title: "Remove river", + $("#alert").dialog({ + resizable: false, + width: "22em", + title: "Remove river", buttons: { - Remove: function() { + Remove: function () { $(this).dialog("close"); const river = +elSelected.attr("id").slice(5); Rivers.remove(river); elSelected.remove(); // keep if river if missed in pack.rivers $("#riverEditor").dialog("close"); }, - Cancel: function() {$(this).dialog("close");} + Cancel: function () { + $(this).dialog("close"); + } } }); } diff --git a/modules/ui/tools.js b/modules/ui/tools.js index e276cfec..7d2fad8f 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -616,10 +616,11 @@ function addRiverOnClick() { const river = rivers.find(r => r.i === riverId); const sourceWidth = 0.1; - const widthFactor = river?.widthFactor || (parent ? 1 : 1.4); + const widthFactor = river?.widthFactor || (!parent || parent === r ? 1.2 : 1); const riverMeandered = Rivers.addMeandering(riverCells, sourceWidth * 10, 0.5); - const [path, length, offset] = Rivers.getPath(riverMeandered, widthFactor, sourceWidth); + lineGen.curve(d3.curveCatmullRom.alpha(0.1)); + const [path, length, offset] = Rivers.getRiverPath(riverMeandered, widthFactor, sourceWidth); viewbox .select("#rivers") .append("path")