"use strict"; function editZones() { closeDialogs(); if (!layerIsOn("toggleZones")) toggleZones(); const body = document.getElementById("zonesBodySection"); zonesEditorAddLines(); if (modules.editZones) return; modules.editZones = true; $("#zonesEditor").dialog({ title: "Zones Editor", resizable: false, width: fitContent(), close: () => exitZonesManualAssignment("close"), position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); // add listeners document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines); document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones")); document.getElementById("zonesLegend").addEventListener("click", toggleLegend); document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode); document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent); document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent); document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent); document.getElementById("zonesAdd").addEventListener("click", addZonesLayer); document.getElementById("zonesExport").addEventListener("click", downloadZonesData); document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode); body.addEventListener("click", function (ev) { const el = ev.target, cl = el.classList, zone = el.parentNode.dataset.id; if (el.tagName === "FILL-BOX") changeFill(el); else if (cl.contains("culturePopulation")) changePopulation(zone); else if (cl.contains("icon-trash-empty")) zoneRemove(zone); else if (cl.contains("icon-eye")) toggleVisibility(el); else if (cl.contains("icon-pin")) toggleFog(zone, cl); if (customization) selectZone(el); }); body.addEventListener("input", function (ev) { const el = ev.target, zone = el.parentNode.dataset.id; if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value); }); // add line for each zone function zonesEditorAddLines() { const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; let lines = ""; zones.selectAll("g").each(function () { const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : []; const description = this.dataset.description; const fill = this.getAttribute("fill"); const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2; const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate; const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization; const population = rural + urban; const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`; const inactive = this.style.display === "none"; const focused = defs.select("#fog #focus" + this.id).size(); lines += `
${c.length}
${si(area) + unit}
${si(population)}
`; }); body.innerHTML = lines; // update footer const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2); const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate; zonesFooterPopulation.dataset.population = totalPop; zonesFooterNumber.innerHTML = zones.selectAll("g").size(); zonesFooterCells.innerHTML = pack.cells.i.length; zonesFooterArea.innerHTML = si(totalArea) + unit; zonesFooterPopulation.innerHTML = si(totalPop); // add listeners body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev))); if (body.dataset.type === "percentage") { body.dataset.type = "absolute"; togglePercentageMode(); } $("#zonesEditor").dialog({width: fitContent()}); } function zoneHighlightOn(event) { const zone = event.target.dataset.id; zones.select("#" + zone).style("outline", "1px solid red"); } function zoneHighlightOff(event) { const zone = event.target.dataset.id; zones.select("#" + zone).style("outline", null); } $(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone}); function movezone(ev, ui) { const zone = $("#" + ui.item.attr("data-id")); const prev = $("#" + ui.item.prev().attr("data-id")); if (prev) { zone.insertAfter(prev); return; } const next = $("#" + ui.item.next().attr("data-id")); if (next) zone.insertBefore(next); } function enterZonesManualAssignent() { if (!layerIsOn("toggleZones")) toggleZones(); customization = 10; document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none")); document.getElementById("zonesManuallyButtons").style.display = "inline-block"; zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden")); zonesFooter.style.display = "none"; body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none")); $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); tip("Click to select a zone, drag to paint a zone", true); viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush); body.querySelector("div").classList.add("selected"); zones.selectAll("g").each(function () { this.setAttribute("data-init", this.getAttribute("data-cells")); }); } function selectZone(el) { body.querySelector("div.selected").classList.remove("selected"); el.classList.add("selected"); } function selectZoneOnMapClick() { if (d3.event.target.parentElement.parentElement.id !== "zones") return; const zone = d3.event.target.parentElement.id; const el = body.querySelector("div[data-id='" + zone + "']"); selectZone(el); } function dragZoneBrush() { const r = +zonesBrush.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 selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)]; if (!selection) return; const selected = body.querySelector("div.selected"); const zone = zones.select("#" + selected.dataset.id); const base = zone.attr("id") + "_"; // id generic part const dataCells = zone.attr("data-cells"); let cells = dataCells ? dataCells.split(",").map(i => +i) : []; const erase = document.getElementById("zonesRemove").classList.contains("pressed"); if (erase) { // remove selection.forEach(i => { const index = cells.indexOf(i); if (index === -1) return; zone.select("polygon#" + base + i).remove(); cells.splice(index, 1); }); } else { // add selection.forEach(i => { if (cells.includes(i)) return; cells.push(i); zone .append("polygon") .attr("points", getPackPolygon(i)) .attr("id", base + i); }); } zone.attr("data-cells", cells); }); } function moveZoneBrush() { showMainTip(); const point = d3.mouse(this); const radius = +zonesBrush.value; moveCircle(point[0], point[1], radius); } function applyZonesManualAssignent() { zones.selectAll("g").each(function () { if (this.dataset.cells) return; // all zone cells are removed unfog("focusZone" + this.id); this.style.display = "block"; }); zonesEditorAddLines(); exitZonesManualAssignment(); } // restore initial zone cells function cancelZonesManualAssignent() { zones.selectAll("g").each(function () { const zone = d3.select(this); const dataCells = zone.attr("data-init"); const cells = dataCells ? dataCells.split(",").map(i => +i) : []; zone.attr("data-cells", cells); zone.selectAll("*").remove(); const base = zone.attr("id") + "_"; // id generic part zone .selectAll("*") .data(cells) .enter() .append("polygon") .attr("points", d => getPackPolygon(d)) .attr("id", d => base + d); }); exitZonesManualAssignment(); } function exitZonesManualAssignment(close) { customization = 0; removeCircle(); document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block")); document.getElementById("zonesManuallyButtons").style.display = "none"; zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden")); zonesFooter.style.display = "block"; body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all")); if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}}); restoreDefaultEvents(); clearMainTip(); zones.selectAll("g").each(function () { this.removeAttribute("data-init"); }); const selected = body.querySelector("div.selected"); if (selected) selected.classList.remove("selected"); } function changeFill(el) { const fill = el.getAttribute("fill"); const callback = newFill => { el.fill = newFill; document.getElementById(el.parentNode.dataset.id).setAttribute("fill", newFill); }; openPicker(fill, callback); } function toggleVisibility(el) { const zone = zones.select("#" + el.parentNode.dataset.id); const inactive = zone.style("display") === "none"; inactive ? zone.style("display", "block") : zone.style("display", "none"); el.classList.toggle("inactive"); } function toggleFog(z, cl) { const dataCells = zones.select("#" + z).attr("data-cells"); if (!dataCells) return; const path = "M" + dataCells .split(",") .map(c => getPackPolygon(+c)) .join("M") + "Z", id = "focusZone" + z; cl.contains("inactive") ? fog(id, path) : unfog(id); cl.toggle("inactive"); } function toggleLegend() { if (legend.selectAll("*").size()) { clearLegend(); return; } // hide legend const data = []; zones.selectAll("g").each(function () { const id = this.dataset.id; const description = this.dataset.description; const fill = this.getAttribute("fill"); data.push([id, fill, description]); }); drawLegend("Zones", data); } function togglePercentageMode() { if (body.dataset.type === "absolute") { body.dataset.type = "percentage"; const totalCells = +zonesFooterCells.innerHTML; const totalArea = +zonesFooterArea.dataset.area; const totalPopulation = +zonesFooterPopulation.dataset.population; body.querySelectorAll(":scope > div").forEach(function (el) { el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%"; el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%"; el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%"; }); } else { body.dataset.type = "absolute"; zonesEditorAddLines(); } } function addZonesLayer() { const id = getNextId("zone"); const description = "Unknown zone"; const fill = "url(#hatch" + (id.slice(4) % 14) + ")"; zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill); const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; const line = `
0
0 ${unit}
0
`; body.insertAdjacentHTML("beforeend", line); zonesFooterNumber.innerHTML = zones.selectAll("g").size(); } function downloadZonesData() { const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value; let data = "Id,Fill,Description,Cells,Area " + unit + ",Population\n"; // headers body.querySelectorAll(":scope > div").forEach(function (el) { data += el.dataset.id + ","; data += el.dataset.fill + ","; data += el.dataset.description + ","; data += el.dataset.cells + ","; data += el.dataset.area + ","; data += el.dataset.population + "\n"; }); const name = getFileName("Zones") + ".csv"; downloadFile(data, name); } function toggleEraseMode() { this.classList.toggle("pressed"); } function changePopulation(zone) { const dataCells = zones.select("#" + zone).attr("data-cells"); const cells = dataCells ? dataCells .split(",") .map(i => +i) .filter(i => pack.cells.h[i] >= 20) : []; if (!cells.length) { tip("Zone does not have any land cells, cannot change population", false, "error"); return; } const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell)); const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate); const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization); const total = rural + urban; const l = n => Number(n).toLocaleString(); alertMessage.innerHTML = ` Rural: Urban:

Total population: ${l(total)} ⇒ ${l(total)} (100%)

`; const update = function () { const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber; if (isNaN(totalNew)) return; totalPop.innerHTML = l(totalNew); totalPopPerc.innerHTML = rn((totalNew / total) * 100); }; ruralPop.oninput = () => update(); urbanPop.oninput = () => update(); $("#alert").dialog({ resizable: false, title: "Change zone population", width: "24em", buttons: { Apply: function () { applyPopulationChange(); $(this).dialog("close"); }, Cancel: function () { $(this).dialog("close"); } }, position: {my: "center", at: "center", of: "svg"} }); function applyPopulationChange() { const ruralChange = ruralPop.value / rural; if (isFinite(ruralChange) && ruralChange !== 1) { cells.forEach(i => (pack.cells.pop[i] *= ruralChange)); } if (!isFinite(ruralChange) && +ruralPop.value > 0) { const points = ruralPop.value / populationRate; const pop = rn(points / cells.length); cells.forEach(i => (pack.cells.pop[i] = pop)); } const urbanChange = urbanPop.value / urban; if (isFinite(urbanChange) && urbanChange !== 1) { burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4))); } if (!isFinite(urbanChange) && +urbanPop.value > 0) { const points = urbanPop.value / populationRate / urbanization; const population = rn(points / burgs.length, 4); burgs.forEach(b => (b.population = population)); } zonesEditorAddLines(); } } function zoneRemove(zone) { zones.select("#" + zone).remove(); unfog("focusZone" + zone); zonesEditorAddLines(); } }