Fantasy-Map-Generator/modules/ui/provinces-editor.js
2019-09-12 23:24:32 +03:00

521 lines
23 KiB
JavaScript

"use strict";
function editProvinces() {
if (customization) return;
closeDialogs("#provincesEditor, .stable");
if (!layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
const body = document.getElementById("provincesBodySection");
refreshProvincesEditor();
if (modules.editProvinces) return;
modules.editProvinces = true;
$("#provincesEditor").dialog({
title: "Provinces Editor", resizable: false, width: fitContent(), close: closeProvincesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("provincesEditorRefresh").addEventListener("click", refreshProvincesEditor);
document.getElementById("provincesFilterState").addEventListener("change", provincesEditorAddLines);
document.getElementById("provincesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("provincesToggleLabels").addEventListener("click", toggleLabels);
document.getElementById("provincesExport").addEventListener("click", downloadProvincesData);
document.getElementById("provincesRemoveAll").addEventListener("click", removeAllProvinces);
document.getElementById("provincesManually").addEventListener("click", enterProvincesManualAssignent);
document.getElementById("provincesManuallyApply").addEventListener("click", applyProvincesManualAssignent);
document.getElementById("provincesManuallyCancel").addEventListener("click", () => exitProvincesManualAssignment());
document.getElementById("provincesAdd").addEventListener("click", enterAddProvinceMode);
body.addEventListener("click", function(ev) {
if (customization) return;
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("zoneFill")) changeFill(el); else
if (cl.contains("icon-fleur")) provinceOpenCOA(p); else
if (cl.contains("icon-star-empty")) capitalZoomIn(p); else
if (cl.contains("icon-pin")) focusOn(p, cl); else
if (cl.contains("icon-trash-empty")) removeProvince(p);
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(p, line); else
if (cl.contains("hoverButton") && cl.contains("stateForm")) regenerateForm(p, line);
});
body.addEventListener("input", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, p = +line.dataset.id;
if (cl.contains("stateName")) changeName(p, line, el.value); else
if (cl.contains("stateForm")) changeForm(p, line, el.value); else
if (cl.contains("cultureBase")) changeCapital(p, line, el.value);
});
function refreshProvincesEditor() {
collectStatistics();
updateFilter();
provincesEditorAddLines();
}
function collectStatistics() {
const cells = pack.cells, provinces = pack.provinces;
provinces.forEach(p => {
if (!p.i) return;
p.area = p.rural = p.urban = 0;
p.burgs = [];
});
for (const i of cells.i) {
const p = cells.province[i];
if (!p) continue;
provinces[p].area += cells.area[i];
provinces[p].rural += cells.pop[i];
if (!cells.burg[i]) continue;
provinces[p].urban += pack.burgs[cells.burg[i]].population;
provinces[p].burgs.push(cells.burg[i]);
}
}
function updateFilter() {
const stateFilter = document.getElementById("provincesFilterState");
const selectedState = stateFilter.value || 1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option(`all`, -1, false, selectedState == -1));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
statesSorted.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
}
// add line for each state
function provincesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const selectedState = +document.getElementById("provincesFilterState").value;
let filtered = pack.provinces.filter(p => p.i && !p.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter(p => p.state === selectedState); // filtered by state
body.innerHTML = "";
let lines = "", totalArea = 0, totalPopulation = 0;
for (const p of filtered) {
const area = p.area * (distanceScaleInput.value ** 2);
totalArea += area;
const rural = p.rural * populationRate.value;
const urban = p.urban * populationRate.value * urbanization.value;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalPopulation += population;
const stateName = pack.states[p.state].name;
const capital = p.burg ? pack.burgs[p.burg].name : '';
const focused = defs.select("#fog #focusProvince"+p.i).size();
lines += `<div class="states" data-id=${p.i} data-name=${p.name} data-form=${p.formName} data-color="${p.color}" data-capital="${capital}" data-state="${stateName}" data-area=${area} data-population=${population}>
<svg data-tip="Province fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${p.color}" class="zoneFill"></svg>
<input data-tip="Province name. Click and type to change" class="stateName" value="${p.name}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate province name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
<span data-tip="Click to open province COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
<input data-tip="Province form name. Click and type to change" class="stateForm" value="${p.formName}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg?'':'placeholder'}"></span>
<select data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital" class="cultureBase hide ${p.burgs.length?'':'placeholder'}">${p.burgs.length ? getCapitalOptions(p.burgs, p.burg) : ''}</select>
<input data-tip="Province owner" class="provinceOwner" value="${stateName}" disabled hide">
<span data-tip="Province area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Province 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="Toggle province focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
provincesFooterNumber.innerHTML = filtered.length;
provincesFooterArea.innerHTML = filtered.length ? si(totalArea / filtered.length) + unit : 0 + unit;
provincesFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
provincesFooterArea.dataset.area = totalArea;
provincesFooterPopulation.dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectProvinceOnLineClick);
el.addEventListener("mouseenter", ev => provinceHighlightOn(ev));
el.addEventListener("mouseleave", ev => provinceHighlightOff(ev));
});
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(provincesHeader);
$("#provincesEditor").dialog({width: fitContent()});
}
function getCapitalOptions(burgs, capital) {
let options = "";
burgs.forEach(b => options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`);
return options;
}
function provinceHighlightOn(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
if (!layerIsOn("toggleProvinces")) return;
if (customization) return;
const province = +event.target.dataset.id;
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
provs.select("#province"+province).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#d0240f");
}
function provinceHighlightOff(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
if (!layerIsOn("toggleProvinces")) return;
const province = +event.target.dataset.id;
provs.select("#province"+province).transition().attr("stroke-width", null).attr("stroke", null);
}
function changeFill(el) {
const currentFill = el.getAttribute("fill");
const p = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.provinces[p].color = fill;
const g = provs.select("#provincesBody");
g.select("#province"+p).attr("fill", fill);
g.select("#province-gap"+p).attr("stroke", fill);
}
openPicker(currentFill, callback);
}
function provinceOpenCOA(p) {
const url = `https://ironarachne.com/heraldry/${seed}-p${p}`;
window.open(url, '_blank');
}
function capitalZoomIn(p) {
const capital = pack.provinces[p].burg;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function focusOn(p, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focusProvince"+p).size()) return;
fogging.attr("display", "block");
const path = provs.select("#province"+p).attr("d");
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusProvince"+p);
fogging.append("path").attr("d", path).attr("id", "focusProvinceHalo"+p)
.attr("fill", "none").attr("stroke", pack.provinces[p].color).attr("filter", "url(#blur5)");
} else unfocus(p);
}
function unfocus(p) {
defs.select("#focusProvince"+p).remove();
fogging.select("#focusProvinceHalo"+p).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all items are de-focused
}
function removeProvince(p) {
pack.cells.province.forEach((province, i) => {if(province === p) pack.cells.province[i] = 0;});
const state = pack.provinces[p].state;
if (pack.states[state].provinces.includes(p)) pack.states[state].provinces.splice(pack.states[state].provinces.indexOf(p), 1);
pack.provinces[p].removed = true;
unfocus(p);
const g = provs.select("#provincesBody");
g.select("#province"+p).remove();
g.select("#province-gap"+p).remove();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
refreshProvincesEditor();
}
function changeName(p, line, value) {
pack.provinces[p].name = line.querySelector(".stateName").value = line.dataset.name = value;
pack.provinces[p].fullName = value + " " + pack.provinces[p].formName;
provs.select("#provinceLabel"+p).text(value);
}
function regenerateName(p, line) {
const c = pack.cells.culture[pack.provinces[p].center];
const name = Names.getState(Names.getCultureShort(c), c);
changeName(p, line, name);
}
function changeForm(p, line, value) {
pack.provinces[p].formName = line.querySelector(".stateForm").value = line.dataset.form = value;
pack.provinces[p].fullName = pack.provinces[p].name + " " + value;
}
function regenerateForm(p, line) {
const forms = ["County", "Earldom", "Shire", "Landgrave", 'Margrave', "Barony", "Province",
"Department", "Governorate", "State", "Canton", "Prefecture", "Parish", "Deanery",
"Council", "District", "Republic", "Territory", "Land", "Region"];
changeForm(p, line, ra(forms));
}
function changeCapital(p, line, value) {
line.dataset.capital = pack.burgs[+value].name;
pack.provinces[p].center = pack.burgs[+value].cell;
pack.provinces[p].burg = +value;
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalArea = +provincesFooterArea.dataset.area;
const totalPopulation = +provincesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
provincesEditorAddLines();
}
}
function toggleLabels() {
const hidden = provs.select("#provinceLabels").style("display") === "none";
provs.select("#provinceLabels").style("display", `${hidden ? "block" : "none"}`);
provs.attr("data-labels", +hidden);
}
function enterProvincesManualAssignent() {
if (!layerIsOn("toggleProvinces")) toggleProvinces();
if (!layerIsOn("toggleBorders")) toggleBorders();
customization = 11;
provs.select("g#provincesBody").append("g").attr("id", "temp");
provs.select("g#provincesBody").append("g").attr("id", "centers")
.attr("fill", "none").attr("stroke", "#ff0000").attr("stroke-width", 1);
document.querySelectorAll("#provincesBottom > *").forEach(el => el.style.display = "none");
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
provincesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
provincesFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click on a province to select, drag the circle to change province", true);
viewbox.style("cursor", "crosshair")
.on("click", selectProvinceOnMapClick)
.call(d3.drag().on("start", dragBrush))
.on("touchmove mousemove", moveBrush);
body.querySelector("div").classList.add("selected");
}
function selectProvinceOnLineClick() {
if (customization !== 11) return;
if (this.parentNode.id !== "provincesBodySection") return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
function selectProvinceOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20 || !pack.cells.state[i]) return;
const assigned = provs.select("g#temp").select("polygon[data-cell='"+i+"']");
const province = assigned.size() ? +assigned.attr("data-province") : pack.cells.province[i];
const editorLine = body.querySelector("div[data-id='"+province+"']");
if (!editorLine) {tip("You cannot select a province if it is not in the Editor list", false, "error"); return;}
body.querySelector("div.selected").classList.remove("selected");
editorLine.classList.add("selected");
}
function dragBrush() {
const r = +provincesManuallyBrush.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) changeForSelection(selection);
});
}
// change province within selection
function changeForSelection(selection) {
const temp = provs.select("#temp"), centers = provs.select("#centers");
const selected = body.querySelector("div.selected");
const provinceNew = +selected.dataset.id;
const state = pack.provinces[provinceNew].state;
const fill = pack.provinces[provinceNew].color || "#ffffff";
const stroke = d3.color(fill).darker(.2).hex();
selection.forEach(i => {
if (!pack.cells.state[i] || pack.cells.state[i] !== state) return;
const exists = temp.select("polygon[data-cell='"+i+"']");
const provinceOld = exists.size() ? +exists.attr("data-province") : pack.cells.province[i];
if (provinceNew === provinceOld) return;
if (i === pack.provinces[provinceOld].center) {
const center = centers.select("polygon[data-center='"+i+"']");
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i));
tip("Province center cannot be assigned to a different region. Please remove the province first", false, "error");
return;
}
// change of append new element
if (exists.size()) exists.attr("data-province", provinceNew).attr("fill", fill).attr("stroke", stroke);
else temp.append("polygon").attr("points", getPackPolygon(i))
.attr("data-cell", i).attr("data-province", provinceNew)
.attr("fill", fill).attr("stroke", stroke);
});
}
function moveBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +provincesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyProvincesManualAssignent() {
provs.select("#temp").selectAll("polygon").each(function() {
const i = +this.dataset.cell;
pack.cells.province[i] = +this.dataset.province;;
});
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
exitProvincesManualAssignment();
refreshProvincesEditor();
}
function exitProvincesManualAssignment(close) {
customization = 0;
provs.select("#temp").remove();
provs.select("#centers").remove();
removeCircle();
document.querySelectorAll("#provincesBottom > *").forEach(el => el.style.display = "inline-block");
document.getElementById("provincesManuallyButtons").style.display = "none";
provincesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
provincesFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddProvinceMode() {
if (this.classList.contains("pressed")) {exitAddProvinceMode(); return;};
customization = 12;
this.classList.add("pressed");
tip("Click on the map to place a new province center", true);
viewbox.style("cursor", "crosshair").on("click", addProvince);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function addProvince() {
const cells = pack.cells, provinces = pack.provinces;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (cells.h[center] < 20) {tip("You cannot place province into the water. Please click on a land cell", false, "error"); return;}
const oldProvince = cells.province[center];
if (oldProvince && provinces[oldProvince].center === center) {tip("The cell is already a center of a different province. Select other cell", false, "error"); return;}
const state = cells.state[center];
if (!state) {tip("You cannot create a province in neutral lands> Please assign this land to a state first", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddProvinceMode();
const province = provinces.length;
pack.states[state].provinces.push(province);
const burg = cells.burg[center];
const c = cells.culture[center];
const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const formName = oldProvince ? provinces[oldProvince].formName : "Province";
const fullName = name + " " + formName;
const stateColor = pack.states[state].color, rndColor = getRandomColor();
const color = stateColor[0] === "#" ? d3.color(d3.interpolate(stateColor, rndColor)(.2)).hex() : rndColor;
provinces.push({i:province, state, center, burg, name, formName, fullName, color});
cells.province[center] = province;
cells.c[center].forEach(c => {
if (cells.h[c] < 20 || cells.state[c] !== state) return;
if (provinces.find(p => !p.removed && p.center === c)) return;
cells.province[c] = province;
});
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn("toggleProvinces")) toggleProvinces(); else drawProvinces();
collectStatistics();
document.getElementById("provincesFilterState").value = state;
provincesEditorAddLines();
}
function exitAddProvinceMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (provincesAdd.classList.contains("pressed")) provincesAdd.classList.remove("pressed");
}
function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Province,Form,State,Color,Capital,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.form + ",";
data += el.dataset.state + ",";
data += el.dataset.color + ",";
data += el.dataset.capital + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = getFileName("Provinces") + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function removeAllProvinces() {
alertMessage.innerHTML = `Are you sure you want to remove all provinces?`;
$("#alert").dialog({resizable: false, title: "Remove all provinces",
buttons: {
Remove: function() {
$(this).dialog("close");
pack.provinces.filter(p => p.i).forEach(p => {
p.removed = true;
unfocus(p.i);
});
pack.cells.i.forEach(i => pack.cells.province[i] = 0);
pack.states.filter(s => s.i && !s.removed).forEach(s => s.provinces = []);
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
provs.select("#provincesBody").remove();
turnButtonOff("toggleProvinces");
provincesEditorAddLines();
},
Cancel: function() {$(this).dialog("close");}
}
});
}
function closeProvincesEditor() {
if (customization === 11) exitProvincesManualAssignment("close");
if (customization === 12) exitAddStateMode();
}
}