mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-18 02:01:22 +01:00
refactor: dynamically load modules
This commit is contained in:
parent
a107c58643
commit
347083291f
46 changed files with 161 additions and 164 deletions
|
|
@ -1,921 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {findAll, findCell, getPackPolygon, isLand} from "utils/graphUtils";
|
||||
import {tip, showMainTip, clearMainTip} from "scripts/tooltips";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {abbreviate} from "utils/languageUtils";
|
||||
import {debounce} from "utils/functionUtils";
|
||||
|
||||
const $body = insertEditorHtml();
|
||||
addListeners();
|
||||
|
||||
const cultureTypes = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
|
||||
|
||||
export function open() {
|
||||
closeDialogs("#culturesEditor, .stable");
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleReligions")) toggleReligions();
|
||||
if (layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
|
||||
refreshCulturesEditor();
|
||||
|
||||
$("#culturesEditor").dialog({
|
||||
title: "Cultures Editor",
|
||||
resizable: false,
|
||||
close: closeCulturesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
$body.focus();
|
||||
}
|
||||
|
||||
function insertEditorHtml() {
|
||||
const editorHtml = /* html */ `<div id="culturesEditor" class="dialog stable">
|
||||
<div id="culturesHeader" class="header" style="grid-template-columns: 10em 7em 8em 4em 8em 5em 8em 8em">
|
||||
<div data-tip="Click to sort by culture name" class="sortable alphabetically" data-sortby="name">Culture </div>
|
||||
<div data-tip="Click to sort by type" class="sortable alphabetically" data-sortby="type">Type </div>
|
||||
<div data-tip="Click to sort by culture namesbase" class="sortable" data-sortby="base">Namesbase </div>
|
||||
<div data-tip="Click to sort by culture cells count" class="sortable hide" data-sortby="cells">Cells </div>
|
||||
<div data-tip="Click to sort by expansionism" class="sortable hide" data-sortby="expansionism">Expansion </div>
|
||||
<div data-tip="Click to sort by culture area" class="sortable hide" data-sortby="area">Area </div>
|
||||
<div data-tip="Click to sort by culture population" class="sortable hide icon-sort-number-down" data-sortby="population">Population </div>
|
||||
<div data-tip="Click to sort by culture emblems shape" class="sortable alphabetically hide" data-sortby="emblems">Emblems </div>
|
||||
</div>
|
||||
<div id="culturesBody" class="table" data-type="absolute"></div>
|
||||
|
||||
<div id="culturesFooter" class="totalLine">
|
||||
<div data-tip="Cultures number" style="margin-left: 12px">Cultures: <span id="culturesFooterCultures">0</span></div>
|
||||
<div data-tip="Total land cells number" style="margin-left: 12px">Cells: <span id="culturesFooterCells">0</span></div>
|
||||
<div data-tip="Total land area" style="margin-left: 12px">Land Area: <span id="culturesFooterArea">0</span></div>
|
||||
<div data-tip="Total population" style="margin-left: 12px">Population: <span id="culturesFooterPopulation">0</span></div>
|
||||
</div>
|
||||
|
||||
<div id="culturesBottom">
|
||||
<button id="culturesEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
|
||||
<button id="culturesEditStyle" data-tip="Edit cultures style in Style Editor" class="icon-adjust"></button>
|
||||
<button id="culturesLegend" data-tip="Toggle Legend box" class="icon-list-bullet"></button>
|
||||
<button id="culturesPercentage" data-tip="Toggle percentage / absolute values display mode" class="icon-percent"></button>
|
||||
<button id="culturesHeirarchy" data-tip="Show cultures hierarchy tree" class="icon-sitemap"></button>
|
||||
<button id="culturesManually" data-tip="Manually re-assign cultures" class="icon-brush"></button>
|
||||
<div id="culturesManuallyButtons" style="display: none">
|
||||
<label data-tip="Change brush size" data-shortcut="+ (increase), – (decrease)" class="italic">Brush size:
|
||||
<input
|
||||
id="culturesManuallyBrush"
|
||||
type="range"
|
||||
min="5"
|
||||
max="99"
|
||||
value="15"
|
||||
style="width: 7em"
|
||||
/>
|
||||
<input
|
||||
id="culturesManuallyBrushNumber"
|
||||
type="number"
|
||||
min="5"
|
||||
max="99"
|
||||
value="15"
|
||||
/> </label><br />
|
||||
<button id="culturesManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
|
||||
<button id="culturesManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
|
||||
</div>
|
||||
<button id="culturesEditNamesBase" data-tip="Edit a database used for names generation" class="icon-font"></button>
|
||||
<button id="culturesAdd" data-tip="Add a new culture. Hold Shift to add multiple" class="icon-plus"></button>
|
||||
<button id="culturesExport" data-tip="Download cultures-related data" class="icon-download"></button>
|
||||
<button id="culturesImport" data-tip="Upload cultures-related data" class="icon-upload"></button>
|
||||
<button id="culturesRecalculate" data-tip="Recalculate cultures based on current values of growth-related attributes" class="icon-retweet"></button>
|
||||
<span data-tip="Allow culture centers, expansion and type changes to take an immediate effect">
|
||||
<input id="culturesAutoChange" class="checkbox" type="checkbox" />
|
||||
<label for="culturesAutoChange" class="checkbox-label"><i>auto-apply changes</i></label>
|
||||
</span>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", editorHtml);
|
||||
return byId("culturesBody");
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
applySortingByHeader("culturesHeader");
|
||||
|
||||
byId("culturesEditorRefresh").on("click", refreshCulturesEditor);
|
||||
byId("culturesEditStyle").on("click", () => editStyle("cults"));
|
||||
byId("culturesLegend").on("click", toggleLegend);
|
||||
byId("culturesPercentage").on("click", togglePercentageMode);
|
||||
byId("culturesHeirarchy").on("click", showHierarchy);
|
||||
byId("culturesRecalculate").on("click", () => recalculateCultures(true));
|
||||
byId("culturesManually").on("click", enterCultureManualAssignent);
|
||||
byId("culturesManuallyApply").on("click", applyCultureManualAssignent);
|
||||
byId("culturesManuallyCancel").on("click", () => exitCulturesManualAssignment());
|
||||
byId("culturesEditNamesBase").on("click", editNamesbase);
|
||||
byId("culturesAdd").on("click", enterAddCulturesMode);
|
||||
byId("culturesExport").on("click", downloadCulturesCsv);
|
||||
byId("culturesImport").on("click", () => byId("culturesCSVToLoad").click());
|
||||
byId("culturesCSVToLoad").on("change", uploadCulturesData);
|
||||
}
|
||||
|
||||
function refreshCulturesEditor() {
|
||||
culturesCollectStatistics();
|
||||
culturesEditorAddLines();
|
||||
drawCultureCenters();
|
||||
}
|
||||
|
||||
function culturesCollectStatistics() {
|
||||
const {cells, cultures, burgs} = pack;
|
||||
cultures.forEach(c => {
|
||||
c.cells = c.area = c.rural = c.urban = 0;
|
||||
});
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const cultureId = cells.culture[i];
|
||||
cultures[cultureId].cells += 1;
|
||||
cultures[cultureId].area += cells.area[i];
|
||||
cultures[cultureId].rural += cells.pop[i];
|
||||
const burgId = cells.burg[i];
|
||||
if (burgId) cultures[cultureId].urban += burgs[burgId].population;
|
||||
}
|
||||
}
|
||||
|
||||
function culturesEditorAddLines() {
|
||||
const unit = getAreaUnit();
|
||||
let lines = "";
|
||||
let totalArea = 0;
|
||||
let totalPopulation = 0;
|
||||
|
||||
const emblemShapeGroup = byId("emblemShape")?.selectedOptions[0]?.parentNode?.label;
|
||||
const selectShape = emblemShapeGroup === "Diversiform";
|
||||
|
||||
for (const c of pack.cultures) {
|
||||
if (c.removed) continue;
|
||||
const area = getArea(c.area);
|
||||
const rural = c.rural * populationRate;
|
||||
const urban = c.urban * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}. Rural population: ${si(rural)}. Urban population: ${si(
|
||||
urban
|
||||
)}. Click to edit`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
if (!c.i) {
|
||||
// Uncultured (neutral) line
|
||||
lines += /* html */ `<div
|
||||
class="states"
|
||||
data-id="${c.i}"
|
||||
data-name="${c.name}"
|
||||
data-color=""
|
||||
data-cells="${c.cells}"
|
||||
data-area="${area}"
|
||||
data-population="${population}"
|
||||
data-base="${c.base}"
|
||||
data-type=""
|
||||
data-expansionism=""
|
||||
data-emblems="${c.shield}"
|
||||
>
|
||||
<svg width="11" height="11" class="placeholder"></svg>
|
||||
<input data-tip="Neutral culture name. Click and type to change" class="cultureName italic" style="width: 7em"
|
||||
value="${c.name}" autocorrect="off" spellcheck="false" />
|
||||
<span class="icon-cw placeholder"></span>
|
||||
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
|
||||
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names"
|
||||
class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="cultureCells hide" style="width: 4em">${c.cells}</div>
|
||||
<span class="icon-resize-full placeholder hide"></span>
|
||||
<input class="cultureExpan placeholder hide" type="number" />
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide pointer"
|
||||
style="width: 5em">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
|
||||
${getShapeOptions(selectShape, c.shield)}
|
||||
</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
lines += /* html */ `<div
|
||||
class="states"
|
||||
data-id="${c.i}"
|
||||
data-name="${c.name}"
|
||||
data-color="${c.color}"
|
||||
data-cells="${c.cells}"
|
||||
data-area="${area}"
|
||||
data-population="${population}"
|
||||
data-base="${c.base}"
|
||||
data-type="${c.type}"
|
||||
data-expansionism="${c.expansionism}"
|
||||
data-emblems="${c.shield}"
|
||||
>
|
||||
<fill-box fill="${c.color}"></fill-box>
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName" style="width: 7em"
|
||||
value="${c.name}" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
|
||||
<select data-tip="Culture type. Defines growth model. Click to change"
|
||||
class="cultureType">${getTypeOptions(c.type)}</select>
|
||||
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names"
|
||||
class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="cultureCells hide" style="width: 4em">${c.cells}</div>
|
||||
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
|
||||
<input
|
||||
data-tip="Culture expansionism. Defines competitive size. Click to change, then click Recalculate to apply change"
|
||||
class="cultureExpan hide"
|
||||
type="number"
|
||||
min="0"
|
||||
max="99"
|
||||
step=".1"
|
||||
value=${c.expansionism}
|
||||
/>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Culture area" class="cultureArea hide" style="width: 6em">${si(area)} ${unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation hide pointer"
|
||||
style="width: 5em">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
|
||||
${getShapeOptions(selectShape, c.shield)}
|
||||
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
$body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
byId("culturesFooterCultures").innerHTML = pack.cultures.filter(c => c.i && !c.removed).length;
|
||||
byId("culturesFooterCells").innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||
byId("culturesFooterArea").innerHTML = `${si(totalArea)} ${unit}`;
|
||||
byId("culturesFooterPopulation").innerHTML = si(totalPopulation);
|
||||
byId("culturesFooterArea").dataset.area = totalArea;
|
||||
byId("culturesFooterPopulation").dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
$body.querySelectorAll(":scope > div").forEach($line => {
|
||||
$line.on("mouseenter", cultureHighlightOn);
|
||||
$line.on("mouseleave", cultureHighlightOff);
|
||||
$line.on("click", selectCultureOnLineClick);
|
||||
});
|
||||
$body.querySelectorAll("fill-box").forEach($el => $el.on("click", cultureChangeColor));
|
||||
$body.querySelectorAll("div > input.cultureName").forEach($el => $el.on("input", cultureChangeName));
|
||||
$body.querySelectorAll("div > span.icon-cw").forEach($el => $el.on("click", cultureRegenerateName));
|
||||
$body.querySelectorAll("div > input.cultureExpan").forEach($el => $el.on("input", cultureChangeExpansionism));
|
||||
$body.querySelectorAll("div > select.cultureType").forEach($el => $el.on("change", cultureChangeType));
|
||||
$body.querySelectorAll("div > select.cultureBase").forEach($el => $el.on("change", cultureChangeBase));
|
||||
$body.querySelectorAll("div > select.cultureEmblems").forEach($el => $el.on("change", cultureChangeEmblemsShape));
|
||||
$body.querySelectorAll("div > div.culturePopulation").forEach($el => $el.on("click", changePopulation));
|
||||
$body.querySelectorAll("div > span.icon-arrows-cw").forEach($el => $el.on("click", cultureRegenerateBurgs));
|
||||
$body.querySelectorAll("div > span.icon-trash-empty").forEach($el => $el.on("click", cultureRemovePrompt));
|
||||
|
||||
const $culturesHeader = byId("culturesHeader");
|
||||
$culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? "inline-block" : "none";
|
||||
|
||||
if ($body.dataset.type === "percentage") {
|
||||
$body.dataset.type = "absolute";
|
||||
togglePercentageMode();
|
||||
}
|
||||
applySorting($culturesHeader);
|
||||
$("#culturesEditor").dialog({width: "fit-content"});
|
||||
}
|
||||
|
||||
function getTypeOptions(type) {
|
||||
let options = "";
|
||||
cultureTypes.forEach(t => (options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`));
|
||||
return options;
|
||||
}
|
||||
|
||||
function getBaseOptions(base) {
|
||||
let options = "";
|
||||
nameBases.forEach((n, i) => (options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`));
|
||||
return options;
|
||||
}
|
||||
|
||||
function getShapeOptions(selectShape, selected) {
|
||||
if (!selectShape) return "";
|
||||
|
||||
const shapes = Object.keys(COA.shields.types)
|
||||
.map(type => Object.keys(COA.shields[type]))
|
||||
.flat();
|
||||
const options = shapes.map(
|
||||
shape => `<option ${shape === selected ? "selected" : ""} value="${shape}">${capitalize(shape)}</option>`
|
||||
);
|
||||
return `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureEmblems hide">${options}</select>`;
|
||||
}
|
||||
|
||||
const cultureHighlightOn = debounce(event => {
|
||||
const cultureId = Number(event.id || event.target.dataset.id);
|
||||
|
||||
if (!layerIsOn("toggleCultures")) return;
|
||||
if (customization) return;
|
||||
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
cults
|
||||
.select("#culture" + cultureId)
|
||||
.raise()
|
||||
.transition(animate)
|
||||
.attr("stroke-width", 2.5)
|
||||
.attr("stroke", "#d0240f");
|
||||
debug
|
||||
.select("#cultureCenter" + cultureId)
|
||||
.raise()
|
||||
.transition(animate)
|
||||
.attr("r", 8)
|
||||
.attr("stroke", "#d0240f");
|
||||
}, 200);
|
||||
|
||||
function cultureHighlightOff(event) {
|
||||
const cultureId = Number(event.id || event.target.dataset.id);
|
||||
|
||||
if (!layerIsOn("toggleCultures")) return;
|
||||
cults
|
||||
.select("#culture" + cultureId)
|
||||
.transition()
|
||||
.attr("stroke-width", null)
|
||||
.attr("stroke", null);
|
||||
debug
|
||||
.select("#cultureCenter" + cultureId)
|
||||
.transition()
|
||||
.attr("r", 6)
|
||||
.attr("stroke", null);
|
||||
}
|
||||
|
||||
function cultureChangeColor() {
|
||||
const $el = this;
|
||||
const currentFill = $el.getAttribute("fill");
|
||||
const cultureId = +$el.parentNode.dataset.id;
|
||||
|
||||
const callback = newFill => {
|
||||
$el.fill = newFill;
|
||||
pack.cultures[cultureId].color = newFill;
|
||||
cults
|
||||
.select("#culture" + cultureId)
|
||||
.attr("fill", newFill)
|
||||
.attr("stroke", newFill);
|
||||
debug.select("#cultureCenter" + cultureId).attr("fill", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function cultureChangeName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.cultures[culture].name = this.value;
|
||||
pack.cultures[culture].code = abbreviate(
|
||||
this.value,
|
||||
pack.cultures.map(c => c.code)
|
||||
);
|
||||
}
|
||||
|
||||
function cultureRegenerateName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const name = Names.getCultureShort(culture);
|
||||
this.parentNode.querySelector("input.cultureName").value = name;
|
||||
pack.cultures[culture].name = name;
|
||||
}
|
||||
|
||||
function cultureChangeExpansionism() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.expansionism = this.value;
|
||||
pack.cultures[culture].expansionism = +this.value;
|
||||
recalculateCultures();
|
||||
}
|
||||
|
||||
function cultureChangeType() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.type = this.value;
|
||||
pack.cultures[culture].type = this.value;
|
||||
recalculateCultures();
|
||||
}
|
||||
|
||||
function cultureChangeBase() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const v = +this.value;
|
||||
this.parentNode.dataset.base = pack.cultures[culture].base = v;
|
||||
}
|
||||
|
||||
function cultureChangeEmblemsShape() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const shape = this.value;
|
||||
this.parentNode.dataset.emblems = pack.cultures[culture].shield = shape;
|
||||
|
||||
const rerenderCOA = (id, coa) => {
|
||||
const $coa = byId(id);
|
||||
if (!$coa) return; // not rendered
|
||||
$coa.remove();
|
||||
COArenderer.trigger(id, coa);
|
||||
};
|
||||
|
||||
pack.states.forEach(state => {
|
||||
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === "custom") return;
|
||||
if (shape === state.coa.shield) return;
|
||||
state.coa.shield = shape;
|
||||
rerenderCOA("stateCOA" + state.i, state.coa);
|
||||
});
|
||||
|
||||
pack.provinces.forEach(province => {
|
||||
if (
|
||||
pack.cells.culture[province.center] !== culture ||
|
||||
!province.i ||
|
||||
province.removed ||
|
||||
!province.coa ||
|
||||
province.coa === "custom"
|
||||
)
|
||||
return;
|
||||
if (shape === province.coa.shield) return;
|
||||
province.coa.shield = shape;
|
||||
rerenderCOA("provinceCOA" + province.i, province.coa);
|
||||
});
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === "custom") return;
|
||||
if (shape === burg.coa.shield) return;
|
||||
burg.coa.shield = shape;
|
||||
rerenderCOA("burgCOA" + burg.i, burg.coa);
|
||||
});
|
||||
}
|
||||
|
||||
function changePopulation() {
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const culture = pack.cultures[cultureId];
|
||||
if (!culture.cells) return tip("Culture does not have any cells, cannot change population", false, "error");
|
||||
|
||||
const rural = rn(culture.rural * populationRate);
|
||||
const urban = rn(culture.urban * populationRate * urbanization);
|
||||
const total = rural + urban;
|
||||
const format = n => Number(n).toLocaleString();
|
||||
const burgs = pack.burgs.filter(b => !b.removed && b.culture === cultureId);
|
||||
|
||||
alertMessage.innerHTML = /* html */ `<div>
|
||||
<i>Change population of all cells assigned to the culture</i>
|
||||
<div style="margin: 0.5em 0">
|
||||
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"} />
|
||||
</div>
|
||||
<div>Total population: ${format(total)} ⇒ <span id="totalPop">${format(total)}</span>
|
||||
(<span id="totalPopPerc">100</span>%)
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
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 culture population",
|
||||
width: "24em",
|
||||
buttons: {
|
||||
Apply: function () {
|
||||
applyPopulationChange(rural, urban, ruralPop.value, urbanPop.value, cultureId);
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
function applyPopulationChange(oldRural, oldUrban, newRural, newUrban, culture) {
|
||||
const ruralChange = newRural / oldRural;
|
||||
if (isFinite(ruralChange) && ruralChange !== 1) {
|
||||
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
|
||||
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
|
||||
}
|
||||
if (!isFinite(ruralChange) && +newRural > 0) {
|
||||
const points = newRural / populationRate;
|
||||
const cells = pack.cells.i.filter(i => pack.cells.culture[i] === culture);
|
||||
const pop = rn(points / cells.length);
|
||||
cells.forEach(i => (pack.cells.pop[i] = pop));
|
||||
}
|
||||
|
||||
const burgs = pack.burgs.filter(b => !b.removed && b.culture === culture);
|
||||
const urbanChange = newUrban / oldUrban;
|
||||
if (isFinite(urbanChange) && urbanChange !== 1) {
|
||||
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
|
||||
}
|
||||
if (!isFinite(urbanChange) && +newUrban > 0) {
|
||||
const points = newUrban / populationRate / urbanization;
|
||||
const population = rn(points / burgs.length, 4);
|
||||
burgs.forEach(b => (b.population = population));
|
||||
}
|
||||
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
function cultureRegenerateBurgs() {
|
||||
if (customization === 4) return;
|
||||
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
const cBurgs = pack.burgs.filter(b => b.culture === cultureId && !b.lock);
|
||||
cBurgs.forEach(b => {
|
||||
b.name = Names.getCulture(cultureId);
|
||||
labels.select("[data-id='" + b.i + "']").text(b.name);
|
||||
});
|
||||
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, "success");
|
||||
}
|
||||
|
||||
function removeCulture(cultureId) {
|
||||
cults.select("#culture" + cultureId).remove();
|
||||
debug.select("#cultureCenter" + cultureId).remove();
|
||||
|
||||
const {burgs, states, cells, cultures} = pack;
|
||||
|
||||
burgs.filter(b => b.culture == cultureId).forEach(b => (b.culture = 0));
|
||||
states.forEach(s => {
|
||||
if (s.culture === cultureId) s.culture = 0;
|
||||
});
|
||||
cells.culture.forEach((c, i) => {
|
||||
if (c === cultureId) cells.culture[i] = 0;
|
||||
});
|
||||
cultures[cultureId].removed = true;
|
||||
|
||||
cultures
|
||||
.filter(c => c.i && !c.removed)
|
||||
.forEach(c => {
|
||||
c.origins = c.origins.filter(origin => origin !== cultureId);
|
||||
if (!c.origins.length) c.origins = [0];
|
||||
});
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
function cultureRemovePrompt() {
|
||||
if (customization) return;
|
||||
|
||||
const cultureId = +this.parentNode.dataset.id;
|
||||
confirmationDialog({
|
||||
title: "Remove culture",
|
||||
message: "Are you sure you want to remove the culture? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => removeCulture(cultureId)
|
||||
});
|
||||
}
|
||||
|
||||
function drawCultureCenters() {
|
||||
const tooltip = "Drag to move the culture center (ancestral home)";
|
||||
debug.select("#cultureCenters").remove();
|
||||
const cultureCenters = debug
|
||||
.append("g")
|
||||
.attr("id", "cultureCenters")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("stroke", "#444444")
|
||||
.style("cursor", "move");
|
||||
|
||||
const data = pack.cultures.filter(c => c.i && !c.removed);
|
||||
cultureCenters
|
||||
.selectAll("circle")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "cultureCenter" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("r", 6)
|
||||
.attr("fill", d => d.color)
|
||||
.attr("cx", d => pack.cells.p[d.center][0])
|
||||
.attr("cy", d => pack.cells.p[d.center][1])
|
||||
.on("mouseenter", d => {
|
||||
tip(tooltip, true);
|
||||
$body.querySelector(`div[data-id='${d.i}']`).classList.add("selected");
|
||||
cultureHighlightOn(event);
|
||||
})
|
||||
.on("mouseleave", d => {
|
||||
tip("", true);
|
||||
$body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected");
|
||||
cultureHighlightOff(event);
|
||||
})
|
||||
.call(d3.drag().on("start", cultureCenterDrag));
|
||||
}
|
||||
|
||||
function cultureCenterDrag() {
|
||||
const $el = d3.select(this);
|
||||
const cultureId = +this.id.slice(13);
|
||||
d3.event.on("drag", () => {
|
||||
const {x, y} = d3.event;
|
||||
$el.attr("cx", x).attr("cy", y);
|
||||
const cell = findCell(x, y);
|
||||
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
|
||||
pack.cultures[cultureId].center = cell;
|
||||
recalculateCultures();
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) return clearLegend();
|
||||
|
||||
const data = pack.cultures
|
||||
.filter(c => c.i && !c.removed && c.cells)
|
||||
.sort((a, b) => b.area - a.area)
|
||||
.map(c => [c.i, c.color, c.name]);
|
||||
drawLegend("Cultures", data);
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if ($body.dataset.type === "absolute") {
|
||||
$body.dataset.type = "percentage";
|
||||
const totalCells = +byId("culturesFooterCells").innerText;
|
||||
const totalArea = +byId("culturesFooterArea").dataset.area;
|
||||
const totalPopulation = +byId("culturesFooterPopulation").dataset.population;
|
||||
|
||||
$body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
const {cells, area, population} = el.dataset;
|
||||
el.querySelector(".cultureCells").innerText = rn((+cells / totalCells) * 100) + "%";
|
||||
el.querySelector(".cultureArea").innerText = rn((+area / totalArea) * 100) + "%";
|
||||
el.querySelector(".culturePopulation").innerText = rn((+population / totalPopulation) * 100) + "%";
|
||||
});
|
||||
} else {
|
||||
$body.dataset.type = "absolute";
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
async function showHierarchy() {
|
||||
if (customization) return;
|
||||
const HeirarchyTree = await import("../hierarchy-tree.js");
|
||||
|
||||
const getDescription = culture => {
|
||||
const {name, type, rural, urban} = culture;
|
||||
|
||||
const population = rural * populationRate + urban * populationRate * urbanization;
|
||||
const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
|
||||
return `${name} culture. ${type}. ${populationText}`;
|
||||
};
|
||||
|
||||
const getShape = ({type}) => {
|
||||
if (type === "Generic") return "circle";
|
||||
if (type === "River") return "diamond";
|
||||
if (type === "Lake") return "hexagon";
|
||||
if (type === "Naval") return "square";
|
||||
if (type === "Highland") return "concave";
|
||||
if (type === "Nomadic") return "octagon";
|
||||
if (type === "Hunting") return "pentagon";
|
||||
};
|
||||
|
||||
HeirarchyTree.open({
|
||||
type: "cultures",
|
||||
data: pack.cultures,
|
||||
onNodeEnter: cultureHighlightOn,
|
||||
onNodeLeave: cultureHighlightOff,
|
||||
getDescription,
|
||||
getShape
|
||||
});
|
||||
}
|
||||
|
||||
function recalculateCultures(must) {
|
||||
if (!must && !culturesAutoChange.checked) return;
|
||||
|
||||
pack.cells.culture = new Uint16Array(pack.cells.i.length);
|
||||
pack.cultures.forEach(function (c) {
|
||||
if (!c.i || c.removed) return;
|
||||
pack.cells.culture[c.center] = c.i;
|
||||
});
|
||||
|
||||
Cultures.expand();
|
||||
drawCultures();
|
||||
pack.burgs.forEach(b => (b.culture = pack.cells.culture[b.cell]));
|
||||
refreshCulturesEditor();
|
||||
document.querySelector("input.cultureExpan").focus(); // to not trigger hotkeys
|
||||
}
|
||||
|
||||
function enterCultureManualAssignent() {
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
customization = 4;
|
||||
cults.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#culturesBottom > *").forEach(el => (el.style.display = "none"));
|
||||
byId("culturesManuallyButtons").style.display = "inline-block";
|
||||
debug.select("#cultureCenters").style("display", "none");
|
||||
|
||||
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
culturesFooter.style.display = "none";
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||
$("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
tip("Click on culture to select, drag the circle to change culture", true);
|
||||
viewbox
|
||||
.style("cursor", "crosshair")
|
||||
.on("click", selectCultureOnMapClick)
|
||||
.call(d3.drag().on("start", dragCultureBrush))
|
||||
.on("touchmove mousemove", moveCultureBrush);
|
||||
|
||||
$body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnLineClick(i) {
|
||||
if (customization !== 4) return;
|
||||
$body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnMapClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) return;
|
||||
|
||||
const assigned = cults.select("#temp").select("polygon[data-cell='" + i + "']");
|
||||
const culture = assigned.size() ? +assigned.attr("data-culture") : pack.cells.culture[i];
|
||||
|
||||
$body.querySelector("div.selected").classList.remove("selected");
|
||||
$body.querySelector("div[data-id='" + culture + "']").classList.add("selected");
|
||||
}
|
||||
|
||||
function dragCultureBrush() {
|
||||
const radius = +culturesManuallyBrush.value;
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], radius);
|
||||
|
||||
const found = radius > 5 ? findAll(p[0], p[1], radius) : [findCell(p[0], p[1], radius)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeCultureForSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
function changeCultureForSelection(selection) {
|
||||
const temp = cults.select("#temp");
|
||||
const selected = $body.querySelector("div.selected");
|
||||
|
||||
const cultureNew = +selected.dataset.id;
|
||||
const color = pack.cultures[cultureNew].color || "#ffffff";
|
||||
|
||||
selection.forEach(function (i) {
|
||||
const exists = temp.select("polygon[data-cell='" + i + "']");
|
||||
const cultureOld = exists.size() ? +exists.attr("data-culture") : pack.cells.culture[i];
|
||||
if (cultureNew === cultureOld) return;
|
||||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
|
||||
else
|
||||
temp
|
||||
.append("polygon")
|
||||
.attr("data-cell", i)
|
||||
.attr("data-culture", cultureNew)
|
||||
.attr("points", getPackPolygon(i))
|
||||
.attr("fill", color)
|
||||
.attr("stroke", color);
|
||||
});
|
||||
}
|
||||
|
||||
function moveCultureBrush() {
|
||||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +culturesManuallyBrush.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
}
|
||||
|
||||
function applyCultureManualAssignent() {
|
||||
const changed = cults.select("#temp").selectAll("polygon");
|
||||
changed.each(function () {
|
||||
const i = +this.dataset.cell;
|
||||
const c = +this.dataset.culture;
|
||||
pack.cells.culture[i] = c;
|
||||
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
drawCultures();
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
exitCulturesManualAssignment();
|
||||
}
|
||||
|
||||
function exitCulturesManualAssignment(close) {
|
||||
customization = 0;
|
||||
cults.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#culturesBottom > *").forEach(el => (el.style.display = "inline-block"));
|
||||
byId("culturesManuallyButtons").style.display = "none";
|
||||
|
||||
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
|
||||
culturesFooter.style.display = "block";
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||
if (!close) $("#culturesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
debug.select("#cultureCenters").style("display", null);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = $body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function enterAddCulturesMode() {
|
||||
if (this.classList.contains("pressed")) return exitAddCultureMode();
|
||||
|
||||
customization = 9;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to add a new culture", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addCulture);
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||
}
|
||||
|
||||
function exitAddCultureMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||
if (culturesAdd.classList.contains("pressed")) culturesAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function addCulture() {
|
||||
const point = d3.mouse(this);
|
||||
const center = findCell(point[0], point[1]);
|
||||
|
||||
if (pack.cells.h[center] < 20)
|
||||
return tip("You cannot place culture center into the water. Please click on a land cell", false, "error");
|
||||
const occupied = pack.cultures.some(c => !c.removed && c.center === center);
|
||||
if (occupied) return tip("This cell is already a culture center. Please select a different cell", false, "error");
|
||||
|
||||
if (d3.event.shiftKey === false) exitAddCultureMode();
|
||||
Cultures.add(center);
|
||||
|
||||
drawCultureCenters();
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
|
||||
function downloadCulturesCsv() {
|
||||
const unit = getAreaUnit("2");
|
||||
const headers = `Id,Name,Color,Cells,Expansionism,Type,Area ${unit},Population,Namesbase,Emblems Shape,Origins`;
|
||||
const lines = Array.from($body.querySelectorAll(":scope > div"));
|
||||
const data = lines.map($line => {
|
||||
const {id, name, color, cells, expansionism, type, area, population, emblems, base} = $line.dataset;
|
||||
const namesbase = nameBases[+base].name;
|
||||
const {origins} = pack.cultures[+id];
|
||||
const originList = origins.filter(origin => origin).map(origin => pack.cultures[origin].name);
|
||||
const originText = '"' + originList.join(", ") + '"';
|
||||
return [id, name, color, cells, expansionism, type, area, population, namesbase, emblems, originText].join(",");
|
||||
});
|
||||
const csvData = [headers].concat(data).join("\n");
|
||||
|
||||
const name = getFileName("Cultures") + ".csv";
|
||||
downloadFile(csvData, name);
|
||||
}
|
||||
|
||||
function closeCulturesEditor() {
|
||||
debug.select("#cultureCenters").remove();
|
||||
exitCulturesManualAssignment("close");
|
||||
exitAddCultureMode();
|
||||
}
|
||||
|
||||
async function uploadCulturesData() {
|
||||
const csv = await Formats.csvParser(this.files[0]);
|
||||
this.value = "";
|
||||
|
||||
const {cultures, cells} = pack;
|
||||
const shapes = Object.keys(COA.shields.types)
|
||||
.map(type => Object.keys(COA.shields[type]))
|
||||
.flat();
|
||||
|
||||
const populated = cells.pop.map((c, i) => (c ? i : null)).filter(c => c);
|
||||
cultures.forEach(item => {
|
||||
if (item.i) item.removed = true;
|
||||
});
|
||||
|
||||
for (const c of csv.iterator((a, b) => +a[0] > +b[0])) {
|
||||
let current;
|
||||
if (+c.id < cultures.length) {
|
||||
current = cultures[c.id];
|
||||
|
||||
const ratio = current.urban / (current.rural + current.urban);
|
||||
applyPopulationChange(current.rural, current.urban, c.population * (1 - ratio), c.population * ratio, +c.id);
|
||||
} else {
|
||||
current = {i: cultures.length, center: ra(populated), area: 0, cells: 0, origin: 0, rural: 0, urban: 0};
|
||||
cultures.push(current);
|
||||
}
|
||||
|
||||
current.removed = false;
|
||||
current.name = c.culture;
|
||||
current.code = abbreviate(
|
||||
current.name,
|
||||
cultures.map(c => c.code)
|
||||
);
|
||||
|
||||
current.color = c.color;
|
||||
current.expansionism = +c.expansionism;
|
||||
current.origins = JSON.parse(c.origins);
|
||||
|
||||
if (cultureTypes.includes(c.type)) current.type = c.type;
|
||||
else current.type = "Generic";
|
||||
|
||||
const shieldShape = c["emblems shape"].toLowerCase();
|
||||
if (shapes.includes(shieldShape)) current.shield = shieldShape;
|
||||
else current.shield = "heater";
|
||||
|
||||
const nameBaseIndex = nameBases.findIndex(n => n.name == c.namesbase);
|
||||
current.base = nameBaseIndex === -1 ? 0 : nameBaseIndex;
|
||||
}
|
||||
|
||||
cultures.filter(c => c.removed).forEach(c => removeCulture(c.i));
|
||||
|
||||
drawCultures();
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
|
@ -1,768 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {findAll, findCell, getPackPolygon, isLand} from "utils/graphUtils";
|
||||
import {tip, showMainTip, clearMainTip} from "scripts/tooltips";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {abbreviate} from "utils/languageUtils";
|
||||
import {debounce} from "utils/functionUtils";
|
||||
|
||||
const $body = insertEditorHtml();
|
||||
addListeners();
|
||||
|
||||
export function open() {
|
||||
closeDialogs("#religionsEditor, .stable");
|
||||
if (!layerIsOn("toggleReligions")) toggleCultures();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleCultures")) toggleReligions();
|
||||
if (layerIsOn("toggleProvinces")) toggleProvinces();
|
||||
|
||||
refreshReligionsEditor();
|
||||
drawReligionCenters();
|
||||
|
||||
$("#religionsEditor").dialog({
|
||||
title: "Religions Editor",
|
||||
resizable: false,
|
||||
close: closeReligionsEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
$body.focus();
|
||||
}
|
||||
|
||||
function insertEditorHtml() {
|
||||
const editorHtml = /* html */ `<div id="religionsEditor" class="dialog stable">
|
||||
<div id="religionsHeader" class="header" style="grid-template-columns: 13em 6em 7em 18em 5em 6em">
|
||||
<div data-tip="Click to sort by religion name" class="sortable alphabetically" data-sortby="name">Religion </div>
|
||||
<div data-tip="Click to sort by religion type" class="sortable alphabetically icon-sort-name-down" data-sortby="type">Type </div>
|
||||
<div data-tip="Click to sort by religion form" class="sortable alphabetically hide" data-sortby="form">Form </div>
|
||||
<div data-tip="Click to sort by supreme deity" class="sortable alphabetically hide" data-sortby="deity">Supreme Deity </div>
|
||||
<div data-tip="Click to sort by religion area" class="sortable hide" data-sortby="area">Area </div>
|
||||
<div data-tip="Click to sort by number of believers (religion area population)" class="sortable hide" data-sortby="population">Believers </div>
|
||||
</div>
|
||||
<div id="religionsBody" class="table" data-type="absolute"></div>
|
||||
|
||||
<div id="religionsFooter" class="totalLine">
|
||||
<div data-tip="Total number of organized religions" style="margin-left: 12px">
|
||||
Organized: <span id="religionsOrganized">0</span>
|
||||
</div>
|
||||
<div data-tip="Total number of heresies" style="margin-left: 12px">
|
||||
Heresies: <span id="religionsHeresies">0</span>
|
||||
</div>
|
||||
<div data-tip="Total number of cults" style="margin-left: 12px">
|
||||
Cults: <span id="religionsCults">0</span>
|
||||
</div>
|
||||
<div data-tip="Total number of folk religions" style="margin-left: 12px">
|
||||
Folk: <span id="religionsFolk">0</span>
|
||||
</div>
|
||||
<div data-tip="Total land area" style="margin-left: 12px">
|
||||
Land Area: <span id="religionsFooterArea">0</span>
|
||||
</div>
|
||||
<div data-tip="Total number of believers (population)" style="margin-left: 12px">
|
||||
Believers: <span id="religionsFooterPopulation">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="religionsBottom">
|
||||
<button id="religionsEditorRefresh" data-tip="Refresh the Editor" class="icon-cw"></button>
|
||||
<button id="religionsEditStyle" data-tip="Edit religions style in Style Editor" class="icon-adjust"></button>
|
||||
<button id="religionsLegend" data-tip="Toggle Legend box" class="icon-list-bullet"></button>
|
||||
<button id="religionsPercentage" data-tip="Toggle percentage / absolute values display mode" class="icon-percent"></button>
|
||||
<button id="religionsHeirarchy" data-tip="Show religions hierarchy tree" class="icon-sitemap"></button>
|
||||
<button id="religionsExtinct" data-tip="Show/hide extinct religions (religions without cells)" class="icon-eye-off"></button>
|
||||
|
||||
<button id="religionsManually" data-tip="Manually re-assign religions" class="icon-brush"></button>
|
||||
<div id="religionsManuallyButtons" style="display: none">
|
||||
<label data-tip="Change brush size" data-shortcut="+ (increase), – (decrease)" class="italic">Brush size:
|
||||
<input
|
||||
id="religionsManuallyBrush"
|
||||
oninput="tip('Brush size: '+this.value); religionsManuallyBrushNumber.value = this.value"
|
||||
type="range"
|
||||
min="5"
|
||||
max="99"
|
||||
value="15"
|
||||
style="width: 7em"
|
||||
/>
|
||||
<input
|
||||
id="religionsManuallyBrushNumber"
|
||||
oninput="tip('Brush size: '+this.value); religionsManuallyBrush.value = this.value"
|
||||
type="number"
|
||||
min="5"
|
||||
max="99"
|
||||
value="15"
|
||||
/> </label
|
||||
><br />
|
||||
<button id="religionsManuallyApply" data-tip="Apply assignment" class="icon-check"></button>
|
||||
<button id="religionsManuallyCancel" data-tip="Cancel assignment" class="icon-cancel"></button>
|
||||
</div>
|
||||
<button id="religionsAdd" data-tip="Add a new religion. Hold Shift to add multiple" class="icon-plus"></button>
|
||||
<button id="religionsExport" data-tip="Download religions-related data" class="icon-download"></button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", editorHtml);
|
||||
return byId("religionsBody");
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
applySortingByHeader("religionsHeader");
|
||||
|
||||
byId("religionsEditorRefresh").on("click", refreshReligionsEditor);
|
||||
byId("religionsEditStyle").on("click", () => editStyle("relig"));
|
||||
byId("religionsLegend").on("click", toggleLegend);
|
||||
byId("religionsPercentage").on("click", togglePercentageMode);
|
||||
byId("religionsHeirarchy").on("click", showHierarchy);
|
||||
byId("religionsExtinct").on("click", toggleExtinct);
|
||||
byId("religionsManually").on("click", enterReligionsManualAssignent);
|
||||
byId("religionsManuallyApply").on("click", applyReligionsManualAssignent);
|
||||
byId("religionsManuallyCancel").on("click", () => exitReligionsManualAssignment());
|
||||
byId("religionsAdd").on("click", enterAddReligionMode);
|
||||
byId("religionsExport").on("click", downloadReligionsCsv);
|
||||
}
|
||||
|
||||
function refreshReligionsEditor() {
|
||||
religionsCollectStatistics();
|
||||
religionsEditorAddLines();
|
||||
}
|
||||
|
||||
function religionsCollectStatistics() {
|
||||
const {cells, religions, burgs} = pack;
|
||||
religions.forEach(r => {
|
||||
r.cells = r.area = r.rural = r.urban = 0;
|
||||
});
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const religionId = cells.religion[i];
|
||||
religions[religionId].cells += 1;
|
||||
religions[religionId].area += cells.area[i];
|
||||
religions[religionId].rural += cells.pop[i];
|
||||
const burgId = cells.burg[i];
|
||||
if (burgId) religions[religionId].urban += burgs[burgId].population;
|
||||
}
|
||||
}
|
||||
|
||||
// add line for each religion
|
||||
function religionsEditorAddLines() {
|
||||
const unit = " " + getAreaUnit();
|
||||
let lines = "";
|
||||
let totalArea = 0;
|
||||
let totalPopulation = 0;
|
||||
|
||||
for (const r of pack.religions) {
|
||||
if (r.removed) continue;
|
||||
if (r.i && !r.cells && $body.dataset.extinct !== "show") continue; // hide extinct religions
|
||||
|
||||
const area = getArea(r.area);
|
||||
const rural = r.rural * populationRate;
|
||||
const urban = r.urban * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si(
|
||||
urban
|
||||
)}. Click to change`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
if (!r.i) {
|
||||
// No religion (neutral) line
|
||||
lines += /* html */ `<div
|
||||
class="states"
|
||||
data-id="${r.i}"
|
||||
data-name="${r.name}"
|
||||
data-color=""
|
||||
data-area="${area}"
|
||||
data-population="${population}"
|
||||
data-type=""
|
||||
data-form=""
|
||||
data-deity=""
|
||||
data-expansionism=""
|
||||
>
|
||||
<svg width="11" height="11" class="placeholder"></svg>
|
||||
<input data-tip="Religion name. Click and type to change" class="religionName italic" style="width: 11em"
|
||||
value="${r.name}" autocorrect="off" spellcheck="false" />
|
||||
<select data-tip="Religion type" class="religionType placeholder" style="width: 5em">
|
||||
${getTypeOptions(r.type)}
|
||||
</select>
|
||||
<input data-tip="Religion form" class="religionForm placeholder hide" style="width: 6em" value="" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw placeholder hide"></span>
|
||||
<input data-tip="Religion supreme deity" class="religionDeity placeholder hide" style="width: 17em" value="" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div>
|
||||
</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
lines += /* html */ `<div
|
||||
class="states"
|
||||
data-id=${r.i}
|
||||
data-name="${r.name}"
|
||||
data-color="${r.color}"
|
||||
data-area=${area}
|
||||
data-population=${population}
|
||||
data-type="${r.type}"
|
||||
data-form="${r.form}"
|
||||
data-deity="${r.deity || ""}"
|
||||
data-expansionism="${r.expansionism}"
|
||||
>
|
||||
<fill-box fill="${r.color}"></fill-box>
|
||||
<input data-tip="Religion name. Click and type to change" class="religionName" style="width: 11em"
|
||||
value="${r.name}" autocorrect="off" spellcheck="false" />
|
||||
<select data-tip="Religion type" class="religionType" style="width: 5em">
|
||||
${getTypeOptions(r.type)}
|
||||
</select>
|
||||
<input data-tip="Religion form" class="religionForm hide" style="width: 6em"
|
||||
value="${r.form}" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Click to re-generate supreme deity" class="icon-arrows-cw hide"></span>
|
||||
<input data-tip="Religion supreme deity" class="religionDeity hide" style="width: 17em"
|
||||
value="${r.deity || ""}" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Religion area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Religion area" class="religionArea hide" style="width: 5em">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="religionPopulation hide pointer">${si(population)}</div>
|
||||
<span data-tip="Remove religion" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
}
|
||||
$body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
const validReligions = pack.religions.filter(r => r.i && !r.removed);
|
||||
byId("religionsOrganized").innerHTML = validReligions.filter(r => r.type === "Organized").length;
|
||||
byId("religionsHeresies").innerHTML = validReligions.filter(r => r.type === "Heresy").length;
|
||||
byId("religionsCults").innerHTML = validReligions.filter(r => r.type === "Cult").length;
|
||||
byId("religionsFolk").innerHTML = validReligions.filter(r => r.type === "Folk").length;
|
||||
byId("religionsFooterArea").innerHTML = si(totalArea) + unit;
|
||||
byId("religionsFooterPopulation").innerHTML = si(totalPopulation);
|
||||
byId("religionsFooterArea").dataset.area = totalArea;
|
||||
byId("religionsFooterPopulation").dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
$body.querySelectorAll(":scope > div").forEach($line => {
|
||||
$line.on("mouseenter", religionHighlightOn);
|
||||
$line.on("mouseleave", religionHighlightOff);
|
||||
$line.on("click", selectReligionOnLineClick);
|
||||
});
|
||||
$body.querySelectorAll("fill-box").forEach(el => el.on("click", religionChangeColor));
|
||||
$body.querySelectorAll("div > input.religionName").forEach(el => el.on("input", religionChangeName));
|
||||
$body.querySelectorAll("div > select.religionType").forEach(el => el.on("change", religionChangeType));
|
||||
$body.querySelectorAll("div > input.religionForm").forEach(el => el.on("input", religionChangeForm));
|
||||
$body.querySelectorAll("div > input.religionDeity").forEach(el => el.on("input", religionChangeDeity));
|
||||
$body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.on("click", regenerateDeity));
|
||||
$body.querySelectorAll("div > div.religionPopulation").forEach(el => el.on("click", changePopulation));
|
||||
$body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", religionRemovePrompt));
|
||||
|
||||
if ($body.dataset.type === "percentage") {
|
||||
$body.dataset.type = "absolute";
|
||||
togglePercentageMode();
|
||||
}
|
||||
applySorting(religionsHeader);
|
||||
$("#religionsEditor").dialog({width: "fit-content"});
|
||||
}
|
||||
|
||||
function getTypeOptions(type) {
|
||||
let options = "";
|
||||
const types = ["Folk", "Organized", "Cult", "Heresy"];
|
||||
types.forEach(t => (options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`));
|
||||
return options;
|
||||
}
|
||||
|
||||
const religionHighlightOn = debounce(event => {
|
||||
const religionId = Number(event.id || event.target.dataset.id);
|
||||
const $el = $body.querySelector(`div[data-id='${religionId}']`);
|
||||
if ($el) $el.classList.add("active");
|
||||
|
||||
if (!layerIsOn("toggleReligions")) return;
|
||||
if (customization) return;
|
||||
|
||||
const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
|
||||
relig
|
||||
.select("#religion" + religionId)
|
||||
.raise()
|
||||
.transition(animate)
|
||||
.attr("stroke-width", 2.5)
|
||||
.attr("stroke", "#c13119");
|
||||
debug
|
||||
.select("#religionsCenter" + religionId)
|
||||
.raise()
|
||||
.transition(animate)
|
||||
.attr("r", 8)
|
||||
.attr("stroke-width", 2)
|
||||
.attr("stroke", "#c13119");
|
||||
}, 200);
|
||||
|
||||
function religionHighlightOff(event) {
|
||||
const religionId = Number(event.id || event.target.dataset.id);
|
||||
const $el = $body.querySelector(`div[data-id='${religionId}']`);
|
||||
if ($el) $el.classList.remove("active");
|
||||
|
||||
relig
|
||||
.select("#religion" + religionId)
|
||||
.transition()
|
||||
.attr("stroke-width", null)
|
||||
.attr("stroke", null);
|
||||
debug
|
||||
.select("#religionsCenter" + religionId)
|
||||
.transition()
|
||||
.attr("r", 4)
|
||||
.attr("stroke-width", 1.2)
|
||||
.attr("stroke", null);
|
||||
}
|
||||
|
||||
function religionChangeColor() {
|
||||
const $el = this;
|
||||
const currentFill = $el.getAttribute("fill");
|
||||
const religionId = +$el.parentNode.dataset.id;
|
||||
|
||||
const callback = newFill => {
|
||||
$el.fill = newFill;
|
||||
pack.religions[religionId].color = newFill;
|
||||
relig.select("#religion" + religionId).attr("fill", newFill);
|
||||
debug.select("#religionsCenter" + religionId).attr("fill", newFill);
|
||||
};
|
||||
|
||||
openPicker(currentFill, callback);
|
||||
}
|
||||
|
||||
function religionChangeName() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.religions[religionId].name = this.value;
|
||||
pack.religions[religionId].code = abbreviate(
|
||||
this.value,
|
||||
pack.religions.map(c => c.code)
|
||||
);
|
||||
}
|
||||
|
||||
function religionChangeType() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.type = this.value;
|
||||
pack.religions[religionId].type = this.value;
|
||||
}
|
||||
|
||||
function religionChangeForm() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.form = this.value;
|
||||
pack.religions[religionId].form = this.value;
|
||||
}
|
||||
|
||||
function religionChangeDeity() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.deity = this.value;
|
||||
pack.religions[religionId].deity = this.value;
|
||||
}
|
||||
|
||||
function regenerateDeity() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
const cultureId = pack.religions[religionId].culture;
|
||||
const deity = Religions.getDeityName(cultureId);
|
||||
this.parentNode.dataset.deity = deity;
|
||||
pack.religions[religionId].deity = deity;
|
||||
this.nextElementSibling.value = deity;
|
||||
}
|
||||
|
||||
function changePopulation() {
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
const religion = pack.religions[religionId];
|
||||
if (!religion.cells) return tip("Religion does not have any cells, cannot change population", false, "error");
|
||||
|
||||
const rural = rn(religion.rural * populationRate);
|
||||
const urban = rn(religion.urban * populationRate * urbanization);
|
||||
const total = rural + urban;
|
||||
const format = n => Number(n).toLocaleString();
|
||||
const burgs = pack.burgs.filter(b => !b.removed && pack.cells.religion[b.cell] === religionId);
|
||||
|
||||
alertMessage.innerHTML = /* html */ `<div>
|
||||
<i>All population of religion territory is considered believers of this religion. It means believers number change will directly affect population</i>
|
||||
<div style="margin: 0.5em 0">
|
||||
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"} />
|
||||
</div>
|
||||
<div>Total population: ${format(total)} ⇒ <span id="totalPop">${format(total)}</span>
|
||||
(<span id="totalPopPerc">100</span>%)
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const update = function () {
|
||||
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
||||
if (isNaN(totalNew)) return;
|
||||
totalPop.innerHTML = format(totalNew);
|
||||
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
|
||||
};
|
||||
|
||||
ruralPop.oninput = () => update();
|
||||
urbanPop.oninput = () => update();
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Change believers number",
|
||||
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) {
|
||||
const cells = pack.cells.i.filter(i => pack.cells.religion[i] === religionId);
|
||||
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
|
||||
}
|
||||
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
|
||||
const points = ruralPop.value / populationRate;
|
||||
const cells = pack.cells.i.filter(i => pack.cells.religion[i] === religionId);
|
||||
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));
|
||||
}
|
||||
|
||||
refreshReligionsEditor();
|
||||
}
|
||||
}
|
||||
|
||||
function religionRemovePrompt() {
|
||||
if (customization) return;
|
||||
|
||||
const religionId = +this.parentNode.dataset.id;
|
||||
confirmationDialog({
|
||||
title: "Remove religion",
|
||||
message: "Are you sure you want to remove the religion? <br>This action cannot be reverted",
|
||||
confirm: "Remove",
|
||||
onConfirm: () => removeReligion(religionId)
|
||||
});
|
||||
}
|
||||
|
||||
function removeReligion(religionId) {
|
||||
relig.select("#religion" + religionId).remove();
|
||||
relig.select("#religion-gap" + religionId).remove();
|
||||
debug.select("#religionsCenter" + religionId).remove();
|
||||
|
||||
pack.cells.religion.forEach((r, i) => {
|
||||
if (r === religionId) pack.cells.religion[i] = 0;
|
||||
});
|
||||
pack.religions[religionId].removed = true;
|
||||
|
||||
pack.religions
|
||||
.filter(r => r.i && !r.removed)
|
||||
.forEach(r => {
|
||||
r.origins = r.origins.filter(origin => origin !== religionId);
|
||||
if (!r.origins.length) r.origins = [0];
|
||||
});
|
||||
|
||||
refreshReligionsEditor();
|
||||
}
|
||||
|
||||
function drawReligionCenters() {
|
||||
debug.select("#religionCenters").remove();
|
||||
const religionCenters = debug
|
||||
.append("g")
|
||||
.attr("id", "religionCenters")
|
||||
.attr("stroke-width", 1.2)
|
||||
.attr("stroke", "#444444")
|
||||
.style("cursor", "move");
|
||||
|
||||
const data = pack.religions.filter(r => r.i && r.center && r.cells && !r.removed);
|
||||
religionCenters
|
||||
.selectAll("circle")
|
||||
.data(data)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("id", d => "religionsCenter" + d.i)
|
||||
.attr("data-id", d => d.i)
|
||||
.attr("r", 4)
|
||||
.attr("fill", d => d.color)
|
||||
.attr("cx", d => pack.cells.p[d.center][0])
|
||||
.attr("cy", d => pack.cells.p[d.center][1])
|
||||
.on("mouseenter", d => {
|
||||
tip(d.name + ". Drag to move the religion center", true);
|
||||
religionHighlightOn(event);
|
||||
})
|
||||
.on("mouseleave", d => {
|
||||
tip("", true);
|
||||
religionHighlightOff(event);
|
||||
})
|
||||
.call(d3.drag().on("start", religionCenterDrag));
|
||||
}
|
||||
|
||||
function religionCenterDrag() {
|
||||
const $el = d3.select(this);
|
||||
const religionId = +this.dataset.id;
|
||||
d3.event.on("drag", () => {
|
||||
const {x, y} = d3.event;
|
||||
$el.attr("cx", x).attr("cy", y);
|
||||
const cell = findCell(x, y);
|
||||
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
|
||||
pack.religions[religionId].center = cell;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleLegend() {
|
||||
if (legend.selectAll("*").size()) return clearLegend(); // hide legend
|
||||
|
||||
const data = pack.religions
|
||||
.filter(r => r.i && !r.removed && r.area)
|
||||
.sort((a, b) => b.area - a.area)
|
||||
.map(r => [r.i, r.color, r.name]);
|
||||
drawLegend("Religions", data);
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if ($body.dataset.type === "absolute") {
|
||||
$body.dataset.type = "percentage";
|
||||
const totalArea = +byId("religionsFooterArea").dataset.area;
|
||||
const totalPopulation = +byId("religionsFooterPopulation").dataset.population;
|
||||
|
||||
$body.querySelectorAll(":scope > div").forEach($el => {
|
||||
const {area, population} = $el.dataset;
|
||||
$el.querySelector(".religionArea").innerText = rn((+area / totalArea) * 100) + "%";
|
||||
$el.querySelector(".religionPopulation").innerText = rn((+population / totalPopulation) * 100) + "%";
|
||||
});
|
||||
} else {
|
||||
$body.dataset.type = "absolute";
|
||||
religionsEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
async function showHierarchy() {
|
||||
if (customization) return;
|
||||
const HeirarchyTree = await import("../hierarchy-tree.js");
|
||||
|
||||
const getDescription = religion => {
|
||||
const {name, type, form, rural, urban} = religion;
|
||||
|
||||
const getTypeText = () => {
|
||||
if (name.includes(type)) return "";
|
||||
if (form.includes(type)) return "";
|
||||
if (type === "Folk" || type === "Organized") return `. ${type} religion`;
|
||||
return `. ${type}`;
|
||||
};
|
||||
|
||||
const formText = form === type ? "" : ". " + form;
|
||||
const population = rural * populationRate + urban * populationRate * urbanization;
|
||||
const populationText = population > 0 ? si(rn(population)) + " people" : "Extinct";
|
||||
|
||||
return `${name}${getTypeText()}${formText}. ${populationText}`;
|
||||
};
|
||||
|
||||
const getShape = ({type}) => {
|
||||
if (type === "Folk") return "circle";
|
||||
if (type === "Organized") return "square";
|
||||
if (type === "Cult") return "hexagon";
|
||||
if (type === "Heresy") return "diamond";
|
||||
};
|
||||
|
||||
HeirarchyTree.open({
|
||||
type: "religions",
|
||||
data: pack.religions,
|
||||
onNodeEnter: religionHighlightOn,
|
||||
onNodeLeave: religionHighlightOff,
|
||||
getDescription,
|
||||
getShape
|
||||
});
|
||||
}
|
||||
|
||||
function toggleExtinct() {
|
||||
$body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide";
|
||||
religionsEditorAddLines();
|
||||
}
|
||||
|
||||
function enterReligionsManualAssignent() {
|
||||
if (!layerIsOn("toggleReligions")) toggleReligions();
|
||||
customization = 7;
|
||||
relig.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "none"));
|
||||
byId("religionsManuallyButtons").style.display = "inline-block";
|
||||
debug.select("#religionCenters").style("display", "none");
|
||||
|
||||
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
|
||||
religionsFooter.style.display = "none";
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||
$("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
tip("Click on religion to select, drag the circle to change religion", true);
|
||||
viewbox
|
||||
.style("cursor", "crosshair")
|
||||
.on("click", selectReligionOnMapClick)
|
||||
.call(d3.drag().on("start", dragReligionBrush))
|
||||
.on("touchmove mousemove", moveReligionBrush);
|
||||
|
||||
$body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectReligionOnLineClick(i) {
|
||||
if (customization !== 7) return;
|
||||
$body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectReligionOnMapClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) return;
|
||||
|
||||
const assigned = relig.select("#temp").select("polygon[data-cell='" + i + "']");
|
||||
const religion = assigned.size() ? +assigned.attr("data-religion") : pack.cells.religion[i];
|
||||
|
||||
$body.querySelector("div.selected").classList.remove("selected");
|
||||
$body.querySelector("div[data-id='" + religion + "']").classList.add("selected");
|
||||
}
|
||||
|
||||
function dragReligionBrush() {
|
||||
const radius = +byId("religionsManuallyBrushNumber").value;
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
if (!d3.event.dx && !d3.event.dy) return;
|
||||
const [x, y] = d3.mouse(this);
|
||||
moveCircle(x, y, radius);
|
||||
|
||||
const found = radius > 5 ? findAll(x, y, radius) : [findCell(x, y, radius)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeReligionForSelection(selection);
|
||||
});
|
||||
}
|
||||
|
||||
// change religion within selection
|
||||
function changeReligionForSelection(selection) {
|
||||
const temp = relig.select("#temp");
|
||||
const selected = $body.querySelector("div.selected");
|
||||
const religionNew = +selected.dataset.id;
|
||||
const color = pack.religions[religionNew].color || "#ffffff";
|
||||
|
||||
selection.forEach(function (i) {
|
||||
const exists = temp.select("polygon[data-cell='" + i + "']");
|
||||
const religionOld = exists.size() ? +exists.attr("data-religion") : pack.cells.religion[i];
|
||||
if (religionNew === religionOld) return;
|
||||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-religion", religionNew).attr("fill", color);
|
||||
else
|
||||
temp
|
||||
.append("polygon")
|
||||
.attr("data-cell", i)
|
||||
.attr("data-religion", religionNew)
|
||||
.attr("points", getPackPolygon(i))
|
||||
.attr("fill", color);
|
||||
});
|
||||
}
|
||||
|
||||
function moveReligionBrush() {
|
||||
showMainTip();
|
||||
const [x, y] = d3.mouse(this);
|
||||
const radius = +byId("religionsManuallyBrushNumber").value;
|
||||
moveCircle(x, y, radius);
|
||||
}
|
||||
|
||||
function applyReligionsManualAssignent() {
|
||||
const changed = relig.select("#temp").selectAll("polygon");
|
||||
changed.each(function () {
|
||||
const i = +this.dataset.cell;
|
||||
const r = +this.dataset.religion;
|
||||
pack.cells.religion[i] = r;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
drawReligions();
|
||||
refreshReligionsEditor();
|
||||
drawReligionCenters();
|
||||
}
|
||||
exitReligionsManualAssignment();
|
||||
}
|
||||
|
||||
function exitReligionsManualAssignment(close) {
|
||||
customization = 0;
|
||||
relig.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#religionsBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||
byId("religionsManuallyButtons").style.display = "none";
|
||||
|
||||
byId("religionsEditor")
|
||||
.querySelectorAll(".hide")
|
||||
.forEach(el => el.classList.remove("hidden"));
|
||||
byId("religionsFooter").style.display = "block";
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||
if (!close) $("#religionsEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
|
||||
|
||||
debug.select("#religionCenters").style("display", null);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const $selected = $body.querySelector("div.selected");
|
||||
if ($selected) $selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function enterAddReligionMode() {
|
||||
if (this.classList.contains("pressed")) return exitAddReligionMode();
|
||||
|
||||
customization = 8;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to add a new religion", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addReligion);
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
|
||||
}
|
||||
|
||||
function exitAddReligionMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
$body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||
if (religionsAdd.classList.contains("pressed")) religionsAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function addReligion() {
|
||||
const [x, y] = d3.mouse(this);
|
||||
const center = findCell(x, y);
|
||||
if (pack.cells.h[center] < 20)
|
||||
return tip("You cannot place religion center into the water. Please click on a land cell", false, "error");
|
||||
|
||||
const occupied = pack.religions.some(r => !r.removed && r.center === center);
|
||||
if (occupied) return tip("This cell is already a religion center. Please select a different cell", false, "error");
|
||||
|
||||
if (d3.event.shiftKey === false) exitAddReligionMode();
|
||||
Religions.add(center);
|
||||
|
||||
drawReligions();
|
||||
refreshReligionsEditor();
|
||||
drawReligionCenters();
|
||||
}
|
||||
|
||||
function downloadReligionsCsv() {
|
||||
const unit = getAreaUnit("2");
|
||||
const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins`;
|
||||
const lines = Array.from($body.querySelectorAll(":scope > div"));
|
||||
const data = lines.map($line => {
|
||||
const {id, name, color, type, form, deity, area, population} = $line.dataset;
|
||||
const deityText = '"' + deity + '"';
|
||||
const {origins} = pack.religions[+id];
|
||||
const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name);
|
||||
const originText = '"' + originList.join(", ") + '"';
|
||||
return [id, name, color, type, form, deityText, area, population, originText].join(",");
|
||||
});
|
||||
const csvData = [headers].concat(data).join("\n");
|
||||
|
||||
const name = getFileName("Religions") + ".csv";
|
||||
downloadFile(csvData, name);
|
||||
}
|
||||
|
||||
function closeReligionsEditor() {
|
||||
debug.select("#religionCenters").remove();
|
||||
exitReligionsManualAssignment("close");
|
||||
exitAddReligionMode();
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,5 @@
|
|||
import {tip} from "scripts/tooltips";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function exportToJson(type) {
|
||||
if (customization)
|
||||
|
|
|
|||
|
|
@ -1,351 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {heightmapTemplates} from "config/heightmap-templates";
|
||||
import {precreatedHeightmaps} from "config/precreated-heightmaps";
|
||||
import {shouldRegenerateGrid, generateGrid} from "utils/graphUtils";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {generateSeed} from "utils/probabilityUtils";
|
||||
import {getColorScheme} from "utils/colorUtils";
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
|
||||
const initialSeed = generateSeed();
|
||||
let graph = getGraph(grid);
|
||||
|
||||
appendStyleSheet();
|
||||
insertHtml();
|
||||
addListeners();
|
||||
|
||||
export function open() {
|
||||
closeDialogs(".stable");
|
||||
|
||||
const $templateInput = byId("templateInput");
|
||||
setSelected($templateInput.value);
|
||||
graph = getGraph(graph);
|
||||
|
||||
$("#heightmapSelection").dialog({
|
||||
title: "Select Heightmap",
|
||||
resizable: false,
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Select: function () {
|
||||
const id = getSelected();
|
||||
applyDropdownOption($templateInput, id, getName(id));
|
||||
lock("template");
|
||||
|
||||
$(this).dialog("close");
|
||||
},
|
||||
"New Map": function () {
|
||||
const id = getSelected();
|
||||
applyDropdownOption($templateInput, id, getName(id));
|
||||
lock("template");
|
||||
|
||||
const seed = getSeed();
|
||||
regeneratePrompt({seed, graph});
|
||||
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendStyleSheet() {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = /* css */ `
|
||||
div.dialog > div.heightmap-selection {
|
||||
width: 70vw;
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.heightmap-selection_container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
grid-gap: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.heightmap-selection_container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
grid-gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
.heightmap-selection_container {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
grid-gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.heightmap-selection_options {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:first-child {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
align-items: center;
|
||||
justify-self: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.heightmap-selection_options {
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:first-child {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.heightmap-selection_options > div:last-child {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.heightmap-selection article {
|
||||
padding: 4px;
|
||||
border-radius: 8px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
filter: drop-shadow(1px 1px 4px #999);
|
||||
}
|
||||
|
||||
.heightmap-selection article:hover {
|
||||
background-color: #ddd;
|
||||
filter: drop-shadow(1px 1px 8px #999);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.heightmap-selection article.selected {
|
||||
background-color: #ccc;
|
||||
outline: 1px solid var(--dark-solid);
|
||||
filter: drop-shadow(1px 1px 8px #999);
|
||||
}
|
||||
|
||||
.heightmap-selection article > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 2px 1px;
|
||||
}
|
||||
|
||||
.heightmap-selection article > img {
|
||||
width: 100%;
|
||||
aspect-ratio: ${graphWidth}/${graphHeight};
|
||||
border-radius: 8px;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview {
|
||||
outline: 1px solid #bbb;
|
||||
padding: 1px 3px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview:hover {
|
||||
outline: 1px solid #666;
|
||||
}
|
||||
|
||||
.heightmap-selection article .regeneratePreview:active {
|
||||
outline: 1px solid #333;
|
||||
color: #000;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function insertHtml() {
|
||||
const heightmapSelectionHtml = /* html */ `<div id="heightmapSelection" class="dialog stable">
|
||||
<div class="heightmap-selection">
|
||||
<section data-tip="Select heightmap template – template provides unique, but similar-looking maps on generation">
|
||||
<header><h1>Heightmap templates</h1></header>
|
||||
<div class="heightmap-selection_container"></div>
|
||||
</section>
|
||||
<section data-tip="Select precreated heightmap – it will be the same for each map">
|
||||
<header><h1>Precreated heightmaps</h1></header>
|
||||
<div class="heightmap-selection_container"></div>
|
||||
</section>
|
||||
<section>
|
||||
<header><h1>Options</h1></header>
|
||||
<div class="heightmap-selection_options">
|
||||
<div>
|
||||
<label data-tip="Rerender all preview images" class="checkbox-label" id="heightmapSelectionRedrawPreview">
|
||||
<i class="icon-cw"></i>
|
||||
Redraw preview
|
||||
</label>
|
||||
<div>
|
||||
<input id="heightmapSelectionRenderOcean" class="checkbox" type="checkbox" />
|
||||
<label data-tip="Draw heights of water cells" for="heightmapSelectionRenderOcean" class="checkbox-label">Render ocean heights</label>
|
||||
</div>
|
||||
<div data-tip="Color scheme used for heightmap preview">
|
||||
Color scheme
|
||||
<select id="heightmapSelectionColorScheme">
|
||||
<option value="bright" selected>Bright</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="green">Green</option>
|
||||
<option value="monochrome">Monochrome</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button data-tip="Open Template Editor" data-tool="templateEditor" id="heightmapSelectionEditTemplates">Edit Templates</button>
|
||||
<button data-tip="Open Image Converter" data-tool="imageConverter" id="heightmapSelectionImportHeightmap">Import Heightmap</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", heightmapSelectionHtml);
|
||||
|
||||
const sections = document.getElementsByClassName("heightmap-selection_container");
|
||||
|
||||
sections[0].innerHTML = Object.keys(heightmapTemplates)
|
||||
.map(key => {
|
||||
const name = heightmapTemplates[key].name;
|
||||
Math.random = aleaPRNG(initialSeed);
|
||||
const heights = HeightmapGenerator.fromTemplate(graph, key);
|
||||
const dataUrl = drawHeights(heights);
|
||||
|
||||
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
|
||||
<img src="${dataUrl}" alt="${name}" />
|
||||
<div>
|
||||
${name}
|
||||
<span data-tip="Regenerate preview" class="icon-cw regeneratePreview"></span>
|
||||
</div>
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
sections[1].innerHTML = Object.keys(precreatedHeightmaps)
|
||||
.map(key => {
|
||||
const name = precreatedHeightmaps[key].name;
|
||||
drawPrecreatedHeightmap(key);
|
||||
|
||||
return /* html */ `<article data-id="${key}" data-seed="${initialSeed}">
|
||||
<img alt="${name}" />
|
||||
<div>${name}</div>
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
byId("heightmapSelection").on("click", event => {
|
||||
const article = event.target.closest("#heightmapSelection article");
|
||||
if (!article) return;
|
||||
|
||||
const id = article.dataset.id;
|
||||
if (event.target.matches("span.icon-cw")) regeneratePreview(article, id);
|
||||
setSelected(id);
|
||||
});
|
||||
|
||||
byId("heightmapSelectionRenderOcean").on("change", redrawAll);
|
||||
byId("heightmapSelectionColorScheme").on("change", redrawAll);
|
||||
byId("heightmapSelectionRedrawPreview").on("click", redrawAll);
|
||||
byId("heightmapSelectionEditTemplates").on("click", confirmHeightmapEdit);
|
||||
byId("heightmapSelectionImportHeightmap").on("click", confirmHeightmapEdit);
|
||||
}
|
||||
|
||||
function getSelected() {
|
||||
return byId("heightmapSelection").querySelector(".selected")?.dataset?.id;
|
||||
}
|
||||
|
||||
function setSelected(id) {
|
||||
const $heightmapSelection = byId("heightmapSelection");
|
||||
$heightmapSelection.querySelector(".selected")?.classList?.remove("selected");
|
||||
$heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected");
|
||||
}
|
||||
|
||||
function getSeed() {
|
||||
return byId("heightmapSelection").querySelector(".selected")?.dataset?.seed;
|
||||
}
|
||||
|
||||
function getName(id) {
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
return isTemplate ? heightmapTemplates[id].name : precreatedHeightmaps[id].name;
|
||||
}
|
||||
|
||||
function getGraph(currentGraph) {
|
||||
const newGraph = shouldRegenerateGrid(currentGraph) ? generateGrid() : structuredClone(currentGraph);
|
||||
delete newGraph.cells.h;
|
||||
return newGraph;
|
||||
}
|
||||
|
||||
function drawHeights(heights) {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = graph.cellsX;
|
||||
canvas.height = graph.cellsY;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const imageData = ctx.createImageData(graph.cellsX, graph.cellsY);
|
||||
|
||||
const schemeId = byId("heightmapSelectionColorScheme").value;
|
||||
const scheme = getColorScheme(schemeId);
|
||||
const renderOcean = byId("heightmapSelectionRenderOcean").checked;
|
||||
const getHeight = height => (height < 20 ? (renderOcean ? height : 0) : height);
|
||||
|
||||
for (let i = 0; i < heights.length; i++) {
|
||||
const color = scheme(1 - getHeight(heights[i]) / 100);
|
||||
const {r, g, b} = d3.color(color);
|
||||
|
||||
const n = i * 4;
|
||||
imageData.data[n] = r;
|
||||
imageData.data[n + 1] = g;
|
||||
imageData.data[n + 2] = b;
|
||||
imageData.data[n + 3] = 255;
|
||||
}
|
||||
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
return canvas.toDataURL("image/png");
|
||||
}
|
||||
|
||||
function drawTemplatePreview(id) {
|
||||
const heights = HeightmapGenerator.fromTemplate(graph, id);
|
||||
const dataUrl = drawHeights(heights);
|
||||
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
|
||||
article.querySelector("img").src = dataUrl;
|
||||
}
|
||||
|
||||
async function drawPrecreatedHeightmap(id) {
|
||||
const heights = await HeightmapGenerator.fromPrecreated(graph, id);
|
||||
const dataUrl = drawHeights(heights);
|
||||
const article = byId("heightmapSelection").querySelector(`[data-id="${id}"]`);
|
||||
article.querySelector("img").src = dataUrl;
|
||||
}
|
||||
|
||||
function regeneratePreview(article, id) {
|
||||
graph = getGraph(graph);
|
||||
const seed = generateSeed();
|
||||
article.dataset.seed = seed;
|
||||
Math.random = aleaPRNG(seed);
|
||||
drawTemplatePreview(id);
|
||||
}
|
||||
|
||||
function redrawAll() {
|
||||
graph = getGraph(graph);
|
||||
const articles = byId("heightmapSelection").querySelectorAll(`article`);
|
||||
for (const article of articles) {
|
||||
const {id, seed} = article.dataset;
|
||||
Math.random = aleaPRNG(seed);
|
||||
|
||||
const isTemplate = id in heightmapTemplates;
|
||||
if (isTemplate) drawTemplatePreview(id);
|
||||
else drawPrecreatedHeightmap(id);
|
||||
}
|
||||
}
|
||||
|
||||
function confirmHeightmapEdit() {
|
||||
const tool = this.dataset.tool;
|
||||
|
||||
confirmationDialog({
|
||||
title: this.dataset.tip,
|
||||
message: "Opening the tool will erase the current map. Are you sure you want to proceed?",
|
||||
confirm: "Continue",
|
||||
onConfirm: () => editHeightmap({mode: "erase", tool})
|
||||
});
|
||||
}
|
||||
|
|
@ -1,514 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {byId} from "utils/shorthands";
|
||||
import {tip} from "scripts/tooltips";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
|
||||
appendStyleSheet();
|
||||
insertHtml();
|
||||
|
||||
const MARGINS = {top: 10, right: 10, bottom: -5, left: 10};
|
||||
|
||||
const handleZoom = () => viewbox.attr("transform", d3.event.transform);
|
||||
const zoom = d3.zoom().scaleExtent([0.2, 1.5]).on("zoom", handleZoom);
|
||||
|
||||
// store old root for transitions
|
||||
let oldRoot;
|
||||
|
||||
// define svg elements
|
||||
const svg = d3.select("#hierarchyTree > svg").call(zoom);
|
||||
const viewbox = svg.select("g#hierarchyTree_viewbox");
|
||||
const primaryLinks = viewbox.select("g#hierarchyTree_linksPrimary");
|
||||
const secondaryLinks = viewbox.select("g#hierarchyTree_linksSecondary");
|
||||
const nodes = viewbox.select("g#hierarchyTree_nodes");
|
||||
const dragLine = viewbox.select("path#hierarchyTree_dragLine");
|
||||
|
||||
// properties
|
||||
let dataElements; // {i, name, type, origins}[], e.g. path.religions
|
||||
let validElements; // not-removed dataElements
|
||||
let onNodeEnter; // d3Data => void
|
||||
let onNodeLeave; // d3Data => void
|
||||
let getDescription; // dataElement => string
|
||||
let getShape; // dataElement => string;
|
||||
|
||||
export function open(props) {
|
||||
closeDialogs("#hierarchyTree, .stable");
|
||||
|
||||
dataElements = props.data;
|
||||
dataElements[0].origins = [null];
|
||||
validElements = dataElements.filter(r => !r.removed);
|
||||
if (validElements.length < 3) return tip(`Not enough ${props.type} to show hierarchy`, false, "error");
|
||||
|
||||
onNodeEnter = props.onNodeEnter;
|
||||
onNodeLeave = props.onNodeLeave;
|
||||
getDescription = props.getDescription;
|
||||
getShape = props.getShape;
|
||||
|
||||
const root = getRoot();
|
||||
const treeWidth = root.leaves().length * 50;
|
||||
const treeHeight = root.height * 50;
|
||||
|
||||
const w = treeWidth - MARGINS.left - MARGINS.right;
|
||||
const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom;
|
||||
const treeLayout = d3.tree().size([w, h]);
|
||||
|
||||
const width = minmax(treeWidth, 300, innerWidth * 0.75);
|
||||
const height = minmax(treeHeight, 200, innerHeight * 0.75);
|
||||
|
||||
zoom.extent([Array(2).fill(0), [width, height]]);
|
||||
svg.attr("viewBox", `0, 0, ${width}, ${height}`);
|
||||
|
||||
$("#hierarchyTree").dialog({
|
||||
title: `${capitalize(props.type)} tree`,
|
||||
position: {my: "left center", at: "left+10 center", of: "svg"},
|
||||
width
|
||||
});
|
||||
|
||||
renderTree(root, treeLayout);
|
||||
}
|
||||
|
||||
function appendStyleSheet() {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = /* css */ `
|
||||
#hierarchyTree_selectedOrigins > button {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
#hierarchyTree {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#hierarchyTree > svg {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hierarchyTree_selectedOrigins {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.hierarchyTree_selectedOrigin {
|
||||
border: 1px solid #aaa;
|
||||
background: none;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.hierarchyTree_selectedOrigin:hover {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.hierarchyTree_selectedOrigin::after {
|
||||
content: "✕";
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.hierarchyTree_selectedOrigin:hover:after {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#hierarchyTree_originSelector {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#hierarchyTree_originSelector > form > div {
|
||||
padding: 0.3em;
|
||||
margin: 1px 0;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
#hierarchyTree_originSelector > form > div:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
#hierarchyTree_originSelector > form > div[checked] {
|
||||
background-color: #c6d6d6;
|
||||
}
|
||||
|
||||
#hierarchyTree_nodes > g > text {
|
||||
pointer-events: none;
|
||||
stroke: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#hierarchyTree_nodes > g.selected {
|
||||
stroke: #c13119;
|
||||
stroke-width: 1;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#hierarchyTree_dragLine {
|
||||
marker-end: url(#end-arrow);
|
||||
stroke: #333333;
|
||||
stroke-dasharray: 5;
|
||||
stroke-dashoffset: 1000;
|
||||
animation: dash 80s linear backwards;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function insertHtml() {
|
||||
const html = /* html */ `<div id="hierarchyTree" class="dialog" style="overflow: hidden;">
|
||||
<svg>
|
||||
<g id="hierarchyTree_viewbox" style="text-anchor: middle; dominant-baseline: central">
|
||||
<g transform="translate(10, -45)">
|
||||
<g id="hierarchyTree_links" fill="none" stroke="#aaa">
|
||||
<g id="hierarchyTree_linksPrimary"></g>
|
||||
<g id="hierarchyTree_linksSecondary" stroke-dasharray="1"></g>
|
||||
</g>
|
||||
<g id="hierarchyTree_nodes"></g>
|
||||
<path id="hierarchyTree_dragLine" path='' />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div id="hierarchyTree_details" class='chartInfo'>
|
||||
<div id='hierarchyTree_infoLine' style="display: block">‍</div>
|
||||
<div id='hierarchyTree_selected' style="display: none">
|
||||
<span><span id='hierarchyTree_selectedName'></span>. </span>
|
||||
<span data-name="Type short name (abbreviation)">Abbreviation: <input id='hierarchyTree_selectedCode' type='text' maxlength='3' size='3' /></span>
|
||||
<span>Origins: <span id='hierarchyTree_selectedOrigins'></span></span>
|
||||
<button data-tip='Edit this node's origins' class="hierarchyTree_selectedButton" id='hierarchyTree_selectedSelectButton'>Edit</button>
|
||||
<button data-tip='Unselect this node' class="hierarchyTree_selectedButton" id='hierarchyTree_selectedCloseButton'>Unselect</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="hierarchyTree_originSelector"></div>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", html);
|
||||
}
|
||||
|
||||
function getRoot() {
|
||||
const root = d3
|
||||
.stratify()
|
||||
.id(d => d.i)
|
||||
.parentId(d => d.origins[0])(validElements);
|
||||
|
||||
oldRoot = root;
|
||||
return root;
|
||||
}
|
||||
|
||||
function getLinkKey(d) {
|
||||
return `${d.source.id}-${d.target.id}`;
|
||||
}
|
||||
|
||||
function getNodeKey(d) {
|
||||
return d.id;
|
||||
}
|
||||
|
||||
function getLinkPath(d) {
|
||||
const {
|
||||
source: {x: sx, y: sy},
|
||||
target: {x: tx, y: ty}
|
||||
} = d;
|
||||
return `M${sx},${sy} C${sx},${(sy * 3 + ty) / 4} ${tx},${(sy * 2 + ty) / 3} ${tx},${ty}`;
|
||||
}
|
||||
|
||||
function getSecondaryLinks(root) {
|
||||
const nodes = root.descendants();
|
||||
const links = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
const origins = node.data.origins;
|
||||
|
||||
for (let i = 1; i < origins.length; i++) {
|
||||
const source = nodes.find(n => n.data.i === origins[i]);
|
||||
if (source) links.push({source, target: node});
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
}
|
||||
|
||||
const shapesMap = {
|
||||
undefined: "M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0", // small circle
|
||||
circle: "M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0",
|
||||
square: "M-11,-11h22v22h-22Z",
|
||||
hexagon: "M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z",
|
||||
diamond: "M0,-14L14,0L0,14L-14,0Z",
|
||||
concave: "M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z",
|
||||
octagon: "M-4.97,-12.01 l9.95,0 l7.04,7.04 l0,9.95 l-7.04,7.04 l-9.95,0 l-7.04,-7.04 l0,-9.95Z",
|
||||
pentagon: "M0,-14l14,11l-6,14h-16l-6,-14Z"
|
||||
};
|
||||
|
||||
const getSortIndex = node => {
|
||||
const descendants = node.descendants();
|
||||
const secondaryOrigins = descendants.map(({data}) => data.origins.slice(1)).flat();
|
||||
|
||||
if (secondaryOrigins.length === 0) return node.data.i;
|
||||
return d3.mean(secondaryOrigins);
|
||||
};
|
||||
|
||||
function renderTree(root, treeLayout) {
|
||||
treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b)));
|
||||
|
||||
primaryLinks.selectAll("path").data(root.links(), getLinkKey).join("path").attr("d", getLinkPath);
|
||||
secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join("path").attr("d", getLinkPath);
|
||||
|
||||
const node = nodes
|
||||
.selectAll("g")
|
||||
.data(root.descendants(), getNodeKey)
|
||||
.join("g")
|
||||
.attr("data-id", d => d.data.i)
|
||||
.attr("stroke", "#333")
|
||||
.attr("transform", d => `translate(${d.x}, ${d.y})`)
|
||||
.on("mouseenter", handleNoteEnter)
|
||||
.on("mouseleave", handleNodeExit)
|
||||
.on("click", selectElement)
|
||||
.call(d3.drag().on("start", dragToReorigin));
|
||||
|
||||
node
|
||||
.selectAll("path")
|
||||
.data(d => [d])
|
||||
.join("path")
|
||||
.attr("d", d => shapesMap[getShape(d.data)])
|
||||
.attr("fill", d => d.data.color || "#ffffff")
|
||||
.attr("stroke-dasharray", d => (d.data.cells ? "none" : "1"));
|
||||
|
||||
node
|
||||
.selectAll("text")
|
||||
.data(d => [d])
|
||||
.join("text")
|
||||
.text(d => d.data.code || "");
|
||||
}
|
||||
|
||||
function mapCoords(newRoot, prevRoot) {
|
||||
newRoot.x = prevRoot.x;
|
||||
newRoot.y = prevRoot.y;
|
||||
|
||||
for (const node of newRoot.descendants()) {
|
||||
const prevNode = prevRoot.descendants().find(n => n.data.i === node.data.i);
|
||||
if (prevNode) {
|
||||
node.x = prevNode.x;
|
||||
node.y = prevNode.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTree() {
|
||||
const prevRoot = oldRoot;
|
||||
const root = getRoot();
|
||||
mapCoords(root, prevRoot);
|
||||
|
||||
const linksUpdateDuration = 50;
|
||||
const moveDuration = 1000;
|
||||
|
||||
// old layout: update links at old nodes positions
|
||||
const linkEnter = enter =>
|
||||
enter
|
||||
.append("path")
|
||||
.attr("d", getLinkPath)
|
||||
.attr("opacity", 0)
|
||||
.call(enter => enter.transition().duration(linksUpdateDuration).attr("opacity", 1));
|
||||
|
||||
const linkUpdate = update =>
|
||||
update.call(update => update.transition().duration(linksUpdateDuration).attr("d", getLinkPath));
|
||||
|
||||
const linkExit = exit =>
|
||||
exit.call(exit => exit.transition().duration(linksUpdateDuration).attr("opacity", 0).remove());
|
||||
|
||||
primaryLinks.selectAll("path").data(root.links(), getLinkKey).join(linkEnter, linkUpdate, linkExit);
|
||||
secondaryLinks.selectAll("path").data(getSecondaryLinks(root), getLinkKey).join(linkEnter, linkUpdate, linkExit);
|
||||
|
||||
// new layout: move nodes with links to new positions
|
||||
const treeWidth = root.leaves().length * 50;
|
||||
const treeHeight = root.height * 50;
|
||||
|
||||
const w = treeWidth - MARGINS.left - MARGINS.right;
|
||||
const h = treeHeight + 30 - MARGINS.top - MARGINS.bottom;
|
||||
|
||||
const treeLayout = d3.tree().size([w, h]);
|
||||
treeLayout(root.sort((a, b) => getSortIndex(a) - getSortIndex(b)));
|
||||
|
||||
primaryLinks
|
||||
.selectAll("path")
|
||||
.data(root.links(), getLinkKey)
|
||||
.transition()
|
||||
.duration(moveDuration)
|
||||
.delay(linksUpdateDuration)
|
||||
.attr("d", getLinkPath);
|
||||
|
||||
secondaryLinks
|
||||
.selectAll("path")
|
||||
.data(getSecondaryLinks(root), getLinkKey)
|
||||
.transition()
|
||||
.duration(moveDuration)
|
||||
.delay(linksUpdateDuration)
|
||||
.attr("d", getLinkPath);
|
||||
|
||||
nodes
|
||||
.selectAll("g")
|
||||
.data(root.descendants(), getNodeKey)
|
||||
.transition()
|
||||
.delay(linksUpdateDuration)
|
||||
.duration(moveDuration)
|
||||
.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||
}
|
||||
|
||||
function selectElement(d) {
|
||||
const dataElement = d.data;
|
||||
if (d.id == 0) return;
|
||||
|
||||
const node = nodes.select(`g[data-id="${d.id}"]`);
|
||||
nodes.selectAll("g").style("outline", "none");
|
||||
node.style("outline", "1px solid #c13119");
|
||||
|
||||
byId("hierarchyTree_selected").style.display = "block";
|
||||
byId("hierarchyTree_infoLine").style.display = "none";
|
||||
|
||||
byId("hierarchyTree_selectedName").innerText = dataElement.name;
|
||||
byId("hierarchyTree_selectedCode").value = dataElement.code;
|
||||
|
||||
byId("hierarchyTree_selectedCode").onchange = function () {
|
||||
if (this.value.length > 3) return tip("Abbreviation must be 3 characters or less", false, "error", 3000);
|
||||
if (!this.value.length) return tip("Abbreviation cannot be empty", false, "error", 3000);
|
||||
|
||||
node.select("text").text(this.value);
|
||||
dataElement.code = this.value;
|
||||
};
|
||||
|
||||
const createOriginButtons = () => {
|
||||
byId("hierarchyTree_selectedOrigins").innerHTML = dataElement.origins
|
||||
.filter(origin => origin)
|
||||
.map((origin, index) => {
|
||||
const {name, code} = validElements.find(r => r.i === origin) || {};
|
||||
const type = index ? "Secondary" : "Primary";
|
||||
const tip = `${type} origin: ${name}. Click to remove link to that origin`;
|
||||
return `<button data-id="${origin}" class="hierarchyTree_selectedButton hierarchyTree_selectedOrigin" data-tip="${tip}">${code}</button>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
byId("hierarchyTree_selectedOrigins").onclick = event => {
|
||||
const target = event.target;
|
||||
if (target.tagName !== "BUTTON") return;
|
||||
const origin = Number(target.dataset.id);
|
||||
const filtered = dataElement.origins.filter(elementOrigin => elementOrigin !== origin);
|
||||
dataElement.origins = filtered.length ? filtered : [0];
|
||||
target.remove();
|
||||
updateTree();
|
||||
};
|
||||
};
|
||||
|
||||
createOriginButtons();
|
||||
|
||||
byId("hierarchyTree_selectedSelectButton").onclick = () => {
|
||||
const origins = dataElement.origins;
|
||||
|
||||
const descendants = d.descendants().map(d => d.data.i);
|
||||
const selectableElements = validElements.filter(({i}) => !descendants.includes(i));
|
||||
|
||||
const selectableElementsHtml = selectableElements.map(({i, name, code, color}) => {
|
||||
const isPrimary = origins[0] === i ? "checked" : "";
|
||||
const isChecked = origins.includes(i) ? "checked" : "";
|
||||
|
||||
if (i === 0) {
|
||||
return /*html*/ `
|
||||
<div ${isChecked}>
|
||||
<input data-tip="Set as primary origin" type="radio" name="primary" value="${i}" ${isPrimary} />
|
||||
Top level
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return /*html*/ `
|
||||
<div ${isChecked}>
|
||||
<input data-tip="Set as primary origin" type="radio" name="primary" value="${i}" ${isPrimary} />
|
||||
<input data-id="${i}" id="selectElementOrigin${i}" class="checkbox" type="checkbox" ${isChecked} />
|
||||
<label data-tip="Check to set as a secondary origin" for="selectElementOrigin${i}" class="checkbox-label">
|
||||
<fill-box fill="${color}" size=".8em" disabled></fill-box>
|
||||
${code}: ${name}</label>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
byId("hierarchyTree_originSelector").innerHTML = /*html*/ `
|
||||
<form style="max-height: 35vh">
|
||||
${selectableElementsHtml.join("")}
|
||||
</form>
|
||||
`;
|
||||
|
||||
$("#hierarchyTree_originSelector").dialog({
|
||||
title: "Select origins",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Select: () => {
|
||||
$("#hierarchyTree_originSelector").dialog("close");
|
||||
const $selector = byId("hierarchyTree_originSelector");
|
||||
const selectedRadio = $selector.querySelector("input[type='radio']:checked");
|
||||
const selectedCheckboxes = $selector.querySelectorAll("input[type='checkbox']:checked");
|
||||
|
||||
const primary = selectedRadio ? Number(selectedRadio.value) : 0;
|
||||
const secondary = Array.from(selectedCheckboxes)
|
||||
.map(input => Number(input.dataset.id))
|
||||
.filter(origin => origin !== primary);
|
||||
|
||||
dataElement.origins = [primary, ...secondary];
|
||||
|
||||
updateTree();
|
||||
createOriginButtons();
|
||||
},
|
||||
Cancel: () => {
|
||||
$("#hierarchyTree_originSelector").dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
byId("hierarchyTree_selectedCloseButton").onclick = () => {
|
||||
node.style("outline", "none");
|
||||
byId("hierarchyTree_selected").style.display = "none";
|
||||
byId("hierarchyTree_infoLine").style.display = "block";
|
||||
};
|
||||
}
|
||||
|
||||
function handleNoteEnter(d) {
|
||||
if (d.depth === 0) return;
|
||||
|
||||
this.classList.add("selected");
|
||||
onNodeEnter(d);
|
||||
|
||||
byId("hierarchyTree_infoLine").innerText = getDescription(d.data);
|
||||
tip("Drag to other node to add parent, click to edit");
|
||||
}
|
||||
|
||||
function handleNodeExit(d) {
|
||||
this.classList.remove("selected");
|
||||
onNodeLeave(d);
|
||||
|
||||
byId("hierarchyTree_infoLine").innerHTML = "‍";
|
||||
tip("");
|
||||
}
|
||||
|
||||
function dragToReorigin(from) {
|
||||
if (from.id == 0) return;
|
||||
|
||||
dragLine.attr("d", `M${from.x},${from.y}L${from.x},${from.y}`);
|
||||
|
||||
d3.event.on("drag", () => {
|
||||
dragLine.attr("d", `M${from.x},${from.y}L${d3.event.x},${d3.event.y}`);
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
dragLine.attr("d", "");
|
||||
const selected = nodes.select("g.selected");
|
||||
if (!selected.size()) return;
|
||||
|
||||
const elementId = from.data.i;
|
||||
const newOrigin = selected.datum().data.i;
|
||||
if (elementId === newOrigin) return; // dragged to itself
|
||||
if (from.data.origins.includes(newOrigin)) return; // already a child of the selected node
|
||||
if (from.descendants().some(node => node.data.i === newOrigin)) return; // cannot be a child of its own child
|
||||
|
||||
const element = dataElements.find(({i}) => i === elementId);
|
||||
if (!element) return;
|
||||
|
||||
if (element.origins[0] === 0) element.origins = [];
|
||||
element.origins.push(newOrigin);
|
||||
|
||||
selectElement(from);
|
||||
updateTree();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,705 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {isWater} from "utils/graphUtils";
|
||||
import {tip} from "scripts/tooltips";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {si, convertTemperature, getFriendlyPrecipitation} from "utils/unitUtils";
|
||||
import {rollups} from "utils/functionUtils";
|
||||
|
||||
const entitiesMap = {
|
||||
states: {
|
||||
label: "State",
|
||||
getCellsData: () => pack.cells.state,
|
||||
getName: nameGetter("states"),
|
||||
getColors: colorsGetter("states"),
|
||||
landOnly: true
|
||||
},
|
||||
cultures: {
|
||||
label: "Culture",
|
||||
getCellsData: () => pack.cells.culture,
|
||||
getName: nameGetter("cultures"),
|
||||
getColors: colorsGetter("cultures"),
|
||||
landOnly: true
|
||||
},
|
||||
religions: {
|
||||
label: "Religion",
|
||||
getCellsData: () => pack.cells.religion,
|
||||
getName: nameGetter("religions"),
|
||||
getColors: colorsGetter("religions"),
|
||||
landOnly: true
|
||||
},
|
||||
provinces: {
|
||||
label: "Province",
|
||||
getCellsData: () => pack.cells.province,
|
||||
getName: nameGetter("provinces"),
|
||||
getColors: colorsGetter("provinces"),
|
||||
landOnly: true
|
||||
},
|
||||
biomes: {
|
||||
label: "Biome",
|
||||
getCellsData: () => pack.cells.biome,
|
||||
getName: biomeNameGetter,
|
||||
getColors: biomeColorsGetter,
|
||||
landOnly: false
|
||||
}
|
||||
};
|
||||
|
||||
const quantizationMap = {
|
||||
total_population: {
|
||||
label: "Total population",
|
||||
quantize: cellId => getUrbanPopulation(cellId) + getRuralPopulation(cellId),
|
||||
aggregate: values => rn(d3.sum(values)),
|
||||
formatTicks: value => si(value),
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
urban_population: {
|
||||
label: "Urban population",
|
||||
quantize: getUrbanPopulation,
|
||||
aggregate: values => rn(d3.sum(values)),
|
||||
formatTicks: value => si(value),
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
rural_population: {
|
||||
label: "Rural population",
|
||||
quantize: getRuralPopulation,
|
||||
aggregate: values => rn(d3.sum(values)),
|
||||
formatTicks: value => si(value),
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
area: {
|
||||
label: "Land area",
|
||||
quantize: cellId => getArea(pack.cells.area[cellId]),
|
||||
aggregate: values => rn(d3.sum(values)),
|
||||
formatTicks: value => `${si(value)} ${getAreaUnit()}`,
|
||||
stringify: value => `${value.toLocaleString()} ${getAreaUnit()}`,
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
cells: {
|
||||
label: "Number of cells",
|
||||
quantize: () => 1,
|
||||
aggregate: values => d3.sum(values),
|
||||
formatTicks: value => value,
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
burgs_number: {
|
||||
label: "Number of burgs",
|
||||
quantize: cellId => (pack.cells.burg[cellId] ? 1 : 0),
|
||||
aggregate: values => d3.sum(values),
|
||||
formatTicks: value => value,
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
average_elevation: {
|
||||
label: "Average elevation",
|
||||
quantize: cellId => pack.cells.h[cellId],
|
||||
aggregate: values => d3.mean(values),
|
||||
formatTicks: value => getHeight(value),
|
||||
stringify: value => getHeight(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
max_elevation: {
|
||||
label: "Maximum mean elevation",
|
||||
quantize: cellId => pack.cells.h[cellId],
|
||||
aggregate: values => d3.max(values),
|
||||
formatTicks: value => getHeight(value),
|
||||
stringify: value => getHeight(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
min_elevation: {
|
||||
label: "Minimum mean elevation",
|
||||
quantize: cellId => pack.cells.h[cellId],
|
||||
aggregate: values => d3.min(values),
|
||||
formatTicks: value => getHeight(value),
|
||||
stringify: value => getHeight(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
average_temperature: {
|
||||
label: "Annual mean temperature",
|
||||
quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
|
||||
aggregate: values => d3.mean(values),
|
||||
formatTicks: value => convertTemperature(value),
|
||||
stringify: value => convertTemperature(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
max_temperature: {
|
||||
label: "Mean annual maximum temperature",
|
||||
quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
|
||||
aggregate: values => d3.max(values),
|
||||
formatTicks: value => convertTemperature(value),
|
||||
stringify: value => convertTemperature(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
min_temperature: {
|
||||
label: "Mean annual minimum temperature",
|
||||
quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
|
||||
aggregate: values => d3.min(values),
|
||||
formatTicks: value => convertTemperature(value),
|
||||
stringify: value => convertTemperature(value),
|
||||
stackable: false,
|
||||
landOnly: false
|
||||
},
|
||||
average_precipitation: {
|
||||
label: "Annual mean precipitation",
|
||||
quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
|
||||
aggregate: values => rn(d3.mean(values)),
|
||||
formatTicks: value => getFriendlyPrecipitation(rn(value)),
|
||||
stringify: value => getFriendlyPrecipitation(rn(value)),
|
||||
stackable: false,
|
||||
landOnly: true
|
||||
},
|
||||
max_precipitation: {
|
||||
label: "Mean annual maximum precipitation",
|
||||
quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
|
||||
aggregate: values => rn(d3.max(values)),
|
||||
formatTicks: value => getFriendlyPrecipitation(rn(value)),
|
||||
stringify: value => getFriendlyPrecipitation(rn(value)),
|
||||
stackable: false,
|
||||
landOnly: true
|
||||
},
|
||||
min_precipitation: {
|
||||
label: "Mean annual minimum precipitation",
|
||||
quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
|
||||
aggregate: values => rn(d3.min(values)),
|
||||
formatTicks: value => getFriendlyPrecipitation(rn(value)),
|
||||
stringify: value => getFriendlyPrecipitation(rn(value)),
|
||||
stackable: false,
|
||||
landOnly: true
|
||||
},
|
||||
coastal_cells: {
|
||||
label: "Number of coastal cells",
|
||||
quantize: cellId => (pack.cells.t[cellId] === 1 ? 1 : 0),
|
||||
aggregate: values => d3.sum(values),
|
||||
formatTicks: value => value,
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
},
|
||||
river_cells: {
|
||||
label: "Number of river cells",
|
||||
quantize: cellId => (pack.cells.r[cellId] ? 1 : 0),
|
||||
aggregate: values => d3.sum(values),
|
||||
formatTicks: value => value,
|
||||
stringify: value => value.toLocaleString(),
|
||||
stackable: true,
|
||||
landOnly: true
|
||||
}
|
||||
};
|
||||
|
||||
const plotTypeMap = {
|
||||
stackedBar: {offset: d3.stackOffsetDiverging},
|
||||
normalizedStackedBar: {offset: d3.stackOffsetExpand, formatX: value => rn(value * 100) + "%"}
|
||||
};
|
||||
|
||||
let charts = []; // store charts data
|
||||
let prevMapId = mapId;
|
||||
|
||||
appendStyleSheet();
|
||||
insertHtml();
|
||||
addListeners();
|
||||
changeViewColumns();
|
||||
|
||||
export function open() {
|
||||
closeDialogs("#chartsOverview, .stable");
|
||||
|
||||
if (prevMapId !== mapId) {
|
||||
charts = [];
|
||||
prevMapId = mapId;
|
||||
}
|
||||
|
||||
if (!charts.length) addChart();
|
||||
else charts.forEach(chart => renderChart(chart));
|
||||
|
||||
$("#chartsOverview").dialog({
|
||||
title: "Data Charts",
|
||||
position: {my: "center", at: "center", of: "svg"},
|
||||
close: handleClose
|
||||
});
|
||||
}
|
||||
|
||||
function appendStyleSheet() {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = /* css */ `
|
||||
#chartsOverview {
|
||||
max-width: 90vw !important;
|
||||
max-height: 90vh !important;
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
|
||||
#chartsOverview__form {
|
||||
font-size: 1.1em;
|
||||
margin: 0.3em 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 0.3em;
|
||||
align-items: start;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
#chartsOverview__form {
|
||||
font-size: 1em;
|
||||
grid-template-columns: 1fr;
|
||||
justify-items: normal;
|
||||
}
|
||||
}
|
||||
|
||||
#chartsOverview__charts {
|
||||
overflow: auto;
|
||||
scroll-behavior: smooth;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
#chartsOverview__charts figure {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#chartsOverview__charts figcaption {
|
||||
font-size: 1.2em;
|
||||
margin: 0 1% 0 4%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
`;
|
||||
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function insertHtml() {
|
||||
const entities = Object.entries(entitiesMap).map(([entity, {label}]) => [entity, label]);
|
||||
const plotBy = Object.entries(quantizationMap).map(([plotBy, {label}]) => [plotBy, label]);
|
||||
|
||||
const createOption = ([value, label]) => `<option value="${value}">${label}</option>`;
|
||||
const createOptions = values => values.map(createOption).join("");
|
||||
|
||||
const html = /* html */ `<div id="chartsOverview" class="dialog stable">
|
||||
<form id="chartsOverview__form">
|
||||
<div>
|
||||
<button data-tip="Add a chart" type="submit">Plot</button>
|
||||
|
||||
<select data-tip="Select entity (y axis)" id="chartsOverview__entitiesSelect">
|
||||
${createOptions(entities)}
|
||||
</select>
|
||||
|
||||
<label>by
|
||||
<select data-tip="Select value to plot by (x axis)" id="chartsOverview__plotBySelect">
|
||||
${createOptions(plotBy)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>grouped by
|
||||
<select data-tip="Select entoty to group by. If you don't need grouping, set it the same as the entity" id="chartsOverview__groupBySelect">
|
||||
${createOptions(entities)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label data-tip="Sorting type">sorted
|
||||
<select id="chartsOverview__sortingSelect">
|
||||
<option value="value">by value</option>
|
||||
<option value="name">by name</option>
|
||||
<option value="natural">naturally</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<span data-tip="Chart type">Type</span>
|
||||
<select id="chartsOverview__chartType">
|
||||
<option value="stackedBar" selected>Stacked Bar</option>
|
||||
<option value="normalizedStackedBar">Normalized Stacked Bar</option>
|
||||
</select>
|
||||
|
||||
<span data-tip="Columns to display">Columns</span>
|
||||
<select id="chartsOverview__viewColumns">
|
||||
<option value="1" selected>1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<section id="chartsOverview__charts"></section>
|
||||
</div>`;
|
||||
|
||||
byId("dialogs").insertAdjacentHTML("beforeend", html);
|
||||
|
||||
// set defaults
|
||||
byId("chartsOverview__entitiesSelect").value = "states";
|
||||
byId("chartsOverview__plotBySelect").value = "total_population";
|
||||
byId("chartsOverview__groupBySelect").value = "cultures";
|
||||
}
|
||||
|
||||
function addListeners() {
|
||||
byId("chartsOverview__form").on("submit", addChart);
|
||||
byId("chartsOverview__viewColumns").on("change", changeViewColumns);
|
||||
}
|
||||
|
||||
function addChart(event) {
|
||||
if (event) event.preventDefault();
|
||||
|
||||
const entity = byId("chartsOverview__entitiesSelect").value;
|
||||
const plotBy = byId("chartsOverview__plotBySelect").value;
|
||||
let groupBy = byId("chartsOverview__groupBySelect").value;
|
||||
const sorting = byId("chartsOverview__sortingSelect").value;
|
||||
const type = byId("chartsOverview__chartType").value;
|
||||
|
||||
const {stackable} = quantizationMap[plotBy];
|
||||
|
||||
if (!stackable && groupBy !== entity) {
|
||||
tip(`Grouping is not supported for ${plotByLabel}`, false, "warn", 4000);
|
||||
groupBy = entity;
|
||||
}
|
||||
|
||||
const chartOptions = {id: Date.now(), entity, plotBy, groupBy, sorting, type};
|
||||
charts.push(chartOptions);
|
||||
renderChart(chartOptions);
|
||||
updateDialogPosition();
|
||||
}
|
||||
|
||||
function renderChart({id, entity, plotBy, groupBy, sorting, type}) {
|
||||
const {
|
||||
label: plotByLabel,
|
||||
stringify,
|
||||
quantize,
|
||||
aggregate,
|
||||
formatTicks,
|
||||
landOnly: plotByLandOnly
|
||||
} = quantizationMap[plotBy];
|
||||
|
||||
const noGrouping = groupBy === entity;
|
||||
|
||||
const {
|
||||
label: entityLabel,
|
||||
getName: getEntityName,
|
||||
getCellsData: getEntityCellsData,
|
||||
landOnly: entityLandOnly
|
||||
} = entitiesMap[entity];
|
||||
const {label: groupLabel, getName: getGroupName, getCellsData: getGroupCellsData, getColors} = entitiesMap[groupBy];
|
||||
|
||||
const entityCells = getEntityCellsData();
|
||||
const groupCells = getGroupCellsData();
|
||||
|
||||
const title = `${capitalize(entity)} by ${plotByLabel}${noGrouping ? "" : " grouped by " + groupLabel}`;
|
||||
|
||||
const tooltip = (entity, group, value, percentage) => {
|
||||
const entityTip = `${entityLabel}: ${entity}`;
|
||||
const groupTip = noGrouping ? "" : `${groupLabel}: ${group}`;
|
||||
let valueTip = `${plotByLabel}: ${stringify(value)}`;
|
||||
if (!noGrouping) valueTip += ` (${rn(percentage * 100)}%)`;
|
||||
return [entityTip, groupTip, valueTip].filter(Boolean);
|
||||
};
|
||||
|
||||
const dataCollection = {};
|
||||
const groups = new Set();
|
||||
|
||||
for (const cellId of pack.cells.i) {
|
||||
if ((entityLandOnly || plotByLandOnly) && isWater(cellId)) continue;
|
||||
const entityId = entityCells[cellId];
|
||||
const groupId = groupCells[cellId];
|
||||
const value = quantize(cellId);
|
||||
|
||||
if (!dataCollection[entityId]) dataCollection[entityId] = {[groupId]: [value]};
|
||||
else if (!dataCollection[entityId][groupId]) dataCollection[entityId][groupId] = [value];
|
||||
else dataCollection[entityId][groupId].push(value);
|
||||
|
||||
groups.add(groupId);
|
||||
}
|
||||
|
||||
const chartData = Object.entries(dataCollection)
|
||||
.map(([entityId, groupData]) => {
|
||||
const name = getEntityName(entityId);
|
||||
return Object.entries(groupData).map(([groupId, values]) => {
|
||||
const group = getGroupName(groupId);
|
||||
const value = aggregate(values);
|
||||
return {name, group, value};
|
||||
});
|
||||
})
|
||||
.flat();
|
||||
|
||||
const colors = getColors();
|
||||
const {offset, formatX = formatTicks} = plotTypeMap[type];
|
||||
|
||||
const $chart = createStackedBarChart(chartData, {sorting, colors, tooltip, offset, formatX});
|
||||
insertChart(id, $chart, title);
|
||||
|
||||
byId("chartsOverview__charts").lastChild.scrollIntoView();
|
||||
}
|
||||
|
||||
// based on observablehq.com/@d3/stacked-horizontal-bar-chart
|
||||
function createStackedBarChart(data, {sorting, colors, tooltip, offset, formatX}) {
|
||||
const sortedData = sortData(data, sorting);
|
||||
|
||||
const X = sortedData.map(d => d.value);
|
||||
const Y = sortedData.map(d => d.name);
|
||||
const Z = sortedData.map(d => d.group);
|
||||
|
||||
const yDomain = new Set(Y);
|
||||
const zDomain = new Set(Z);
|
||||
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
|
||||
|
||||
const entities = Array.from(yDomain);
|
||||
const groups = Array.from(zDomain);
|
||||
|
||||
const yScaleMinWidth = getTextMinWidth(entities);
|
||||
const legendRows = calculateLegendRows(groups, WIDTH - yScaleMinWidth - 15);
|
||||
|
||||
const margin = {top: 30, right: 15, bottom: legendRows * 20 + 10, left: yScaleMinWidth};
|
||||
const xRange = [margin.left, WIDTH - margin.right];
|
||||
const height = yDomain.size * 25 + margin.top + margin.bottom;
|
||||
const yRange = [height - margin.bottom, margin.top];
|
||||
|
||||
const rolled = rollups(...[I, ([i]) => i, i => Y[i], i => Z[i]]);
|
||||
|
||||
const series = d3
|
||||
.stack()
|
||||
.keys(groups)
|
||||
.value(([, I], z) => X[new Map(I).get(z)])
|
||||
.order(d3.stackOrderNone)
|
||||
.offset(offset)(rolled)
|
||||
.map(s => {
|
||||
const defined = s.filter(d => !isNaN(d[1]));
|
||||
const data = defined.map(d => Object.assign(d, {i: new Map(d.data[1]).get(s.key)}));
|
||||
return {key: s.key, data};
|
||||
});
|
||||
|
||||
const xDomain = d3.extent(series.map(d => d.data).flat(2));
|
||||
|
||||
const xScale = d3.scaleLinear(xDomain, xRange);
|
||||
const yScale = d3.scaleBand(entities, yRange).paddingInner(Y_PADDING);
|
||||
|
||||
const xAxis = d3.axisTop(xScale).ticks(WIDTH / 80, null);
|
||||
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
|
||||
|
||||
const svg = d3
|
||||
.create("svg")
|
||||
.attr("version", "1.1")
|
||||
.attr("xmlns", "http://www.w3.org/2000/svg")
|
||||
.attr("viewBox", [0, 0, WIDTH, height])
|
||||
.attr("style", "max-width: 100%; height: auto; height: intrinsic;");
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(0,${margin.top})`)
|
||||
.call(xAxis)
|
||||
.call(g => g.select(".domain").remove())
|
||||
.call(g => g.selectAll("text").text(d => formatX(d)))
|
||||
.call(g =>
|
||||
g
|
||||
.selectAll(".tick line")
|
||||
.clone()
|
||||
.attr("y2", height - margin.top - margin.bottom)
|
||||
.attr("stroke-opacity", 0.1)
|
||||
);
|
||||
|
||||
const bar = svg
|
||||
.append("g")
|
||||
.attr("stroke", "#666")
|
||||
.attr("stroke-width", 0.5)
|
||||
.selectAll("g")
|
||||
.data(series)
|
||||
.join("g")
|
||||
.attr("fill", d => colors[d.key])
|
||||
.selectAll("rect")
|
||||
.data(d => d.data.filter(([x1, x2]) => x1 !== x2))
|
||||
.join("rect")
|
||||
.attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
|
||||
.attr("y", ({i}) => yScale(Y[i]))
|
||||
.attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
|
||||
.attr("height", yScale.bandwidth());
|
||||
|
||||
const totalZ = Object.fromEntries(
|
||||
rollups(...[I, ([i]) => i, i => Y[i], i => X[i]]).map(([y, yz]) => [y, d3.sum(yz, yz => yz[0])])
|
||||
);
|
||||
const getTooltip = ({i}) => tooltip(Y[i], Z[i], X[i], X[i] / totalZ[Y[i]]);
|
||||
|
||||
bar.append("title").text(d => getTooltip(d).join("\r\n"));
|
||||
bar.on("mouseover", d => tip(getTooltip(d).join(". ")));
|
||||
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(${xScale(0)},0)`)
|
||||
.call(yAxis);
|
||||
|
||||
const rowElements = Math.ceil(groups.length / legendRows);
|
||||
const columnWidth = WIDTH / (rowElements + 0.5);
|
||||
|
||||
const ROW_HEIGHT = 20;
|
||||
|
||||
const getLegendX = (d, i) => (i % rowElements) * columnWidth;
|
||||
const getLegendLabelX = (d, i) => getLegendX(d, i) + LABEL_GAP;
|
||||
const getLegendY = (d, i) => Math.floor(i / rowElements) * ROW_HEIGHT;
|
||||
|
||||
const legend = svg
|
||||
.append("g")
|
||||
.attr("stroke", "#666")
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("dominant-baseline", "central")
|
||||
.attr("transform", `translate(${margin.left},${height - margin.bottom + 15})`);
|
||||
|
||||
legend
|
||||
.selectAll("circle")
|
||||
.data(groups)
|
||||
.join("rect")
|
||||
.attr("x", getLegendX)
|
||||
.attr("y", getLegendY)
|
||||
.attr("width", 10)
|
||||
.attr("height", 10)
|
||||
.attr("transform", "translate(-5, -5)")
|
||||
.attr("fill", d => colors[d]);
|
||||
|
||||
legend
|
||||
.selectAll("text")
|
||||
.data(groups)
|
||||
.join("text")
|
||||
.attr("x", getLegendLabelX)
|
||||
.attr("y", getLegendY)
|
||||
.text(d => d);
|
||||
|
||||
return svg.node();
|
||||
}
|
||||
|
||||
function insertChart(id, $chart, title) {
|
||||
const $chartContainer = byId("chartsOverview__charts");
|
||||
|
||||
const $figure = document.createElement("figure");
|
||||
const $caption = document.createElement("figcaption");
|
||||
|
||||
const figureNo = $chartContainer.childElementCount + 1;
|
||||
$caption.innerHTML = /* html */ `
|
||||
<div>
|
||||
<strong>Figure ${figureNo}</strong>. ${title}
|
||||
</div>
|
||||
<div>
|
||||
<button data-tip="Download the chart in svg format (can open in browser or Inkscape)" class="icon-download"></button>
|
||||
<button data-tip="Remove the chart" class="icon-trash"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$figure.appendChild($chart);
|
||||
$figure.appendChild($caption);
|
||||
$chartContainer.appendChild($figure);
|
||||
|
||||
const downloadChart = () => {
|
||||
const name = `${getFileName(title)}.svg`;
|
||||
downloadFile($chart.outerHTML, name);
|
||||
};
|
||||
|
||||
const removeChart = () => {
|
||||
$figure.remove();
|
||||
charts = charts.filter(chart => chart.id !== id);
|
||||
updateDialogPosition();
|
||||
};
|
||||
|
||||
$figure.querySelector("button.icon-download").on("click", downloadChart);
|
||||
$figure.querySelector("button.icon-trash").on("click", removeChart);
|
||||
}
|
||||
|
||||
function changeViewColumns() {
|
||||
const columns = byId("chartsOverview__viewColumns").value;
|
||||
const $charts = byId("chartsOverview__charts");
|
||||
$charts.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||
updateDialogPosition();
|
||||
}
|
||||
|
||||
function updateDialogPosition() {
|
||||
$("#chartsOverview").dialog({position: {my: "center", at: "center", of: "svg"}});
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
const $chartContainer = byId("chartsOverview__charts");
|
||||
$chartContainer.innerHTML = "";
|
||||
$("#chartsOverview").dialog("destroy");
|
||||
}
|
||||
|
||||
// config
|
||||
const NEUTRAL_COLOR = "#ccc";
|
||||
const EMPTY_NAME = "no";
|
||||
|
||||
const WIDTH = 800;
|
||||
const Y_PADDING = 0.2;
|
||||
|
||||
const RESERVED_PX_PER_CHAR = 7;
|
||||
const LABEL_GAP = 10;
|
||||
|
||||
function getTextMinWidth(entities) {
|
||||
return d3.max(entities.map(name => name.length)) * RESERVED_PX_PER_CHAR;
|
||||
}
|
||||
|
||||
function calculateLegendRows(groups, availableWidth) {
|
||||
const minWidth = LABEL_GAP + getTextMinWidth(groups);
|
||||
const maxInRow = Math.floor(availableWidth / minWidth);
|
||||
const legendRows = Math.ceil(groups.length / maxInRow);
|
||||
return legendRows;
|
||||
}
|
||||
|
||||
function nameGetter(entity) {
|
||||
return i => pack[entity][i].name || EMPTY_NAME;
|
||||
}
|
||||
|
||||
function colorsGetter(entity) {
|
||||
return () => Object.fromEntries(pack[entity].map(({name, color}) => [name || EMPTY_NAME, color || NEUTRAL_COLOR]));
|
||||
}
|
||||
|
||||
function biomeNameGetter(i) {
|
||||
return biomesData.name[i] || EMPTY_NAME;
|
||||
}
|
||||
|
||||
function biomeColorsGetter() {
|
||||
return Object.fromEntries(biomesData.i.map(i => [biomesData.name[i], biomesData.color[i]]));
|
||||
}
|
||||
|
||||
function getUrbanPopulation(cellId) {
|
||||
const burgId = pack.cells.burg[cellId];
|
||||
if (!burgId) return 0;
|
||||
const populationPoints = pack.burgs[burgId].population;
|
||||
return populationPoints * populationRate * urbanization;
|
||||
}
|
||||
|
||||
function getRuralPopulation(cellId) {
|
||||
return pack.cells.pop[cellId] * populationRate;
|
||||
}
|
||||
|
||||
function sortData(data, sorting) {
|
||||
if (sorting === "natural") return data;
|
||||
|
||||
if (sorting === "name") {
|
||||
return data.sort((a, b) => {
|
||||
if (a.name !== b.name) return b.name.localeCompare(a.name); // reversed as 1st element is the bottom
|
||||
return a.group.localeCompare(b.group);
|
||||
});
|
||||
}
|
||||
|
||||
if (sorting === "value") {
|
||||
const entitySum = {};
|
||||
const groupSum = {};
|
||||
for (const {name, group, value} of data) {
|
||||
entitySum[name] = (entitySum[name] || 0) + value;
|
||||
groupSum[group] = (groupSum[group] || 0) + value;
|
||||
}
|
||||
|
||||
return data.sort((a, b) => {
|
||||
if (a.name !== b.name) return entitySum[a.name] - entitySum[b.name]; // reversed as 1st element is the bottom
|
||||
return groupSum[b.group] - groupSum[a.group];
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import {link} from "utils/linkUtils";
|
|||
import {minmax, rn} from "utils/numberUtils";
|
||||
import {regenerateMap} from "scripts/generation";
|
||||
import {reMarkFeatures} from "modules/markup";
|
||||
import {editUnits} from "modules/ui/editors";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
// add drag to upload logic, pull request from @evyatron
|
||||
export function addDragToUpload() {
|
||||
|
|
@ -480,13 +480,6 @@ async function parseLoadedData(data) {
|
|||
updatePresetInput();
|
||||
})();
|
||||
|
||||
void (function restoreEvents() {
|
||||
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", editUnits);
|
||||
legend
|
||||
.on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
|
||||
.on("click", () => clearLegend());
|
||||
})();
|
||||
|
||||
{
|
||||
// dynamically import and run auto-udpdate script
|
||||
const versionNumber = parseFloat(params[0]);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import {tip} from "scripts/tooltips";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {ldb} from "scripts/indexedDB";
|
||||
import {ra} from "utils/probabilityUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
// functions to save project as .map file
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {rn, minmax} from "utils/numberUtils";
|
|||
import {rand, P, Pint} from "utils/probabilityUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {getAdjective, list} from "utils/languageUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export class Battle {
|
||||
constructor(attacker, defender) {
|
||||
|
|
@ -172,35 +173,38 @@ export class Battle {
|
|||
const state = pack.states[regiment.state];
|
||||
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
|
||||
const color = state.color[0] === "#" ? state.color : "#999";
|
||||
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
|
||||
const icon = /* html */ `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
|
||||
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
|
||||
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
|
||||
|
||||
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
|
||||
regiment.name
|
||||
}">${regiment.name.slice(0, 24)}</td>`;
|
||||
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(
|
||||
0,
|
||||
26
|
||||
)}</td>`;
|
||||
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
|
||||
let initial = /* html */ `<tr class="battleInitial">
|
||||
<td>${icon}</td>
|
||||
<td class="regiment" data-tip="${regiment.name}">${regiment.name.slice(0, 24)}</td>
|
||||
`;
|
||||
|
||||
let casualties = /* html */ `<tr class="battleCasualties">
|
||||
<td></td>
|
||||
<td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>
|
||||
`;
|
||||
|
||||
let survivors = /* html */ `<tr class="battleSurvivors">
|
||||
<td></td>
|
||||
<td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>
|
||||
`;
|
||||
|
||||
for (const u of options.military) {
|
||||
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${
|
||||
regiment.u[u.name] || 0
|
||||
}</td>`;
|
||||
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">
|
||||
${regiment.u[u.name] || 0}</td>`;
|
||||
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
|
||||
regiment.u[u.name] || 0
|
||||
}</td>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">
|
||||
${regiment.u[u.name] || 0}</td>`;
|
||||
}
|
||||
|
||||
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`;
|
||||
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
|
||||
regiment.a || 0
|
||||
}</td></tr>`;
|
||||
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">
|
||||
${regiment.a || 0}</td></tr>`;
|
||||
|
||||
const div = side === "attackers" ? battleAttackers : battleDefenders;
|
||||
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
|
||||
|
|
|
|||
|
|
@ -1,486 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {findAll, findCell, getPackPolygon, isLand} from "utils/graphUtils";
|
||||
import {tip, showMainTip, clearMainTip} from "scripts/tooltips";
|
||||
import {getRandomColor} from "utils/colorUtils";
|
||||
import {openURL} from "utils/linkUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
|
||||
export 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 (fmg.modules.editBiomes) return;
|
||||
fmg.modules.editBiomes = true;
|
||||
|
||||
$("#biomesEditor").dialog({
|
||||
title: "Biomes Editor",
|
||||
resizable: false,
|
||||
width: "fit-content",
|
||||
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;
|
||||
const cl = el.classList;
|
||||
if (el.tagName === "FILL-BOX") 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 = " " + getAreaUnit();
|
||||
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 = getArea(b.area[i]);
|
||||
const rural = b.rural[i] * populationRate;
|
||||
const urban = b.urban[i] * populationRate * urbanization;
|
||||
const population = rn(rural + urban);
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
|
||||
rural
|
||||
)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
lines += /* html */ `
|
||||
<div
|
||||
class="states biomes"
|
||||
data-id="${i}"
|
||||
data-name="${b.name[i]}"
|
||||
data-habitability="${b.habitability[i]}"
|
||||
data-cells=${b.cells[i]}
|
||||
data-area=${area}
|
||||
data-population=${population}
|
||||
data-color=${b.color[i]}
|
||||
>
|
||||
<fill-box fill="${b.color[i]}"></fill-box>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${
|
||||
b.name[i]
|
||||
}" autocorrect="off" spellcheck="false" />
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input
|
||||
data-tip="Biome habitability percent. Click and set new value to change"
|
||||
type="number"
|
||||
min="0"
|
||||
max="9999"
|
||||
class="biomeHabitability hide"
|
||||
value=${b.habitability[i]}
|
||||
/>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Biome area" class="biomeArea hide">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male hide"></span>
|
||||
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
|
||||
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
|
||||
${
|
||||
i > 12 && !b.cells[i]
|
||||
? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>'
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
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: "fit-content"});
|
||||
}
|
||||
|
||||
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", 0.7)
|
||||
.attr("stroke", color);
|
||||
}
|
||||
|
||||
function biomeChangeColor(el) {
|
||||
const currentFill = el.getAttribute("fill");
|
||||
const biome = +el.parentNode.dataset.id;
|
||||
|
||||
const callback = newFill => {
|
||||
el.fill = newFill;
|
||||
biomesData.color[biome] = newFill;
|
||||
biomes
|
||||
.select("#biome" + biome)
|
||||
.attr("fill", newFill)
|
||||
.attr("stroke", newFill);
|
||||
};
|
||||
|
||||
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 biomeName = el.parentNode.dataset.name;
|
||||
if (biomeName === "Custom" || !biomeName) return tip("Please fill in the biome name", false, "error");
|
||||
|
||||
const wikiBase = "https://en.wikipedia.org/wiki/";
|
||||
const pages = {
|
||||
"Hot desert": "Desert_climate#Hot_desert_climates",
|
||||
"Cold desert": "Desert_climate#Cold_desert_climates",
|
||||
Savanna: "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands",
|
||||
Grassland: "Temperate_grasslands,_savannas,_and_shrublands",
|
||||
"Tropical seasonal forest": "Seasonal_tropical_forest",
|
||||
"Temperate deciduous forest": "Temperate_deciduous_forest",
|
||||
"Tropical rainforest": "Tropical_rainforest",
|
||||
"Temperate rainforest": "Temperate_rainforest",
|
||||
Taiga: "Taiga",
|
||||
Tundra: "Tundra",
|
||||
Glacier: "Glacier",
|
||||
Wetland: "Wetland"
|
||||
};
|
||||
const customBiomeLink = `https://en.wikipedia.org/w/index.php?search=${biomeName}`;
|
||||
const link = pages[biomeName] ? wikiBase + pages[biomeName] : customBiomeLink;
|
||||
openURL(link);
|
||||
}
|
||||
|
||||
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 = getAreaUnit();
|
||||
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
|
||||
<fill-box fill="${b.color[i]}"></fill-box>
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent" class="hide">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability hide" value=${b.habitability[i]}>
|
||||
<span data-tip="Cells count" class="icon-check-empty hide"></span>
|
||||
<div data-tip="Cells count" class="biomeCells hide">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o hide"></span>
|
||||
<div data-tip="Biome area" class="biomeArea hide">0 ${unit}</div>
|
||||
<span data-tip="Total population: 0" class="icon-male hide"></span>
|
||||
<div data-tip="Total population: 0" class="biomePopulation hide">0</div>
|
||||
<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>
|
||||
</div>`;
|
||||
|
||||
body.insertAdjacentHTML("beforeend", line);
|
||||
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
|
||||
$("#biomesEditor").dialog({width: "fit-content"});
|
||||
}
|
||||
|
||||
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();
|
||||
Biomes.define();
|
||||
drawBiomes();
|
||||
recalculatePopulation();
|
||||
refreshBiomesEditor();
|
||||
}
|
||||
|
||||
function closeBiomesEditor() {
|
||||
exitBiomesCustomizationMode("close");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,595 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {findCell} from "utils/graphUtils";
|
||||
import {tip, clearMainTip} from "scripts/tooltips";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {prompt} from "scripts/prompt";
|
||||
import {rand} from "utils/probabilityUtils";
|
||||
import {parseTransform} from "utils/stringUtils";
|
||||
import {getHeight} from "utils/unitUtils";
|
||||
|
||||
export function editBurg(id) {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const burg = id || d3.event.target.dataset.id;
|
||||
elSelected = burgLabels.select("[data-id='" + burg + "']");
|
||||
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
|
||||
updateBurgValues();
|
||||
|
||||
$("#burgEditor").dialog({
|
||||
title: "Edit Burg",
|
||||
resizable: false,
|
||||
close: closeBurgEditor,
|
||||
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
if (fmg.modules.editBurg) return;
|
||||
fmg.modules.editBurg = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
|
||||
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
|
||||
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
|
||||
|
||||
document.getElementById("burgName").addEventListener("input", changeName);
|
||||
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
|
||||
document.getElementById("burgType").addEventListener("input", changeType);
|
||||
document.getElementById("burgCulture").addEventListener("input", changeCulture);
|
||||
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
|
||||
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
|
||||
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
|
||||
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
|
||||
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
|
||||
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
|
||||
|
||||
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
|
||||
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
|
||||
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
|
||||
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
|
||||
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
|
||||
|
||||
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
|
||||
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
|
||||
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
|
||||
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
|
||||
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
|
||||
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
|
||||
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
|
||||
document.getElementById("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
|
||||
|
||||
function updateBurgValues() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const b = pack.burgs[id];
|
||||
const province = pack.cells.province[b.cell];
|
||||
const provinceName = province ? pack.provinces[province].fullName + ", " : "";
|
||||
const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
|
||||
document.getElementById("burgProvinceAndState").innerHTML = provinceName + stateName;
|
||||
|
||||
document.getElementById("burgName").value = b.name;
|
||||
document.getElementById("burgType").value = b.type || "Generic";
|
||||
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization);
|
||||
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
|
||||
|
||||
// update list and select culture
|
||||
const cultureSelect = document.getElementById("burgCulture");
|
||||
cultureSelect.options.length = 0;
|
||||
const cultures = pack.cultures.filter(c => !c.removed);
|
||||
cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
|
||||
|
||||
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
|
||||
document.getElementById("burgTemperature").innerHTML = convertTemperature(temperature);
|
||||
document.getElementById("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
|
||||
document.getElementById("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
|
||||
|
||||
// toggle features
|
||||
if (b.capital) document.getElementById("burgCapital").classList.remove("inactive");
|
||||
else document.getElementById("burgCapital").classList.add("inactive");
|
||||
if (b.port) document.getElementById("burgPort").classList.remove("inactive");
|
||||
else document.getElementById("burgPort").classList.add("inactive");
|
||||
if (b.citadel) document.getElementById("burgCitadel").classList.remove("inactive");
|
||||
else document.getElementById("burgCitadel").classList.add("inactive");
|
||||
if (b.walls) document.getElementById("burgWalls").classList.remove("inactive");
|
||||
else document.getElementById("burgWalls").classList.add("inactive");
|
||||
if (b.plaza) document.getElementById("burgPlaza").classList.remove("inactive");
|
||||
else document.getElementById("burgPlaza").classList.add("inactive");
|
||||
if (b.temple) document.getElementById("burgTemple").classList.remove("inactive");
|
||||
else document.getElementById("burgTemple").classList.add("inactive");
|
||||
if (b.shanty) document.getElementById("burgShanty").classList.remove("inactive");
|
||||
else document.getElementById("burgShanty").classList.add("inactive");
|
||||
|
||||
//toggle lock
|
||||
updateBurgLockIcon();
|
||||
|
||||
// select group
|
||||
const group = elSelected.node().parentNode.id;
|
||||
const select = document.getElementById("burgSelectGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
burgLabels.selectAll("g").each(function () {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
});
|
||||
|
||||
// set emlem image
|
||||
const coaID = "burgCOA" + id;
|
||||
COArenderer.trigger(coaID, b.coa);
|
||||
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
|
||||
|
||||
if (options.showMFCGMap) {
|
||||
document.getElementById("mfcgPreviewSection").style.display = "block";
|
||||
updateMFCGFrame(b);
|
||||
|
||||
if (b.link) {
|
||||
document.getElementById("mfcgBurgSeedSection").style.display = "none";
|
||||
} else {
|
||||
document.getElementById("mfcgBurgSeedSection").style.display = "inline-block";
|
||||
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
|
||||
}
|
||||
} else {
|
||||
document.getElementById("mfcgPreviewSection").style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
|
||||
function getTemperatureLikeness(temperature) {
|
||||
if (temperature < -5) return "Yakutsk";
|
||||
const cities = [
|
||||
"Snag (Yukon)",
|
||||
"Yellowknife (Canada)",
|
||||
"Okhotsk (Russia)",
|
||||
"Fairbanks (Alaska)",
|
||||
"Nuuk (Greenland)",
|
||||
"Murmansk", // -5 - 0
|
||||
"Arkhangelsk",
|
||||
"Anchorage",
|
||||
"Tromsø",
|
||||
"Reykjavik",
|
||||
"Riga",
|
||||
"Stockholm",
|
||||
"Halifax",
|
||||
"Prague",
|
||||
"Copenhagen",
|
||||
"London", // 1 - 10
|
||||
"Antwerp",
|
||||
"Paris",
|
||||
"Milan",
|
||||
"Batumi",
|
||||
"Rome",
|
||||
"Dubrovnik",
|
||||
"Lisbon",
|
||||
"Barcelona",
|
||||
"Marrakesh",
|
||||
"Alexandria", // 11 - 20
|
||||
"Tegucigalpa",
|
||||
"Guangzhou",
|
||||
"Rio de Janeiro",
|
||||
"Dakar",
|
||||
"Miami",
|
||||
"Jakarta",
|
||||
"Mogadishu",
|
||||
"Bangkok",
|
||||
"Aden",
|
||||
"Khartoum"
|
||||
]; // 21 - 30
|
||||
if (temperature > 30) return "Mecca";
|
||||
return cities[temperature + 5] || null;
|
||||
}
|
||||
|
||||
function dragBurgLabel() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const dx = +tr[0] - d3.event.x,
|
||||
dy = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const x = d3.event.x,
|
||||
y = d3.event.y;
|
||||
this.setAttribute("transform", `translate(${dx + x},${dy + y})`);
|
||||
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning");
|
||||
});
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("burgGroupSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||
document.getElementById("burgGroupSection").style.display = "none";
|
||||
document.getElementById("burgInputGroup").style.display = "none";
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
document.getElementById("burgSelectGroup").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function changeGroup() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
moveBurgToGroup(id, this.value);
|
||||
}
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
if (burgInputGroup.style.display === "none") {
|
||||
burgInputGroup.style.display = "inline-block";
|
||||
burgInputGroup.focus();
|
||||
burgSelectGroup.style.display = "none";
|
||||
} else {
|
||||
burgInputGroup.style.display = "none";
|
||||
burgSelectGroup.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {
|
||||
tip("Please provide a valid group name", false, "error");
|
||||
return;
|
||||
}
|
||||
const group = this.value
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "_")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isFinite(+group.charAt(0))) {
|
||||
tip("Group name should start with a letter", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = +elSelected.attr("data-id");
|
||||
const oldGroup = elSelected.node().parentNode.id;
|
||||
|
||||
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||
if (!label || !icon) {
|
||||
ERROR && console.error("Cannot find label or icon elements");
|
||||
return;
|
||||
}
|
||||
|
||||
const labelG = document.querySelector("#burgLabels > #" + oldGroup);
|
||||
const iconG = document.querySelector("#burgIcons > #" + oldGroup);
|
||||
const anchorG = document.querySelector("#anchors > #" + oldGroup);
|
||||
|
||||
// just rename if only 1 element left
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
|
||||
document.getElementById("burgSelectGroup").selectedOptions[0].remove();
|
||||
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
labelG.id = group;
|
||||
iconG.id = group;
|
||||
if (anchor) anchorG.id = group;
|
||||
return;
|
||||
}
|
||||
|
||||
// create new groups
|
||||
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
|
||||
addBurgsGroup(group);
|
||||
moveBurgToGroup(id, group);
|
||||
}
|
||||
|
||||
function removeBurgsGroup() {
|
||||
const group = elSelected.node().parentNode;
|
||||
const basic = group.id === "cities" || group.id === "towns";
|
||||
|
||||
const burgsInGroup = [];
|
||||
for (let i = 0; i < group.children.length; i++) {
|
||||
burgsInGroup.push(+group.children[i].dataset.id);
|
||||
}
|
||||
const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock));
|
||||
const capital = burgsToRemove.length < burgsInGroup.length;
|
||||
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
|
||||
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
|
||||
}?
|
||||
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
|
||||
burgsToRemove.length
|
||||
}`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove burg group",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
$("#burgEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
burgsToRemove.forEach(b => removeBurg(b));
|
||||
|
||||
if (!basic && !capital) {
|
||||
// entirely remove group
|
||||
const labelG = document.querySelector("#burgLabels > #" + group.id);
|
||||
const iconG = document.querySelector("#burgIcons > #" + group.id);
|
||||
const anchorG = document.querySelector("#anchors > #" + group.id);
|
||||
if (labelG) labelG.remove();
|
||||
if (iconG) iconG.remove();
|
||||
if (anchorG) anchorG.remove();
|
||||
}
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
pack.burgs[id].name = burgName.value;
|
||||
elSelected.text(burgName.value);
|
||||
}
|
||||
|
||||
function generateNameRandom() {
|
||||
const base = rand(nameBases.length - 1);
|
||||
burgName.value = Names.getBase(base);
|
||||
changeName();
|
||||
}
|
||||
|
||||
function changeType() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
pack.burgs[id].type = this.value;
|
||||
}
|
||||
|
||||
function changeCulture() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
pack.burgs[id].culture = +this.value;
|
||||
}
|
||||
|
||||
function generateNameCulture() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const culture = pack.burgs[id].culture;
|
||||
burgName.value = Names.getCulture(culture);
|
||||
changeName();
|
||||
}
|
||||
|
||||
function changePopulation() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
|
||||
}
|
||||
|
||||
function toggleFeature() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
const feature = this.dataset.feature;
|
||||
const turnOn = this.classList.contains("inactive");
|
||||
if (feature === "port") togglePort(id);
|
||||
else if (feature === "capital") toggleCapital(id);
|
||||
else burg[feature] = +turnOn;
|
||||
if (burg[feature]) this.classList.remove("inactive");
|
||||
else if (!burg[feature]) this.classList.add("inactive");
|
||||
|
||||
if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
|
||||
else document.getElementById("burgEditAnchorStyle").style.display = "none";
|
||||
updateMFCGFrame(burg);
|
||||
}
|
||||
|
||||
function toggleBurgLockButton() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
burg.lock = !burg.lock;
|
||||
|
||||
updateBurgLockIcon();
|
||||
}
|
||||
|
||||
function updateBurgLockIcon() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const b = pack.burgs[id];
|
||||
if (b.lock) {
|
||||
document.getElementById("burgLock").classList.remove("icon-lock-open");
|
||||
document.getElementById("burgLock").classList.add("icon-lock");
|
||||
} else {
|
||||
document.getElementById("burgLock").classList.remove("icon-lock");
|
||||
document.getElementById("burgLock").classList.add("icon-lock-open");
|
||||
}
|
||||
}
|
||||
|
||||
function showStyleSection() {
|
||||
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("burgStyleSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideStyleSection() {
|
||||
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
|
||||
document.getElementById("burgStyleSection").style.display = "none";
|
||||
}
|
||||
|
||||
function editGroupLabelStyle() {
|
||||
const g = elSelected.node().parentNode.id;
|
||||
editStyle("labels", g);
|
||||
}
|
||||
|
||||
function editGroupIconStyle() {
|
||||
const g = elSelected.node().parentNode.id;
|
||||
editStyle("burgIcons", g);
|
||||
}
|
||||
|
||||
function editGroupAnchorStyle() {
|
||||
const g = elSelected.node().parentNode.id;
|
||||
editStyle("anchors", g);
|
||||
}
|
||||
|
||||
function updateMFCGFrame(burg) {
|
||||
const mfcgURL = getMFCGlink(burg);
|
||||
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL + "&preview=1");
|
||||
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
|
||||
}
|
||||
|
||||
function changeSeed() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
const burgSeed = +this.value;
|
||||
burg.MFCG = burgSeed;
|
||||
updateMFCGFrame(burg);
|
||||
}
|
||||
|
||||
function randomizeSeed() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
const burgSeed = rand(1e9 - 1);
|
||||
burg.MFCG = burgSeed;
|
||||
updateMFCGFrame(burg);
|
||||
document.getElementById("mfcgBurgSeed").value = burgSeed;
|
||||
}
|
||||
|
||||
function addCustomMfcgLink() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
const message =
|
||||
"Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
|
||||
prompt(message, {default: burg.link || "", required: false}, link => {
|
||||
if (link) burg.link = link;
|
||||
else delete burg.link;
|
||||
updateMFCGFrame(burg);
|
||||
});
|
||||
}
|
||||
|
||||
function openEmblemEdit() {
|
||||
const id = +elSelected.attr("data-id"),
|
||||
burg = pack.burgs[id];
|
||||
editEmblem("burg", "burgCOA" + id, burg);
|
||||
}
|
||||
|
||||
function toggleMFCGMap() {
|
||||
options.showMFCGMap = !options.showMFCGMap;
|
||||
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
|
||||
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
|
||||
}
|
||||
|
||||
function toggleRelocateBurg() {
|
||||
const toggler = document.getElementById("toggleCells");
|
||||
document.getElementById("burgRelocate").classList.toggle("pressed");
|
||||
if (document.getElementById("burgRelocate").classList.contains("pressed")) {
|
||||
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
|
||||
tip("Click on map to relocate burg. Hold Shift for continuous move", true);
|
||||
if (!layerIsOn("toggleCells")) {
|
||||
toggleCells();
|
||||
toggler.dataset.forced = true;
|
||||
}
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
if (layerIsOn("toggleCells") && toggler.dataset.forced) {
|
||||
toggleCells();
|
||||
toggler.dataset.forced = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function relocateBurgOnClick() {
|
||||
const cells = pack.cells;
|
||||
const point = d3.mouse(this);
|
||||
const cell = findCell(point[0], point[1]);
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
|
||||
if (cells.h[cell] < 20) {
|
||||
tip("Cannot place burg into the water! Select a land cell", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cells.burg[cell] && cells.burg[cell] !== id) {
|
||||
tip("There is already a burg in this cell. Please select a free cell", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = cells.state[cell];
|
||||
const oldState = burg.state;
|
||||
|
||||
if (newState !== oldState && burg.capital) {
|
||||
tip("Capital cannot be relocated into another state!", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// change UI
|
||||
const x = rn(point[0], 2),
|
||||
y = rn(point[1], 2);
|
||||
burgIcons
|
||||
.select("[data-id='" + id + "']")
|
||||
.attr("transform", null)
|
||||
.attr("cx", x)
|
||||
.attr("cy", y);
|
||||
burgLabels
|
||||
.select("text[data-id='" + id + "']")
|
||||
.attr("transform", null)
|
||||
.attr("x", x)
|
||||
.attr("y", y);
|
||||
const anchor = anchors.select("use[data-id='" + id + "']");
|
||||
if (anchor.size()) {
|
||||
const size = anchor.attr("width");
|
||||
const xa = rn(x - size * 0.47, 2);
|
||||
const ya = rn(y - size * 0.47, 2);
|
||||
anchor.attr("transform", null).attr("x", xa).attr("y", ya);
|
||||
}
|
||||
|
||||
// change data
|
||||
cells.burg[burg.cell] = 0;
|
||||
cells.burg[cell] = id;
|
||||
burg.cell = cell;
|
||||
burg.state = newState;
|
||||
burg.x = x;
|
||||
burg.y = y;
|
||||
if (burg.capital) pack.states[newState].center = burg.cell;
|
||||
|
||||
if (d3.event.shiftKey === false) toggleRelocateBurg();
|
||||
}
|
||||
|
||||
function editBurgLegend() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const name = elSelected.text();
|
||||
editNotes("burg" + id, name);
|
||||
}
|
||||
|
||||
function showTemperatureGraph() {
|
||||
const id = elSelected.attr("data-id");
|
||||
showBurgTemperatureGraph(id);
|
||||
}
|
||||
|
||||
function removeSelectedBurg() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
if (pack.burgs[id].capital) {
|
||||
alertMessage.innerHTML = /* html */ `You cannot remove the burg as it is a state capital.<br /><br />
|
||||
You can change the capital using Burgs Editor (shift + T)`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove burg",
|
||||
buttons: {
|
||||
Ok: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove burg",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
removeBurg(id); // see Editors module
|
||||
$("#burgEditor").dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeBurgEditor() {
|
||||
document.getElementById("burgRelocate").classList.remove("pressed");
|
||||
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||||
unselect();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import {getCoordinates} from "utils/coordinateUtils";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {si, siToInteger} from "utils/unitUtils";
|
||||
import {getHeight} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
import {openDialog} from "dialogs";
|
||||
|
||||
export function overviewBurgs() {
|
||||
if (customization) return;
|
||||
|
|
@ -241,8 +243,7 @@ export function overviewBurgs() {
|
|||
}
|
||||
|
||||
function openBurgEditor() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
editBurg(burg);
|
||||
openDialog("burgEditor", null, {id: +this.parentNode.dataset.id});
|
||||
}
|
||||
|
||||
function triggerBurgRemove() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {clipPoly} from "utils/lineUtils";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {round} from "utils/stringUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editCoastline(node = d3.event.target) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import * as d3 from "d3";
|
|||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {findCell} from "utils/graphUtils";
|
||||
import {tip, clearMainTip} from "scripts/tooltips";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editDiplomacy() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {byId} from "utils/shorthands";
|
|||
import {tip} from "scripts/tooltips";
|
||||
import {rn, minmax, normalize} from "utils/numberUtils";
|
||||
import {parseTransform} from "utils/stringUtils";
|
||||
import {each} from "utils/probabilityUtils";
|
||||
|
||||
// clear elSelected variable
|
||||
export function unselect() {
|
||||
|
|
@ -17,17 +18,6 @@ export function unselect() {
|
|||
elSelected = null;
|
||||
}
|
||||
|
||||
// close all dialogs except stated
|
||||
export function closeDialogs(except = "#except") {
|
||||
try {
|
||||
$(".dialog:visible")
|
||||
.not(except)
|
||||
.each(function () {
|
||||
$(this).dialog("close");
|
||||
});
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// move brush radius circle
|
||||
export function moveCircle(x, y, r = 20) {
|
||||
let circle = byId("brushCircle");
|
||||
|
|
@ -256,11 +246,11 @@ function togglePort(burg) {
|
|||
.attr("height", size);
|
||||
}
|
||||
|
||||
function getBurgSeed(burg) {
|
||||
export function getBurgSeed(burg) {
|
||||
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
|
||||
}
|
||||
|
||||
function getMFCGlink(burg) {
|
||||
export function getMFCGlink(burg) {
|
||||
if (burg.link) return burg.link;
|
||||
|
||||
const {cells} = pack;
|
||||
|
|
@ -1025,27 +1015,3 @@ function refreshAllEditors() {
|
|||
if (byId("zonesEditorRefresh")?.offsetParent) zonesEditorRefresh.click();
|
||||
TIME && console.timeEnd("refreshAllEditors");
|
||||
}
|
||||
|
||||
// dynamically loaded editors
|
||||
export async function editStates() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/states-editor.js");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
export async function editCultures() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/cultures-editor.js");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
export async function editReligions() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/religions-editor.js");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
export async function editUnits() {
|
||||
const {open} = await import("./units-editor.js");
|
||||
open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {prompt} from "scripts/prompt";
|
|||
import {clearMainTip, showMainTip, tip} from "scripts/tooltips";
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {undraw} from "scripts/generation";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editHeightmap(options) {
|
||||
const {mode, tool} = options || {};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import {byId} from "utils/shorthands";
|
||||
import {openDialog} from "dialogs";
|
||||
import {toggleLayer} from "layers";
|
||||
import {showAboutDialog} from "scripts/options/about";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
||||
document.on("keydown", handleKeydown);
|
||||
|
|
@ -40,17 +42,17 @@ function handleKeyup(event) {
|
|||
else if (ctrl && code === "KeyY" && redo?.offsetParent) redo.click();
|
||||
else if (shift && code === "KeyH") editHeightmap();
|
||||
else if (shift && code === "KeyB") editBiomes();
|
||||
else if (shift && code === "KeyS") editStates();
|
||||
else if (shift && code === "KeyS") openDialog("statesEditor");
|
||||
else if (shift && code === "KeyP") editProvinces();
|
||||
else if (shift && code === "KeyD") editDiplomacy();
|
||||
else if (shift && code === "KeyC") editCultures();
|
||||
else if (shift && code === "KeyC") openDialog("culturesEditor");
|
||||
else if (shift && code === "KeyN") editNamesbase();
|
||||
else if (shift && code === "KeyZ") editZones();
|
||||
else if (shift && code === "KeyR") editReligions();
|
||||
else if (shift && code === "KeyR") openDialog("religionsEditor");
|
||||
else if (shift && code === "KeyY") openEmblemEditor();
|
||||
else if (shift && code === "KeyQ") editUnits();
|
||||
else if (shift && code === "KeyQ") openDialog("unitsEditor");
|
||||
else if (shift && code === "KeyO") editNotes();
|
||||
else if (shift && code === "KeyA") overviewCharts();
|
||||
else if (shift && code === "KeyA") openDialog("chartsOverview");
|
||||
else if (shift && code === "KeyT") overviewBurgs();
|
||||
else if (shift && code === "KeyV") overviewRivers();
|
||||
else if (shift && code === "KeyM") overviewMilitary();
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {tip, clearMainTip} from "scripts/tooltips";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {ra} from "utils/probabilityUtils";
|
||||
import {parseTransform} from "utils/stringUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editIce() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {findCell} from "utils/graphUtils";
|
||||
import {tip, showMainTip} from "scripts/tooltips";
|
||||
import {round, parseTransform} from "utils/stringUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editLabel() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {rn} from "utils/numberUtils";
|
|||
import {rand} from "utils/probabilityUtils";
|
||||
import {round} from "utils/stringUtils";
|
||||
import {si, getHeight} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editLake() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {restoreDefaultEvents} from "scripts/events";
|
|||
import {findCell} from "utils/graphUtils";
|
||||
import {clearMainTip} from "scripts/tooltips";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editMarker(markerI) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {clearMainTip} from "scripts/tooltips";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function overviewMarkers() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {wiki} from "utils/linkUtils";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function overviewMilitary() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {unique} from "utils/arrayUtils";
|
|||
import {tip} from "scripts/tooltips";
|
||||
import {openURL} from "utils/linkUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editNamesbase() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import {gauss, P, rand, rw} from "utils/probabilityUtils";
|
|||
import {byId, stored} from "utils/shorthands";
|
||||
import {regenerateMap} from "scripts/generation";
|
||||
import {fitScaleBar} from "modules/measurers";
|
||||
import {openDialog} from "dialogs";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
|
||||
$("#exitCustomization").draggable({handle: "div"});
|
||||
|
|
@ -160,7 +162,7 @@ optionsContent.addEventListener("click", function (event) {
|
|||
else if (id === "optionsMapHistory") showSeedHistoryDialog();
|
||||
else if (id === "optionsCopySeed") copyMapURL();
|
||||
else if (id === "optionsEraRegenerate") regenerateEra();
|
||||
else if (id === "templateInputContainer") openTemplateSelectionDialog();
|
||||
else if (id === "templateInputContainer") openDialog("heightmapSelection");
|
||||
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
|
||||
else if (id === "translateExtent") toggleTranslateExtent(event.target);
|
||||
else if (id === "speakerTest") testSpeaker();
|
||||
|
|
@ -650,11 +652,6 @@ function changeEra() {
|
|||
options.era = eraInput.value;
|
||||
}
|
||||
|
||||
async function openTemplateSelectionDialog() {
|
||||
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js");
|
||||
HeightmapSelectionDialog.open();
|
||||
}
|
||||
|
||||
// remove all saved data from LocalStorage and reload the page
|
||||
function restoreDefaultOptions() {
|
||||
localStorage.clear();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {parseTransform} from "utils/stringUtils";
|
|||
import {si} from "utils/unitUtils";
|
||||
import {turnLayerButtonOff} from "layers";
|
||||
import {byId} from "utils/shorthands";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editProvinces() {
|
||||
if (customization) return;
|
||||
|
|
@ -387,7 +388,7 @@ export function editProvinces() {
|
|||
|
||||
unfog();
|
||||
closeDialogs();
|
||||
editStates();
|
||||
openDialog("statesEditor");
|
||||
}
|
||||
|
||||
function changePopulation(province) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {last} from "utils/arrayUtils";
|
|||
import {tip, clearMainTip} from "scripts/tooltips";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editRegiment(selector) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {tip, clearMainTip} from "scripts/tooltips";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {capitalize} from "utils/stringUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function overviewRegiments(state) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {restoreDefaultEvents} from "scripts/events";
|
|||
import {findCell} from "utils/graphUtils";
|
||||
import {tip, showMainTip, clearMainTip} from "scripts/tooltips";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editReliefIcon() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {getPackPolygon, findCell} from "utils/graphUtils";
|
|||
import {last} from "utils/arrayUtils";
|
||||
import {tip, clearMainTip} from "scripts/tooltips";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function createRiver() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {tip, clearMainTip} from "scripts/tooltips";
|
|||
import {getSegmentId} from "utils/lineUtils";
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {rand} from "utils/probabilityUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editRiver(id) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {rn} from "utils/numberUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function overviewRivers() {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {getSegmentId} from "utils/lineUtils";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {getNextId} from "utils/nodeUtils";
|
||||
import {round} from "utils/stringUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editRoute(onClick) {
|
||||
if (customization) return;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {rn, minmax} from "utils/numberUtils";
|
|||
import {debounce} from "utils/functionUtils";
|
||||
import {restoreLayers} from "layers";
|
||||
import {undraw} from "scripts/generation";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
window.UISubmap = (function () {
|
||||
byId("submapPointsInput").addEventListener("input", function () {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {openDialog} from "dialogs";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
import {turnLayerButtonOn} from "layers";
|
||||
import {editUnits} from "modules/ui/editors";
|
||||
import {aleaPRNG} from "scripts/aleaPRNG";
|
||||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {prompt} from "scripts/prompt";
|
||||
|
|
@ -21,17 +22,17 @@ toolsContent.addEventListener("click", function (event) {
|
|||
// click on open Editor buttons
|
||||
if (button === "editHeightmapButton") editHeightmap();
|
||||
else if (button === "editBiomesButton") editBiomes();
|
||||
else if (button === "editStatesButton") editStates();
|
||||
else if (button === "editStatesButton") openDialog("statesEditor");
|
||||
else if (button === "editProvincesButton") editProvinces();
|
||||
else if (button === "editDiplomacyButton") editDiplomacy();
|
||||
else if (button === "editCulturesButton") editCultures();
|
||||
else if (button === "editReligions") editReligions();
|
||||
else if (button === "editCulturesButton") openDialog("culturesEditor");
|
||||
else if (button === "editReligions") openDialog("religionsEditor");
|
||||
else if (button === "editEmblemButton") openEmblemEditor();
|
||||
else if (button === "editNamesBaseButton") editNamesbase();
|
||||
else if (button === "editUnitsButton") editUnits();
|
||||
else if (button === "editUnitsButton") openDialog("unitsEditor");
|
||||
else if (button === "editNotesButton") editNotes();
|
||||
else if (button === "editZonesButton") editZones();
|
||||
else if (button === "overviewChartsButton") overviewCharts();
|
||||
else if (button === "overviewChartsButton") openDialog("chartsOverview");
|
||||
else if (button === "overviewBurgsButton") overviewBurgs();
|
||||
else if (button === "overviewRiversButton") overviewRivers();
|
||||
else if (button === "overviewMilitaryButton") overviewMilitary();
|
||||
|
|
@ -867,8 +868,3 @@ function viewCellDetails() {
|
|||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
}
|
||||
|
||||
async function overviewCharts() {
|
||||
const Overview = await import("../dynamic/overview/charts-overview.js");
|
||||
Overview.open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,303 +0,0 @@
|
|||
import * as d3 from "d3";
|
||||
|
||||
import {restoreDefaultEvents} from "scripts/events";
|
||||
import {findCell} from "utils/graphUtils";
|
||||
import {tip} from "scripts/tooltips";
|
||||
import {prompt} from "scripts/prompt";
|
||||
|
||||
export function open() {
|
||||
closeDialogs("#unitsEditor, .stable");
|
||||
$("#unitsEditor").dialog();
|
||||
|
||||
if (fmg.modules.editUnits) return;
|
||||
fmg.modules.editUnits = true;
|
||||
|
||||
$("#unitsEditor").dialog({
|
||||
title: "Units Editor",
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
const drawBar = () => drawScaleBar(scale);
|
||||
|
||||
// add listeners
|
||||
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
|
||||
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
|
||||
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
|
||||
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
|
||||
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
|
||||
document.getElementById("barSizeOutput").addEventListener("input", drawBar);
|
||||
document.getElementById("barSizeInput").addEventListener("input", drawBar);
|
||||
document.getElementById("barLabel").addEventListener("input", drawBar);
|
||||
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
|
||||
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
|
||||
|
||||
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
|
||||
document.getElementById("populationRateInput").addEventListener("change", changePopulationRate);
|
||||
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
|
||||
document.getElementById("urbanizationInput").addEventListener("change", changeUrbanizationRate);
|
||||
document.getElementById("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
|
||||
document.getElementById("urbanDensityInput").addEventListener("change", changeUrbanDensity);
|
||||
|
||||
document.getElementById("addLinearRuler").addEventListener("click", addRuler);
|
||||
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
|
||||
document.getElementById("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
|
||||
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
|
||||
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
|
||||
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
|
||||
|
||||
function changeDistanceUnit() {
|
||||
if (this.value === "custom_name") {
|
||||
prompt("Provide a custom name for a distance unit", {default: ""}, custom => {
|
||||
this.options.add(new Option(custom, custom, false, true));
|
||||
lock("distanceUnit");
|
||||
drawScaleBar(scale);
|
||||
calculateFriendlyGridSize();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
drawScaleBar(scale);
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
||||
function changeDistanceScale() {
|
||||
drawScaleBar(scale);
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
||||
function changeHeightUnit() {
|
||||
if (this.value !== "custom_name") return;
|
||||
|
||||
prompt("Provide a custom name for a height unit", {default: ""}, custom => {
|
||||
this.options.add(new Option(custom, custom, false, true));
|
||||
lock("heightUnit");
|
||||
});
|
||||
}
|
||||
|
||||
function changeHeightExponent() {
|
||||
calculateTemperatures();
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
}
|
||||
|
||||
function changeTemperatureScale() {
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
}
|
||||
|
||||
function changeScaleBarOpacity() {
|
||||
scaleBar.select("rect").attr("opacity", this.value);
|
||||
}
|
||||
|
||||
function changeScaleBarColor() {
|
||||
scaleBar.select("rect").attr("fill", this.value);
|
||||
}
|
||||
|
||||
function changePopulationRate() {
|
||||
populationRate = +this.value;
|
||||
}
|
||||
|
||||
function changeUrbanizationRate() {
|
||||
urbanization = +this.value;
|
||||
}
|
||||
|
||||
function changeUrbanDensity() {
|
||||
urbanDensity = +this.value;
|
||||
}
|
||||
|
||||
function restoreDefaultUnits() {
|
||||
// distanceScale
|
||||
distanceScale = 3;
|
||||
document.getElementById("distanceScaleOutput").value = 3;
|
||||
document.getElementById("distanceScaleInput").value = 3;
|
||||
unlock("distanceScale");
|
||||
|
||||
// units
|
||||
const US = navigator.language === "en-US";
|
||||
const UK = navigator.language === "en-GB";
|
||||
distanceUnitInput.value = US || UK ? "mi" : "km";
|
||||
heightUnit.value = US || UK ? "ft" : "m";
|
||||
temperatureScale.value = US ? "°F" : "°C";
|
||||
areaUnit.value = "square";
|
||||
localStorage.removeItem("distanceUnit");
|
||||
localStorage.removeItem("heightUnit");
|
||||
localStorage.removeItem("temperatureScale");
|
||||
localStorage.removeItem("areaUnit");
|
||||
calculateFriendlyGridSize();
|
||||
|
||||
// height exponent
|
||||
heightExponentInput.value = heightExponentOutput.value = 1.8;
|
||||
localStorage.removeItem("heightExponent");
|
||||
calculateTemperatures();
|
||||
|
||||
// scale bar
|
||||
barSizeOutput.value = barSizeInput.value = 2;
|
||||
barLabel.value = "";
|
||||
barBackOpacity.value = 0.2;
|
||||
barBackColor.value = "#ffffff";
|
||||
barPosX.value = barPosY.value = 99;
|
||||
|
||||
localStorage.removeItem("barSize");
|
||||
localStorage.removeItem("barLabel");
|
||||
localStorage.removeItem("barBackOpacity");
|
||||
localStorage.removeItem("barBackColor");
|
||||
localStorage.removeItem("barPosX");
|
||||
localStorage.removeItem("barPosY");
|
||||
drawScaleBar(scale);
|
||||
|
||||
// population
|
||||
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
|
||||
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
|
||||
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
|
||||
localStorage.removeItem("populationRate");
|
||||
localStorage.removeItem("urbanization");
|
||||
localStorage.removeItem("urbanDensity");
|
||||
}
|
||||
|
||||
function addRuler() {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
const pt = document.getElementById("map").createSVGPoint();
|
||||
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
|
||||
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
|
||||
const dx = graphWidth / 4 / scale;
|
||||
const dy = (rulers.data.length * 40) % (graphHeight / 2);
|
||||
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
|
||||
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
|
||||
rulers.create(Ruler, [from, to]).draw();
|
||||
}
|
||||
|
||||
function toggleOpisometerMode() {
|
||||
if (this.classList.contains("pressed")) {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
this.classList.remove("pressed");
|
||||
} else {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
tip("Draw a curve to measure length. Hold Shift to disallow path optimization", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
viewbox.style("cursor", "crosshair").call(
|
||||
d3.drag().on("start", function () {
|
||||
const point = d3.mouse(this);
|
||||
const opisometer = rulers.create(Opisometer, [point]).draw();
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const point = d3.mouse(this);
|
||||
opisometer.addPoint(point);
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addOpisometer.classList.remove("pressed");
|
||||
if (opisometer.points.length < 2) rulers.remove(opisometer.id);
|
||||
if (!d3.event.sourceEvent.shiftKey) opisometer.optimize();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRouteOpisometerMode() {
|
||||
if (this.classList.contains("pressed")) {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
this.classList.remove("pressed");
|
||||
} else {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
viewbox.style("cursor", "crosshair").call(
|
||||
d3.drag().on("start", function () {
|
||||
const cells = pack.cells;
|
||||
const burgs = pack.burgs;
|
||||
const point = d3.mouse(this);
|
||||
const c = findCell(point[0], point[1]);
|
||||
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
|
||||
const b = cells.burg[c];
|
||||
const x = b ? burgs[b].x : cells.p[c][0];
|
||||
const y = b ? burgs[b].y : cells.p[c][1];
|
||||
const routeOpisometer = rulers.create(RouteOpisometer, [[x, y]]).draw();
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const point = d3.mouse(this);
|
||||
const c = findCell(point[0], point[1]);
|
||||
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
|
||||
routeOpisometer.trackCell(c, true);
|
||||
}
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addRouteOpisometer.classList.remove("pressed");
|
||||
if (routeOpisometer.points.length < 2) {
|
||||
rulers.remove(routeOpisometer.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addRouteOpisometer.classList.remove("pressed");
|
||||
tip("Must start in a cell with a route in it", false, "error");
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlanimeterMode() {
|
||||
if (this.classList.contains("pressed")) {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
this.classList.remove("pressed");
|
||||
} else {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
tip("Draw a curve to measure its area. Hold Shift to disallow path optimization", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
viewbox.style("cursor", "crosshair").call(
|
||||
d3.drag().on("start", function () {
|
||||
const point = d3.mouse(this);
|
||||
const planimeter = rulers.create(Planimeter, [point]).draw();
|
||||
|
||||
d3.event.on("drag", function () {
|
||||
const point = d3.mouse(this);
|
||||
planimeter.addPoint(point);
|
||||
});
|
||||
|
||||
d3.event.on("end", function () {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addPlanimeter.classList.remove("pressed");
|
||||
if (planimeter.points.length < 3) rulers.remove(planimeter.id);
|
||||
else if (!d3.event.sourceEvent.shiftKey) planimeter.optimize();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllRulers() {
|
||||
if (!rulers.data.length) return;
|
||||
alertMessage.innerHTML = /* html */ ` Are you sure you want to remove all placed rulers?
|
||||
<br />If you just want to hide rulers, toggle the Rulers layer off in Menu`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove all rulers",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
rulers.undraw();
|
||||
rulers = new Rulers();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import {tip, showMainTip, clearMainTip} from "scripts/tooltips";
|
|||
import {rn} from "utils/numberUtils";
|
||||
import {getNextId} from "utils/nodeUtils";
|
||||
import {si} from "utils/unitUtils";
|
||||
import {closeDialogs} from "dialogs/utils";
|
||||
|
||||
export function editZones() {
|
||||
closeDialogs();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue