-
+
+
@@ -4219,6 +4228,7 @@
+
diff --git a/main.js b/main.js
index c0a96eb2..d66c5300 100644
--- a/main.js
+++ b/main.js
@@ -2,7 +2,7 @@
// https://github.com/Azgaar/Fantasy-Map-Generator
"use strict";
-const version = "1.64"; // generator version
+const version = "1.65"; // generator version1
document.title += " v" + version;
// Switches to disable/enable logging features
@@ -389,7 +389,6 @@ function applyDefaultBiomesSystem() {
}
function showWelcomeMessage() {
- const post = link("https://www.patreon.com/posts/48228540", "Main changes:");
const changelog = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "previous version");
const reddit = link("https://www.reddit.com/r/FantasyMapGenerator", "Reddit community");
const discord = link("https://discordapp.com/invite/X7E84HU", "Discord server");
@@ -397,18 +396,10 @@ function showWelcomeMessage() {
alertMessage.innerHTML = `The Fantasy Map Generator is updated up to version
${version}.
This version is compatible with ${changelog}, loaded
.map files will be auto-updated.
-
${post}
- - River overview and River editor rework
- - River generation code refactored and optimized
- - Rivers discharge (flux) and mouth width calculated
- - Lake editor rework
- - Lake type based on evaporation and river system
- - Lake flux, inlets and outlet tracked properly
- - Lake outlet width depends on flux
- - Lakes now have names
- - Rulers rework (v1.61)
- - New ocean pattern by Kiwiroo (v1.61)
- - Water erosion rework (v1.62)
+ Main changes:
+ - Ability to add river selecting its cells
+ - Keep river course on edit
+ - Refactor river rendering code
Join our ${discord} and ${reddit} to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.
@@ -624,6 +615,7 @@ function generate() {
drawCoastline();
Rivers.generate();
+ drawRivers();
Lakes.defineGroup();
defineBiomes();
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 0dccc4ab..7ed9d724 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -80,6 +80,7 @@
TIME && console.time("createStates");
const states = [{i: 0, name: "Neutrals"}];
const colors = getColors(burgs.length - 1);
+ const each5th = each(5);
burgs.forEach(function (b, i) {
if (!i) return; // skip first element
@@ -93,7 +94,7 @@
// states data
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
- const basename = b.name.length < 9 && b.cell % 5 === 0 ? b.name : Names.getCultureShort(b.culture);
+ const basename = b.name.length < 9 && each5th(b.cell) ? b.name : Names.getCultureShort(b.culture);
const name = Names.getState(basename, b.culture);
const type = cultures[b.culture].type;
diff --git a/modules/fonts.js b/modules/fonts.js
index 44950808..fedcbd85 100644
--- a/modules/fonts.js
+++ b/modules/fonts.js
@@ -33,7 +33,7 @@ async function addFonts(url) {
function loadUsedFonts() {
const fontsInUse = getFontsList(svg);
const fontsToLoad = fontsInUse.filter(font => !fonts.includes(font));
- if (fontsToLoad) {
+ if (fontsToLoad?.length) {
const url = "https://fonts.googleapis.com/css?family=" + fontsToLoad.join("|");
addFonts(url);
}
diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js
index 8e0dc42a..ab2f51a0 100644
--- a/modules/heightmap-generator.js
+++ b/modules/heightmap-generator.js
@@ -422,7 +422,6 @@
if (d % 6 !== 0) return;
for (const l of d3.range(i)) {
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
- //debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
cur = min;
}
diff --git a/modules/load.js b/modules/load.js
index a8a33ac0..96a2845d 100644
--- a/modules/load.js
+++ b/modules/load.js
@@ -271,12 +271,13 @@ function parseLoadedData(data) {
}
})();
- const notHidden = selection => selection.node() && selection.style("display") !== "none";
- const hasChildren = selection => selection.node()?.hasChildNodes();
- const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
- const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
-
void (function restoreLayersState() {
+ // helper functions
+ const notHidden = selection => selection.node() && selection.style("display") !== "none";
+ const hasChildren = selection => selection.node()?.hasChildNodes();
+ const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
+ const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
+
// turn all layers off
document
.getElementById("mapLayers")
@@ -291,7 +292,7 @@ function parseLoadedData(data) {
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
if (notHidden(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
- if (notHidden(rivers)) turnOn("toggleRivers");
+ if (hasChildren(rivers)) turnOn("toggleRivers");
if (notHidden(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
if (hasChildren(relig)) turnOn("toggleReligions");
if (hasChildren(cults)) turnOn("toggleCultures");
@@ -705,6 +706,33 @@ function parseLoadedData(data) {
statesHalo.attr("opacity", opacity).attr("filter", "blur(5px)");
regions.attr("opacity", null).attr("filter", null);
}
+
+ if (version < 1.65) {
+ // v 1.65 changed rivers data
+ for (const river of pack.rivers) {
+ const node = document.getElementById("river" + river.i);
+ if (node && !river.cells) {
+ const riverCells = new Set();
+ const length = node.getTotalLength() / 2;
+ const segments = Math.ceil(length / 6);
+ const increment = length / segments;
+ for (let i = increment * segments, c = i; i >= 0; i -= increment, c += increment) {
+ const p1 = node.getPointAtLength(i);
+ const p2 = node.getPointAtLength(c);
+ const x = (p1.x + p2.x) / 2;
+ const y = (p1.y + p2.y) / 2;
+ const cell = findCell(x, y, 6);
+ if (cell) riverCells.add(cell);
+ }
+
+ river.cells = Array.from(riverCells);
+ }
+
+ pack.cells.i.forEach(i => {
+ if (pack.cells.r[i] && pack.cells.h[i] < 20) pack.cells.r[i] = 0;
+ });
+ }
+ }
})();
void (function checkDataIntegrity() {
diff --git a/modules/river-generator.js b/modules/river-generator.js
index 948afd07..a9baf466 100644
--- a/modules/river-generator.js
+++ b/modules/river-generator.js
@@ -7,9 +7,14 @@
TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed);
const {cells, features} = pack;
- const p = cells.p;
- const riversData = []; // rivers data
+ 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
@@ -20,6 +25,7 @@
resolveDepressions(h);
drainWater();
defineRivers();
+ calculateConfluenceFlux();
Lakes.cleanupLakeData();
if (allowErosion) cells.h = Uint8Array.from(h); // apply changed heights as basic one
@@ -28,51 +34,48 @@
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});
+ addCellToRiver(lakeCell, lake.river);
} else {
cells.r[lakeCell] = riverNext;
- riversData.push({river: riverNext, cell: lakeCell, x, y, flux});
+ addCellToRiver(lakeCell, riverNext);
riverNext++;
}
}
lake.outlet = cells.r[lakeCell];
- flowDown(i, cells.fl[i], cells.fl[lakeCell], lake.outlet);
+ flowDown(i, cells.fl[lakeCell], lake.outlet);
}
// assign all tributary rivers to outlet basin
- for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
- lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet));
+ const outlet = lakes[0]?.outlet;
+ for (const lake of lakes) {
+ if (!Array.isArray(lake.inlets)) continue;
+ for (const inlet of lake.inlets) {
+ riverParents[inlet] = outlet;
+ }
}
// 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]});
- return;
- }
+ if (cells.b[i] && cells.r[i]) return addCellToRiver(-1, cells.r[i]);
// downhill cell (make sure it's not in the source lake)
let min = null;
@@ -89,31 +92,35 @@
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]});
+ addCellToRiver(i, riverNext);
riverNext++;
}
- flowDown(min, cells.fl[min], cells.fl[i], cells.r[i], i);
+ flowDown(min, cells.fl[i], cells.r[i]);
});
}
- function flowDown(toCell, toFlux, fromFlux, river, fromCell = 0) {
- if (cells.r[toCell]) {
+ function flowDown(toCell, fromFlux, river) {
+ const toFlux = cells.fl[toCell] - cells.conf[toCell];
+ const toRiver = cells.r[toCell];
+
+ if (toRiver) {
// downhill cell already has river assigned
- if (toFlux < fromFlux) {
- 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 (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
} 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) riverParents[river] = toRiver; // current river is a tributary of min river
}
} else cells.r[toCell] = river; // assign the river to the downhill cell
@@ -126,53 +133,60 @@
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});
+ addCellToRiver(toCell, river);
}
function defineRivers() {
- cells.r = new Uint16Array(cells.i.length); // re-initiate rivers array
- pack.rivers = []; // rivers data
- const riverPaths = [];
+ // re-initialize rivers and confluence arrays
+ cells.r = new Uint16Array(cells.i.length);
+ cells.conf = new Uint16Array(cells.i.length);
+ pack.rivers = [];
- for (let r = 1; r <= riverNext; r++) {
- const riverPoints = riversData.filter(d => d.river === r);
- if (riverPoints.length < 3) continue;
+ for (const key in riversData) {
+ const riverCells = riversData[key];
+ if (riverCells.length < 3) continue; // exclude tiny rivers
- for (const segment of riverPoints) {
- const i = segment.cell;
- if (cells.r[i]) continue;
- if (cells.h[i] < 20) continue;
- cells.r[i] = r;
+ const riverId = +key;
+ for (const cell of riverCells) {
+ 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;
}
- const source = riverPoints[0].cell;
- const mouth = riverPoints[riverPoints.length - 2].cell;
+ const source = riverCells[0];
+ const mouth = riverCells[riverCells.length - 2];
+ const parent = riverParents[key] || 0;
- const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
- const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2);
+ const widthFactor = !parent || parent === riverId ? 1.2 : 1;
+ const meanderedPoints = addMeandering(riverCells);
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(meanderedPoints);
+ const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, 0));
- const riverCells = riverPoints.map(point => point.cell);
- const riverMeandered = addMeandering(riverCells, sourceWidth * 10, 0.5);
- const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
- riverPaths.push([path, r]);
-
- const parent = riverPoints[0].parent || 0;
- const width = rn(offset ** 2, 2); // mounth width in km
- const discharge = last(riverPoints).flux; // in m3/s
-
- pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells});
+ pack.rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells});
}
+ }
- // 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);
+ }
}
};
@@ -245,102 +259,131 @@
};
// add points at 1/3 and 2/3 of a line between adjacents river cells
- const addMeandering = function (riverCells, width = 1, meandering = 0.5) {
+ const addMeandering = function (riverCells, riverPoints = null, meandering = 0.5) {
+ const {fl, conf, h} = pack.cells;
const meandered = [];
- const {p, conf} = pack.cells;
- const lastCell = riverCells.length - 1;
+ const lastStep = riverCells.length - 1;
+ const points = getRiverPoints(riverCells, riverPoints);
+ let step = h[riverCells[0]] < 20 ? 1 : 10;
- for (let i = 0; i <= lastCell; i++, width++) {
+ let fluxPrev = 0;
+ const getFlux = (step, flux) => (step === lastStep ? fluxPrev : flux);
+
+ for (let i = 0; i <= lastStep; i++, step++) {
const cell = riverCells[i];
- const [x1, y1] = p[cell];
- meandered.push([x1, y1, conf[cell]]);
+ const isLastCell = i === lastStep;
- if (i === lastCell) break;
+ const [x1, y1] = points[i];
+ const flux1 = getFlux(i, fl[cell]);
+ fluxPrev = flux1;
+
+ meandered.push([x1, y1, flux1]);
+ if (isLastCell) break;
const nextCell = riverCells[i + 1];
+ const [x2, y2] = points[i + 1];
+
if (nextCell === -1) {
- meandered.push(getBorderPoint(cell));
+ meandered.push([x2, y2, fluxPrev]);
break;
}
- const [x2, y2] = p[nextCell];
- const angle = Math.atan2(y2 - y1, x2 - x1);
- const sin = Math.sin(angle);
- const cos = Math.cos(angle);
-
- const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
const dist2 = (x2 - x1) ** 2 + (y2 - y1) ** 2; // square distance between cells
+ if (dist2 <= 25 && riverCells.length >= 6) continue;
- if (width < 10 && (dist2 > 64 || (dist2 > 36 && riverCells.length < 5))) {
+ const flux2 = getFlux(i + 1, fl[nextCell]);
+ const keepInitialFlux = conf[nextCell] || flux1 === flux2;
+
+ const meander = meandering + 1 / step + Math.max(meandering - step / 100, 0);
+ 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 p2x = (x1 + x2 * 2) / 3 + sin * meander;
- const p2y = (y1 + y2 * 2) / 3 + cos * meander;
- meandered.push([p1x, p1y], [p2x, p2y]);
+ const p1x = (x1 * 2 + x2) / 3 + -sinMeander;
+ const p1y = (y1 * 2 + y2) / 3 + cosMeander;
+ const p2x = (x1 + x2 * 2) / 3 + sinMeander / 2;
+ const p2y = (y1 + y2 * 2) / 3 - cosMeander / 2;
+ 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;
- meandered.push([p1x, p1y]);
+ const p1x = (x1 + x2) / 2 + -sinMeander;
+ const p1y = (y1 + y2) / 2 + cosMeander;
+ const p1fl = keepInitialFlux ? flux1 : (flux1 + flux2) / 2;
+ meandered.push([p1x, p1y, p1fl]);
}
}
return meandered;
};
- const getPath = function (points, widthFactor = 1, width = 0.1) {
- 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); // sum of segments length
- const widening = 1000 + riverLength * 30;
- const factor = riverLength / points.length;
- let offset;
+ const getRiverPoints = (riverCells, riverPoints) => {
+ const {p} = pack.cells;
+ return riverCells.map((cell, i) => {
+ if (riverPoints && riverPoints[i]) return riverPoints[i];
+ if (cell === -1) return getBorderPoint(riverCells[i - 1]);
+ return p[cell];
+ });
+ };
- // store points on both sides to build a valid polygon
+ const getBorderPoint = i => {
+ const [x, y] = pack.cells.p[i];
+ const min = Math.min(y, graphHeight - y, x, graphWidth - x);
+ if (min === y) return [x, 0];
+ else if (min === graphHeight - y) return [x, graphHeight];
+ else if (min === x) return [0, y];
+ return [graphWidth, y];
+ };
+
+ const FLUX_FACTOR = 500;
+ const MAX_FLUX_WIDTH = 2;
+ const LENGTH_FACTOR = 200;
+ const STEP_WIDTH = 1 / LENGTH_FACTOR;
+ const LENGTH_PROGRESSION = [1, 1, 2, 3, 5, 8, 13, 21, 34].map(n => n / LENGTH_FACTOR);
+ const MAX_PROGRESSION = last(LENGTH_PROGRESSION);
+
+ const getOffset = (flux, pointNumber, widthFactor = 1, startingWidth = 0) => {
+ const fluxWidth = Math.min(flux ** 0.9 / FLUX_FACTOR, MAX_FLUX_WIDTH);
+ const lengthWidth = pointNumber * STEP_WIDTH + (LENGTH_PROGRESSION[pointNumber] || MAX_PROGRESSION);
+ return widthFactor * (lengthWidth + fluxWidth) + startingWidth;
+ };
+
+ // build polygon from a list of points and calculated offset (width)
+ const getRiverPath = function (points, widthFactor = 1, startingWidth = 0) {
const riverPointsLeft = [];
const riverPointsRight = [];
for (let p = 0; p < points.length; p++) {
const [x0, y0] = points[p - 1] || points[p];
- const [x1, y1] = points[p];
+ const [x1, y1, flux] = points[p];
const [x2, y2] = points[p + 1] || points[p];
- offset = width + (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor;
-
- if (points[p + 2] && points[p + 1][2]) {
- const confluence = points[p + 1][2];
- width += Math.atan((confluence * 5) / widening);
- }
-
+ const offset = getOffset(flux, p, widthFactor, startingWidth);
const angle = Math.atan2(y0 - y2, x0 - x2);
const sinOffset = Math.sin(angle) * offset;
const cosOffset = Math.cos(angle) * offset;
riverPointsLeft.push([x1 - sinOffset, y1 + cosOffset]);
- riverPointsRight.unshift([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);
+ const right = lineGen(riverPointsRight.reverse());
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
- return [round(right + left, 2), rn(riverLength, 2), offset];
+ return round(right + left, 1);
};
const specify = function () {
const rivers = pack.rivers;
if (!rivers.length) return;
- Math.random = aleaPRNG(seed);
- const thresholdElement = Math.ceil(rivers.length * 0.15);
- const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[thresholdElement];
- const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types
- for (const r of rivers) {
- r.basin = getBasin(r.i);
- r.name = getName(r.mouth);
- const small = r.length < smallLength;
- r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
+ for (const river of rivers) {
+ river.basin = getBasin(river.i);
+ river.name = getName(river.mouth);
+ river.type = getType(river);
}
};
@@ -348,6 +391,36 @@
return Names.getCulture(pack.cells.culture[cell]);
};
+ // weighted arrays of river type names
+ const riverTypes = {
+ main: {
+ big: {River: 1},
+ small: {Creek: 9, River: 3, Brook: 3, Stream: 1}
+ },
+ fork: {
+ big: {Fork: 1},
+ small: {Branch: 1}
+ }
+ };
+
+ let smallLength = null;
+ const getType = function ({i, length, parent}) {
+ if (smallLength === null) {
+ const threshold = Math.ceil(pack.rivers.length * 0.15);
+ smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[threshold];
+ }
+
+ const isSmall = length < smallLength;
+ const isFork = each(3)(i) && parent && parent !== i;
+ return rw(riverTypes[isFork ? "fork" : "main"][isSmall ? "small" : "big"]);
+ };
+
+ const getApproximateLength = points => points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i - 1][0], v[1] - p[i - 1][1]) : 0), 0);
+
+ // 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
+
// remove river and all its tributaries
const remove = function (id) {
const cells = pack.cells;
@@ -368,14 +441,5 @@
return getBasin(parent);
};
- const getBorderPoint = i => {
- const [x, y] = pack.cells.p[i];
- const min = Math.min(y, graphHeight - y, x, graphWidth - x);
- if (min === y) return [x, 0];
- else if (min === graphHeight - y) return [x, graphHeight];
- else if (min === x) return [0, y];
- return [graphWidth, y];
- };
-
- return {generate, alterHeights, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
+ return {generate, alterHeights, resolveDepressions, addMeandering, getRiverPath, specify, getName, getType, getBasin, getWidth, getOffset, getApproximateLength, getRiverPoints, remove};
});
diff --git a/modules/save.js b/modules/save.js
index 24fbcf23..277f4168 100644
--- a/modules/save.js
+++ b/modules/save.js
@@ -144,7 +144,7 @@ async function getMapURL(type, options = {}) {
cloneEl.id = "fantasyMap";
document.body.appendChild(cloneEl);
const clone = d3.select(cloneEl);
- if (debug) clone.select("#debug").remove();
+ if (!debug) clone.select("#debug").remove();
const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
const svgDefs = document.getElementById("defElements");
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index cac16d4c..9acf751c 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -6,10 +6,7 @@ restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events
function restoreDefaultEvents() {
svg.call(zoom);
- viewbox.style("cursor", "default")
- .on(".drag", null)
- .on("click", clicked)
- .on("touchmove mousemove", moved);
+ viewbox.style("cursor", "default").on(".drag", null).on("click", clicked).on("touchmove mousemove", moved);
legend.call(d3.drag().on("start", dragLegendBox));
}
@@ -17,12 +14,14 @@ function restoreDefaultEvents() {
function clicked() {
const el = d3.event.target;
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
- const parent = el.parentElement, grand = parent.parentElement, great = grand.parentElement;
+ const parent = el.parentElement;
+ const grand = parent.parentElement;
+ const great = grand.parentElement;
const p = d3.mouse(this);
const i = findCell(p[0], p[1]);
if (grand.id === "emblems") editEmblem();
- else if (parent.id === "rivers") editRiver();
+ else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute();
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg();
@@ -33,10 +32,9 @@ function clicked() {
else if (grand.id === "coastline") editCoastline();
else if (great.id === "armies") editRegiment();
else if (pack.cells.t[i] === 1) {
- const node = document.getElementById("island_"+pack.cells.f[i]);
+ const node = document.getElementById("island_" + pack.cells.f[i]);
editCoastline(node);
- }
- else if (grand.id === "lakes") editLake();
+ } else if (grand.id === "lakes") editLake();
}
// clear elSelected variable
@@ -51,9 +49,11 @@ function unselect() {
// close all dialogs except stated
function closeDialogs(except = "#except") {
- $(".dialog:visible").not(except).each(function() {
- $(this).dialog("close");
- });
+ $(".dialog:visible")
+ .not(except)
+ .each(function () {
+ $(this).dialog("close");
+ });
}
// move brush radius circle
@@ -79,8 +79,10 @@ function fitContent() {
}
// apply sorting behaviour for lines on Editor header click
-document.querySelectorAll(".sortable").forEach(function(e) {
- e.addEventListener("click", function(e) {sortLines(this);});
+document.querySelectorAll(".sortable").forEach(function (e) {
+ e.addEventListener("click", function (e) {
+ sortLines(this);
+ });
});
function sortLines(header) {
@@ -90,7 +92,9 @@ function sortLines(header) {
const headers = header.parentNode;
headers.querySelectorAll("div.sortable").forEach(e => {
- e.classList.forEach(c => {if(c.includes("icon-sort")) e.classList.remove(c);});
+ e.classList.forEach(c => {
+ if (c.includes("icon-sort")) e.classList.remove(c);
+ });
});
header.classList.add("icon-sort-" + type + order);
applySorting(headers);
@@ -105,16 +109,19 @@ function applySorting(headers) {
const list = headers.nextElementSibling;
const lines = Array.from(list.children);
- lines.sort((a, b) => {
- const an = name ? a.dataset[sortby] : +a.dataset[sortby];
- const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
- return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
- }).forEach(line => list.appendChild(line));
+ lines
+ .sort((a, b) => {
+ const an = name ? a.dataset[sortby] : +a.dataset[sortby];
+ const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
+ return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
+ })
+ .forEach(line => list.appendChild(line));
}
function addBurg(point) {
const cells = pack.cells;
- const x = rn(point[0], 2), y = rn(point[1], 2);
+ const x = rn(point[0], 2),
+ y = rn(point[1], 2);
const cell = findCell(x, point[1]);
const i = pack.burgs.length;
const culture = cells.culture[cell];
@@ -123,11 +130,11 @@ function addBurg(point) {
const feature = cells.f[cell];
const temple = pack.states[state].form === "Theocracy";
- const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1);
+ const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cell, false);
// generate emblem
- const coa = COA.generate(pack.states[state].coa, .25, null, type);
+ const coa = COA.generate(pack.states[state].coa, 0.25, null, type);
coa.shield = COA.getShield(culture, state);
COArenderer.add("burg", i, coa, x, y);
@@ -135,10 +142,23 @@ function addBurg(point) {
cells.burg[cell] = i;
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
- burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i)
- .attr("cx", x).attr("cy", y).attr("r", townSize);
- burgLabels.select("#towns").append("text").attr("id", "burgLabel"+i).attr("data-id", i)
- .attr("x", x).attr("y", y).attr("dy", `${townSize * -1.5}px`).text(name);
+ burgIcons
+ .select("#towns")
+ .append("circle")
+ .attr("id", "burg" + i)
+ .attr("data-id", i)
+ .attr("cx", x)
+ .attr("cy", y)
+ .attr("r", townSize);
+ burgLabels
+ .select("#towns")
+ .append("text")
+ .attr("id", "burgLabel" + i)
+ .attr("data-id", i)
+ .attr("x", x)
+ .attr("y", y)
+ .attr("dy", `${townSize * -1.5}px`)
+ .text(name);
BurgsAndStates.defineBurgFeatures(pack.burgs[i]);
return i;
@@ -148,17 +168,20 @@ function moveBurgToGroup(id, g) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
- if (!label || !icon) {ERROR && console.error("Cannot find label or icon elements"); return;}
+ if (!label || !icon) {
+ ERROR && console.error("Cannot find label or icon elements");
+ return;
+ }
- document.querySelector("#burgLabels > #"+g).appendChild(label);
- document.querySelector("#burgIcons > #"+g).appendChild(icon);
+ document.querySelector("#burgLabels > #" + g).appendChild(label);
+ document.querySelector("#burgIcons > #" + g).appendChild(icon);
const iconSize = icon.parentNode.getAttribute("size");
icon.setAttribute("r", iconSize);
label.setAttribute("dy", `${iconSize * -1.5}px`);
if (anchor) {
- document.querySelector("#anchors > #"+g).appendChild(anchor);
+ document.querySelector("#anchors > #" + g).appendChild(anchor);
const anchorSize = +anchor.parentNode.getAttribute("size");
anchor.setAttribute("width", anchorSize);
anchor.setAttribute("height", anchorSize);
@@ -175,7 +198,8 @@ function removeBurg(id) {
if (icon) icon.remove();
if (anchor) anchor.remove();
- const cells = pack.cells, burg = pack.burgs[id];
+ const cells = pack.cells,
+ burg = pack.burgs[id];
burg.removed = true;
cells.burg[burg.cell] = 0;
@@ -189,8 +213,14 @@ function removeBurg(id) {
function toggleCapital(burg) {
const state = pack.burgs[burg].state;
- if (!state) {tip("Neutral lands cannot have a capital", false, "error"); return;}
- if (pack.burgs[burg].capital) {tip("To change capital please assign a capital status to another burg of this state", false, "error"); return;}
+ if (!state) {
+ tip("Neutral lands cannot have a capital", false, "error");
+ return;
+ }
+ if (pack.burgs[burg].capital) {
+ tip("To change capital please assign a capital status to another burg of this state", false, "error");
+ return;
+ }
const old = pack.states[state].capital;
// change statuses
@@ -206,7 +236,10 @@ function togglePort(burg) {
const anchor = document.querySelector("#anchors [data-id='" + burg + "']");
if (anchor) anchor.remove();
const b = pack.burgs[burg];
- if (b.port) {b.port = 0; return;} // not a port anymore
+ if (b.port) {
+ b.port = 0;
+ return;
+ } // not a port anymore
const haven = pack.cells.haven[b.cell];
const port = haven ? pack.cells.f[haven] : -1;
@@ -214,11 +247,16 @@ function togglePort(burg) {
b.port = port;
const g = b.capital ? "cities" : "towns";
- const group = anchors.select("g#"+g);
+ const group = anchors.select("g#" + g);
const size = +group.attr("size");
- group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", burg)
- .attr("x", rn(b.x - size * .47, 2)).attr("y", rn(b.y - size * .47, 2))
- .attr("width", size).attr("height", size);
+ group
+ .append("use")
+ .attr("xlink:href", "#icon-anchor")
+ .attr("data-id", burg)
+ .attr("x", rn(b.x - size * 0.47, 2))
+ .attr("y", rn(b.y - size * 0.47, 2))
+ .attr("width", size)
+ .attr("height", size);
}
function toggleBurgLock(burg) {
@@ -251,38 +289,49 @@ function drawLegend(name, data) {
const vOffset = fontSize / 2;
// append items
- const boxes = legend.append("g").attr("stroke-width", .5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
+ const boxes = legend.append("g").attr("stroke-width", 0.5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none");
const columns = Math.ceil(data.length / itemsInCol);
- for (let column=0, i=0; column < columns; column++) {
+ for (let column = 0, i = 0; column < columns; column++) {
const linesInColumn = Math.ceil(data.length / columns);
const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset;
- for (let l=0; l < linesInColumn && data[i]; l++, i++) {
- boxes.append("rect").attr("fill", data[i][1])
- .attr("x", offset).attr("y", lineHeight + l*lineHeight + vOffset)
- .attr("width", colorBoxSize).attr("height", colorBoxSize);
+ for (let l = 0; l < linesInColumn && data[i]; l++, i++) {
+ boxes
+ .append("rect")
+ .attr("fill", data[i][1])
+ .attr("x", offset)
+ .attr("y", lineHeight + l * lineHeight + vOffset)
+ .attr("width", colorBoxSize)
+ .attr("height", colorBoxSize);
- labels.append("text").text(data[i][2])
- .attr("x", offset + colorBoxSize * 1.6).attr("y", fontSize/1.6 + lineHeight + l*lineHeight + vOffset);
+ labels
+ .append("text")
+ .text(data[i][2])
+ .attr("x", offset + colorBoxSize * 1.6)
+ .attr("y", fontSize / 1.6 + lineHeight + l * lineHeight + vOffset);
}
}
// append label
const offset = colOffset + legend.node().getBBox().width / 2;
- labels.append("text")
- .attr("text-anchor", "middle").attr("font-weight", "bold").attr("font-size", "1.2em")
- .attr("id", "legendLabel").text(name).attr("x", offset).attr("y", fontSize * 1.1 + vOffset / 2);
+ labels
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("font-weight", "bold")
+ .attr("font-size", "1.2em")
+ .attr("id", "legendLabel")
+ .text(name)
+ .attr("x", offset)
+ .attr("y", fontSize * 1.1 + vOffset / 2);
// append box
const bbox = legend.node().getBBox();
const width = bbox.width + colOffset * 2;
const height = bbox.height + colOffset / 2 + vOffset;
- legend.insert("rect", ":first-child").attr("id", "legendBox")
- .attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
- .attr("fill", backClr).attr("fill-opacity", opacity);
+ legend.insert("rect", ":first-child").attr("id", "legendBox").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", backClr).attr("fill-opacity", opacity);
fitLegendBox();
}
@@ -293,7 +342,8 @@ function fitLegendBox() {
const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100;
const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100;
const bbox = legend.node().getBBox();
- const x = rn(svgWidth * px - bbox.width), y = rn(svgHeight * py - bbox.height);
+ const x = rn(svgWidth * px - bbox.width),
+ y = rn(svgHeight * py - bbox.height);
legend.attr("transform", `translate(${x},${y})`);
}
@@ -301,19 +351,23 @@ function fitLegendBox() {
function redrawLegend() {
if (!legend.select("rect").size()) return;
const name = legend.select("#legendLabel").text();
- const data = legend.attr("data").split("|").map(l => l.split(","));
+ const data = legend
+ .attr("data")
+ .split("|")
+ .map(l => l.split(","));
drawLegend(name, data);
}
function dragLegendBox() {
const tr = parseTransform(this.getAttribute("transform"));
- const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
+ const x = +tr[0] - d3.event.x,
+ y = +tr[1] - d3.event.y;
const bbox = legend.node().getBBox();
- d3.event.on("drag", function() {
- const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
- const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
- const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
+ d3.event.on("drag", function () {
+ const px = rn(((x + d3.event.x + bbox.width) / svgWidth) * 100, 2);
+ const py = rn(((y + d3.event.y + bbox.height) / svgHeight) * 100, 2);
+ const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
legend.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
@@ -330,9 +384,16 @@ function createPicker() {
const closePicker = () => contaiter.style("display", "none");
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
- contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", .2)
- .on("mousemove", cl).on("click", closePicker);
- const picker = contaiter.append("g").attr("id", "picker").call(d3.drag().filter(() => event.target.tagName !== "INPUT").on("start", dragPicker));
+ contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", 0.2).on("mousemove", cl).on("click", closePicker);
+ const picker = contaiter
+ .append("g")
+ .attr("id", "picker")
+ .call(
+ d3
+ .drag()
+ .filter(() => event.target.tagName !== "INPUT")
+ .on("start", dragPicker)
+ );
const controls = picker.append("g").attr("id", "pickerControls");
const h = controls.append("g");
@@ -343,7 +404,7 @@ function createPicker() {
const s = controls.append("g");
s.append("text").attr("x", 113).attr("y", 14).text("S:");
- s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10)
+ s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10);
s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS");
s.on("mousemove", () => tip("Set palette saturation"));
@@ -356,8 +417,13 @@ function createPicker() {
controls.selectAll("line").on("click", clickPickerControl);
controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl));
- const spaces = picker.append("foreignObject").attr("id", "pickerSpaces")
- .attr("x", 4).attr("y", 20).attr("width", 303).attr("height", 20)
+ const spaces = picker
+ .append("foreignObject")
+ .attr("id", "pickerSpaces")
+ .attr("x", 4)
+ .attr("y", 20)
+ .attr("width", 303)
+ .attr("height", 20)
.on("mousemove", () => tip("Color value in different color spaces. Edit to change"));
const html = `
`;
- spaces.node().insertAdjacentHTML('beforeend', html);
+ spaces.node().insertAdjacentHTML("beforeend", html);
spaces.selectAll("input").on("change", changePickerSpace);
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
@@ -379,19 +445,38 @@ function createPicker() {
const hatching = d3.selectAll("g#hatching > pattern");
const number = hatching.size();
- const clr = d3.range(number).map(i => d3.hsl(i/number*360, .7, .7).hex());
- clr.forEach(function(d, i) {
- colors.append("rect").attr("id", "picker_" + d).attr("fill", d).attr("class", i?"":"selected")
- .attr("x", i*22+4).attr("y", 40).attr("width", 16).attr("height", 16);
+ const clr = d3.range(number).map(i => d3.hsl((i / number) * 360, 0.7, 0.7).hex());
+ clr.forEach(function (d, i) {
+ colors
+ .append("rect")
+ .attr("id", "picker_" + d)
+ .attr("fill", d)
+ .attr("class", i ? "" : "selected")
+ .attr("x", i * 22 + 4)
+ .attr("y", 40)
+ .attr("width", 16)
+ .attr("height", 16);
});
- hatching.each(function(d, i) {
- hatches.append("rect").attr("id", "picker_" + this.id).attr("fill", "url(#" + this.id + ")")
- .attr("x", i*22+4).attr("y", 61).attr("width", 16).attr("height", 16);
+ hatching.each(function (d, i) {
+ hatches
+ .append("rect")
+ .attr("id", "picker_" + this.id)
+ .attr("fill", "url(#" + this.id + ")")
+ .attr("x", i * 22 + 4)
+ .attr("y", 61)
+ .attr("width", 16)
+ .attr("height", 16);
});
- colors.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the color"));
- hatches.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the hatching"));
+ colors
+ .selectAll("rect")
+ .on("click", pickerFillClicked)
+ .on("mousemove", () => tip("Click to fill with the color"));
+ hatches
+ .selectAll("rect")
+ .on("click", pickerFillClicked)
+ .on("mousemove", () => tip("Click to fill with the hatching"));
// append box
const bbox = picker.node().getBBox();
@@ -403,12 +488,15 @@ function createPicker() {
picker.insert("rect", ":first-child").attr("x", 288).attr("y", -21).attr("id", "pickerCloseRect").attr("width", 14).attr("height", 14).on("mousemove", cl).on("click", closePicker);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
- picker.attr("transform", `translate(${(svgWidth-width)/2},${(svgHeight-height)/2})`);
+ picker.attr("transform", `translate(${(svgWidth - width) / 2},${(svgHeight - height) / 2})`);
}
function updateSelectedRect(fill) {
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
- document.getElementById("picker").querySelector("rect[fill='"+fill.toLowerCase()+"']").classList.add("selected");
+ document
+ .getElementById("picker")
+ .querySelector("rect[fill='" + fill.toLowerCase() + "']")
+ .classList.add("selected");
}
function updateSpaces() {
@@ -438,8 +526,8 @@ function updatePickerColors() {
const s = getPickerControl(pickerS, 1);
const l = getPickerControl(pickerL, 1);
- colors.each(function(d, i) {
- const clr = d3.hsl(i/number*180+h, s, l).hex();
+ colors.each(function (d, i) {
+ const clr = d3.hsl((i / number) * 180 + h, s, l).hex();
this.setAttribute("id", "picker_" + clr);
this.setAttribute("fill", clr);
});
@@ -461,11 +549,11 @@ function openPicker(fill, callback) {
updateSelectedRect(fill);
- openPicker.updateFill = function() {
+ openPicker.updateFill = function () {
const selected = document.getElementById("picker").querySelector("rect.selected");
if (!selected) return;
callback(selected.getAttribute("fill"));
- }
+ };
}
function setPickerControl(control, value, max) {
@@ -479,19 +567,20 @@ function getPickerControl(control, max) {
const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min;
const current = +control.getAttribute("cx") - min;
- return current / delta * max;
+ return (current / delta) * max;
}
function dragPicker() {
const tr = parseTransform(this.getAttribute("transform"));
- const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
+ const x = +tr[0] - d3.event.x,
+ y = +tr[1] - d3.event.y;
const picker = d3.select("#picker");
const bbox = picker.node().getBBox();
- d3.event.on("drag", function() {
- const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
- const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
- const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
+ d3.event.on("drag", function () {
+ const px = rn(((x + d3.event.x + bbox.width) / svgWidth) * 100, 2);
+ const py = rn(((y + d3.event.y + bbox.height) / svgHeight) * 100, 2);
+ const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
picker.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
@@ -519,7 +608,7 @@ function dragPickerControl() {
const min = +this.previousSibling.getAttribute("x1");
const max = +this.previousSibling.getAttribute("x2");
- d3.event.on("drag", function() {
+ d3.event.on("drag", function () {
const x = Math.max(Math.min(d3.event.x, max), min);
this.setAttribute("cx", x);
updateSpaces();
@@ -530,16 +619,20 @@ function dragPickerControl() {
function changePickerSpace() {
const valid = this.checkValidity();
- if (!valid) {tip("You must provide a correct value", false, "error"); return;}
+ if (!valid) {
+ tip("You must provide a correct value", false, "error");
+ return;
+ }
const space = this.dataset.space;
const i = Array.from(this.parentNode.querySelectorAll("input")).map(input => input.value); // inputs
- const fill = space === "hex" ? d3.rgb(this.value)
- : space === "rgb" ? d3.rgb(i[0], i[1], i[2])
- : d3.hsl(i[0], i[1]/100, i[2]/100);
+ const fill = space === "hex" ? d3.rgb(this.value) : space === "rgb" ? d3.rgb(i[0], i[1], i[2]) : d3.hsl(i[0], i[1] / 100, i[2] / 100);
const hsl = d3.hsl(fill);
- if (isNaN(hsl.l)) {tip("You must provide a correct value", false, "error"); return;}
+ if (isNaN(hsl.l)) {
+ tip("You must provide a correct value", false, "error");
+ return;
+ }
if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360);
if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1);
if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1);
@@ -551,7 +644,7 @@ function changePickerSpace() {
// add fogging
function fog(id, path) {
- if (defs.select("#fog #"+id).size()) return;
+ if (defs.select("#fog #" + id).size()) return;
const fadeIn = d3.transition().duration(2000).ease(d3.easeSinInOut);
if (defs.select("#fog path").size()) {
defs.select("#fog").append("path").attr("d", path).attr("id", id).attr("opacity", 0).transition(fadeIn).attr("opacity", 1);
@@ -564,7 +657,7 @@ function fog(id, path) {
// remove fogging
function unfog(id) {
- let el = defs.select("#fog #"+id);
+ let el = defs.select("#fog #" + id);
if (!id || !el.size()) el = defs.select("#fog").selectAll("path");
el.remove();
@@ -572,7 +665,7 @@ function unfog(id) {
}
function getFileName(dataType) {
- const formatTime = time => time < 10 ? "0" + time : time;
+ const formatTime = time => (time < 10 ? "0" + time : time);
const name = mapName.value;
const type = dataType ? dataType + " " : "";
const date = new Date();
@@ -581,7 +674,7 @@ function getFileName(dataType) {
const day = formatTime(date.getDate());
const hour = formatTime(date.getHours());
const minutes = formatTime(date.getMinutes());
- const dateString = [year, month, day, hour, minutes].join('-');
+ const dateString = [year, month, day, hour, minutes].join("-");
return name + " " + type + dateString;
}
@@ -609,12 +702,9 @@ function highlightElement(element) {
const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
const exit = d3.transition().duration(500).ease(d3.easeLinear);
- const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
- .attr("width", box.width).attr("height", box.height).attr("transform", transform);
+ const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y).attr("width", box.width).attr("height", box.height).attr("transform", transform);
- highlight.classed("highlighted", 1)
- .transition(enter).style("outline-offset", "0px")
- .transition(exit).style("outline-color", "transparent").delay(1000).remove();
+ highlight.classed("highlighted", 1).transition(enter).style("outline-offset", "0px").transition(exit).style("outline-color", "transparent").delay(1000).remove();
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
@@ -633,45 +723,239 @@ function selectIcon(initial, callback) {
input.value = initial;
if (!table.innerHTML) {
- const icons = ["⚔️","🏹","🐴","💣","🌊","🎯","⚓","🔮","📯","⚒️","🛡️","👑","⚜️",
- "☠️","🎆","🗡️","🔪","⛏️","🔥","🩸","💧","🐾","🎪","🏰","🏯","⛓️","❤️","💘","💜","📜","🔔",
- "🔱","💎","🌈","🌠","✨","💥","☀️","🌙","⚡","❄️","♨️","🎲","🚨","🌉","🗻","🌋","🧱",
- "⚖️","✂️","🎵","👗","🎻","🎨","🎭","⛲","💉","📖","📕","🎁","💍","⏳","🕸️","⚗️","☣️","☢️",
- "🔰","🎖️","🚩","🏳️","🏴","💪","✊","👊","🤜","🤝","🙏","🧙","🧙♀️","💂","🤴","🧛","🧟","🧞","🧝","👼",
- "👻","👺","👹","🦄","🐲","🐉","🐎","🦓","🐺","🦊","🐱","🐈","🦁","🐯","🐅","🐆","🐕","🦌","🐵","🐒","🦍",
- "🦅","🕊️","🐓","🦇","🦜","🐦","🦉","🐮","🐄","🐂","🐃","🐷","🐖","🐗","🐏","🐑","🐐","🐫","🦒","🐘","🦏","🐭","🐁","🐀",
- "🐹","🐰","🐇","🦔","🐸","🐊","🐢","🦎","🐍","🐳","🐬","🦈","🐠","🐙","🦑","🐌","🦋","🐜","🐝","🐞","🦗","🕷️","🦂","🦀",
- "🌳","🌲","🎄","🌴","🍂","🍁","🌵","☘️","🍀","🌿","🌱","🌾","🍄","🌽","🌸","🌹","🌻",
- "🍒","🍏","🍇","🍉","🍅","🍓","🥔","🥕","🥩","🍗","🍞","🍻","🍺","🍲","🍷"
+ const icons = [
+ "⚔️",
+ "🏹",
+ "🐴",
+ "💣",
+ "🌊",
+ "🎯",
+ "⚓",
+ "🔮",
+ "📯",
+ "⚒️",
+ "🛡️",
+ "👑",
+ "⚜️",
+ "☠️",
+ "🎆",
+ "🗡️",
+ "🔪",
+ "⛏️",
+ "🔥",
+ "🩸",
+ "💧",
+ "🐾",
+ "🎪",
+ "🏰",
+ "🏯",
+ "⛓️",
+ "❤️",
+ "💘",
+ "💜",
+ "📜",
+ "🔔",
+ "🔱",
+ "💎",
+ "🌈",
+ "🌠",
+ "✨",
+ "💥",
+ "☀️",
+ "🌙",
+ "⚡",
+ "❄️",
+ "♨️",
+ "🎲",
+ "🚨",
+ "🌉",
+ "🗻",
+ "🌋",
+ "🧱",
+ "⚖️",
+ "✂️",
+ "🎵",
+ "👗",
+ "🎻",
+ "🎨",
+ "🎭",
+ "⛲",
+ "💉",
+ "📖",
+ "📕",
+ "🎁",
+ "💍",
+ "⏳",
+ "🕸️",
+ "⚗️",
+ "☣️",
+ "☢️",
+ "🔰",
+ "🎖️",
+ "🚩",
+ "🏳️",
+ "🏴",
+ "💪",
+ "✊",
+ "👊",
+ "🤜",
+ "🤝",
+ "🙏",
+ "🧙",
+ "🧙♀️",
+ "💂",
+ "🤴",
+ "🧛",
+ "🧟",
+ "🧞",
+ "🧝",
+ "👼",
+ "👻",
+ "👺",
+ "👹",
+ "🦄",
+ "🐲",
+ "🐉",
+ "🐎",
+ "🦓",
+ "🐺",
+ "🦊",
+ "🐱",
+ "🐈",
+ "🦁",
+ "🐯",
+ "🐅",
+ "🐆",
+ "🐕",
+ "🦌",
+ "🐵",
+ "🐒",
+ "🦍",
+ "🦅",
+ "🕊️",
+ "🐓",
+ "🦇",
+ "🦜",
+ "🐦",
+ "🦉",
+ "🐮",
+ "🐄",
+ "🐂",
+ "🐃",
+ "🐷",
+ "🐖",
+ "🐗",
+ "🐏",
+ "🐑",
+ "🐐",
+ "🐫",
+ "🦒",
+ "🐘",
+ "🦏",
+ "🐭",
+ "🐁",
+ "🐀",
+ "🐹",
+ "🐰",
+ "🐇",
+ "🦔",
+ "🐸",
+ "🐊",
+ "🐢",
+ "🦎",
+ "🐍",
+ "🐳",
+ "🐬",
+ "🦈",
+ "🐠",
+ "🐙",
+ "🦑",
+ "🐌",
+ "🦋",
+ "🐜",
+ "🐝",
+ "🐞",
+ "🦗",
+ "🕷️",
+ "🦂",
+ "🦀",
+ "🌳",
+ "🌲",
+ "🎄",
+ "🌴",
+ "🍂",
+ "🍁",
+ "🌵",
+ "☘️",
+ "🍀",
+ "🌿",
+ "🌱",
+ "🌾",
+ "🍄",
+ "🌽",
+ "🌸",
+ "🌹",
+ "🌻",
+ "🍒",
+ "🍏",
+ "🍇",
+ "🍉",
+ "🍅",
+ "🍓",
+ "🥔",
+ "🥕",
+ "🥩",
+ "🍗",
+ "🍞",
+ "🍻",
+ "🍺",
+ "🍲",
+ "🍷"
];
let row = "";
- for (let i=0; i < icons.length; i++) {
- if (i%17 === 0) row = table.insertRow(i/17|0);
- const cell = row.insertCell(i%17);
+ for (let i = 0; i < icons.length; i++) {
+ if (i % 17 === 0) row = table.insertRow((i / 17) | 0);
+ const cell = row.insertCell(i % 17);
cell.innerHTML = icons[i];
}
}
- table.onclick = e => {if (e.target.tagName === "TD") {input.value = e.target.innerHTML; callback(input.value)}};
- table.onmouseover = e => {if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`)};
+ table.onclick = e => {
+ if (e.target.tagName === "TD") {
+ input.value = e.target.innerHTML;
+ callback(input.value);
+ }
+ };
+ table.onmouseover = e => {
+ if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`);
+ };
- $("#iconSelector").dialog({width: fitContent(), title: "Select Icon",
- buttons: {
- Apply: function() {callback(input.value||"⠀"); $(this).dialog("close")},
- Close: function() {callback(initial); $(this).dialog("close")}}
+ $("#iconSelector").dialog({
+ width: fitContent(),
+ title: "Select Icon",
+ buttons: {
+ Apply: function () {
+ callback(input.value || "⠀");
+ $(this).dialog("close");
+ },
+ Close: function () {
+ callback(initial);
+ $(this).dialog("close");
+ }
+ }
});
}
// Calls the refresh functionality on all editors currently open.
function refreshAllEditors() {
- TIME && console.time('refreshAllEditors');
- if (document.getElementById('culturesEditorRefresh').offsetParent) culturesEditorRefresh.click();
- if (document.getElementById('biomesEditorRefresh').offsetParent) biomesEditorRefresh.click();
- if (document.getElementById('diplomacyEditorRefresh').offsetParent) diplomacyEditorRefresh.click();
- if (document.getElementById('provincesEditorRefresh').offsetParent) provincesEditorRefresh.click();
- if (document.getElementById('religionsEditorRefresh').offsetParent) religionsEditorRefresh.click();
- if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
- if (document.getElementById('zonesEditorRefresh').offsetParent) zonesEditorRefresh.click();
- TIME && console.timeEnd('refreshAllEditors');
+ TIME && console.time("refreshAllEditors");
+ if (document.getElementById("culturesEditorRefresh").offsetParent) culturesEditorRefresh.click();
+ if (document.getElementById("biomesEditorRefresh").offsetParent) biomesEditorRefresh.click();
+ if (document.getElementById("diplomacyEditorRefresh").offsetParent) diplomacyEditorRefresh.click();
+ if (document.getElementById("provincesEditorRefresh").offsetParent) provincesEditorRefresh.click();
+ if (document.getElementById("religionsEditorRefresh").offsetParent) religionsEditorRefresh.click();
+ if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
+ if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
+ TIME && console.timeEnd("refreshAllEditors");
}
diff --git a/modules/ui/general.js b/modules/ui/general.js
index 5da081b5..b99ae898 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -96,10 +96,7 @@ function showMapTooltip(point, e, i, g) {
const land = pack.cells.h[i] >= 20;
// specific elements
- if (group === "armies") {
- tip(e.target.parentNode.dataset.name + ". Click to edit");
- return;
- }
+ if (group === "armies") return tip(e.target.parentNode.dataset.name + ". Click to edit");
if (group === "emblems" && e.target.tagName === "use") {
const parent = e.target.parentNode;
@@ -123,14 +120,11 @@ function showMapTooltip(point, e, i, g) {
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
- if (group === "routes") {
- tip("Click to edit the Route");
- return;
- }
- if (group === "terrain") {
- tip("Click to edit the Relief Icon");
- return;
- }
+
+ if (group === "routes") return tip("Click to edit the Route");
+
+ if (group === "terrain") return tip("Click to edit the Relief Icon");
+
if (subgroup === "burgLabels" || subgroup === "burgIcons") {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
@@ -139,50 +133,25 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
- if (group === "labels") {
- tip("Click to edit the Label");
- return;
- }
- if (group === "markers") {
- tip("Click to edit the Marker");
- return;
- }
+ if (group === "labels") return tip("Click to edit the Label");
+
+ if (group === "markers") return tip("Click to edit the Marker");
+
if (group === "ruler") {
const tag = e.target.tagName;
const className = e.target.getAttribute("class");
- if (tag === "circle" && className === "edge") {
- tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point");
- return;
- }
- if (tag === "circle" && className === "control") {
- tip("Drag to adjust. Hold Shifta and drag to keep axial direction. Click to remove the point");
- return;
- }
- if (tag === "circle") {
- tip("Drag to adjust the measurer");
- return;
- }
- if (tag === "polyline") {
- tip("Click on drag to add a control point");
- return;
- }
- if (tag === "path") {
- tip("Drag to move the measurer");
- return;
- }
- if (tag === "text") {
- tip("Drag to move, click to remove the measurer");
- return;
- }
- }
- if (subgroup === "burgIcons") {
- tip("Click to edit the Burg");
- return;
- }
- if (subgroup === "burgLabels") {
- tip("Click to edit the Burg");
- return;
+ if (tag === "circle" && className === "edge") return tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point");
+ if (tag === "circle" && className === "control") return tip("Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point");
+ if (tag === "circle") return tip("Drag to adjust the measurer");
+ if (tag === "polyline") return tip("Click on drag to add a control point");
+ if (tag === "path") return tip("Drag to move the measurer");
+ if (tag === "text") return tip("Drag to move, click to remove the measurer");
}
+
+ if (subgroup === "burgIcons") return tip("Click to edit the Burg");
+
+ if (subgroup === "burgLabels") return tip("Click to edit the Burg");
+
if (group === "lakes" && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
@@ -190,20 +159,16 @@ function showMapTooltip(point, e, i, g) {
tip(`${fullName} lake. Click to edit`);
return;
}
- if (group === "coastline") {
- tip("Click to edit the coastline");
- return;
- }
+ if (group === "coastline") return tip("Click to edit the coastline");
+
if (group === "zones") {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
- if (group === "ice") {
- tip("Click to edit the Ice");
- return;
- }
+
+ if (group === "ice") return tip("Click to edit the Ice");
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: " + getFriendlyPrecipitation(i));
diff --git a/modules/ui/heightmap-editor.js b/modules/ui/heightmap-editor.js
index 4cac39f3..f19e8cf0 100644
--- a/modules/ui/heightmap-editor.js
+++ b/modules/ui/heightmap-editor.js
@@ -197,6 +197,7 @@ function editHeightmap() {
}
}
+ drawRivers();
Lakes.defineGroup();
defineBiomes();
rankCells();
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index b2bad889..7b9d23f4 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -875,7 +875,6 @@ function toggleStates(event) {
}
}
-// draw states
function drawStates() {
TIME && console.time("drawStates");
regions.selectAll("path").remove();
@@ -1015,6 +1014,21 @@ function drawStates() {
TIME && console.timeEnd("drawStates");
}
+function toggleBorders(event) {
+ if (!layerIsOn("toggleBorders")) {
+ turnButtonOn("toggleBorders");
+ drawBorders();
+ if (event && isCtrlClick(event)) editStyle("borders");
+ } else {
+ if (event && isCtrlClick(event)) {
+ editStyle("borders");
+ return;
+ }
+ turnButtonOff("toggleBorders");
+ borders.selectAll("path").remove();
+ }
+}
+
// draw state and province borders
function drawBorders() {
TIME && console.time("drawBorders");
@@ -1118,21 +1132,6 @@ function drawBorders() {
TIME && console.timeEnd("drawBorders");
}
-function toggleBorders(event) {
- if (!layerIsOn("toggleBorders")) {
- turnButtonOn("toggleBorders");
- $("#borders").fadeIn();
- if (event && isCtrlClick(event)) editStyle("borders");
- } else {
- if (event && isCtrlClick(event)) {
- editStyle("borders");
- return;
- }
- turnButtonOff("toggleBorders");
- $("#borders").fadeOut();
- }
-}
-
function toggleProvinces(event) {
if (!layerIsOn("toggleProvinces")) {
turnButtonOn("toggleProvinces");
@@ -1444,18 +1443,30 @@ function toggleTexture(event) {
function toggleRivers(event) {
if (!layerIsOn("toggleRivers")) {
turnButtonOn("toggleRivers");
- $("#rivers").fadeIn();
+ drawRivers();
if (event && isCtrlClick(event)) editStyle("rivers");
} else {
- if (event && isCtrlClick(event)) {
- editStyle("rivers");
- return;
- }
- $("#rivers").fadeOut();
+ if (event && isCtrlClick(event)) return editStyle("rivers");
+ rivers.selectAll("*").remove();
turnButtonOff("toggleRivers");
}
}
+function drawRivers() {
+ TIME && console.time("drawRivers");
+ const {addMeandering, getRiverPath} = Rivers;
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const riverPaths = pack.rivers.map(river => {
+ const meanderedPoints = addMeandering(river.cells, river.points);
+ const widthFactor = river.widthFactor || 1;
+ const startingWidth = river.sourceWidth || 0;
+ const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
+ return ``;
+ });
+ rivers.html(riverPaths.join(""));
+ TIME && console.timeEnd("drawRivers");
+}
+
function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes");
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 36160e72..e6f1d629 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -98,7 +98,7 @@ function showSupporters() {
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
- Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray`;
+ Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 金,Chris Gray,Phoenix Boatwright`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, "")
@@ -777,6 +777,12 @@ document
.forEach(el => el.addEventListener("input", updateTilesOptions));
function updateTilesOptions() {
+ if (this?.tagName === "INPUT") {
+ const {nextElementSibling: next, previousElementSibling: prev} = this;
+ if (next?.tagName === "INPUT") next.value = this.value;
+ if (prev?.tagName === "INPUT") prev.value = this.value;
+ }
+
const tileSize = document.getElementById("tileSize");
const tilesX = +document.getElementById("tileColsOutput").value;
const tilesY = +document.getElementById("tileRowsOutput").value;
diff --git a/modules/ui/rivers-creator.js b/modules/ui/rivers-creator.js
new file mode 100644
index 00000000..22f88b70
--- /dev/null
+++ b/modules/ui/rivers-creator.js
@@ -0,0 +1,125 @@
+"use strict";
+function createRiver() {
+ if (customization) return;
+ closeDialogs();
+ if (!layerIsOn("toggleRivers")) toggleRivers();
+
+ document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
+ if (!layerIsOn("toggleCells")) toggleCells();
+
+ tip("Click to add river point, click again to remove", true);
+ debug.append("g").attr("id", "controlCells");
+ viewbox.style("cursor", "crosshair").on("click", onCellClick);
+
+ createRiver.cells = [];
+ const body = document.getElementById("riverCreatorBody");
+
+ $("#riverCreator").dialog({
+ title: "Create River",
+ resizable: false,
+ position: {my: "left top", at: "left+10 top+10", of: "#map"},
+ close: closeRiverCreator
+ });
+
+ if (modules.createRiver) return;
+ modules.createRiver = true;
+
+ // add listeners
+ document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
+ document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
+ body.addEventListener("click", function (ev) {
+ const el = ev.target;
+ const cl = el.classList;
+ const cell = +el.parentNode.dataset.cell;
+ if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
+ else if (cl.contains("icon-trash-empty")) removeCell(cell);
+ });
+
+ function onCellClick() {
+ const cell = findCell(...d3.mouse(this));
+
+ if (createRiver.cells.includes(cell)) removeCell(cell);
+ else addCell(cell);
+ }
+
+ function addCell(cell) {
+ createRiver.cells.push(cell);
+ drawCells(createRiver.cells);
+
+ const flux = pack.cells.fl[cell];
+ const line = `
+ Cell ${cell}
+ Flux
+
+
+
`;
+ body.innerHTML += line;
+ }
+
+ function removeCell(cell) {
+ createRiver.cells = createRiver.cells.filter(c => c !== cell);
+ drawCells(createRiver.cells);
+ body.querySelector(`div[data-cell='${cell}']`)?.remove();
+ }
+
+ function drawCells(cells) {
+ debug
+ .select("#controlCells")
+ .selectAll(`polygon`)
+ .data(cells)
+ .join("polygon")
+ .attr("points", d => getPackPolygon(d))
+ .attr("class", "current");
+ }
+
+ function addRiver() {
+ const {rivers, cells} = pack;
+ const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
+
+ const riverCells = createRiver.cells;
+ if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
+
+ const riverId = last(rivers).i + 1;
+ const parent = cells.r[last(riverCells)] || riverId;
+
+ riverCells.forEach(cell => {
+ if (!cells.r[cell]) cells.r[cell] = riverId;
+ });
+
+ const source = riverCells[0];
+ const mouth = parent === riverId ? last(riverCells) : riverCells[riverCells.length - 2];
+ const sourceWidth = 0.05;
+ const widthFactor = 1.2;
+
+ const meanderedPoints = addMeandering(riverCells);
+
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(meanderedPoints);
+ const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
+ const name = getName(mouth);
+ const basin = getBasin(parent);
+
+ rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
+
+ // render river
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ viewbox
+ .select("#rivers")
+ .append("path")
+ .attr("id", "river" + riverId)
+ .attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
+
+ editRiver(riverId);
+ }
+
+ function closeRiverCreator() {
+ body.innerHTML = "";
+ debug.select("#controlCells").remove();
+ restoreDefaultEvents();
+ clearMainTip();
+
+ const forced = +document.getElementById("toggleCells").dataset.forced;
+ document.getElementById("toggleCells").dataset.forced = 0;
+ if (forced && layerIsOn("toggleCells")) toggleCells();
+ }
+}
diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js
index bc8181ee..f0ae2f2a 100644
--- a/modules/ui/rivers-editor.js
+++ b/modules/ui/rivers-editor.js
@@ -1,20 +1,31 @@
"use strict";
function editRiver(id) {
if (customization) return;
- if (elSelected && d3.event && d3.event.target.id === elSelected.attr("id")) return;
+ if (elSelected && id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
- const node = id ? document.getElementById(id) : d3.event.target;
- elSelected = d3.select(node).on("click", addInterimControlPoint);
- viewbox.on("touchmove mousemove", showEditorTips);
- debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
+ document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
+ if (!layerIsOn("toggleCells")) toggleCells();
+
+ elSelected = d3.select("#" + id);
+
+ tip("Drag control points to change the river course. For major changes please create a new river instead", true);
+ debug.append("g").attr("id", "controlCells");
+ debug.append("g").attr("id", "controlPoints");
+
updateRiverData();
- drawControlPoints(node);
+
+ const river = getRiver();
+ const {cells, points} = river;
+ const riverPoints = Rivers.getRiverPoints(cells, points);
+ drawControlPoints(riverPoints, cells);
+ drawCells(cells, "current");
$("#riverEditor").dialog({
- title: "Edit River", resizable: false,
- position: {my: "center top+80", at: "top", of: node, collision: "fit"},
+ title: "Edit River",
+ resizable: false,
+ position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRiverEditor
});
@@ -22,27 +33,19 @@ function editRiver(id) {
modules.editRiver = true;
// add listeners
+ document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver);
+ document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
+ document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
+ document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
+ document.getElementById("riverRemove").addEventListener("click", removeRiver);
document.getElementById("riverName").addEventListener("input", changeName);
document.getElementById("riverType").addEventListener("input", changeType);
document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("riverMainstem").addEventListener("change", changeParent);
-
document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth);
document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor);
- document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
- document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
- document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
- document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
- document.getElementById("riverRemove").addEventListener("click", removeRiver);
-
- 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");
- }
-
function getRiver() {
const riverId = +elSelected.attr("id").slice(5);
const river = pack.rivers.find(r => r.i === riverId);
@@ -58,7 +61,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);
@@ -66,85 +69,112 @@ function editRiver(id) {
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
document.getElementById("riverDischarge").value = r.discharge + " m³/s";
- r.length = elSelected.node().getTotalLength() / 2;
- const length = rn(r.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
- document.getElementById("riverLength").value = length;
- const width = rn(r.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value;
- document.getElementById("riverWidth").value = width;
-
document.getElementById("riverSourceWidth").value = r.sourceWidth;
document.getElementById("riverWidthFactor").value = r.widthFactor;
+
+ updateRiverLength(r);
+ updateRiverWidth(r);
}
- function drawControlPoints(node) {
- const length = getRiver().length;
- 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 p1 = node.getPointAtLength(i / 1e5);
- const p2 = node.getPointAtLength(c / 1e5);
- addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
- }
+ function updateRiverLength(river) {
+ river.length = rn(elSelected.node().getTotalLength() / 2, 2);
+ const length = `${river.length * distanceScaleInput.value} ${distanceUnitInput.value}`;
+ document.getElementById("riverLength").value = length;
}
- 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);
+ function updateRiverWidth(river) {
+ const {addMeandering, getWidth, getOffset} = Rivers;
+ const {cells, discharge, widthFactor, sourceWidth} = river;
+ const meanderedPoints = addMeandering(cells);
+ river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
+
+ const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
+ document.getElementById("riverWidth").value = width;
+ }
+
+ function drawControlPoints(points, cells) {
+ debug
+ .select("#controlPoints")
+ .selectAll("circle")
+ .data(points)
+ .enter()
+ .append("circle")
+ .attr("cx", d => d[0])
+ .attr("cy", d => d[1])
+ .attr("r", 0.6)
+ .attr("data-cell", (d, i) => cells[i])
+ .attr("data-i", (d, i) => i)
+ .call(d3.drag().on("start", dragControlPoint));
+ }
+
+ function drawCells(cells, type) {
+ debug
+ .select("#controlCells")
+ .selectAll(`polygon.${type}`)
+ .data(cells)
+ .join("polygon")
+ .attr("points", d => getPackPolygon(d))
+ .attr("class", type);
}
function dragControlPoint() {
- this.setAttribute("cx", d3.event.x);
- this.setAttribute("cy", d3.event.y);
- redrawRiver();
+ const {i, r, fl} = pack.cells;
+ const river = getRiver();
+
+ const initCell = +this.dataset.cell;
+ const index = +this.dataset.i;
+
+ const occupiedCells = i.filter(i => r[i] && !river.cells.includes(i));
+ drawCells(occupiedCells, "occupied");
+ let movedToCell = null;
+
+ d3.event.on("drag", function () {
+ const {x, y} = d3.event;
+ const currentCell = findCell(x, y);
+
+ if (initCell !== currentCell) {
+ if (occupiedCells.includes(currentCell)) return;
+ movedToCell = currentCell;
+ } else movedToCell = null;
+
+ this.setAttribute("cx", x);
+ this.setAttribute("cy", y);
+ this.__data__ = [rn(x, 1), rn(y, 1)];
+ redrawRiver();
+ });
+
+ d3.event.on("end", () => {
+ if (movedToCell) {
+ this.dataset.cell = movedToCell;
+ river.cells[index] = movedToCell;
+ drawCells(river.cells, "current");
+
+ // swap river data
+ r[initCell] = 0;
+ r[movedToCell] = river.i;
+ const sourceFlux = fl[initCell];
+ fl[initCell] = fl[movedToCell];
+ fl[movedToCell] = sourceFlux;
+ }
+
+ debug.select("#controlCells").selectAll("polygon.available, polygon.occupied").remove();
+ });
}
function redrawRiver() {
- const points = [];
- debug.select("#controlPoints").selectAll("circle").each(function() {
- points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]);
- });
+ const river = getRiver();
+ river.points = debug.selectAll("#controlPoints > *").data();
+ const {cells, widthFactor, sourceWidth} = river;
+ const meanderedPoints = Rivers.addMeandering(cells, river.points);
- if (points.length < 2) return;
- if (points.length === 2) {
- 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`);
- 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 = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
elSelected.attr("d", path);
- const r = getRiver();
- if (r) {
- r.width = rn(offset ** 2, 2);
- r.length = length;
- updateRiverData();
- }
-
+ updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
}
- function clickControlPoint() {
- this.remove();
- redrawRiver();
- }
-
- function addInterimControlPoint() {
- const point = d3.mouse(this);
- 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) + ")");
-
- redrawRiver();
- }
-
function changeName() {
getRiver().name = this.value;
}
@@ -160,7 +190,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() {
@@ -171,12 +201,16 @@ function editRiver(id) {
}
function changeSourceWidth() {
- getRiver().sourceWidth = +this.value;
+ const river = getRiver();
+ river.sourceWidth = +this.value;
+ updateRiverWidth(river);
redrawRiver();
}
function changeWidthFactor() {
- getRiver().widthFactor = +this.value;
+ const river = getRiver();
+ river.widthFactor = +this.value;
+ updateRiverWidth(river);
redrawRiver();
}
@@ -191,83 +225,35 @@ function editRiver(id) {
editNotes(id, river.name + " " + river.type);
}
- function toggleRiverCreationMode() {
- if (document.getElementById("riverNew").classList.contains("pressed")) exitRiverCreationMode();
- else {
- document.getElementById("riverNew").classList.add("pressed");
- tip("Click on map to add control points", true, "warn");
- viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
- elSelected.on("click", null);
- }
- }
-
- function addPointOnClick() {
- if (!elSelected.attr("data-new")) {
- debug.select("#controlPoints").selectAll("circle").remove();
- const id = getNextId("river");
- elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id).attr("data-new", 1);
- }
-
- // add control point
- const point = d3.mouse(this);
- addControlPoint([point[0], point[1]]);
- redrawRiver();
- }
-
- function exitRiverCreationMode() {
- riverNew.classList.remove("pressed");
- clearMainTip();
- viewbox.on("click", clicked).style("cursor", "default");
- elSelected.on("click", addInterimControlPoint);
-
- if (!elSelected.attr("data-new")) return; // no need to create a new river
- elSelected.attr("data-new", null);
-
- // add a river
- const r = +elSelected.attr("id").slice(5);
- const node = elSelected.node(), length = node.getTotalLength() / 2;
-
- const cells = [];
- const segments = Math.ceil(length / 4), 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);
- if (!pack.cells.r[cell]) pack.cells.r[cell] = r;
- cells.push(cell);
- }
-
- const source = cells[0], 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 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});
- }
-
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",
+ alertMessage.innerHTML = "Are you sure you want to remove the river and all its tributaries";
+ $("#alert").dialog({
+ resizable: false,
+ width: "22em",
+ title: "Remove river and tributaries",
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
+ elSelected.remove();
$("#riverEditor").dialog("close");
},
- Cancel: function() {$(this).dialog("close");}
+ Cancel: function () {
+ $(this).dialog("close");
+ }
}
});
}
function closeRiverEditor() {
- exitRiverCreationMode();
- elSelected.on("click", null);
debug.select("#controlPoints").remove();
+ debug.select("#controlCells").remove();
unselect();
+ clearMainTip();
+
+ const forced = +document.getElementById("toggleCells").dataset.forced;
+ document.getElementById("toggleCells").dataset.forced = 0;
+ if (forced && layerIsOn("toggleCells")) toggleCells();
}
}
diff --git a/modules/ui/rivers-overview.js b/modules/ui/rivers-overview.js
index 63dd3b81..fcde2ae5 100644
--- a/modules/ui/rivers-overview.js
+++ b/modules/ui/rivers-overview.js
@@ -21,6 +21,7 @@ function overviewRivers() {
// add listeners
document.getElementById("riversOverviewRefresh").addEventListener("click", riversOverviewAddLines);
document.getElementById("addNewRiver").addEventListener("click", toggleAddRiver);
+ document.getElementById("riverCreateNew").addEventListener("click", createRiver);
document.getElementById("riversBasinHighlight").addEventListener("click", toggleBasinsHightlight);
document.getElementById("riversExport").addEventListener("click", downloadRiversData);
document.getElementById("riversRemoveAll").addEventListener("click", triggerAllRiversRemove);
@@ -129,7 +130,8 @@ function overviewRivers() {
}
function openRiverEditor() {
- editRiver("river" + this.parentNode.dataset.id);
+ const id = "river" + this.parentNode.dataset.id;
+ editRiver(id);
}
function triggerRiverRemove() {
diff --git a/modules/ui/tools.js b/modules/ui/tools.js
index f744eac7..5243cd95 100644
--- a/modules/ui/tools.js
+++ b/modules/ui/tools.js
@@ -531,22 +531,22 @@ function toggleAddRiver() {
function addRiverOnClick() {
const {cells, rivers} = pack;
- const point = d3.mouse(this);
- let i = findCell(point[0], point[1]);
+ let i = findCell(...d3.mouse(this));
- if (cells.r[i]) return tip("There already a river here", false, "error");
+ if (cells.r[i]) return tip("There is already a river here", false, "error");
if (cells.h[i] < 20) return tip("Cannot create river in water cell", false, "error");
if (cells.b[i]) return;
+ const {alterHeights, resolveDepressions, addMeandering, getRiverPath, getBasin, getName, getType, getWidth, getOffset, getApproximateLength} = Rivers;
const riverCells = [];
- let riverId = +getNextId("river").slice(5);
- let parent = 0;
+ let riverId = last(rivers).i + 1;
+ let parent = riverId;
const initialFlux = grid.cells.prec[cells.g[i]];
cells.fl[i] = initialFlux;
- const h = Rivers.alterHeights();
- Rivers.resolveDepressions(h);
+ const h = alterHeights();
+ resolveDepressions(h);
while (i) {
cells.r[i] = riverId;
@@ -555,15 +555,13 @@ function addRiverOnClick() {
const min = cells.c[i].sort((a, b) => h[a] - h[b])[0]; // downhill cell
if (h[i] <= h[min]) return tip(`Cell ${i} is depressed, river cannot flow further`, false, "error");
- const [tx, ty] = cells.p[min];
-
// pour to water body
if (h[min] < 20) {
riverCells.push(min);
const feature = pack.features[cells.f[min]];
if (feature.type === "lake") {
- parent = feature.outlet || 0;
+ if (feature.outlet) parent = feature.outlet;
feature.inlets ? feature.inlets.push(riverId) : (feature.inlets = [riverId]);
}
break;
@@ -615,22 +613,15 @@ function addRiverOnClick() {
}
const river = rivers.find(r => r.i === riverId);
- const sourceWidth = 0.1;
- const widthFactor = river?.widthFactor || rn(0.8 + Math.random() * 0.4, 1);
- const riverMeandered = Rivers.addMeandering(riverCells, sourceWidth * 10, 0.5);
- const [path, length, offset] = Rivers.getPath(riverMeandered, widthFactor, sourceWidth);
- viewbox
- .select("#rivers")
- .append("path")
- .attr("d", path)
- .attr("id", "river" + riverId);
-
- // add new river to data or change extended river attributes
const source = riverCells[0];
- const mouth = last(riverCells);
- const discharge = cells.fl[mouth]; // in m3/s
- const width = rn(offset ** 2, 2); // mounth width in km
+ const mouth = riverCells[riverCells.length - 2];
+ const widthFactor = river?.widthFactor || (!parent || parent === riverId ? 1.2 : 1);
+ const meanderedPoints = addMeandering(riverCells);
+
+ const discharge = cells.fl[mouth]; // m3 in second
+ const length = getApproximateLength(meanderedPoints);
+ const width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor));
if (river) {
river.source = source;
@@ -639,14 +630,20 @@ function addRiverOnClick() {
river.width = width;
river.cells = riverCells;
} else {
- const basin = Rivers.getBasin(parent);
- const name = Rivers.getName(mouth);
- const smallLength = 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 basin = getBasin(parent);
+ const name = getName(mouth);
+ const type = getType({i: riverId, length, parent});
- rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type});
+ rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth: 0, parent, cells: riverCells, basin, name, type});
}
+ // render river
+ lineGen.curve(d3.curveCatmullRom.alpha(0.1));
+ const path = getRiverPath(meanderedPoints, widthFactor);
+ const id = "river" + riverId;
+ const riversG = viewbox.select("#rivers");
+ riversG.append("path").attr("id", id).attr("d", path);
+
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();
diff --git a/modules/utils.js b/modules/utils.js
index 9850ea5e..84389a3d 100644
--- a/modules/utils.js
+++ b/modules/utils.js
@@ -236,6 +236,10 @@ function P(probability) {
return Math.random() < probability;
}
+function each(n) {
+ return i => i % n === 0;
+}
+
// random number (normal or gaussian distribution)
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);