"use strict"; function editBiomes() { if (customization) return; closeDialogs("#biomesEditor, .stable"); if (!layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleStates")) toggleStates(); if (layerIsOn("toggleCultures")) toggleCultures(); if (layerIsOn("toggleReligions")) toggleReligions(); if (layerIsOn("toggleProvinces")) toggleProvinces(); const body = document.getElementById("biomesBody"); const animate = d3.transition().duration(2000).ease(d3.easeSinIn); refreshBiomesEditor(); if (modules.editBiomes) return; modules.editBiomes = true; $("#biomesEditor").dialog({ title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor, position: {my: "right top", at: "right-10 top+10", of: "svg"} }); // add listeners document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor); document.getElementById("biomesEditStyle").addEventListener("click", () => editStyle("biomes")); document.getElementById("biomesLegend").addEventListener("click", toggleLegend); document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode); document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode); document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange); document.getElementById("biomesManuallyCancel").addEventListener("click", () => exitBiomesCustomizationMode()); document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes); document.getElementById("biomesAdd").addEventListener("click", addCustomBiome); document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons); document.getElementById("biomesExport").addEventListener("click", downloadBiomesData); body.addEventListener("click", function(ev) { const el = ev.target, cl = el.classList; if (cl.contains("fillRect")) biomeChangeColor(el); else if (cl.contains("icon-info-circled")) openWiki(el); else if (cl.contains("icon-trash-empty")) removeCustomBiome(el); if (customization === 6) selectBiomeOnLineClick(el); }); body.addEventListener("change", function(ev) { const el = ev.target, cl = el.classList; if (cl.contains("biomeName")) biomeChangeName(el); else if (cl.contains("biomeHabitability")) biomeChangeHabitability(el); }); function refreshBiomesEditor() { biomesCollectStatistics(); biomesEditorAddLines(); } function biomesCollectStatistics() { const cells = pack.cells; const array = new Uint8Array(biomesData.i.length); biomesData.cells = Array.from(array); biomesData.area = Array.from(array); biomesData.rural = Array.from(array); biomesData.urban = Array.from(array); for (const i of cells.i) { if (cells.h[i] < 20) continue; const b = cells.biome[i]; biomesData.cells[b] += 1; biomesData.area[b] += cells.area[i]; biomesData.rural[b] += cells.pop[i]; if (cells.burg[i]) biomesData.urban[b] += pack.burgs[cells.burg[i]].population; } } function biomesEditorAddLines() { const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; const b = biomesData; let lines = "", totalArea = 0, totalPopulation = 0;; for (const i of b.i) { if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes const area = b.area[i] * distanceScaleInput.value ** 2; const rural = b.rural[i] * populationRate.value; const urban = b.urban[i] * populationRate.value * urbanization.value; const population = rn(rural + urban); const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`; totalArea += area; totalPopulation += population; lines += `
%
${b.cells[i]}
${si(area) + unit}
${si(population)}
${i>12 && !b.cells[i] ? '' : ''}
`; } body.innerHTML = lines; // update footer biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length; biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length; biomesFooterArea.innerHTML = si(totalArea) + unit; biomesFooterPopulation.innerHTML = si(totalPopulation); biomesFooterArea.dataset.area = totalArea; biomesFooterPopulation.dataset.population = totalPopulation; // add listeners body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev))); body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev))); if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();} applySorting(biomesHeader); $("#biomesEditor").dialog({width: fitContent()}); } function biomeHighlightOn(event) { if (customization === 6) return; const biome = +event.target.dataset.id; biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11"); } function biomeHighlightOff(event) { if (customization === 6) return; const biome = +event.target.dataset.id; const color = biomesData.color[biome]; biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color); } function biomeChangeColor(el) { const currentFill = el.getAttribute("fill"); const biome = +el.parentNode.parentNode.dataset.id; const callback = function(fill) { el.setAttribute("fill", fill); biomesData.color[biome] = fill; biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill); } openPicker(currentFill, callback); } function biomeChangeName(el) { const biome = +el.parentNode.dataset.id; el.parentNode.dataset.name = el.value; biomesData.name[biome] = el.value; } function biomeChangeHabitability(el) { const biome = +el.parentNode.dataset.id; const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999; if (failed) { el.value = biomesData.habitability[biome]; tip("Please provide a valid number in range 0-9999", false, "error"); return; } biomesData.habitability[biome] = +el.value; el.parentNode.dataset.habitability = el.value; recalculatePopulation(); refreshBiomesEditor(); } function openWiki(el) { const name = el.parentNode.dataset.name; if (name === "Custom" || !name) {tip("Please provide a biome name", false, "error"); return;} const wiki = "https://en.wikipedia.org/wiki/"; switch (name) { case "Hot desert": openURL(wiki + "Desert_climate#Hot_desert_climates"); case "Cold desert": openURL(wiki + "Desert_climate#Cold_desert_climates"); case "Savanna": openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands"); case "Grassland": openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands"); case "Tropical seasonal forest": openURL(wiki + "Seasonal_tropical_forest"); case "Temperate deciduous forest": openURL(wiki + "Temperate_deciduous_forest"); case "Tropical rainforest": openURL(wiki + "Tropical_rainforest"); case "Temperate rainforest": openURL(wiki + "Temperate_rainforest"); case "Taiga": openURL(wiki + "Taiga"); case "Tundra": openURL(wiki + "Tundra"); case "Glacier": openURL(wiki + "Glacier"); case "Wetland": openURL(wiki + "Wetland"); default: openURL(`https://en.wikipedia.org/w/index.php?search=${name}`); } } function toggleLegend() { if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend const d = biomesData; const data = Array.from(d.i).filter(i => d.cells[i]).sort((a, b) => d.area[b] - d.area[a]).map(i => [i, d.color[i], d.name[i]]); drawLegend("Biomes", data); } function togglePercentageMode() { if (body.dataset.type === "absolute") { body.dataset.type = "percentage"; const totalCells = +biomesFooterCells.innerHTML; const totalArea = +biomesFooterArea.dataset.area; const totalPopulation = +biomesFooterPopulation.dataset.population; body.querySelectorAll(":scope> div").forEach(function(el) { el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%"; el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%"; el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%"; }); } else { body.dataset.type = "absolute"; biomesEditorAddLines(); } } function addCustomBiome() { const b = biomesData, i = biomesData.i.length; if (i > 254) {tip("Maximum number of biomes reached (255), data cleansing is required", false, "error"); return;} b.i.push(i); b.color.push(getRandomColor()); b.habitability.push(50); b.name.push("Custom"); b.iconsDensity.push(0); b.icons.push([]); b.cost.push(50); b.rural.push(0); b.urban.push(0); b.cells.push(0); b.area.push(0); const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; const line = `
%
${b.cells[i]}
0 ${unit}
0
`; body.insertAdjacentHTML("beforeend", line); biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length; $("#biomesEditor").dialog({width: fitContent()}); } function removeCustomBiome(el) { const biome = +el.parentNode.dataset.id; el.parentNode.remove(); biomesData.name[biome] = "removed"; biomesFooterBiomes.innerHTML = +biomesFooterBiomes.innerHTML - 1; } function regenerateIcons() { ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief(); } function downloadBiomesData() { const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers body.querySelectorAll(":scope > div").forEach(function(el) { data += el.dataset.id + ","; data += el.dataset.name + ","; data += el.dataset.color + ","; data += el.dataset.habitability + "%,"; data += el.dataset.cells + ","; data += el.dataset.area + ","; data += el.dataset.population + "\n"; }); const name = getFileName("Biomes") + ".csv"; downloadFile(data, name); } function enterBiomesCustomizationMode() { if (!layerIsOn("toggleBiomes")) toggleBiomes(); customization = 6; biomes.append("g").attr("id", "temp"); document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none"); document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block"); body.querySelector("div.biomes").classList.add("selected"); biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none"); biomesFooter.style.display = "none"; $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}}); tip("Click on biome to select, drag the circle to change biome", true); viewbox.style("cursor", "crosshair") .on("click", selectBiomeOnMapClick) .call(d3.drag().on("start", dragBiomeBrush)) .on("touchmove mousemove", moveBiomeBrush); } function selectBiomeOnLineClick(line) { const selected = body.querySelector("div.selected"); if (selected) selected.classList.remove("selected"); line.classList.add("selected"); } function selectBiomeOnMapClick() { const point = d3.mouse(this); const i = findCell(point[0], point[1]); if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;} const assigned = biomes.select("#temp").select("polygon[data-cell='"+i+"']"); const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i]; body.querySelector("div.selected").classList.remove("selected"); body.querySelector("div[data-id='"+biome+"']").classList.add("selected"); } function dragBiomeBrush() { const r = +biomesManuallyBrush.value; d3.event.on("drag", () => { if (!d3.event.dx && !d3.event.dy) return; const p = d3.mouse(this); moveCircle(p[0], p[1], r); const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; const selection = found.filter(isLand); if (selection) changeBiomeForSelection(selection); }); } // change region within selection function changeBiomeForSelection(selection) { const temp = biomes.select("#temp"); const selected = body.querySelector("div.selected"); const biomeNew = selected.dataset.id; const color = biomesData.color[biomeNew]; selection.forEach(function(i) { const exists = temp.select("polygon[data-cell='"+i+"']"); const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i]; if (biomeNew === biomeOld) return; // change of append new element if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color); else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color); }); } function moveBiomeBrush() { showMainTip(); const point = d3.mouse(this); const radius = +biomesManuallyBrush.value; moveCircle(point[0], point[1], radius); } function applyBiomesChange() { const changed = biomes.select("#temp").selectAll("polygon"); changed.each(function() { const i = +this.dataset.cell; const b = +this.dataset.biome; pack.cells.biome[i] = b; }); if (changed.size()) { drawBiomes(); refreshBiomesEditor(); } exitBiomesCustomizationMode(); } function exitBiomesCustomizationMode(close) { customization = 0; biomes.select("#temp").remove(); removeCircle(); document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block"); document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none"); body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all"); biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden")); biomesFooter.style.display = "block"; if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}}); restoreDefaultEvents(); clearMainTip(); const selected = document.querySelector("#biomesBody > div.selected"); if (selected) selected.classList.remove("selected"); } function restoreInitialBiomes() { biomesData = applyDefaultBiomesSystem(); defineBiomes(); drawBiomes(); recalculatePopulation(); refreshBiomesEditor(); } function closeBiomesEditor() { exitBiomesCustomizationMode("close"); } }