Fantasy-Map-Generator/modules/ui/zones-editor.js
Ángel Montero Lamas 81f037004c
Color picker hatches 14-60 (#726)
* Color picker hatches 14-60

Added hatches to number 14 to 60.
Updated the code of the color picker to accept multiples rows of hatches.

Changed the size of fillRectPointer from 0.9 em to 1.5 em.

Added an Update code to upgrade maps to 1.72 to have a grid of 60 svg hatches. Without the update, loaded files appeared with the rectangles for hatches 14-60 empty.

* Hatches ordered

Reworked the code to put back rows of 14 hatches. Reordered the first 14 hatches to defaault.
Made 3 rows, so 42 hatches.

* Cleaned code and fixes

Cleaned </pattern> in index.html line 152.
Put back: width=".9em" height=".9em" style="margin-bottom:-1px"> for fill.

* mouseover hatches

Fixed some typos. Changed mousemove for mouseover and moved the calling of the function tip from the each function to the original place at 519.
The arrow function changed to one that tells the id.

* Copies hatching from defElements into map svg, and hatching removed from saved map files.

* Removed haching completely from map svg

* Hatching copy for clone is now done before unused pattern removal

* Added back code that removes the unused hatching group

Co-authored-by: Evolvedexperiment <evolvedexperiment@gmail.com>
Co-authored-by: Azgaar <maxganiev@yandex.ru>
2022-02-06 21:56:16 +03:00

458 lines
18 KiB
JavaScript

"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 += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}"
data-cells=${c.length} data-area=${area} data-population=${population}>
<fill-box fill="${fill}"></fill-box>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
});
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) % 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 = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<fill-box fill="${fill}"></fill-box>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">0</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">0 ${unit}</div>
<span class="icon-male hide"></span>
<div class="culturePopulation hide">0</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin inactive hide placeholder"></span>
<span data-tip="Toggle zone visibility" class="icon-eye hide placeholder"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
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: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"}>
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
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();
}
}