fix: skip coastline out points wip

This commit is contained in:
max 2022-07-23 15:02:25 +03:00
parent 3215b6f0d2
commit 19d7f239c1
9 changed files with 262 additions and 64 deletions

View file

@ -1,7 +1,7 @@
import * as d3 from "d3"; import * as d3 from "d3";
import {simplify} from "scripts/simplify"; import {simplify} from "scripts/simplify";
import {clipPoly} from "utils/lineUtils"; import {filterOutOfCanvasPoints} from "utils/lineUtils";
import {round} from "utils/stringUtils"; import {round} from "utils/stringUtils";
export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) { export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures) {
@ -11,14 +11,38 @@ export function drawCoastline(vertices: IGraphVertices, features: TPackFeatures)
const lineGen = d3.line().curve(d3.curveBasisClosed); const lineGen = d3.line().curve(d3.curveBasisClosed);
const SIMPLIFICATION_TOLERANCE = 0.5; // px const SIMPLIFICATION_TOLERANCE = 0.5; // px
// map edge rectangle
debug
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 0.1);
for (const feature of features) { for (const feature of features) {
if (!feature) continue; if (!feature) continue;
if (feature.type === "ocean") continue; if (feature.type === "ocean") continue;
const points = clipPoly(feature.vertices.map(vertex => vertices.p[vertex])); const points = feature.vertices.map(vertex => vertices.p[vertex]);
const simplifiedPoints = simplify(points, SIMPLIFICATION_TOLERANCE); const filteredPoints = filterOutOfCanvasPoints(points);
const simplifiedPoints = simplify(filteredPoints, SIMPLIFICATION_TOLERANCE);
const path = round(lineGen(simplifiedPoints)!); const path = round(lineGen(simplifiedPoints)!);
points.forEach(([x, y]) => {
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "red");
});
filteredPoints.forEach(([x, y]) => {
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "blue");
});
simplifiedPoints.forEach(([x, y]) => {
debug.append("circle").attr("cx", x).attr("cy", y).attr("r", 0.3).attr("fill", "green");
});
if (feature.type === "lake") { if (feature.type === "lake") {
landMask landMask
.append("path") .append("path")

View file

@ -0,0 +1,107 @@
// Custom fork of d3.curveBasisClosed
// The idea is to not interpolate (curve) line along the frame
// points = [[0, 833],[0, 0],[1007, 0],[1007, 833]]
// d3.line().curve(d3.curveBasisClosed)(points) // => M 167.8,138.8 C 335.7,0,671.3,0,839.2,138.8 C1007,277.7,1007,555.3,839.2,694.2C671.3,833,335.7,833,167.8,694.2 C0,555.3,0,277.7,167.8,138.8
// d3.line().curve(d3.curveBasisFramedClosed)(points) // => M 0,833 L 0,0 L 1007,0 L 1007,833
const PRECISION = 2;
const round = number => Number(number.toFixed(PRECISION));
function noop() {}
function point(that, x, y) {
that._context.bezierCurveTo(
round((2 * that._x0 + that._x1) / 3),
round((2 * that._y0 + that._y1) / 3),
round((that._x0 + 2 * that._x1) / 3),
round((that._y0 + 2 * that._y1) / 3),
round((that._x0 + 4 * that._x1 + x) / 6),
round((that._y0 + 4 * that._y1 + y) / 6)
);
}
function isEdge(x, y) {
return x <= 0 || y <= 0 || x >= graphWidth || y >= graphHeight;
}
function BasisFramedClosed(context) {
this._context = context;
}
BasisFramedClosed.prototype = {
areaStart: noop,
areaEnd: noop,
lineStart: function () {
this._x0 = this._x1 = this._x2 = this._x3 = this._x4 = this._y0 = this._y1 = this._y2 = this._y3 = this._y4 = NaN;
this._point = 0;
},
lineEnd: function () {
switch (this._point) {
case 1: {
this._context.moveTo(this._x2, this._y2);
this._context.closePath();
break;
}
case 2: {
this._context.moveTo(round((this._x2 + 2 * this._x3) / 3), round((this._y2 + 2 * this._y3) / 3));
this._context.lineTo(round((this._x3 + 2 * this._x2) / 3), round((this._y3 + 2 * this._y2) / 3));
this._context.closePath();
break;
}
case 3: {
this.point(this._x2, this._y2);
this.point(this._x3, this._y3);
this.point(this._x4, this._y4);
break;
}
}
},
point: function (x, y) {
const edge = isEdge(x, y);
if (x <= 0) x -= 20;
if (y <= 0) y -= 20;
if (x >= graphWidth) x += 20;
if (y >= graphHeight) y += 20;
switch (this._point) {
case 0:
this._point = 1;
this._x2 = x;
this._y2 = y;
break;
case 1:
this._point = 2;
this._x3 = x;
this._y3 = y;
break;
case 2:
this._point = 3;
this._x4 = x;
this._y4 = y;
if (edge) {
this._context.moveTo(round((this._x1 + x) / 2), round((this._y1 + y) / 2));
break;
}
this._context.moveTo(round((this._x0 + 4 * this._x1 + x) / 6), round((this._y0 + 4 * this._y1 + y) / 6));
break;
default:
if (edge) {
this._context.lineTo(x, y);
break;
}
point(this, x, y);
break;
}
this._x0 = this._x1;
this._x1 = x;
this._y0 = this._y1;
this._y1 = y;
}
};
export function curveBasisFramedClosed(context) {
return new BasisFramedClosed(context);
}

View file

@ -10,11 +10,8 @@ import {clearLegend, dragLegendBox} from "modules/legend";
export function setDefaultEventHandlers() { export function setDefaultEventHandlers() {
window.Zoom.setZoomBehavior(); window.Zoom.setZoomBehavior();
viewbox viewbox.style("cursor", "default").on(".drag", null).on("click", handleMapClick);
.style("cursor", "default") //.on("touchmove mousemove", onMouseMove);
.on(".drag", null)
.on("click", handleMapClick)
.on("touchmove mousemove", onMouseMove);
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor")); scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => openDialog("unitsEditor"));

View file

@ -24,7 +24,6 @@ import {debounce} from "utils/functionUtils";
import {rn} from "utils/numberUtils"; import {rn} from "utils/numberUtils";
import {generateSeed} from "utils/probabilityUtils"; import {generateSeed} from "utils/probabilityUtils";
import {byId} from "utils/shorthands"; import {byId} from "utils/shorthands";
import {showStatistics} from "../statistics";
import {createGrid} from "./grid"; import {createGrid} from "./grid";
import {createPack} from "./pack/pack"; import {createPack} from "./pack/pack";
import {getInputValue, setInputValue} from "utils/nodeUtils"; import {getInputValue, setInputValue} from "utils/nodeUtils";

View file

@ -15,7 +15,7 @@ export interface ILakeClimateData extends IPackFeatureLake {
export const getClimateData = function ( export const getClimateData = function (
lakes: IPackFeatureLake[], lakes: IPackFeatureLake[],
heights: number[], heights: Float32Array,
drainableLakes: Dict<boolean>, drainableLakes: Dict<boolean>,
gridReference: IPack["cells"]["g"], gridReference: IPack["cells"]["g"],
precipitation: IGrid["cells"]["prec"], precipitation: IGrid["cells"]["prec"],

View file

@ -1,6 +1,6 @@
import * as d3 from "d3"; import * as d3 from "d3";
import {TIME, WARN} from "config/logging"; import {INFO, TIME, WARN} from "config/logging";
import {rn} from "utils/numberUtils"; import {rn} from "utils/numberUtils";
import {aleaPRNG} from "scripts/aleaPRNG"; import {aleaPRNG} from "scripts/aleaPRNG";
import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation"; import {DISTANCE_FIELD, MAX_HEIGHT, MIN_LAND_HEIGHT} from "config/generation";
@ -8,6 +8,7 @@ import {getInputNumber} from "utils/nodeUtils";
import {pick} from "utils/functionUtils"; import {pick} from "utils/functionUtils";
import {byId} from "utils/shorthands"; import {byId} from "utils/shorthands";
import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes"; import {mergeLakeData, getClimateData, ILakeClimateData} from "./lakes";
import {drawArrow} from "utils/debugUtils";
const {Rivers} = window; const {Rivers} = window;
const {LAND_COAST} = DISTANCE_FIELD; const {LAND_COAST} = DISTANCE_FIELD;
@ -96,7 +97,7 @@ export function generateRivers(
} }
lake.outlet = riverIds[lakeCell]; lake.outlet = riverIds[lakeCell];
flowDown(cellId, flux[lakeCell], lake.outlet); flowDown(lakeCell, cellId, flux[lakeCell], lake.outlet);
} }
if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) { if (lakesDrainingToCell.length && lakesDrainingToCell[0].outlet) {
@ -127,15 +128,6 @@ export function generateRivers(
// cells is depressed // cells is depressed
if (currentCellHeights[cellId] <= currentCellHeights[min]) return; if (currentCellHeights[cellId] <= currentCellHeights[min]) return;
debug
.append("line")
.attr("x1", cells.p[cellId][0])
.attr("y1", cells.p[cellId][1])
.attr("x2", cells.p[min][0])
.attr("y2", cells.p[min][1])
.attr("stroke", "#333")
.attr("stroke-width", 0.1);
if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) { if (flux[cellId] < MIN_FLUX_TO_FORM_RIVER) {
// flux is too small to operate as a river // flux is too small to operate as a river
if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId]; if (currentCellHeights[min] >= MIN_LAND_HEIGHT) flux[min] += flux[cellId];
@ -149,12 +141,14 @@ export function generateRivers(
nextRiverId++; nextRiverId++;
} }
flowDown(min, flux[cellId], riverIds[cellId]); flowDown(cellId, min, flux[cellId], riverIds[cellId]);
}); });
return {flux, lakeData}; return {flux, lakeData};
function flowDown(toCell: number, fromFlux: number, riverId: number) { function flowDown(fromCell: number, toCell: number, fromFlux: number, riverId: number) {
// drawArrow(cells.p[fromCell], cells.p[toCell]);
const toFlux = flux[toCell] - confluence[toCell]; const toFlux = flux[toCell] - confluence[toCell];
const toRiver = riverIds[toCell]; const toRiver = riverIds[toCell];
@ -262,7 +256,7 @@ export function generateRivers(
return {r, conf, rivers}; return {r, conf, rivers};
} }
function downcutRivers(heights: number[]) { function downcutRivers(heights: Float32Array) {
const MAX_DOWNCUT = 5; const MAX_DOWNCUT = 5;
const MIN_HEIGHT_TO_DOWNCUT = 35; const MIN_HEIGHT_TO_DOWNCUT = 35;
@ -284,10 +278,10 @@ export function generateRivers(
// add distance to water value to land cells to make map less depressed // add distance to water value to land cells to make map less depressed
const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => { const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) => {
return Array.from(h).map((height, index) => { return new Float32Array(h.length).map((_, index) => {
if (height < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return height; if (h[index] < MIN_LAND_HEIGHT || t[index] < LAND_COAST) return h[index];
const mean = d3.mean(c[index].map(c => t[c])) || 0; const mean = d3.mean(c[index].map(c => t[c])) || 0;
return height + t[index] / 100 + mean / 10000; return h[index] + t[index] / 100 + mean / 10000;
}); });
}; };
@ -295,52 +289,55 @@ const applyDistanceField = ({h, c, t}: Pick<IPack["cells"], "h" | "c" | "t">) =>
const resolveDepressions = function ( const resolveDepressions = function (
cells: Pick<IPack["cells"], "i" | "c" | "b" | "f">, cells: Pick<IPack["cells"], "i" | "c" | "b" | "f">,
features: TPackFeatures, features: TPackFeatures,
heights: number[] initialCellHeights: Float32Array
): [number[], Dict<boolean>] { ): [Float32Array, Dict<boolean>] {
TIME && console.time("resolveDepressions");
const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput"); const MAX_INTERATIONS = getInputNumber("resolveDepressionsStepsOutput");
const checkLakeMaxIteration = MAX_INTERATIONS * 0.85; const checkLakeMaxIteration = MAX_INTERATIONS * 0.85;
const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75; const elevateLakeMaxIteration = MAX_INTERATIONS * 0.75;
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
const LAND_ELEVATION_INCREMENT = 0.1; const LAND_ELEVATION_INCREMENT = 0.1;
const LAKE_ELEVATION_INCREMENT = 0.2; const LAKE_ELEVATION_INCREMENT = 0.2;
const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[]; const lakes = features.filter(feature => feature && feature.type === "lake") as IPackFeatureLake[];
lakes.sort((a, b) => a.height - b.height); // lowest lakes go first lakes.sort((a, b) => a.height - b.height); // lowest lakes go first
const currentCellHeights = Array.from(heights);
const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i]; const getHeight = (i: number) => currentLakeHeights[cells.f[i]] || currentCellHeights[i];
const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight)); const getMinHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(getHeight));
const getMinLandHeight = (cellsIds: number[]) => Math.min(...cellsIds.map(i => currentCellHeights[i]));
const drainableLakes = checkLakesDrainability(); const landCells = cells.i.filter(i => initialCellHeights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
landCells.sort((a, b) => initialCellHeights[a] - initialCellHeights[b]); // lowest cells go first
const landCells = cells.i.filter(i => heights[i] >= MIN_LAND_HEIGHT && !cells.b[i]);
landCells.sort((a, b) => heights[a] - heights[b]); // lowest cells go first
const currentCellHeights = Float32Array.from(initialCellHeights);
const currentLakeHeights = Object.fromEntries(lakes.map(({i, height}) => [i, height]));
const currentDrainableLakes = checkLakesDrainability();
const depressions: number[] = []; const depressions: number[] = [];
for (let iteration = 0; iteration && depressions.at(-1) && iteration < MAX_INTERATIONS; iteration++) { let bestDepressions = Infinity;
let bestCellHeights: typeof currentCellHeights | null = null;
let bestDrainableLakes: typeof currentDrainableLakes | null = null;
for (let iteration = 0; depressions.at(-1) !== 0 && iteration < MAX_INTERATIONS; iteration++) {
let depressionsLeft = 0; let depressionsLeft = 0;
// elevate potentially drainable lakes // elevate potentially drainable lakes
if (iteration < checkLakeMaxIteration) { if (iteration < checkLakeMaxIteration) {
for (const lake of lakes) { for (const lake of lakes) {
if (drainableLakes[lake.i] !== true) continue; if (currentDrainableLakes[lake.i] !== true) continue;
const minShoreHeight = getMinHeight(lake.shoreline); const minShoreHeight = getMinLandHeight(lake.shoreline);
if (minShoreHeight >= MAX_HEIGHT || lake.height > minShoreHeight) continue; if (minShoreHeight >= MAX_HEIGHT || currentLakeHeights[lake.i] > minShoreHeight) continue;
if (iteration > elevateLakeMaxIteration) { if (iteration > elevateLakeMaxIteration) {
// reset heights
for (const shoreCellId of lake.shoreline) { for (const shoreCellId of lake.shoreline) {
// reset heights currentCellHeights[shoreCellId] = initialCellHeights[shoreCellId];
currentCellHeights[shoreCellId] = heights[shoreCellId];
currentLakeHeights[lake.i] = lake.height;
} }
currentLakeHeights[lake.i] = lake.height;
drainableLakes[lake.i] = false; currentDrainableLakes[lake.i] = false;
continue; continue;
} }
@ -358,23 +355,36 @@ const resolveDepressions = function (
} }
depressions.push(depressionsLeft); depressions.push(depressionsLeft);
if (depressionsLeft < bestDepressions) {
// check depression resolving progress bestDepressions = depressionsLeft;
if (depressions.length > 5) { bestCellHeights = Float32Array.from(currentCellHeights);
const depressionsInitial = depressions.at(0) || 0; bestDrainableLakes = structuredClone(currentDrainableLakes);
const depressiosRecently = depressions.at(-6) || 0;
const isProgressingOverall = depressionsInitial < depressionsLeft;
if (!isProgressingOverall) return [heights, drainableLakes];
const isProgressingRecently = depressiosRecently < depressionsLeft;
if (!isProgressingRecently) return [currentCellHeights, drainableLakes];
} }
} }
TIME && console.timeEnd("resolveDepressions");
const depressionsLeft = depressions.at(-1);
if (depressionsLeft) {
if (bestCellHeights && bestDrainableLakes) {
WARN &&
console.warn(`Cannot resolve all depressions. Depressions: ${depressions[0]}. Best result: ${bestDepressions}`);
return [bestCellHeights, bestDrainableLakes];
}
WARN && console.warn(`Cannot resolve depressions. Depressions: ${depressionsLeft}`);
return [initialCellHeights, {}];
}
INFO &&
console.info(`ⓘ Resolved all depressions. Depressions: ${depressions[0]}. Interations: ${depressions.length}`);
return [currentCellHeights, currentDrainableLakes];
// define lakes that potentially can be open (drained into another water body) // define lakes that potentially can be open (drained into another water body)
function checkLakesDrainability() { function checkLakesDrainability() {
const canBeDrained: Dict<boolean> = {}; // all false by default const canBeDrained: Dict<boolean> = {}; // all false by default
const ELEVATION_LIMIT = getInputNumber("lakeElevationLimitOutput");
const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT; const drainAllLakes = ELEVATION_LIMIT === MAX_HEIGHT - MIN_LAND_HEIGHT;
for (const lake of lakes) { for (const lake of lakes) {
@ -385,7 +395,8 @@ const resolveDepressions = function (
canBeDrained[lake.i] = false; canBeDrained[lake.i] = false;
const minShoreHeight = getMinHeight(lake.shoreline); const minShoreHeight = getMinHeight(lake.shoreline);
const minHeightShoreCell = lake.shoreline.find(cellId => heights[cellId] === minShoreHeight) || lake.shoreline[0]; const minHeightShoreCell =
lake.shoreline.find(cellId => initialCellHeights[cellId] === minShoreHeight) || lake.shoreline[0];
const queue = [minHeightShoreCell]; const queue = [minHeightShoreCell];
const checked = []; const checked = [];
@ -397,9 +408,9 @@ const resolveDepressions = function (
for (const neibCellId of cells.c[cellId]) { for (const neibCellId of cells.c[cellId]) {
if (checked[neibCellId]) continue; if (checked[neibCellId]) continue;
if (heights[neibCellId] >= breakableHeight) continue; if (initialCellHeights[neibCellId] >= breakableHeight) continue;
if (heights[neibCellId] < MIN_LAND_HEIGHT) { if (initialCellHeights[neibCellId] < MIN_LAND_HEIGHT) {
const waterFeatureMet = features[cells.f[neibCellId]]; const waterFeatureMet = features[cells.f[neibCellId]];
const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean"; const isOceanMet = waterFeatureMet && waterFeatureMet.type === "ocean";
const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake"; const isLakeMet = waterFeatureMet && waterFeatureMet.type === "lake";
@ -418,8 +429,4 @@ const resolveDepressions = function (
return canBeDrained; return canBeDrained;
} }
depressions && WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
return [currentCellHeights, drainableLakes];
}; };

View file

@ -8,6 +8,27 @@ export function unique<T>(array: T[]) {
return [...new Set(array)]; return [...new Set(array)];
} }
export function sliceFragment<T>(array: T[], index: number, zone: number) {
if (zone + 1 + zone >= array.length) {
return array.slice();
}
const start = index - zone;
const end = index + zone;
if (start < 0) {
return array.slice(0, end).concat(array.slice(start));
}
if (end >= array.length) {
return array.slice(start).concat(array.slice(0, end % array.length));
}
return array.slice(start, end);
}
window.sliceFragment = sliceFragment;
interface ICreateTypesArrayLength { interface ICreateTypesArrayLength {
maxValue: number; maxValue: number;
length: number; length: number;

17
src/utils/debugUtils.ts Normal file
View file

@ -0,0 +1,17 @@
// utils used for debugging (not in PROD) only
export function drawArrow([x1, y1]: TPoint, [x2, y2]: TPoint, width = 1, color = "#444"): void {
const angle = Math.atan2(y2 - y1, x2 - x1);
const normal = angle + Math.PI / 2;
const [xMid, yMid] = [(x1 + x2) / 2, (y1 + y2) / 2];
const [xLeft, yLeft] = [xMid + width * Math.cos(normal), yMid + width * Math.sin(normal)];
const [xRight, yRight] = [xMid - width * Math.cos(normal), yMid - width * Math.sin(normal)];
debug
.append("path")
.attr("d", `M${x1},${y1} L${xMid},${yMid} ${xLeft},${yLeft} ${x2},${y2} ${xRight},${yRight} ${xMid},${yMid} Z`)
.attr("fill", color)
.attr("stroke", color)
.attr("stroke-width", width / 2);
}

View file

@ -52,3 +52,29 @@ export function getMiddlePoint(cell1: number, cell2: number) {
return [x, y]; return [x, y];
} }
function getOffCanvasSide([x, y]: TPoint) {
if (y <= 0) return "top";
if (y >= graphHeight) return "bottom";
if (x <= 0) return "left";
if (x >= graphWidth) return "right";
return false;
}
// remove intermediate out-of-canvas points from polyline
export function filterOutOfCanvasPoints(points: TPoints) {
const pointsOutSide = points.map(getOffCanvasSide);
const SAFE_ZONE = 3;
const filterOutCanvasPoint = (i: number) => {
const pointSide = pointsOutSide[i];
if (pointSide === false) return true;
if (pointsOutSide.slice(i - SAFE_ZONE, i + SAFE_ZONE).some(side => !side || side !== pointSide)) return true;
return false;
};
const result = points.filter((_, i) => filterOutCanvasPoint(i));
return result;
}