// heightmap-editor module. To be added to window as for now
"use strict";
function editHeightmap() {
void function selectEditMode() {
alertMessage.innerHTML = `
Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to erase the secondary data and let the system automatically regenerate it on edit completion.
You can also keep all the data as is, but you won't be able to change the coastline.
If you need to change the coastline and keep the data, you may try the risk edit option.
The secondary data will be kept with burgs placed on water being removed,
but the landmass change can cause unexpected data fluctuation and errors.
`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300,
buttons: {
Erase: function() {enterHeightmapEditMode("erase");},
Keep: function() {enterHeightmapEditMode("keep");},
Risk: function() {enterHeightmapEditMode("risk");},
Cancel: function() {$(this).dialog("close");}
}
});
}()
let edits = [];
restartHistory();
if (modules.editHeightmap) return;
modules.editHeightmap = true;
// add listeners
document.getElementById("paintBrushes").addEventListener("click", openBrushesPanel);
document.getElementById("applyTemplate").addEventListener("click", openTemplateEditor);
document.getElementById("convertImage").addEventListener("click", openImageConverter);
document.getElementById("heightmapPreview").addEventListener("click", toggleHeightmapPreview);
document.getElementById("perspectiveView").addEventListener("click", openPerspectivePanel);
document.getElementById("finalizeHeightmap").addEventListener("click", finalizeHeightmap);
document.getElementById("renderOcean").addEventListener("click", mockHeightmap);
document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
function enterHeightmapEditMode(type) {
editHeightmap.layers = getLayersState();
customization = 1;
closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
customizationMenu.style.display = "block";
toolsContent.style.display = "none";
heightmapEditMode.innerHTML = type;
if (type === "erase") {
terrs.attr("mask", null);
undraw();
changeOnlyLand.checked = false;
} else if (type === "keep") {
viewbox.selectAll("#landmass, #lakes").attr("display", "none");
changeOnlyLand.checked = true;
} else if (type === "risk") {
terrs.attr("mask", null);
defs.selectAll("#land, #water").selectAll("path").remove();
viewbox.selectAll("#coastline *, #lakes *, #oceanLayers path").remove();
changeOnlyLand.checked = false;
}
// hide convert and template buttons for the Keep mode
applyTemplate.style.display = type === "keep" ? "none" : "inline-block";
convertImage.style.display = type === "keep" ? "none" : "inline-block";
openBrushesPanel();
turnButtonOn("toggleHeight");
layersPreset.value = "heightmap";
layersPreset.disabled = true;
mockHeightmap();
viewbox.on("touchmove mousemove", moveCursor);
}
function getLayersState() {
const layers = [];
mapLayers.querySelectorAll("li").forEach(l => {
if (l.id === "toggleScaleBar") return;
if (!l.classList.contains("buttonoff")) {layers.push(l.id); l.click();}
});
return layers;
}
function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]);
heightmapInfoY.innerHTML = rn(p[1]);
heightmapInfoCell.innerHTML = cell;
heightmapInfoHeight.innerHTML = grid.cells.h[cell];
tip("Height: " + getFriendlyHeight(grid.cells.h[cell]));
// move radius circle if drag mode is active
const pressed = document.querySelector("#brushesButtons > button.pressed");
if (!pressed) return;
moveCircle(p[0], p[1], brushRadius.valueAsNumber, "#333");
}
// Exit customization mode
function finalizeHeightmap() {
if (terrs.selectAll("*").size() < 200) {
tip("Insufficient land area! There should be at least 200 land cells to finalize the heightmap", null, "error");
return;
}
customization = 0;
customizationMenu.style.display = "none";
toolsContent.style.display = "block";
restoreDefaultEvents();
clearMainTip();
closeDialogs();
resetZoom();
restartHistory();
if (document.getElementById("preview")) document.getElementById("preview").remove();
const mode = heightmapEditMode.innerHTML;
if (mode === "erase") regenerateErasedData();
else if (mode === "keep") restoreKeptData();
else if (mode === "risk") restoreRiskedData();
terrs.selectAll("*").remove();
turnButtonOff("toggleHeight");
changePreset("landmass");
editHeightmap.layers.forEach(l => document.getElementById(l).click());
layersPreset.disabled = false;
}
function regenerateErasedData() {
console.group("Edit Heightmap");
console.time("regenerateErasedData");
terrs.attr("mask", "url(#land)");
const change = changeHeights.checked;
markFeatures();
if (change) openNearSeaLakes();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
elevateLakes();
Rivers.generate();
if (!change) {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
if (pack.cells.h[i] !== grid.cells.h[g] && pack.cells.h[i] >= 20 === grid.cells.h[g] >= 20) pack.cells.h[i] = grid.cells.h[g];
}
}
defineBiomes();
rankCells();
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
BurgsAndStates.drawStateLabels();
console.timeEnd("regenerateErasedData");
console.groupEnd("Edit Heightmap");
}
function restoreKeptData() {
viewbox.selectAll("#landmass, #lakes").attr("display", null);
for (const i of pack.cells.i) {
pack.cells.h[i] = grid.cells.h[pack.cells.g[i]];
}
}
function restoreRiskedData() {
console.group("Edit Heightmap");
console.time("restoreRiskedData");
terrs.attr("mask", "url(#land)");
// assign pack data to grid cells
const change = changeHeights.checked;
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const conf = new Uint8Array(l);
const culture = new Int8Array(l);
const fl = new Uint16Array(l);
const pop = new Uint16Array(l);
const r = new Uint16Array(l);
const road = new Uint16Array(l);
const s = new Uint16Array(l);
const state = new Uint8Array(l);
const burg = new Uint8Array(l);
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
biome[g] = pack.cells.biome[i];
conf[g] = pack.cells.conf[i];
culture[g] = pack.cells.culture[i];
fl[g] = pack.cells.fl[i];
pop[g] = pack.cells.pop[i];
r[g] = pack.cells.r[i];
road[g] = pack.cells.road[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
burg[g] = pack.cells.burg[i];
}
// do not allow to remove land with burgs
for (const i of grid.cells.i) {
if (!burg[i]) continue;
if (grid.cells.h[i] < 20) grid.cells.h[i] = 20;
}
markFeatures();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
reGraph();
drawCoastline();
if (change) {
elevateLakes();
Rivers.generate();
defineBiomes();
}
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.burg = new Uint16Array(n);
pack.cells.culture = new Int8Array(n);
pack.cells.pop = new Uint16Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.s = new Uint16Array(n);
pack.cells.state = new Uint8Array(n);
if (!change) {
pack.cells.r = new Uint16Array(n);
pack.cells.conf = new Uint8Array(n);
pack.cells.fl = new Uint16Array(n);
pack.cells.biome = new Uint8Array(n);
}
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
const land = pack.cells.h[i] >= 20;
if (!change) {
pack.cells.r[i] = r[g];
pack.cells.conf[i] = conf[g];
pack.cells.fl[i] = fl[g];
if (land && !biome[g]) pack.cells.biome[i] = getBiomeId(grid.cells.prec[g], grid.cells.temp[g]); else
if (!land && biome[g]) pack.cells.biome[i] = 0; else
pack.cells.biome[i] = biome[g];
}
if (!land) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
}
for (const b of pack.burgs) {
if (!b.i) continue;
b.cell = findCell(b.x, b.y);
b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i;
if (!b.capital && pack.cells.h[b.cell] < 20) removeBurg(b.i);
if (b.capital) pack.states[b.state].center = b.cell;
}
console.timeEnd("restoreRiskedData");
console.groupEnd("Edit Heightmap");
}
// trigger heightmap redraw and history update if at least 1 cell is changed
function updateHeightmap() {
const prev = last(edits);
const changed = grid.cells.h.reduce((s, h, i) => h !== prev[i] ? s+1 : s, 0);
tip("Cells changed: " + changed);
if (!changed) return;
// check ocean cells are not checged if olny land edit is allowed
if (changeOnlyLand.checked) {
for (const i of grid.cells.i) {
if (prev[i] < 20 || grid.cells.h[i] < 20) grid.cells.h[i] = prev[i];
}
}
mockHeightmap();
updateHistory();
}
// draw or update heightmap
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
terrs.selectAll("polygon").data(data).join("polygon").attr("points", d => getGridPolygon(d))
.attr("id", d => "cell"+d).attr("fill", d => getColor(grid.cells.h[d], scheme));
}
// draw or update heightmap for a selection of cells
function mockHeightmapSelection(selection) {
const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function(i) {
let cell = terrs.select("#cell"+i);
if (!ocean && grid.cells.h[i] < 20) {cell.remove(); return;}
if (!cell.size()) cell = terrs.append("polygon").attr("points", getGridPolygon(i)).attr("id", "cell"+i);
cell.attr("fill", getColor(grid.cells.h[i], scheme));
});
}
function updateStatistics() {
const landCells = grid.cells.h.reduce((s, h) => h >= 20 ? s+1 : s);
landmassCounter.innerHTML = `${landCells} (${rn(landCells/grid.cells.i.length*100)}%)`;
landmassAverage.innerHTML = rn(d3.mean(grid.cells.h));
}
function updateHistory(noStat) {
const step = edits.n;
edits = edits.slice(0, step);
edits[step] = grid.cells.h.slice();
edits.n = step + 1;
undo.disabled = templateUndo.disabled = edits.n <= 1;
redo.disabled = templateRedo.disabled = true;
if (!noStat) updateStatistics();
if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened
if ($("#perspectivePanel").is(":visible")) drawPerspective(); // update perspective view if opened
}
// restoreHistory
function restoreHistory(step) {
edits.n = step;
redo.disabled = templateRedo.disabled = edits.n >= edits.length;
undo.disabled = templateUndo.disabled = edits.n <= 1;
if (edits[edits.n - 1] === undefined) return;
grid.cells.h = edits[edits.n - 1].slice();
mockHeightmap();
updateStatistics();
if (document.getElementById("preview")) drawHeightmapPreview(); // update heightmap preview if opened
if ($("#perspectivePanel").is(":visible")) drawPerspective(); // update perspective view if opened
}
// restart edits from 1st step
function restartHistory() {
edits = [];
edits.n = 0;
redo.disabled = templateRedo.disabled = true;
undo.disabled = templateUndo.disabled = true;
updateHistory();
}
function openBrushesPanel() {
if ($("#brushesPanel").is(":visible")) return;
$("#brushesPanel").dialog({
title: "Paint Brushes", minHeight: 40, width: "auto", maxWidth: 200, resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
}).on('dialogclose', exitBrushMode);
if (modules.openBrushesPanel) return;
modules.openBrushesPanel = true;
// add listeners
document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e));
document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e));
document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
document.getElementById("rescaleShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleSection").style.display = "block";
});
document.getElementById("rescaleHide").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "block";
document.getElementById("rescaleSection").style.display = "none";
});
document.getElementById("rescaler").addEventListener("change", (e) => rescale(e.target.valueAsNumber));
document.getElementById("rescaleCondShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleCondSection").style.display = "block";
});
document.getElementById("rescaleCondHide").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "block";
document.getElementById("rescaleCondSection").style.display = "none";
});
document.getElementById("rescaleExecute").addEventListener("click", rescaleWithCondition);
document.getElementById("smoothHeights").addEventListener("click", smoothAllHeights);
document.getElementById("disruptHeights").addEventListener("click", disruptAllHeights);
document.getElementById("brushClear").addEventListener("click", startFromScratch);
function exitBrushMode() {
const pressed = document.querySelector("#brushesButtons > button.pressed");
if (!pressed) return;
pressed.classList.remove("pressed");
viewbox.style("cursor", "default").on(".drag", null);
removeCircle();
document.getElementById("brushesSliders").style.display = "none";
}
function toggleBrushMode(e) {
if (e.target.classList.contains("pressed")) {exitBrushMode(); return;}
exitBrushMode();
document.getElementById("brushesSliders").style.display = "block";
e.target.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragBrush));
}
function dragBrush() {
const r = brushRadius.valueAsNumber;
const point = d3.mouse(this);
const start = findGridCell(point[0], point[1]);
d3.event.on("drag", () => {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r, "#333");
const inRadius = findGridAll(p[0], p[1], r);
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius;
if (selection && selection.length) changeHeightForSelection(selection, start);
});
d3.event.on("end", updateHeightmap);
}
function changeHeightForSelection(s, start) {
const power = brushPower.valueAsNumber;
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {return Math.max(Math.min(v, 100), land ? 20 : 0);}
const h = grid.cells.h;
const brush = document.querySelector("#brushesButtons > button.pressed").id;
if (brush === "brushRaise") s.forEach(i => h[i] = h[i] < 20 ? 20 : lim(h[i] + power)); else
if (brush === "brushElevate") s.forEach((i,d) => h[i] = lim(h[i] + interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushLower") s.forEach(i => h[i] = lim(h[i] - power)); else
if (brush === "brushDepress") s.forEach((i,d) => h[i] = lim(h[i] - interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushAlign") s.forEach(i => h[i] = lim(h[start])); else
if (brush === "brushSmooth") s.forEach(i => h[i] = rn((d3.mean(grid.cells.c[i].filter(i => land ? h[i] >= 20 : 1).map(c => h[c])) + h[i]*(10-power)) / (11-power),1)); else
if (brush === "brushDisrupt") s.forEach(i => h[i] = h[i] < 17 ? h[i] : lim(h[i] + power/2 - Math.random()*power));
mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step
}
function changeOnlyLandClick(e) {
if (heightmapEditMode.innerHTML !== "keep") return;
e.preventDefault();
tip("You cannot change the coastline in 'Keep' edit mode", false, "error");
}
function rescale(v) {
const land = changeOnlyLand.checked;
grid.cells.h = grid.cells.h.map(h => land && (h < 20 || h+v < 20) ? h : lim(h+v));
updateHeightmap();
document.getElementById("rescaler").value = 0;
}
function rescaleWithCondition() {
const range = rescaleLower.value + "-" + rescaleHigher.value;
const operator = conditionSign.value;
const operand = rescaleModifier.valueAsNumber;
if (Number.isNaN(operand)) {tip("Operand should be a number", false, "error"); return;}
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) {tip("Operand should be an integer", false, "error"); return;}
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); else
if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); else
if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0); else
if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); else
if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
updateHeightmap();
}
function smoothAllHeights() {
HeightmapGenerator.smooth(4);
updateHeightmap();
}
function disruptAllHeights() {
grid.cells.h = grid.cells.h.map(h => h < 17 ? h : lim(h + 2 - Math.random()*4));
updateHeightmap();
}
function startFromScratch() {
if (changeOnlyLand.checked) {tip("Not allowed when 'Change only land cells' mode is set", false, "error"); return;}
const someHeights = grid.cells.h.some(h => h);
if (!someHeights) {tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); return;}
grid.cells.h = new Uint8Array(grid.cells.i.length);
terrs.selectAll("*").remove();
updateHistory();
}
}
function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return;
$("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "div:not(.elType)"});
// add listeners
document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e));
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return;
const type = e.target.id.replace("template", "");
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 1);
addStep(type);
}
function addStep(type, count, dist, arg4, arg5) {
const body = document.getElementById("templateBody");
body.insertAdjacentHTML("beforeend", getStepHTML(type, count, dist, arg4, arg5));
const elDist = body.querySelector("div:last-child").querySelector(".templateDist");
if (elDist) elDist.addEventListener("change", setRange);
if (dist && elDist && elDist.tagName === "SELECT") {
for (const o of elDist.options) {if (o.value === dist) elDist.value = dist;}
if (elDist.value !== dist) {
const opt = document.createElement("option");
opt.value = opt.innerHTML = dist;
elDist.add(opt);
elDist.value = dist;
}
}
}
function getStepHTML(type, count, arg3, arg4, arg5) {
const Trash = ``;
const TempY = `y:`;
const TempX = `x:`;
const Height = `h:`;
const Count = `n:`;
const Type = `
${type}
`;
const blob = `
${Type}${Trash}${TempY}${TempX}${Height}${Count}
`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `