"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("zonesFilterType").addEventListener("change", refreshZonesEditor); document.getElementById("zonesFilterButton").addEventListener("click", toggleFilterTable); document.getElementById("zonesEditorRefresh").addEventListener("click", refreshZonesEditor); 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("zonesEditTypes").addEventListener("click", addZonesDialog); document.getElementById("zonesNewTypeButton").addEventListener("click", addZonesType); 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); }); function refreshZonesEditor() { updateSVG(); zonesEditorAddLines(); } function updateSVG() { const value = document.getElementById("zonesFilterType").value; zones.selectAll("g").each(function () { if (value == "All" || this.dataset.type == value) { this.style.display = "block"; } else { this.style.display = "none"; } }); } function getZoneTypesList(zoneId, currentType) { let res = `'; return res; } // add line for each zone function zonesEditorAddLines() { const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value; let lines = ""; // make sure all zone types are loaded from the SVG zones.selectAll("g").each(function () { const zoneType = this.dataset.type; if (!zoneTypes.includes(zoneType)) { zoneTypes.push(zoneType); } }); const selectedType = zonesFilterType.value || "All"; zonesFilterType.options.length=0; zonesFilterType.options.add(new Option("All", "All", false, selectedType=="All")); zoneTypes.forEach(function(z, i) { zonesFilterType.options.add(new Option(z, z, false, selectedType==z)); }); let zoneCount=0; zones.selectAll("g").each(function () { zoneCount++; const zoneType = this.dataset.type; if (selectedType !== "All" && (zonesFilterButton.classList.contains("pressed") && zoneType !== selectedType)) return; 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 zoneTypeList = getZoneTypesList(this.id, this.dataset.type); 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)}
${zoneTypeList}
`; }); body.innerHTML = lines; if (body.innerHTML === "") { body.innerHTML = `
Zero entries for this type. To see entries again, select "All" or disable the filter button
`; } for (let i=0; i !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 toggleFilterTable() { this.classList.toggle("pressed"); zonesEditorAddLines(); } function addZonesLayer() { const id = getNextId("zone"); const description = "Unknown zone"; const fill = "url(#hatch" + (id.slice(4) % 42) + ")"; 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 zonesTypesAddLines() { const zoneTypeListBody = document.getElementById("zonesTypesBodySection"); let lines = ""; zoneTypes.forEach(function(z, i) { let count=0; // Amount of zones per type zones.selectAll("g").each(function() { if (this.dataset.type === z) count++; }); lines += `
${z}${count}`; if (i > 5) { let id="removeZoneType" + i; lines += ``; } lines += '
'; }); zoneTypeListBody.innerHTML = lines; zonesTypesFooterNumber.innerHTML = zoneTypes.length; for (let i=0; i