Fantasy-Map-Generator/vue/public/refactor-one.js

2243 lines
87 KiB
JavaScript

// Heighmap Template: Volcano
function templateVolcano(mod) {
addMountain();
modifyHeights("all", 10, 1);
addHill(5, 0.35);
addRange(3);
addRange(-4);
}
// Heighmap Template: High Island
function templateHighIsland(mod) {
addMountain();
modifyHeights("all", 10, 1);
addRange(6);
addHill(12, 0.25);
addRange(-3);
modifyHeights("land", 0, 0.75);
addPit(1);
addHill(3, 0.15);
}
// Heighmap Template: Low Island
function templateLowIsland(mod) {
addMountain();
modifyHeights("all", 10, 1);
smoothHeights(2);
addRange(2);
addHill(4, 0.4);
addHill(12, 0.2);
addRange(-8);
modifyHeights("land", 0, 0.35);
}
// Heighmap Template: Continents
function templateContinents(mod) {
addMountain();
modifyHeights("all", 10, 1);
addHill(30, 0.25);
const count = Math.ceil(Math.random() * 4 + 4);
addStrait(count);
addPit(10);
addRange(-10);
modifyHeights("land", 0, 0.6);
smoothHeights(2);
addRange(3);
}
// Heighmap Template: Archipelago
function templateArchipelago(mod) {
addMountain();
modifyHeights("all", 10, 1);
addHill(12, 0.15);
addRange(8);
const count = Math.ceil(Math.random() * 2 + 2);
addStrait(count);
addRange(-15);
addPit(10);
modifyHeights("land", -5, 0.7);
smoothHeights(3);
}
// Heighmap Template: Atoll
function templateAtoll(mod) {
addMountain();
modifyHeights("all", 10, 1);
addHill(2, 0.35);
addRange(2);
smoothHeights(1);
modifyHeights("27-100", 0, 0.1);
}
// Heighmap Template: Mainland
function templateMainland(mod) {
addMountain();
modifyHeights("all", 10, 1);
addHill(30, 0.2);
addRange(10);
addPit(20);
addHill(10, 0.15);
addRange(-10);
modifyHeights("land", 0, 0.4);
addRange(10);
smoothHeights(3);
}
// Heighmap Template: Peninsulas
function templatePeninsulas(mod) {
addMountain();
modifyHeights("all", 15, 1);
addHill(30, 0);
addRange(5);
addPit(15);
const count = Math.ceil(Math.random() * 5 + 15);
addStrait(count);
}
function addMountain() {
const x = Math.floor(Math.random() * graphWidth / 3 + graphWidth / 3);
const y = Math.floor(Math.random() * graphHeight * 0.2 + graphHeight * 0.4);
const cell = diagram.find(x, y).index;
const height = Math.random() * 10 + 90; // 90-99
add(cell, "mountain", height);
}
// place with shift 0-0.5
function addHill(count, shift) {
for (let c = 0; c < count; c++) {
let limit = 0, cell, height;
do {
height = Math.random() * 40 + 10; // 10-50
const x = Math.floor(Math.random() * graphWidth * (1 - shift * 2) + graphWidth * shift);
const y = Math.floor(Math.random() * graphHeight * (1 - shift * 2) + graphHeight * shift);
cell = diagram.find(x, y).index;
limit++;
} while (heights[cell] + height > 90 && limit < 100);
add(cell, "hill", height);
}
}
function add(start, type, height) {
const session = Math.ceil(Math.random() * 1e5);
let radius;
let hRadius;
let mRadius;
switch (+graphSize) {
case 1: hRadius = 0.991; mRadius = 0.91; break;
case 2: hRadius = 0.9967; mRadius = 0.951; break;
case 3: hRadius = 0.999; mRadius = 0.975; break;
case 4: hRadius = 0.9994; mRadius = 0.98; break;
}
radius = type === "mountain" ? mRadius : hRadius;
const queue = [start];
if (type === "mountain") heights[start] = height;
for (let i=0; i < queue.length && height >= 1; i++) {
if (type === "mountain") {height = heights[queue[i]] * radius - height / 100;}
else {height *= radius;}
cells[queue[i]].neighbors.forEach(function(e) {
if (cells[e].used === session) return;
const mod = Math.random() * 0.2 + 0.9; // 0.9-1.1 random factor
heights[e] += height * mod;
if (heights[e] > 100) heights[e] = 100;
cells[e].used = session;
queue.push(e);
});
}
}
function addRange(mod, height, from, to) {
const session = Math.ceil(Math.random() * 100000);
const count = Math.abs(mod);
let range = [];
for (let c = 0; c < count; c++) {
range = [];
let diff = 0, start = from, end = to;
if (!start || !end) {
do {
const xf = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15;
const yf = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2;
start = diagram.find(xf, yf).index;
const xt = Math.floor(Math.random() * (graphWidth * 0.7)) + graphWidth * 0.15;
const yt = Math.floor(Math.random() * (graphHeight * 0.6)) + graphHeight * 0.2;
end = diagram.find(xt, yt).index;
diff = Math.hypot(xt - xf, yt - yf);
} while (diff < 150 / graphSize || diff > 300 / graphSize)
}
if (start && end) {
for (let l = 0; start != end && l < 10000; l++) {
let min = 10000;
cells[start].neighbors.forEach(function(e) {
diff = Math.hypot(cells[end].data[0] - cells[e].data[0],cells[end].data[1] - cells[e].data[1]);
if (Math.random() > 0.8) diff = diff / 2;
if (diff < min) {min = diff, start = e;}
});
range.push(start);
}
}
const change = height ? height : Math.random() * 10 + 10;
range.map(function(r) {
let rnd = Math.random() * 0.4 + 0.8;
if (mod > 0) heights[r] += change * rnd;
else if (heights[r] >= 10) {heights[r] -= change * rnd;}
cells[r].neighbors.forEach(function(e) {
if (cells[e].used === session) return;
cells[e].used = session;
rnd = Math.random() * 0.4 + 0.8;
const ch = change / 2 * rnd;
if (mod > 0) {heights[e] += ch;} else if (heights[e] >= 10) {heights[e] -= ch;}
if (heights[e] > 100) heights[e] = mod > 0 ? 100 : 5;
});
if (heights[r] > 100) heights[r] = mod > 0 ? 100 : 5;
});
}
return range;
}
function addStrait(width) {
const session = Math.ceil(Math.random() * 100000);
const top = Math.floor(Math.random() * graphWidth * 0.35 + graphWidth * 0.3);
const bottom = Math.floor((graphWidth - top) - (graphWidth * 0.1) + (Math.random() * graphWidth * 0.2));
let start = diagram.find(top, graphHeight * 0.1).index;
const end = diagram.find(bottom, graphHeight * 0.9).index;
let range = [];
for (let l = 0; start !== end && l < 1000; l++) {
let min = 10000; // dummy value
cells[start].neighbors.forEach(function(e) {
let diff = Math.hypot(cells[end].data[0] - cells[e].data[0], cells[end].data[1] - cells[e].data[1]);
if (Math.random() > 0.8) {diff = diff / 2}
if (diff < min) {min = diff; start = e;}
});
range.push(start);
}
const query = [];
for (; width > 0; width--) {
range.map(function(r) {
cells[r].neighbors.forEach(function(e) {
if (cells[e].used === session) {return;}
cells[e].used = session;
query.push(e);
heights[e] *= 0.23;
if (heights[e] > 100 || heights[e] < 5) heights[e] = 5;
});
range = query.slice();
});
}
}
function addPit(count, height, cell) {
const session = Math.ceil(Math.random() * 1e5);
for (let c = 0; c < count; c++) {
let change = height ? height + 10 : Math.random() * 10 + 20;
let start = cell;
if (!start) {
const lowlands = $.grep(cells, function(e) {return (heights[e.index] >= 20);});
if (!lowlands.length) return;
const rnd = Math.floor(Math.random() * lowlands.length);
start = lowlands[rnd].index;
}
let query = [start],newQuery= [];
// depress pit center
heights[start] -= change;
if (heights[start] < 5 || heights[start] > 100) heights[start] = 5;
cells[start].used = session;
for (let i = 1; i < 10000; i++) {
const rnd = Math.random() * 0.4 + 0.8;
change -= i / 0.6 * rnd;
if (change < 1) break;
query.map(function(p) {
cells[p].neighbors.forEach(function(e) {
if (cells[e].used === session) return;
cells[e].used = session;
if (Math.random() > 0.8) return;
newQuery.push(e);
heights[e] -= change;
if (heights[e] < 5 || heights[e] > 100) heights[e] = 5;
});
});
query = newQuery.slice();
newQuery = [];
}
}
}
// Modify heights adding or multiplying by value
function modifyHeights(range, add, mult) {
function modify(v) {
if (add) v += add;
if (mult !== 1) {
if (mult === "^2") mult = (v - 20) / 100;
if (mult === "^3") mult = ((v - 20) * (v - 20)) / 100;
if (range === "land") {v = 20 + (v - 20) * mult;}
else {v *= mult;}
}
if (v < 0) v = 0;
if (v > 100) v = 100;
return v;
}
const limMin = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
const limMax = range === "land" || range === "all" ? 100 : +range.split("-")[1];
for (let i=0; i < heights.length; i++) {
if (heights[i] < limMin || heights[i] > limMax) continue;
heights[i] = modify(heights[i]);
}
}
// Smooth heights using mean of neighbors
function smoothHeights(fraction) {
const fr = fraction || 2;
for (let i=0; i < heights.length; i++) {
const nHeights = [heights[i]];
cells[i].neighbors.forEach(function(e) {nHeights.push(heights[e]);});
heights[i] = (heights[i] * (fr - 1) + d3.mean(nHeights)) / fr;
}
}
// Randomize heights a bit
function disruptHeights() {
for (let i=0; i < heights.length; i++) {
if (heights[i] < 18) continue;
if (Math.random() < 0.5) continue;
heights[i] += 2 - Math.random() * 4;
}
}
// Mark features (ocean, lakes, islands)
function markFeatures() {
console.time("markFeatures");
Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
for (let i=0, queue=[0]; queue.length > 0; i++) {
const cell = cells[queue[0]];
cell.fn = i; // feature number
const land = heights[queue[0]] >= 20;
let border = cell.type === "border";
if (border && land) cell.ctype = 2;
while (queue.length) {
const q = queue.pop();
if (cells[q].type === "border") {
border = true;
if (land) cells[q].ctype = 2;
}
cells[q].neighbors.forEach(function(e) {
const eLand = heights[e] >= 20;
if (land === eLand && cells[e].fn === undefined) {
cells[e].fn = i;
queue.push(e);
}
if (land && !eLand) {
cells[q].ctype = 2;
cells[e].ctype = -1;
cells[q].harbor = cells[q].harbor ? cells[q].harbor + 1 : 1;
}
});
}
features.push({i, land, border});
// find unmarked cell
for (let c=0; c < cells.length; c++) {
if (cells[c].fn === undefined) {
queue[0] = c;
break;
}
}
}
console.timeEnd("markFeatures");
}
// remove closed lakes near ocean
function reduceClosedLakes() {
console.time("reduceClosedLakes");
const fs = JSON.parse(JSON.stringify(features));
let lakesInit = lakesNow = features.reduce(function(s, f) {
return !f.land && !f.border ? s + 1 : s;
}, 0);
for (let c=0; c < cells.length && lakesNow > 0; c++) {
if (heights[c] < 20) continue; // not land
if (cells[c].ctype !== 2) continue; // not near water
let ocean = null, lake = null;
// find land cells with lake and ocean nearby
cells[c].neighbors.forEach(function(n) {
if (heights[n] >= 20) return;
const fn = cells[n].fn;
if (features[fn].border !== false) ocean = fn;
if (fs[fn].border === false) lake = fn;
});
// if found, make it water and turn lake to ocean
if (ocean !== null && lake !== null) {
//debug.append("circle").attr("cx", cells[c].data[0]).attr("cy", cells[c].data[1]).attr("r", 2).attr("fill", "red");
lakesNow --;
fs[lake].border = ocean;
heights[c] = 19;
cells[c].fn = ocean;
cells[c].ctype = -1;
cells[c].neighbors.forEach(function(e) {if (heights[e] >= 20) cells[e].ctype = 2;});
}
}
if (lakesInit === lakesNow) return; // nothing was changed
for (let i=0; i < cells.length; i++) {
if (heights[i] >= 20) continue; // not water
const fn = cells[i].fn;
if (fs[fn].border !== features[fn].border) {
cells[i].fn = fs[fn].border;
//debug.append("circle").attr("cx", cells[i].data[0]).attr("cy", cells[i].data[1]).attr("r", 1).attr("fill", "blue");
}
}
console.timeEnd("reduceClosedLakes");
}
function drawOcean() {
console.time("drawOcean");
let limits = [];
let odd = 0.8; // initial odd for ocean layer is 80%
// Define type of ocean cells based on cell distance form land
let frontier = $.grep(cells, function(e) {return e.ctype === -1;});
if (Math.random() < odd) {limits.push(-1); odd = 0.2;}
for (let c = -2; frontier.length > 0 && c > -10; c--) {
if (Math.random() < odd) {limits.unshift(c); odd = 0.2;} else {odd += 0.2;}
frontier.map(function(i) {
i.neighbors.forEach(function(e) {
if (!cells[e].ctype) cells[e].ctype = c;
});
});
frontier = $.grep(cells, function(e) {return e.ctype === c;});
}
if (outlineLayersInput.value === "none") return;
if (outlineLayersInput.value !== "random") limits = outlineLayersInput.value.split(",");
// Define area edges
const opacity = rn(0.4 / limits.length, 2);
for (let l=0; l < limits.length; l++) {
const edges = [];
const lim = +limits[l];
for (let i = 0; i < cells.length; i++) {
if (cells[i].ctype < lim || cells[i].ctype === undefined) continue;
if (cells[i].ctype > lim && cells[i].type !== "border") continue;
const cell = diagram.cells[i];
cell.halfedges.forEach(function(e) {
const edge = diagram.edges[e];
const start = edge[0].join(" ");
const end = edge[1].join(" ");
if (edge.left && edge.right) {
const ea = edge.left.index === i ? edge.right.index : edge.left.index;
if (cells[ea].ctype < lim) edges.push({start, end});
} else {
edges.push({start, end});
}
});
}
lineGen.curve(d3.curveBasis);
let relax = 0.8 - l / 10;
if (relax < 0.2) relax = 0.2;
const line = getContinuousLine(edges, 0, relax);
oceanLayers.append("path").attr("d", line).attr("fill", "#ecf2f9").style("opacity", opacity);
}
console.timeEnd("drawOcean");
}
// recalculate Voronoi Graph to pack cells
function reGraph() {
console.time("reGraph");
const tempCells = [], newPoints = []; // to store new data
// get average precipitation based on graph size
const avPrec = precInput.value / 5000;
const smallLakesMax = 500;
let smallLakes = 0;
const evaporation = 2;
cells.map(function(i, d) {
let height = i.height || heights[d];
if (height > 100) height = 100;
const pit = i.pit;
const ctype = i.ctype;
if (ctype !== -1 && ctype !== -2 && height < 20) return; // exclude all deep ocean points
const x = rn(i.data[0],1), y = rn(i.data[1],1);
const fn = i.fn;
const harbor = i.harbor;
let lake = i.lake;
// mark potential cells for small lakes to add additional point there
if (smallLakes < smallLakesMax && !lake && pit > evaporation && ctype !== 2) {
lake = 2;
smallLakes++;
}
const region = i.region; // handle value for edit heightmap mode only
const culture = i.culture; // handle value for edit heightmap mode only
let copy = $.grep(newPoints, function(e) {return (e[0] == x && e[1] == y);});
if (!copy.length) {
newPoints.push([x, y]);
tempCells.push({index:tempCells.length, data:[x, y],height, pit, ctype, fn, harbor, lake, region, culture});
}
// add additional points for cells along coast
if (ctype === 2 || ctype === -1) {
if (i.type === "border") return;
if (!features[fn].land && !features[fn].border) return;
i.neighbors.forEach(function(e) {
if (cells[e].ctype === ctype) {
let x1 = (x * 2 + cells[e].data[0]) / 3;
let y1 = (y * 2 + cells[e].data[1]) / 3;
x1 = rn(x1, 1), y1 = rn(y1, 1);
copy = $.grep(newPoints, function(e) {return e[0] === x1 && e[1] === y1;});
if (copy.length) return;
newPoints.push([x1, y1]);
tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, harbor, lake, region, culture});
}
});
}
if (lake === 2) { // add potential small lakes
polygons[i.index].forEach(function(e) {
if (Math.random() > 0.8) return;
let rnd = Math.random() * 0.6 + 0.8;
const x1 = rn((e[0] * rnd + i.data[0]) / (1 + rnd), 2);
rnd = Math.random() * 0.6 + 0.8;
const y1 = rn((e[1] * rnd + i.data[1]) / (1 + rnd), 2);
copy = $.grep(newPoints, function(c) {return x1 === c[0] && y1 === c[1];});
if (copy.length) return;
newPoints.push([x1, y1]);
tempCells.push({index:tempCells.length, data:[x1, y1],height, pit, ctype, fn, region, culture});
});
}
});
console.log( "small lakes candidates: " + smallLakes);
cells = tempCells; // use tempCells as the only cells array
calculateVoronoi(newPoints); // recalculate Voronoi diagram using new points
let gridPath = ""; // store grid as huge single path string
cells.map(function(i, d) {
if (i.height >= 20) {
// calc cell area
i.area = rn(Math.abs(d3.polygonArea(polygons[d])), 2);
const prec = rn(avPrec * i.area, 2);
i.flux = i.lake ? prec * 10 : prec;
}
const neighbors = []; // re-detect neighbors
diagram.cells[d].halfedges.forEach(function(e) {
const edge = diagram.edges[e];
if (edge.left === undefined || edge.right === undefined) {
if (i.height >= 20) i.ctype = 99; // border cell
return;
}
const ea = edge.left.index === d ? edge.right.index : edge.left.index;
neighbors.push(ea);
if (d < ea && i.height >= 20 && i.lake !== 1 && cells[ea].height >= 20 && cells[ea].lake !== 1) {
gridPath += "M" + edge[0][0] + "," + edge[0][1] + "L" + edge[1][0] + "," + edge[1][1];
}
});
i.neighbors = neighbors;
if (i.region === undefined) delete i.region;
if (i.culture === undefined) delete i.culture;
});
grid.append("path").attr("d", gridPath);
console.timeEnd("reGraph");
}
// redraw all cells for Customization 1 mode
function mockHeightmap() {
let landCells = 0;
$("#landmass").empty();
const limit = renderOcean.checked ? 1 : 20;
for (let i=0; i < heights.length; i++) {
if (heights[i] < limit) continue;
if (heights[i] > 100) heights[i] = 100;
const clr = color(1 - heights[i] / 100);
landmass.append("path").attr("id", "cell"+i)
.attr("d", "M" + polygons[i].join("L") + "Z")
.attr("fill", clr).attr("stroke", clr);
}
}
$("#renderOcean").click(mockHeightmap);
// draw or update all cells
function updateHeightmap() {
const limit = renderOcean.checked ? 1 : 20;
for (let i=0; i < heights.length; i++) {
if (heights[i] > 100) heights[i] = 100;
let cell = landmass.select("#cell"+i);
const clr = color(1 - heights[i] / 100);
if (cell.size()) {
if (heights[i] < limit) {cell.remove();}
else {cell.attr("fill", clr).attr("stroke", clr);}
} else if (heights[i] >= limit) {
cell = landmass.append("path").attr("id", "cell"+i)
.attr("d", "M" + polygons[i].join("L") + "Z")
.attr("fill", clr).attr("stroke", clr);
}
}
}
// draw or update cells from the selection
function updateHeightmapSelection(selection) {
if (selection === undefined) return;
const limit = renderOcean.checked ? 1 : 20;
selection.map(function(s) {
if (heights[s] > 100) heights[s] = 100;
let cell = landmass.select("#cell"+s);
const clr = color(1 - heights[s] / 100);
if (cell.size()) {
if (heights[s] < limit) {cell.remove();}
else {cell.attr("fill", clr).attr("stroke", clr);}
} else if (heights[s] >= limit) {
cell = landmass.append("path").attr("id", "cell"+s)
.attr("d", "M" + polygons[s].join("L") + "Z")
.attr("fill", clr).attr("stroke", clr);
}
});
}
function updateHistory() {
let landCells = 0; // count number of land cells
if (renderOcean.checked) {
landCells = heights.reduce(function(s, v) {if (v >= 20) {return s + 1;} else {return s;}}, 0);
} else {
landCells = landmass.selectAll("*").size();
}
history = history.slice(0, historyStage);
history[historyStage] = heights.slice();
historyStage++;
undo.disabled = templateUndo.disabled = historyStage <= 1;
redo.disabled = templateRedo.disabled = true;
const landMean = Math.trunc(d3.mean(heights));
const landRatio = rn(landCells / heights.length * 100);
landmassCounter.innerHTML = landCells;
landmassRatio.innerHTML = landRatio;
landmassAverage.innerHTML = landMean;
// if perspective view dialog is opened, update it
if ($("#perspectivePanel").is(":visible")) drawPerspective();
}
// restoreHistory
function restoreHistory(step) {
historyStage = step;
redo.disabled = templateRedo.disabled = historyStage >= history.length;
undo.disabled = templateUndo.disabled = historyStage <= 1;
if (history[historyStage - 1] === undefined) return;
heights = history[historyStage - 1].slice();
updateHeightmap();
}
// restart history from 1st step
function restartHistory() {
history = [];
historyStage = 0;
redo.disabled = templateRedo.disabled = true;
undo.disabled = templateUndo.disabled = true;
updateHistory();
}
// Detect and draw the coasline
function drawCoastline() {
console.time('drawCoastline');
Math.seedrandom(seed); // reset seed to get the same result on heightmap edit
const shape = defs.append("mask").attr("id", "shape").attr("fill", "black").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%");
$("#landmass").empty();
let minX = graphWidth, maxX = 0; // extreme points
let minXedge, maxXedge; // extreme edges
const oceanEdges = [],lakeEdges = [];
for (let i=0; i < land.length; i++) {
const id = land[i].index, cell = diagram.cells[id];
const f = land[i].fn;
land[i].height = Math.trunc(land[i].height);
if (!oceanEdges[f]) {oceanEdges[f] = []; lakeEdges[f] = [];}
cell.halfedges.forEach(function(e) {
const edge = diagram.edges[e];
const start = edge[0].join(" ");
const end = edge[1].join(" ");
if (edge.left && edge.right) {
const ea = edge.left.index === id ? edge.right.index : edge.left.index;
cells[ea].height = Math.trunc(cells[ea].height);
if (cells[ea].height < 20) {
cells[ea].ctype = -1;
if (land[i].ctype !== 1) {
land[i].ctype = 1; // mark coastal land cells
// move cell point closer to coast
const x = (land[i].data[0] + cells[ea].data[0]) / 2;
const y = (land[i].data[1] + cells[ea].data[1]) / 2;
land[i].haven = ea; // harbor haven (oposite water cell)
land[i].coastX = rn(x + (land[i].data[0] - x) * 0.1, 1);
land[i].coastY = rn(y + (land[i].data[1] - y) * 0.1, 1);
land[i].data[0] = rn(x + (land[i].data[0] - x) * 0.5, 1);
land[i].data[1] = rn(y + (land[i].data[1] - y) * 0.5, 1);
}
if (features[cells[ea].fn].border) {
oceanEdges[f].push({start, end});
// island extreme points
if (edge[0][0] < minX) {minX = edge[0][0]; minXedge = edge[0]}
if (edge[1][0] < minX) {minX = edge[1][0]; minXedge = edge[1]}
if (edge[0][0] > maxX) {maxX = edge[0][0]; maxXedge = edge[0]}
if (edge[1][0] > maxX) {maxX = edge[1][0]; maxXedge = edge[1]}
} else {
const l = cells[ea].fn;
if (!lakeEdges[f][l]) lakeEdges[f][l] = [];
lakeEdges[f][l].push({start, end});
}
}
} else {
oceanEdges[f].push({start, end});
}
});
}
for (let f = 0; f < features.length; f++) {
if (!oceanEdges[f]) continue;
if (!oceanEdges[f].length && lakeEdges[f].length) {
const m = lakeEdges[f].indexOf(d3.max(lakeEdges[f]));
oceanEdges[f] = lakeEdges[f][m];
lakeEdges[f][m] = [];
}
lineGen.curve(d3.curveCatmullRomClosed.alpha(0.1));
const oceanCoastline = getContinuousLine(oceanEdges[f],3, 0);
if (oceanCoastline) {
shape.append("path").attr("d", oceanCoastline).attr("fill", "white"); // draw the mask
coastline.append("path").attr("d", oceanCoastline); // draw the coastline
}
lineGen.curve(d3.curveBasisClosed);
lakeEdges[f].forEach(function(l) {
const lakeCoastline = getContinuousLine(l, 3, 0);
if (lakeCoastline) {
shape.append("path").attr("d", lakeCoastline).attr("fill", "black"); // draw the mask
lakes.append("path").attr("d", lakeCoastline); // draw the lakes
}
});
}
landmass.append("rect").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight); // draw the landmass
drawDefaultRuler(minXedge, maxXedge);
console.timeEnd('drawCoastline');
}
// draw default scale bar
function drawScaleBar() {
if ($("#scaleBar").hasClass("hidden")) return; // no need to re-draw hidden element
svg.select("#scaleBar").remove(); // fully redraw every time
// get size
const size = +barSize.value;
const dScale = distanceScale.value;
const unit = distanceUnit.value;
const scaleBar = svg.append("g").attr("id", "scaleBar")
.on("click", editScale)
.on("mousemove", function () {
tip("Click to open Scale Editor, drag to move");
})
.call(d3.drag().on("start", elementDrag));
const init = 100; // actual length in pixels if scale, dScale and size = 1;
let val = init * size * dScale / scale; // bar length in distance unit
if (val > 900) {val = rn(val, -3);} // round to 1000
else if (val > 90) {val = rn(val, -2);} // round to 100
else if (val > 9) {val = rn(val, -1);} // round to 10
else {val = rn(val)} // round to 1
const l = val * scale / dScale; // actual length in pixels on this scale
const x = 0, y = 0; // initial position
scaleBar.append("line").attr("x1", x+0.5).attr("y1", y).attr("x2", x+l+size-0.5).attr("y2", y).attr("stroke-width", size).attr("stroke", "white");
scaleBar.append("line").attr("x1", x).attr("y1", y + size).attr("x2", x+l+size).attr("y2", y + size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
const dash = size + " " + rn(l / 5 - size, 2);
scaleBar.append("line").attr("x1", x).attr("y1", y).attr("x2", x+l+size).attr("y2", y)
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
// big scale
for (let b = 0; b < 6; b++) {
const value = rn(b * l / 5, 2);
const label = rn(value * dScale / scale);
if (b === 5) {
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label + " " + unit);
} else {
scaleBar.append("text").attr("x", x + value).attr("y", y - 2 * size).attr("font-size", rn(5 * size, 1)).text(label);
}
}
if (barLabel.value !== "") {
scaleBar.append("text").attr("x", x + (l+1) / 2).attr("y", y + 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", rn(5 * size, 1)).text(barLabel.value);
}
const bbox = scaleBar.node().getBBox();
// append backbround rectangle
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
fitScaleBar();
}
// draw default ruler measiring land x-axis edges
function drawDefaultRuler(minXedge, maxXedge) {
const rulerNew = ruler.append("g").attr("class", "linear").call(d3.drag().on("start", elementDrag));
if (!minXedge) minXedge = [0, 0];
if (!maxXedge) maxXedge = [svgWidth, svgHeight];
const x1 = rn(minXedge[0],2), y1 = rn(minXedge[1],2), x2 = rn(maxXedge[0],2), y2 = rn(maxXedge[1],2);
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white");
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-dasharray", 10);
rulerNew.append("circle").attr("r", 2).attr("cx", x1).attr("cy", y1).attr("stroke-width", 0.5).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("r", 2).attr("cx", x2).attr("cy", y2).attr("stroke-width", 0.5).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
const x0 = rn((x1 + x2) / 2, 2), y0 = rn((y1 + y2) / 2, 2);
rulerNew.append("circle").attr("r", 1.2).attr("cx", x0).attr("cy", y0).attr("stroke-width", 0.3).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
const tr = "rotate(" + angle + " " + x0 + " " + y0 +")";
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
rulerNew.append("text").attr("x", x0).attr("y", y0).attr("dy", -1).attr("transform", tr).attr("data-dist", dist).text(label).on("click", removeParent).attr("font-size", 10);
}
// drag any element changing transform
function elementDrag() {
const el = d3.select(this);
const tr = parseTransform(el.attr("transform"));
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
const transform = `translate(${(dx+x)},${(dy+y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]})`;
el.attr("transform", transform);
const pp = this.parentNode.parentNode.id;
if (pp === "burgIcons" || pp === "burgLabels") {
tip('Use dragging for fine-tuning only, to move burg to a different cell use "Relocate" button');
}
if (pp === "labels") {
// also transform curve control circle
debug.select("circle").attr("transform", transform);
}
});
d3.event.on("end", function() {
// remember scaleBar bottom-right position
if (el.attr("id") === "scaleBar") {
const xEnd = d3.event.x, yEnd = d3.event.y;
const diff = Math.abs(dx - xEnd) + Math.abs(dy - yEnd);
if (diff > 5) {
const bbox = el.node().getBoundingClientRect();
sessionStorage.setItem("scaleBar", [bbox.right, bbox.bottom]);
}
}
});
}
// draw ruler circles and update label
function rulerEdgeDrag() {
const group = d3.select(this.parentNode);
const edge = d3.select(this).attr("data-edge");
const x = d3.event.x, y = d3.event.y;
let x0, y0;
d3.select(this).attr("cx", x).attr("cy", y);
const line = group.selectAll("line");
if (edge === "left") {
line.attr("x1", x).attr("y1", y);
x0 = +line.attr("x2"), y0 = +line.attr("y2");
} else {
line.attr("x2", x).attr("y2", y);
x0 = +line.attr("x1"), y0 = +line.attr("y1");
}
const xc = rn((x + x0) / 2, 2), yc = rn((y + y0) / 2, 2);
group.select(".center").attr("cx", xc).attr("cy", yc);
const dist = rn(Math.hypot(x0 - x, y0 - y));
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
const angle = rn(atan * 180 / Math.PI, 3);
const tr = "rotate(" + angle + " " + xc + " " + yc + ")";
group.select("text").attr("x", xc).attr("y", yc).attr("transform", tr).attr("data-dist", dist).text(label);
}
// draw ruler center point to split ruler into 2 parts
function rulerCenterDrag() {
let xc1, yc1, xc2, yc2;
const group = d3.select(this.parentNode); // current ruler group
let x = d3.event.x, y = d3.event.y; // current coords
const line = group.selectAll("line"); // current lines
const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
const rulerNew = ruler.insert("g", ":first-child");
rulerNew.attr("transform", group.attr("transform")).call(d3.drag().on("start", elementDrag));
const factor = rn(1 / Math.pow(scale, 0.3), 1);
rulerNew.append("line").attr("class", "white").attr("stroke-width", factor);
const dash = +group.select(".gray").attr("stroke-dasharray");
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", factor);
rulerNew.append("text").attr("dy", -1).on("click", removeParent).attr("font-size", 10 * factor).attr("stroke-width", factor);
d3.event.on("drag", function() {
x = d3.event.x, y = d3.event.y;
d3.select(this).attr("cx", x).attr("cy", y);
// change first part
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
let dist = rn(Math.hypot(x1 - x, y1 - y));
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
let tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc1 + " " + yc1 + ")";
group.select("text").attr("x", xc1).attr("y", yc1).attr("transform", tr).attr("data-dist", dist).text(label);
// change second (new) part
dist = rn(Math.hypot(x2 - x, y2 - y));
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
tr = "rotate(" + rn(atan * 180 / Math.PI, 3) + " " + xc2 + " " + yc2 +")";
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", tr).attr("data-dist", dist).text(label);
});
d3.event.on("end", function() {
// circles for 1st part
group.selectAll("circle").remove();
group.append("circle").attr("cx", x1).attr("cy", y1).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
group.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
group.append("circle").attr("cx", xc1).attr("cy", yc1).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
// circles for 2nd part
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "left").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * factor).attr("stroke-width", 0.5 * factor).attr("data-edge", "rigth").call(d3.drag().on("drag", rulerEdgeDrag));
rulerNew.append("circle").attr("cx", xc2).attr("cy", yc2).attr("r", 1.2 * factor).attr("stroke-width", 0.3 * factor).attr("class", "center").call(d3.drag().on("start", rulerCenterDrag));
});
}
function opisometerEdgeDrag() {
const el = d3.select(this);
const x0 = +el.attr("cx"), y0 = +el.attr("cy");
const group = d3.select(this.parentNode);
const curve = group.select(".white");
const curveGray = group.select(".gray");
const text = group.select("text");
const points = JSON.parse(text.attr("data-points"));
if (x0 === points[0].scX && y0 === points[0].scY) {points.reverse();}
d3.event.on("drag", function() {
const x = d3.event.x, y = d3.event.y;
el.attr("cx", x).attr("cy", y);
const l = points[points.length - 1];
const diff = Math.hypot(l.scX - x, l.scY - y);
if (diff > 5) {points.push({scX: x, scY: y});} else {return;}
lineGen.curve(d3.curveBasis);
const d = round(lineGen(points));
curve.attr("d", d);
curveGray.attr("d", d);
const dist = rn(curve.node().getTotalLength());
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
text.attr("x", x).attr("y", y).text(label);
});
d3.event.on("end", function() {
const dist = rn(curve.node().getTotalLength());
const c = curve.node().getPointAtLength(dist / 2);
const p = curve.node().getPointAtLength((dist / 2) - 1);
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
const angle = rn(atan * 180 / Math.PI, 3);
const tr = "rotate(" + angle + " " + c.x + " " + c.y + ")";
text.attr("data-points", JSON.stringify(points)).attr("data-dist", dist).attr("x", c.x).attr("y", c.y).attr("transform", tr).text(label);
});
}
function getContinuousLine(edges, indention, relax) {
let line = "";
if (edges.length < 3) return "";
while (edges.length > 2) {
let edgesOrdered = []; // to store points in a correct order
let start = edges[0].start;
let end = edges[0].end;
edges.shift();
let spl = start.split(" ");
edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
spl = end.split(" ");
edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
let x0 = +spl[0],y0 = +spl[1];
for (let i = 0; end !== start && i < 100000; i++) {
let next = null, index = null;
for (let e = 0; e < edges.length; e++) {
const edge = edges[e];
if (edge.start == end || edge.end == end) {
next = edge;
end = next.start == end ? next.end : next.start;
index = e;
break;
}
}
if (!next) {
console.error("Next edge is not found");
return "";
}
spl = end.split(" ");
if (indention || relax) {
const dist = Math.hypot(+spl[0] - x0, +spl[1] - y0);
if (dist >= indention && Math.random() > relax) {
edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
x0 = +spl[0],y0 = +spl[1];
}
} else {
edgesOrdered.push({scX: +spl[0],scY: +spl[1]});
}
edges.splice(index, 1);
if (i === 100000-1) {
console.error("Line not ended, limit reached");
break;
}
}
line += lineGen(edgesOrdered);
}
return round(line, 1);
}
// temporary elevate lakes to min neighbors heights to correctly flux the water
function elevateLakes() {
console.time('elevateLakes');
const lakes = $.grep(cells, function(e, d) {return heights[d] < 20 && !features[e.fn].border;});
lakes.sort(function(a, b) {return heights[b.index] - heights[a.index];});
for (let i=0; i < lakes.length; i++) {
const hs = [],id = lakes[i].index;
cells[id].height = heights[id]; // use height on object level
lakes[i].neighbors.forEach(function(n) {
const nHeight = cells[n].height || heights[n];
if (nHeight >= 20) hs.push(nHeight);
});
if (hs.length) cells[id].height = d3.min(hs) - 1;
if (cells[id].height < 20) cells[id].height = 20;
lakes[i].lake = 1;
}
console.timeEnd('elevateLakes');
}
// Depression filling algorithm (for a correct water flux modeling; phase1)
function resolveDepressionsPrimary() {
console.time('resolveDepressionsPrimary');
land = $.grep(cells, function(e, d) {
if (!e.height) e.height = heights[d]; // use height on object level
return e.height >= 20;
});
land.sort(function(a, b) {return b.height - a.height;});
const limit = 10;
for (let l = 0, depression = 1; depression > 0 && l < limit; l++) {
depression = 0;
for (let i = 0; i < land.length; i++) {
const id = land[i].index;
if (land[i].type === "border") continue;
const hs = land[i].neighbors.map(function(n) {return cells[n].height;});
const minHigh = d3.min(hs);
if (cells[id].height <= minHigh) {
depression++;
land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
cells[id].height = minHigh + 2;
}
}
if (l === 0) console.log(" depressions init: " + depression);
}
console.timeEnd('resolveDepressionsPrimary');
}
// Depression filling algorithm (for a correct water flux modeling; phase2)
function resolveDepressionsSecondary() {
console.time('resolveDepressionsSecondary');
land = $.grep(cells, function(e) {return e.height >= 20;});
land.sort(function(a, b) {return b.height - a.height;});
const limit = 100;
for (let l = 0, depression = 1; depression > 0 && l < limit; l++) {
depression = 0;
for (let i = 0; i < land.length; i++) {
if (land[i].ctype === 99) continue;
const nHeights = land[i].neighbors.map(function(n) {return cells[n].height});
const minHigh = d3.min(nHeights);
if (land[i].height <= minHigh) {
depression++;
land[i].pit = land[i].pit ? land[i].pit + 1 : 1;
land[i].height = Math.trunc(minHigh + 2);
}
}
if (l === 0) console.log(" depressions reGraphed: " + depression);
if (l === limit - 1) console.error("Error: resolveDepressions iteration limit");
}
console.timeEnd('resolveDepressionsSecondary');
}
// restore initial heights if user don't want system to change heightmap
function restoreCustomHeights() {
land.forEach(function(l) {
if (!l.pit) return;
l.height = Math.trunc(l.height - l.pit * 2);
if (l.height < 20) l.height = 20;
});
}
function flux() {
console.time('flux');
riversData = [];
let riverNext = 0;
land.sort(function(a, b) {return b.height - a.height;});
for (let i = 0; i < land.length; i++) {
const id = land[i].index;
const sx = land[i].data[0];
const sy = land[i].data[1];
let fn = land[i].fn;
if (land[i].ctype === 99) {
if (land[i].river !== undefined) {
let x, y;
const min = Math.min(sy, graphHeight - sy, sx, graphWidth - sx);
if (min === sy) {x = sx; y = 0;}
if (min === graphHeight - sy) {x = sx; y = graphHeight;}
if (min === sx) {x = 0; y = sy;}
if (min === graphWidth - sx) {x = graphWidth; y = sy;}
riversData.push({river: land[i].river, cell: id, x, y});
}
continue;
}
if (features[fn].river !== undefined) {
if (land[i].river !== features[fn].river) {
land[i].river = undefined;
land[i].flux = 0;
}
}
let minHeight = 1000, min;
land[i].neighbors.forEach(function(e) {
if (cells[e].height < minHeight) {
minHeight = cells[e].height;
min = e;
}
});
// Define river number
if (min !== undefined && land[i].flux > 1) {
if (land[i].river === undefined) {
// State new River
land[i].river = riverNext;
riversData.push({river: riverNext, cell: id, x: sx, y: sy});
riverNext += 1;
}
// Assing existing River to the downhill cell
if (cells[min].river == undefined) {
cells[min].river = land[i].river;
} else {
const riverTo = cells[min].river;
const iRiver = $.grep(riversData, function (e) {
return (e.river == land[i].river);
});
const minRiver = $.grep(riversData, function (e) {
return (e.river == riverTo);
});
let iRiverL = iRiver.length;
let minRiverL = minRiver.length;
// re-assing river nunber if new part is greater
if (iRiverL >= minRiverL) {
cells[min].river = land[i].river;
iRiverL += 1;
minRiverL -= 1;
}
// mark confluences
if (cells[min].height >= 20 && iRiverL > 1 && minRiverL > 1) {
if (!cells[min].confluence) {
cells[min].confluence = minRiverL-1;
} else {
cells[min].confluence += minRiverL-1;
}
}
}
}
if (cells[min].flux) cells[min].flux += land[i].flux;
if (land[i].river !== undefined) {
const px = cells[min].data[0];
const py = cells[min].data[1];
if (cells[min].height < 20) {
// pour water to the sea
const x = (px + sx) / 2 + (px - sx) / 10;
const y = (py + sy) / 2 + (py - sy) / 10;
riversData.push({river: land[i].river, cell: id, x, y});
} else {
if (cells[min].lake === 1) {
fn = cells[min].fn;
if (features[fn].river === undefined) features[fn].river = land[i].river;
}
// add next River segment
riversData.push({river: land[i].river, cell: min, x: px, y: py});
}
}
}
console.timeEnd('flux');
drawRiverLines(riverNext);
}
function drawRiverLines(riverNext) {
console.time('drawRiverLines');
for (let i = 0; i < riverNext; i++) {
const dataRiver = $.grep(riversData, function (e) {
return e.river === i;
});
if (dataRiver.length > 1) {
const riverAmended = amendRiver(dataRiver, 1);
const width = rn(0.8 + Math.random() * 0.4, 1);
const increment = rn(0.8 + Math.random() * 0.4, 1);
const d = drawRiver(riverAmended, width, increment);
rivers.append("path").attr("d", d).attr("id", "river"+i).attr("data-width", width).attr("data-increment", increment);
}
}
rivers.selectAll("path").on("click", editRiver);
console.timeEnd('drawRiverLines');
}
// add more river points on 1/3 and 2/3 of length
function amendRiver(dataRiver, rndFactor) {
const riverAmended = [];
let side = 1;
for (let r = 0; r < dataRiver.length; r++) {
const dX = dataRiver[r].x;
const dY = dataRiver[r].y;
const cell = dataRiver[r].cell;
const c = cells[cell].confluence || 0;
riverAmended.push([dX, dY, c]);
if (r+1 < dataRiver.length) {
const eX = dataRiver[r + 1].x;
const eY = dataRiver[r + 1].y;
const angle = Math.atan2(eY - dY, eX - dX);
const serpentine = 1 / (r + 1);
const meandr = serpentine + 0.3 + Math.random() * 0.3 * rndFactor;
if (Math.random() > 0.5) {
side *= -1
}
const dist = Math.hypot(eX - dX, eY - dY);
// if dist is big or river is small add 2 extra points
if (dist > 8 || (dist > 4 && dataRiver.length < 6)) {
let stX = (dX * 2 + eX) / 3;
let stY = (dY * 2 + eY) / 3;
let enX = (dX + eX * 2) / 3;
let enY = (dY + eY * 2) / 3;
stX += -Math.sin(angle) * meandr * side;
stY += Math.cos(angle) * meandr * side;
if (Math.random() > 0.8) {
side *= -1
}
enX += Math.sin(angle) * meandr * side;
enY += -Math.cos(angle) * meandr * side;
riverAmended.push([stX, stY],[enX, enY]);
// if dist is medium or river is small add 1 extra point
} else if (dist > 4 || dataRiver.length < 6) {
let scX = (dX + eX) / 2;
let scY = (dY + eY) / 2;
scX += -Math.sin(angle) * meandr * side;
scY += Math.cos(angle) * meandr * side;
riverAmended.push([scX, scY]);
}
}
}
return riverAmended;
}
// draw river polygon using arrpoximation
function drawRiver(points, width, increment) {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
let extraOffset = 0.03; // start offset to make river source visible
width = width || 1; // river width modifier
increment = increment || 1; // river bed widening modifier
let riverLength = 0;
points.map(function(p, i) {
if (i === 0) {return 0;}
riverLength += Math.hypot(p[0] - points[i-1][0],p[1] - points[i-1][1]);
});
const widening = rn((1000 + (riverLength * 30)) * increment);
const riverPointsLeft = [], riverPointsRight = [];
const last = points.length - 1;
const factor = riverLength / points.length;
// first point
let x = points[0][0], y = points[0][1], c;
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
let xLeft = x + -Math.sin(angle) * extraOffset, yLeft = y + Math.cos(angle) * extraOffset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
let xRight = x + Math.sin(angle) * extraOffset, yRight = y + -Math.cos(angle) * extraOffset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
// middle points
for (let p = 1; p < last; p ++) {
x = points[p][0],y = points[p][1],c = points[p][2];
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
const xPrev = points[p - 1][0], yPrev = points[p - 1][1];
const xNext = points[p + 1][0], yNext = points[p + 1][1];
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
var offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
}
// end point
x = points[last][0],y = points[last][1],c = points[last][2];
if (c) {extraOffset += Math.atan(c * 10 / widening);} // confluence
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
xLeft = x + -Math.sin(angle) * offset, yLeft = y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
xRight = x + Math.sin(angle) * offset, yRight = y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
// generate path and return
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return round(right + left, 2);
}
// draw river polygon with best quality
function drawRiverSlow(points, width, increment) {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
width = width || 1;
const extraOffset = 0.02 * width;
increment = increment || 1;
const riverPoints = points.map(function (p) {
return {scX: p[0], scY: p[1]};
});
const river = defs.append("path").attr("d", lineGen(riverPoints));
const riverLength = river.node().getTotalLength();
const widening = rn((1000 + (riverLength * 30)) * increment);
const riverPointsLeft = [], riverPointsRight = [];
for (let l = 0; l < riverLength; l++) {
var point = river.node().getPointAtLength(l);
var from = river.node().getPointAtLength(l - 0.1);
const to = river.node().getPointAtLength(l + 0.1);
var angle = Math.atan2(from.y - to.y, from.x - to.x);
var offset = (Math.atan(Math.pow(l, 2) / widening) / 2 * width) + extraOffset;
var xLeft = point.x + -Math.sin(angle) * offset;
var yLeft = point.y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
var xRight = point.x + Math.sin(angle) * offset;
var yRight = point.y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
}
var point = river.node().getPointAtLength(riverLength);
var from = river.node().getPointAtLength(riverLength - 0.1);
var angle = Math.atan2(from.y - point.y, from.x - point.x);
var offset = (Math.atan(Math.pow(riverLength, 2) / widening) / 2 * width) + extraOffset;
var xLeft = point.x + -Math.sin(angle) * offset;
var yLeft = point.y + Math.cos(angle) * offset;
riverPointsLeft.push({scX:xLeft, scY:yLeft});
var xRight = point.x + Math.sin(angle) * offset;
var yRight = point.y + -Math.cos(angle) * offset;
riverPointsRight.unshift({scX:xRight, scY:yRight});
river.remove();
// generate path and return
const right = lineGen(riverPointsRight);
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return round(right + left, 2);
}
// add lakes on depressed points on river course
function addLakes() {
console.time('addLakes');
let smallLakes = 0;
for (let i=0; i < land.length; i++) {
// elavate all big lakes
if (land[i].lake === 1) {
land[i].height = 19;
land[i].ctype = -1;
}
// define eligible small lakes
if (land[i].lake === 2 && smallLakes < 100) {
if (land[i].river !== undefined) {
land[i].height = 19;
land[i].ctype = -1;
land[i].fn = -1;
smallLakes++;
} else {
land[i].lake = undefined;
land[i].neighbors.forEach(function(n) {
if (cells[n].lake !== 1 && cells[n].river !== undefined) {
cells[n].lake = 2;
cells[n].height = 19;
cells[n].ctype = -1;
cells[n].fn = -1;
smallLakes++;
} else if (cells[n].lake === 2) {
cells[n].lake = undefined;
}
});
}
}
}
console.log( "small lakes: " + smallLakes);
// mark small lakes
let unmarked = $.grep(land, function(e) {return e.fn === -1});
while (unmarked.length) {
let fn = -1, queue = [unmarked[0].index],lakeCells = [];
unmarked[0].session = "addLakes";
while (queue.length) {
const q = queue.pop();
lakeCells.push(q);
if (cells[q].fn !== -1) fn = cells[q].fn;
cells[q].neighbors.forEach(function(e) {
if (cells[e].lake && cells[e].session !== "addLakes") {
cells[e].session = "addLakes";
queue.push(e);
}
});
}
if (fn === -1) {
fn = features.length;
features.push({i: fn, land: false, border: false});
}
lakeCells.forEach(function(c) {cells[c].fn = fn;});
unmarked = $.grep(land, function(e) {return e.fn === -1});
}
land = $.grep(cells, function(e) {return e.height >= 20;});
console.timeEnd('addLakes');
}
function editLabel() {
if (customization) return;
unselect();
closeDialogs("#labelEditor, .stable");
elSelected = d3.select(this).call(d3.drag().on("start", elementDrag)).classed("draggable", true);
// update group parameters
let group = d3.select(this.parentNode);
updateGroupOptions();
labelGroupSelect.value = group.attr("id");
labelFontSelect.value = fonts.indexOf(group.attr("data-font"));
labelSize.value = group.attr("data-size");
labelColor.value = toHEX(group.attr("fill"));
labelOpacity.value = group.attr("opacity");
labelText.value = elSelected.text();
const tr = parseTransform(elSelected.attr("transform"));
labelAngle.value = tr[2];
labelAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
$("#labelEditor").dialog({
title: "Edit Label: " + labelText.value,
minHeight: 30, width: "auto", maxWidth: 275, resizable: false,
position: {my: "center top+10", at: "bottom", of: this},
close: unselect
});
if (modules.editLabel) return;
modules.editLabel = true;
loadDefaultFonts();
function updateGroupOptions() {
labelGroupSelect.innerHTML = "";
labels.selectAll("g:not(#burgLabels)").each(function(d) {
if (this.parentNode.id === "burgLabels") return;
let id = d3.select(this).attr("id");
let opt = document.createElement("option");
opt.value = opt.innerHTML = id;
labelGroupSelect.add(opt);
});
}
$("#labelGroupButton").click(function() {
$("#labelEditor > button").not(this).toggle();
$("#labelGroupButtons").toggle();
});
// on group change
document.getElementById("labelGroupSelect").addEventListener("change", function() {
document.getElementById(this.value).appendChild(elSelected.remove().node());
});
// toggle inputs to declare a new group
document.getElementById("labelGroupNew").addEventListener("click", function() {
if ($("#labelGroupInput").css("display") === "none") {
$("#labelGroupInput").css("display", "inline-block");
$("#labelGroupSelect").css("display", "none");
labelGroupInput.focus();
} else {
$("#labelGroupSelect").css("display", "inline-block");
$("#labelGroupInput").css("display", "none");
}
});
// toggle inputs to select a group
document.getElementById("labelExternalFont").addEventListener("click", function() {
if ($("#labelFontInput").css("display") === "none") {
$("#labelFontInput").css("display", "inline-block");
$("#labelFontSelect").css("display", "none");
labelFontInput.focus();
} else {
$("#labelFontSelect").css("display", "inline-block");
$("#labelFontInput").css("display", "none");
}
});
// on new group creation
document.getElementById("labelGroupInput").addEventListener("change", function() {
if (!this.value) {
tip("Please provide a valid group name");
return;
}
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
// if el with this id exists, add size to id
while (labels.selectAll("#"+group).size()) {group += "_new";}
createNewLabelGroup(group);
});
function createNewLabelGroup(g) {
let group = elSelected.node().parentNode.cloneNode(false);
let groupNew = labels.append(f => group).attr("id", g);
groupNew.append(f => elSelected.remove().node());
updateGroupOptions();
$("#labelGroupSelect, #labelGroupInput").toggle();
labelGroupInput.value = "";
labelGroupSelect.value = g;
updateLabelGroups();
}
// remove label group on click
document.getElementById("labelGroupRemove").addEventListener("click", function() {
let group = d3.select(elSelected.node().parentNode);
let id = group.attr("id");
let count = group.selectAll("text").size();
// remove group with < 2 label without ask
if (count < 2) {
removeAllLabelsInGroup(id);
$("#labelEditor").dialog("close");
return;
}
alertMessage.innerHTML = "Are you sure you want to remove all labels (" + count + ") of that group?";
$("#alert").dialog({resizable: false, title: "Remove label group",
buttons: {
Remove: function() {
$(this).dialog("close");
removeAllLabelsInGroup(id);
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
});
$("#labelTextButton").click(function() {
$("#labelEditor > button").not(this).toggle();
$("#labelTextButtons").toggle();
});
// on label text change
document.getElementById("labelText").addEventListener("input", function() {
if (!this.value) {
tip("Name should not be blank, set opacity to 0 to hide label or click remove button to delete");
return;
}
// change Label text
if (elSelected.select("textPath").size()) elSelected.select("textPath").text(this.value);
else elSelected.text(this.value);
$("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + this.value);
// check if label is a country name
let id = elSelected.attr("id") || "";
if (id.includes("regionLabel")) {
let state = +elSelected.attr("id").slice(11);
states[state].name = this.value;
}
});
// generate a random country name
document.getElementById("labelTextRandom").addEventListener("click", function() {
let name = elSelected.text();
let id = elSelected.attr("id") || "";
if (id.includes("regionLabel")) {
// label is a country name
let state = +elSelected.attr("id").slice(11);
name = generateStateName(state.i);
states[state].name = name;
} else {
// label is not a country name, use random culture
let c = elSelected.node().getBBox();
let closest = cultureTree.find((c.x + c.width / 2), (c.y + c.height / 2));
let culture = Math.floor(Math.random() * cultures.length);
name = generateName(culture);
}
labelText.value = name;
$("div[aria-describedby='labelEditor'] .ui-dialog-title").text("Edit Label: " + name);
// change Label text
if (elSelected.select("textPath").size()) elSelected.select("textPath").text(name);
else elSelected.text(name);
});
$("#labelFontButton").click(function() {
$("#labelEditor > button").not(this).toggle();
$("#labelFontButtons").toggle();
});
// on label font change
document.getElementById("labelFontSelect").addEventListener("change", function() {
let group = elSelected.node().parentNode;
let font = fonts[this.value].split(':')[0].replace(/\+/g, " ");
group.setAttribute("font-family", font);
group.setAttribute("data-font", fonts[this.value]);
});
// on adding custom font
document.getElementById("labelFontInput").addEventListener("change", function() {
fetchFonts(this.value).then(fetched => {
if (!fetched) return;
labelExternalFont.click();
labelFontInput.value = "";
if (fetched === 1) $("#labelFontSelect").val(fonts.length - 1).change();
});
});
// on label size input
document.getElementById("labelSize").addEventListener("input", function() {
let group = elSelected.node().parentNode;
let size = +this.value;
group.setAttribute("data-size", size);
group.setAttribute("font-size", rn((size + (size / scale)) / 2, 2))
});
$("#labelStyleButton").click(function() {
$("#labelEditor > button").not(this).toggle();
$("#labelStyleButtons").toggle();
});
// on label fill color input
document.getElementById("labelColor").addEventListener("input", function() {
let group = elSelected.node().parentNode;
group.setAttribute("fill", this.value);
});
// on label opacity input
document.getElementById("labelOpacity").addEventListener("input", function() {
let group = elSelected.node().parentNode;
group.setAttribute("opacity", this.value);
});
$("#labelAngleButton").click(function() {
$("#labelEditor > button").not(this).toggle();
$("#labelAngleButtons").toggle();
});
// on label angle input
document.getElementById("labelAngle").addEventListener("input", function() {
const tr = parseTransform(elSelected.attr("transform"));
labelAngleValue.innerHTML = Math.abs(+this.value) + "°";
const c = elSelected.node().getBBox();
const angle = +this.value;
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)} ${(c.y+c.height/2)})`;
elSelected.attr("transform", transform);
});
// display control points to curve label (place on path)
document.getElementById("labelCurve").addEventListener("click", function() {
let c = elSelected.node().getBBox();
let cx = c.x + c.width / 2, cy = c.y + c.height / 2;
if (!elSelected.select("textPath").size()) {
let id = elSelected.attr("id");
let pathId = "#textPath_" + id;
let path = `M${cx-c.width},${cy} q${c.width},0 ${c.width * 2},0`;
let text = elSelected.text(), x = elSelected.attr("x"), y = elSelected.attr("y");
elSelected.text(null).attr("data-x", x).attr("data-y", y).attr("x", null).attr("y", null);
defs.append("path").attr("id", "textPath_" + id).attr("d", path);
elSelected.append("textPath").attr("href", pathId).attr("startOffset", "50%").text(text);
}
if (!debug.select("circle").size()) {
debug.append("circle").attr("id", "textPathControl").attr("r", 1.6)
.attr("cx", cx).attr("cy", cy).attr("transform", elSelected.attr("transform") || null)
.call(d3.drag().on("start", textPathControlDrag));
}
});
// drag textPath controle point to curve the label
function textPathControlDrag() {
let textPath = defs.select("#textPath_" + elSelected.attr("id"));
let path = textPath.attr("d").split(" ");
let M = path[0].split(",");
let q = path[1].split(","); // +q[1] to get qy - the only changeble value
let y = d3.event.y;
d3.event.on("drag", function() {
let dy = d3.event.y - y;
let total = +q[1] + dy * 8;
d3.select(this).attr("cy", d3.event.y);
textPath.attr("d", `${M[0]},${+M[1] - dy} ${q[0]},${total} ${path[2]}`);
});
}
// cancel label curvature
document.getElementById("labelCurveCancel").addEventListener("click", function() {
if (!elSelected.select("textPath").size()) return;
let text = elSelected.text(), x = elSelected.attr("data-x"), y = elSelected.attr("data-y");
elSelected.text();
elSelected.attr("x", x).attr("y", y).attr("data-x", null).attr("data-y", null).text(text);
defs.select("#textPath_" + elSelected.attr("id")).remove();
debug.select("circle").remove();
});
// open legendsEditor
document.getElementById("labelLegend").addEventListener("click", function() {
let id = elSelected.attr("id");
let name = elSelected.text();
editLegends(id, name);
});
// copy label on click
document.getElementById("labelCopy").addEventListener("click", function() {
let group = d3.select(elSelected.node().parentNode);
copy = group.append(f => elSelected.node().cloneNode(true));
let id = "label" + Date.now().toString().slice(7);
copy.attr("id", id).attr("class", null).on("click", editLabel);
let shift = +group.attr("font-size") + 1;
if (copy.select("textPath").size()) {
let path = defs.select("#textPath_" + elSelected.attr("id")).attr("d");
let textPath = defs.append("path").attr("id", "textPath_" + id);
copy.select("textPath").attr("href", "#textPath_" + id);
let pathArray = path.split(" ");
let x = +pathArray[0].split(",")[0].slice(1);
let y = +pathArray[0].split(",")[1];
textPath.attr("d", `M${x-shift},${y-shift} ${pathArray[1]} ${pathArray[2]}`);shift
} else {
let x = +elSelected.attr("x") - shift;
let y = +elSelected.attr("y") - shift;
while (group.selectAll("text[x='" + x + "']").size()) {x -= shift; y -= shift;}
copy.attr("x", x).attr("y", y);
}
});
// remove label on click
document.getElementById("labelRemoveSingle").addEventListener("click", function() {
alertMessage.innerHTML = "Are you sure you want to remove the label?";
$("#alert").dialog({resizable: false, title: "Remove label",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
defs.select("#textPath_" + elSelected.attr("id")).remove();
$("#labelEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
});
});
}
function editRiver() {
if (customization) return;
if (elSelected) {
const self = d3.select(this).attr("id") === elSelected.attr("id");
const point = d3.mouse(this);
if (elSelected.attr("data-river") === "new") {
addRiverPoint([point[0],point[1]]);
completeNewRiver();
return;
} else if (self) {
riverAddControlPoint(point);
return;
}
}
unselect();
closeDialogs("#riverEditor, .stable");
elSelected = d3.select(this);
elSelected.call(d3.drag().on("start", riverDrag));
const tr = parseTransform(elSelected.attr("transform"));
riverAngle.value = tr[2];
riverAngleValue.innerHTML = Math.abs(+tr[2]) + "°";
riverScale.value = tr[5];
riverWidthInput.value = +elSelected.attr("data-width");
riverIncrement.value = +elSelected.attr("data-increment");
$("#riverEditor").dialog({
title: "Edit River",
minHeight: 30, width: "auto", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event},
close: function() {
if ($("#riverNew").hasClass('pressed')) completeNewRiver();
unselect();
}
});
if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints");
riverDrawPoints();
if (modules.editRiver) {return;}
modules.editRiver = true;
function riverAddControlPoint(point) {
let dists = [];
debug.select(".controlPoints").selectAll("circle").each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
dists.push(Math.hypot(point[0] - x, point[1] - y));
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) {index = closest+1;} else {index = next+1;}
}
const before = ":nth-child(" + (index + 1) + ")";
debug.select(".controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
.call(d3.drag().on("drag", riverPointDrag))
.on("click", function(d) {
$(this).remove();
redrawRiver();
});
redrawRiver();
}
function riverDrawPoints() {
const node = elSelected.node();
// river is a polygon, so divide length by 2 to get course length
const l = node.getTotalLength() / 2;
const parts = (l / 5) >> 0; // number of points
let inc = l / parts; // increment
if (inc === Infinity) {inc = l;} // 2 control points for short rivers
// draw control points
for (let i = l, c = l; i > 0; i -= inc, c += inc) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const p = [(p1.x + p2.x) / 2, (p1.y + p2.y) / 2];
addRiverPoint(p);
}
// last point should be accurate
const lp1 = node.getPointAtLength(0);
const lp2 = node.getPointAtLength(l * 2);
const p = [(lp1.x + lp2.x) / 2, (lp1.y + lp2.y) / 2];
addRiverPoint(p);
}
function addRiverPoint(point) {
debug.select(".controlPoints").append("circle")
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
.call(d3.drag().on("drag", riverPointDrag))
.on("click", function(d) {
$(this).remove();
redrawRiver();
});
}
function riverPointDrag() {
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
redrawRiver();
}
function riverDrag() {
const x = d3.event.x, y = d3.event.y;
const tr = parseTransform(elSelected.attr("transform"));
d3.event.on("drag", function() {
let xc = d3.event.x, yc = d3.event.y;
let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
elSelected.attr("transform", transform);
debug.select(".controlPoints").attr("transform", transform);
});
}
function redrawRiver() {
let points = [];
debug.select(".controlPoints").selectAll("circle").each(function() {
const el = d3.select(this);
points.push([+el.attr("cx"), +el.attr("cy")]);
});
const width = +riverWidthInput.value;
const increment = +riverIncrement.value;
const d = drawRiverSlow(points, width, increment);
elSelected.attr("d", d);
}
$("#riverWidthInput, #riverIncrement").change(function() {
const width = +riverWidthInput.value;
const increment = +riverIncrement.value;
elSelected.attr("data-width", width).attr("data-increment", increment);
redrawRiver();
});
$("#riverRegenerate").click(function() {
let points = [],amended = [],x, y, p1, p2;
const node = elSelected.node();
const l = node.getTotalLength() / 2;
const parts = (l / 8) >> 0; // number of points
let inc = l / parts; // increment
if (inc === Infinity) {inc = l;} // 2 control points for short rivers
for (let i = l, e = l; i > 0; i -= inc, e += inc) {
p1 = node.getPointAtLength(i);
p2 = node.getPointAtLength(e);
x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
points.push([x, y]);
}
// last point should be accurate
p1 = node.getPointAtLength(0);
p2 = node.getPointAtLength(l * 2);
x = (p1.x + p2.x) / 2, y = (p1.y + p2.y) / 2;
points.push([x, y]);
// amend points
const rndFactor = 0.3 + Math.random() * 1.4; // random factor in range 0.2-1.8
for (let i = 0; i < points.length; i++) {
x = points[i][0],y = points[i][1];
amended.push([x, y]);
// add additional semi-random point
if (i + 1 < points.length) {
const x2 = points[i+1][0],y2 = points[i+1][1];
let side = Math.random() > 0.5 ? 1 : -1;
const angle = Math.atan2(y2 - y, x2 - x);
const serpentine = 2 / (i+1);
const meandr = serpentine + 0.3 + Math.random() * rndFactor;
x = (x + x2) / 2, y = (y + y2) / 2;
x += -Math.sin(angle) * meandr * side;
y += Math.cos(angle) * meandr * side;
amended.push([x, y]);
}
}
const width = +riverWidthInput.value * 0.6 + Math.random();
const increment = +riverIncrement.value * 0.9 + Math.random() * 0.2;
riverWidthInput.value = width;
riverIncrement.value = increment;
elSelected.attr("data-width", width).attr("data-increment", increment);
const d = drawRiverSlow(amended, width, increment);
elSelected.attr("d", d).attr("data-width", width).attr("data-increment", increment);
debug.select(".controlPoints").selectAll("*").remove();
amended.map(function(p) {addRiverPoint(p);});
});
$("#riverAngle").on("input", function() {
const tr = parseTransform(elSelected.attr("transform"));
riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
const c = elSelected.node().getBBox();
const angle = +this.value, scale = +tr[5];
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
elSelected.attr("transform", transform);
debug.select(".controlPoints").attr("transform", transform);
});
$("#riverReset").click(function() {
elSelected.attr("transform", "");
debug.select(".controlPoints").attr("transform", "");
riverAngle.value = 0;
riverAngleValue.innerHTML = "0°";
riverScale.value = 1;
});
$("#riverScale").change(function() {
const tr = parseTransform(elSelected.attr("transform"));
const scaleOld = +tr[5],scale = +this.value;
const c = elSelected.node().getBBox();
const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
const trX = +tr[0] + cx * (scaleOld - scale);
const trY = +tr[1] + cy * (scaleOld - scale);
const scX = +tr[3] * scale/scaleOld;
const scY = +tr[4] * scale/scaleOld;
const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
elSelected.attr("transform", transform);
debug.select(".controlPoints").attr("transform", transform);
});
$("#riverNew").click(function() {
if ($(this).hasClass('pressed')) {
completeNewRiver();
} else {
// enter creation mode
$(".pressed").removeClass('pressed');
$(this).addClass('pressed');
if (elSelected) elSelected.call(d3.drag().on("drag", null));
debug.select(".controlPoints").selectAll("*").remove();
viewbox.style("cursor", "crosshair").on("click", newRiverAddPoint);
}
});
function newRiverAddPoint() {
const point = d3.mouse(this);
addRiverPoint([point[0],point[1]]);
if (!elSelected || elSelected.attr("data-river") !== "new") {
const id = +$("#rivers > path").last().attr("id").slice(5) + 1;
elSelected = rivers.append("path").attr("data-river", "new").attr("id", "river"+id)
.attr("data-width", 2).attr("data-increment", 1).on("click", completeNewRiver);
} else {
redrawRiver();
let cell = diagram.find(point[0],point[1]).index;
let f = cells[cell].fn;
let ocean = !features[f].land && features[f].border;
if (ocean && debug.select(".controlPoints").selectAll("circle").size() > 5) completeNewRiver();
}
}
function completeNewRiver() {
$("#riverNew").removeClass('pressed');
restoreDefaultEvents();
if (!elSelected || elSelected.attr("data-river") !== "new") return;
redrawRiver();
elSelected.attr("data-river", "");
elSelected.call(d3.drag().on("start", riverDrag)).on("click", editRiver);
const r = +elSelected.attr("id").slice(5);
debug.select(".controlPoints").selectAll("circle").each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
const cell = diagram.find(x, y, 3);
if (!cell) return;
if (cells[cell.index].river === undefined) cells[cell.index].river = r;
});
unselect();
debug.append("g").attr("class", "controlPoints");
}
$("#riverCopy").click(function() {
const tr = parseTransform(elSelected.attr("transform"));
const d = elSelected.attr("d");
let x = 2, y = 2;
let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
x += 2; y += 2;
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
}
const river = +$("#rivers > path").last().attr("id").slice(5) + 1;
rivers.append("path").attr("d", d)
.attr("transform", transform)
.attr("id", "river"+river).on("click", editRiver)
.attr("data-width", elSelected.attr("data-width"))
.attr("data-increment", elSelected.attr("data-increment"));
unselect();
});
// open legendsEditor
document.getElementById("riverLegend").addEventListener("click", function() {
let id = elSelected.attr("id");
editLegends(id, id);
});
$("#riverRemove").click(function() {
alertMessage.innerHTML = `Are you sure you want to remove the river?`;
$("#alert").dialog({resizable: false, title: "Remove river",
buttons: {
Remove: function() {
$(this).dialog("close");
const river = +elSelected.attr("id").slice(5);
const avPrec = rn(precInput.value / Math.sqrt(cells.length), 2);
land.map(function(l) {
if (l.river === river) {
l.river = undefined;
l.flux = avPrec;
}
});
elSelected.remove();
unselect();
$("#riverEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
})
});
}
function editRoute() {
if (customization) {return;}
if (elSelected) {
const self = d3.select(this).attr("id") === elSelected.attr("id");
const point = d3.mouse(this);
if (elSelected.attr("data-route") === "new") {
addRoutePoint({x:point[0],y:point[1]});
completeNewRoute();
return;
} else if (self) {
routeAddControlPoint(point);
return;
}
}
unselect();
closeDialogs("#routeEditor, .stable");
if (this && this !== window) {
elSelected = d3.select(this);
if (!debug.select(".controlPoints").size()) debug.append("g").attr("class", "controlPoints");
routeDrawPoints();
routeUpdateGroups();
let routeType = d3.select(this.parentNode).attr("id");
routeGroup.value = routeType;
$("#routeEditor").dialog({
title: "Edit Route",
minHeight: 30, width: "auto", resizable: false,
position: {my: "center top+20", at: "top", of: d3.event},
close: function() {
if ($("#addRoute").hasClass('pressed')) completeNewRoute();
if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
unselect();
}
});
} else {elSelected = null;}
if (modules.editRoute) {return;}
modules.editRoute = true;
function routeAddControlPoint(point) {
let dists = [];
debug.select(".controlPoints").selectAll("circle").each(function() {
const x = +d3.select(this).attr("cx");
const y = +d3.select(this).attr("cy");
dists.push(Math.hypot(point[0] - x, point[1] - y));
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort(function(a, b) {return a-b;});
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) {index = closest+1;} else {index = next+1;}
}
const before = ":nth-child(" + (index + 1) + ")";
debug.select(".controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 0.35)
.call(d3.drag().on("drag", routePointDrag))
.on("click", function(d) {
$(this).remove();
routeRedraw();
});
routeRedraw();
}
function routeDrawPoints() {
if (!elSelected.size()) return;
const node = elSelected.node();
const l = node.getTotalLength();
const parts = (l / 5) >> 0; // number of points
let inc = l / parts; // increment
if (inc === Infinity) inc = l; // 2 control points for short routes
// draw control points
for (let i = 0; i <= l; i += inc) {
const p = node.getPointAtLength(i);
addRoutePoint(p);
}
// convert length to distance
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
}
function addRoutePoint(point) {
const controlPoints = debug.select(".controlPoints").size()
? debug.select(".controlPoints")
: debug.append("g").attr("class", "controlPoints");
controlPoints.append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", 0.35)
.call(d3.drag().on("drag", routePointDrag))
.on("click", function(d) {
if ($("#routeSplit").hasClass('pressed')) {
routeSplitInPoint(this);
} else {
$(this).remove();
routeRedraw();
}
});
}
function routePointDrag() {
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
routeRedraw();
}
function routeRedraw() {
let points = [];
debug.select(".controlPoints").selectAll("circle").each(function() {
const el = d3.select(this);
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
});
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
elSelected.attr("d", lineGen(points));
// get route distance
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
}
function addNewRoute() {
let routeType = elSelected && elSelected.node() ? elSelected.node().parentNode.id : "searoutes";
const group = routes.select("#"+routeType);
const id = routeType + "" + group.selectAll("*").size();
elSelected = group.append("path").attr("data-route", "new").attr("id", id).on("click", editRoute);
routeUpdateGroups();
$("#routeEditor").dialog({
title: "Edit Route", minHeight: 30, width: "auto", resizable: false,
close: function() {
if ($("#addRoute").hasClass('pressed')) completeNewRoute();
if ($("#routeSplit").hasClass('pressed')) $("#routeSplit").removeClass('pressed');
unselect();
}
});
}
function newRouteAddPoint() {
const point = d3.mouse(this);
const x = rn(point[0],2), y = rn(point[1],2);
addRoutePoint({x, y});
routeRedraw();
}
function completeNewRoute() {
$("#routeNew, #addRoute").removeClass('pressed');
restoreDefaultEvents();
if (!elSelected.size()) return;
if (elSelected.attr("data-route") === "new") {
routeRedraw();
elSelected.attr("data-route", "");
const node = elSelected.node();
const l = node.getTotalLength();
let pathCells = [];
for (let i = 0; i <= l; i ++) {
const p = node.getPointAtLength(i);
const cell = diagram.find(p.x, p.y);
if (!cell) {return;}
pathCells.push(cell.index);
}
const uniqueCells = [...new Set(pathCells)];
uniqueCells.map(function(c) {
if (cells[c].path !== undefined) {cells[c].path += 1;}
else {cells[c].path = 1;}
});
}
tip("", true);
}
function routeUpdateGroups() {
routeGroup.innerHTML = "";
routes.selectAll("g").each(function() {
const opt = document.createElement("option");
opt.value = opt.innerHTML = this.id;
routeGroup.add(opt);
});
}
function routeSplitInPoint(clicked) {
const group = d3.select(elSelected.node().parentNode);
$("#routeSplit").removeClass('pressed');
const points1 = [],points2 = [];
let points = points1;
debug.select(".controlPoints").selectAll("circle").each(function() {
const el = d3.select(this);
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
if (this === clicked) {
points = points2;
points.push({scX: +el.attr("cx"), scY: +el.attr("cy")});
}
el.remove();
});
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
elSelected.attr("d", lineGen(points1));
const id = routeGroup.value + "" + group.selectAll("*").size();
group.append("path").attr("id", id).attr("d", lineGen(points2)).on("click", editRoute);
routeDrawPoints();
}
$("#routeGroup").change(function() {
$(elSelected.node()).detach().appendTo($("#"+this.value));
});
// open legendsEditor
document.getElementById("routeLegend").addEventListener("click", function() {
let id = elSelected.attr("id");
editLegends(id, id);
});
$("#routeNew").click(function() {
if ($(this).hasClass('pressed')) {
completeNewRoute();
} else {
// enter creation mode
$(".pressed").removeClass('pressed');
$("#routeNew, #addRoute").addClass('pressed');
debug.select(".controlPoints").selectAll("*").remove();
addNewRoute();
viewbox.style("cursor", "crosshair").on("click", newRouteAddPoint);
tip("Click on map to add route point", true);
}
});
$("#routeRemove").click(function() {
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
$("#alert").dialog({resizable: false, title: "Remove route",
buttons: {
Remove: function() {
$(this).dialog("close");
elSelected.remove();
$("#routeEditor").dialog("close");
},
Cancel: function() {$(this).dialog("close");}
}
})
});
}