This commit is contained in:
Azgaar 2019-08-31 12:16:36 +03:00
parent 5f9cab4f84
commit cab429a346
58 changed files with 6413 additions and 1489 deletions

View file

@ -5,6 +5,8 @@ function editBiomes() {
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);
@ -14,20 +16,35 @@ function editBiomes() {
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
title: "Biomes Editor", resizable: false, width: fitContent(), close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
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("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, cl = el.classList;
if (cl.contains("zoneFill")) biomeChangeColor(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();
@ -35,10 +52,11 @@ function editBiomes() {
function biomesCollectStatistics() {
const cells = pack.cells;
biomesData.cells = new Uint32Array(biomesData.i.length);
biomesData.area = new Uint32Array(biomesData.i.length);
biomesData.rural = new Uint32Array(biomesData.i.length);
biomesData.urban = new Uint32Array(biomesData.i.length);
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;
@ -51,38 +69,39 @@ function editBiomes() {
}
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const b = biomesData;
let lines = "", totalArea = 0, totalPopulation = 0;;
for (const i of b.i) {
if (!i) continue; // ignore marine (water) biome
const area = b.area[i] * distanceScale.value ** 2;
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate.value;
const urban = b.urban[i] * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
lines += `<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]}>
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
<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">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
<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">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
${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 = b.i.length - 1;
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);
@ -92,10 +111,6 @@ function editBiomes() {
// 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)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(biomesHeader);
@ -115,32 +130,46 @@ function editBiomes() {
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
}
function biomeChangeColor() {
const biome = +this.parentNode.dataset.id;
biomesData.color[biome] = this.value;
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const biome = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
biomesData.color[biome] = fill;
biomes.select("#biome"+biome).attr("fill", fill).attr("stroke", fill);
}
openPicker(currentFill, callback);
}
function biomeChangeName() {
const biome = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
biomesData.name[biome] = this.value;
function biomeChangeName(el) {
const biome = +el.parentNode.dataset.id;
el.parentNode.dataset.name = el.value;
biomesData.name[biome] = el.value;
}
function biomeChangeHabitability() {
const biome = +this.parentNode.dataset.id;
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 9999;
function biomeChangeHabitability(el) {
const biome = +el.parentNode.dataset.id;
const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
if (failed) {
this.value = biomesData.habitability[biome];
el.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-9999", false, "error");
return;
}
biomesData.habitability[biome] = +this.value;
this.parentNode.dataset.habitability = this.value;
biomesData.habitability[biome] = +el.value;
el.parentNode.dataset.habitability = el.value;
recalculatePopulation();
refreshBiomesEditor();
}
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";
@ -159,13 +188,55 @@ function editBiomes() {
}
}
function addCustomBiome() {
const b = biomesData, i = biomesData.i.length;
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 = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
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]}>
<svg data-tip="Biomes fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${b.color[i]}" class="zoneFill"></svg>
<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: fitContent()});
}
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" ? distanceUnit.value + "2" : areaUnit.value;
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) {
@ -185,31 +256,34 @@ function editBiomes() {
link.download = "biomes_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
customization = 6;
biomes.append("g").attr("id", "temp");
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
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").call(d3.drag()
.on("drag", dragBiomeBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectBiomeOnMapClick)
.call(d3.drag().on("start", dragBiomeBrush))
.on("touchmove mousemove", moveBiomeBrush);
}
function selectBiomeOnLineClick() {
if (customization !== 6) return;
function selectBiomeOnLineClick(line) {
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
this.classList.add("selected");
line.classList.add("selected");
}
function selectBiomeOnMapClick() {
@ -225,13 +299,17 @@ function editBiomes() {
}
function dragBiomeBrush() {
const p = d3.mouse(this);
const r = +biomesManuallyBrush.value;
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);
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
@ -275,17 +353,23 @@ function editBiomes() {
exitBiomesCustomizationMode();
}
function 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 > *").forEach(e => e.disabled = false);
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");
if (selected) selected.classList.remove("selected");
}
function restoreInitialBiomes() {
@ -297,7 +381,6 @@ function editBiomes() {
}
function closeBiomesEditor() {
//biomes.on("mousemove", null).on("mouseleave", null);
exitBiomesCustomizationMode();
exitBiomesCustomizationMode("close");
}
}

View file

@ -38,6 +38,7 @@ function editBurg() {
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
document.getElementById("burgOpenCOA").addEventListener("click", openInIAHG);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
@ -233,6 +234,12 @@ function editBurg() {
window.open(url, '_blank');
}
function openInIAHG() {
const id = elSelected.attr("data-id");
const url = `https://ironarachne.com/heraldry/${seed}-b${id}`;
window.open(url, '_blank');
}
function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed");
@ -299,7 +306,7 @@ function editBurg() {
function editBurgLegend() {
const id = elSelected.attr("data-id");
const name = elSelected.text();
editLegends("burg"+id, name);
editNotes("burg"+id, name);
}
function removeSelectedBurg() {

View file

@ -8,11 +8,13 @@ function editBurgs() {
const body = document.getElementById("burgsBody");
updateFilter();
burgsEditorAddLines();
$("#burgsEditor").dialog();
if (modules.editBurgs) return;
modules.editBurgs = true;
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
$("#burgsEditor").dialog({
title: "Burgs Editor", resizable: false, width: fitContent(), close: exitAddBurgMode,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
@ -64,7 +66,7 @@ function editBurgs() {
let lines = "", totalPopulation = 0;
for (const b of filtered) {
const population = rn(b.population * populationRate.value * urbanization.value);
const population = b.population * populationRate.value * urbanization.value;
totalPopulation += population;
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
const state = pack.states[b.state].name;
@ -76,7 +78,7 @@ function editBurgs() {
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
@ -88,7 +90,7 @@ function editBurgs() {
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
@ -102,7 +104,6 @@ function editBurgs() {
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
applySorting(burgsHeader);
$("#burgsEditor").dialog();
}
function getCultureOptions(culture) {
@ -147,16 +148,17 @@ function editBurgs() {
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == "" || isNaN(+this.value)) {
tip("Please provide a valid number", false, "error");
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
tip("Please provide an integer number", false, "error");
this.value = si(pack.burgs[burg].population * populationRate.value * urbanization.value);
return;
}
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
this.parentNode.dataset.population = this.value;
this.value = si(this.value);
const population = [];
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
body.querySelectorAll(":scope > div").forEach(el => population.push(+getInteger(el.dataset.population)));
burgsFooterPopulation.innerHTML = si(d3.mean(population));
}
function toggleCapitalStatus() {
@ -286,14 +288,14 @@ function editBurgs() {
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
let change = [];
let message = `Burgs will be renamed as below. Please confirm`;
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
let message = `Burgs will be renamed as below. Please confirm;
<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
for (let i=0; i < data.length && i <= pack.burgs.length; i++) {
const v = data[i];
if (!v || v == pack.burgs[i].name) continue;
change.push({i, name: v});
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
if (!v || !pack.burgs[i+1] || v == pack.burgs[i+1].name) continue;
change.push({id:i+1, name: v});
message += `<tr><td style="width:20%">${i+1}</td><td style="width:40%">${pack.burgs[i+1].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table></div>`;
alertMessage.innerHTML = message;
@ -303,7 +305,7 @@ function editBurgs() {
Cancel: function() {$(this).dialog("close");},
Confirm: function() {
for (let i=0; i < change.length; i++) {
const id = change[i].i;
const id = change[i].id;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
@ -337,3 +339,4 @@ function editBurgs() {
}
}

View file

@ -5,9 +5,10 @@ function editCultures() {
if (!layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("culturesBody");
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
drawCultureCenters();
refreshCulturesEditor();
@ -15,19 +16,20 @@ function editCultures() {
modules.editCultures = true;
$("#culturesEditor").dialog({
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
title: "Cultures Editor", resizable: false, width: fitContent(), close: closeCulturesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
document.getElementById("culturesLegend").addEventListener("click", toggleLegend);
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
document.getElementById("culturesRecalculate").addEventListener("click", () => recalculateCultures(true));
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
document.getElementById("culturesManuallyCancel").addEventListener("click", () => exitCulturesManualAssignment());
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
document.getElementById("culturesAdd").addEventListener("click", addCulture);
document.getElementById("culturesAdd").addEventListener("click", enterAddCulturesMode);
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
function refreshCulturesEditor() {
@ -51,15 +53,15 @@ function editCultures() {
// add line for each culture
function culturesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * (distanceScale.value ** 2);
const area = c.area * (distanceScaleInput.value ** 2);
const rural = c.rural * populationRate.value;
const urban = c.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
@ -68,18 +70,18 @@ function editCultures() {
// Uncultured (neutral) line
lines += `<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="">
<input class="stateColor placeholder" type="color">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span class="icon-resize-full placeholder"></span>
<input class="statePower placeholder" type="number">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
<input class="statePower placeholder hide" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
</div>`;
continue;
@ -87,20 +89,20 @@ function editCultures() {
lines += `<div class="states cultures" 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}>
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
<svg data-tip="Culture fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${c.color}" class="zoneFill"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full hide"></span>
<input data-tip="Expansionism (defines competitive size)" class="statePower hide" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
<select data-tip="Culture type" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
<span data-tip="Remove culture" class="icon-trash-empty"></span>
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
@ -117,7 +119,7 @@ function editCultures() {
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
body.querySelectorAll("rect.zoneFill").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
@ -144,31 +146,40 @@ function editCultures() {
}
function cultureHighlightOn(event) {
if (customization === 4) return;
if (!layerIsOn("toggleCultures")) return;
if (customization) return;
const culture = +event.target.dataset.id;
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#d0240f");
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8).attr("stroke", "#d0240f");
}
function cultureHighlightOff(event) {
if (customization === 4) return;
if (!layerIsOn("toggleCultures")) return;
const culture = +event.target.dataset.id;
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
cults.select("#culture"+culture).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#cultureCenter"+culture).transition().attr("r", 6).attr("stroke", null);
}
function cultureChangeColor() {
const culture = +this.parentNode.dataset.id;
pack.cultures[culture].color = this.value;
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
debug.select("#cultureCenter"+culture).attr("fill", this.value);
const el = this;
const currentFill = el.getAttribute("fill");
const culture = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.cultures[culture].color = fill;
cults.select("#culture"+culture).attr("fill", fill).attr("stroke", fill);
debug.select("#cultureCenter"+culture).attr("fill", fill);
}
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].name = this.value;
}
function cultureChangeExpansionism() {
@ -218,7 +229,8 @@ function editCultures() {
function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select("#cultureCenters").remove();
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
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")
@ -241,7 +253,13 @@ function editCultures() {
recalculateCultures();
});
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
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";
@ -261,8 +279,10 @@ function editCultures() {
}
// re-calculate cultures
function recalculateCultures() {
pack.cells.culture = new Int8Array(pack.cells.i.length);
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;
@ -274,27 +294,32 @@ function editCultures() {
}
function enterCultureManualAssignent() {
if (!layerIsOn("toggleCultures")) toggleCultures();
if (!layerIsOn("toggleCultures")) toggleCultures();
customization = 4;
cults.append("g").attr("id", "temp");
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "none");
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
debug.select("#cultureCenters").style("display", "none");
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
culturesFooter.style.display = "none";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "21px";
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").call(d3.drag()
.on("drag", dragCultureBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectCultureOnLineClick(i) {
if (customization !== 4) return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
this.classList.add("selected");
}
function selectCultureOnMapClick() {
@ -310,13 +335,17 @@ function editCultures() {
}
function dragCultureBrush() {
const p = d3.mouse(this);
const r = +culturesManuallyBrush.value;
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) changeCultureForSelection(selection);
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) changeCultureForSelection(selection);
});
}
// change culture within selection
@ -325,7 +354,7 @@ function editCultures() {
const selected = body.querySelector("div.selected");
const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color;
const color = pack.cultures[cultureNew].color || "#ffffff";
selection.forEach(function(i) {
const exists = temp.select("polygon[data-cell='"+i+"']");
@ -361,13 +390,19 @@ function editCultures() {
exitCulturesManualAssignment();
}
function exitCulturesManualAssignment() {
function exitCulturesManualAssignment(close) {
customization = 0;
cults.select("#temp").remove();
removeCircle();
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
document.querySelectorAll("#culturesBottom > *").forEach(el => el.style.display = "inline-block");
document.getElementById("culturesManuallyButtons").style.display = "none";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
culturesFooter.style.display = "block";
culturesHeader.querySelector("div[data-sortby='base']").style.marginLeft = "2px";
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();
@ -375,7 +410,32 @@ function editCultures() {
if (selected) selected.classList.remove("selected");
}
function enterAddCulturesMode() {
if (this.classList.contains("pressed")) {exitAddCultureMode(); return;};
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) {tip("You cannot place culture center into the water. Please click on a land cell", false, "error"); return;}
const occupied = pack.cultures.some(c => !c.removed && c.center === center);
if (occupied) {tip("This cell is already a culture center. Please select a different cell", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddCultureMode();
const defaultCultures = Cultures.getDefault();
let culture, base, name;
if (pack.cultures.length < defaultCultures.length) {
@ -391,15 +451,13 @@ function editCultures() {
}
const i = pack.cultures.length;
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
const land = pack.cells.i.filter(isLand);
const center = land[Math.floor(Math.random() * land.length - 1)];
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
drawCultureCenters();
culturesEditorAddLines();
}
function downloadCulturesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
@ -427,7 +485,8 @@ function editCultures() {
function closeCulturesEditor() {
debug.select("#cultureCenters").remove();
exitCulturesManualAssignment();
exitCulturesManualAssignment("close");
exitAddCultureMode()
}
}

View file

@ -0,0 +1,252 @@
"use strict";
function editDiplomacy() {
if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
tip("There should be at least 2 states to edit the diplomacy", false, "Error");
return;
}
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
const body = document.getElementById("diplomacyBodySection");
const statuses = ["Ally", "Sympathy", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#f3c7c4", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
refreshDiplomacyEditor();
tip("Click on a state to see its diplomatical relations", false, "warning");
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
function refreshDiplomacyEditor() {
diplomacyEditorAddLines();
showStateRelations();
}
// add line for each state
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
let lines = `<div class="states Self" data-id=${sel}>
<div data-tip="Selected state" style="width: 100%">${states[sel].fullName}</div>
</div>`;
for (const s of states) {
if (!s.i || s.removed || s.i === sel) continue;
const color = colors[statuses.indexOf(s.diplomacy[sel])];
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${s.diplomacy[sel]}">
<div data-tip="Click to show relations for this state" class="stateName">${s.fullName}</div>
<input data-tip="Relations color" class="stateColor" type="color" value="${color}" disabled>
<select data-tip="Diplomacal relations. Click to change" class="diplomacyRelations">${getRelations(s.diplomacy[sel])}</select>
</div>`;
}
body.innerHTML = lines;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("click", ev => ev.stopPropagation()));
body.querySelectorAll("div > select.diplomacyRelations").forEach(el => el.addEventListener("change", diplomacyChangeRelations));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const path = regions.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition);
}
function transition(path) {
const duration = (path.node().getTotalLength() + 5000) / 2;
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
}
function tweenDash() {
const l = this.getTotalLength();
const i = d3.interpolateString("0," + l, l + "," + l);
return t => i(t);
}
function removePath(path) {
path.transition().duration(1000).attr("opacity", 0).remove();
}
function stateHighlightOff() {
debug.selectAll(".highlight").each(function(el) {
d3.select(this).call(removePath);
});
}
function getRelations(relations) {
let options = "";
statuses.forEach(s => options += `<option ${relations === s ? "selected" : ""} value="${s}">${s}</option>`);
return options;
}
function showStateRelations() {
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find(s => s.i && !s.removed).i;
if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates();
statesBody.selectAll("path").each(function() {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
const id = +this.id.slice(5); // state id
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
this.setAttribute("fill", clr);
statesBody.select("#state-gap"+id).attr("stroke", clr);
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
});
}
function selectStateOnLineClick() {
if (this.classList.contains("Self")) return;
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self");
refreshDiplomacyEditor();
}
function selectStateOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
const state = pack.cells.state[i];
if (!state) return;
const selectedLine = body.querySelector("div.Self");
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
refreshDiplomacyEditor();
}
function diplomacyChangeRelations() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
if (!sel) return;
const state = +this.parentNode.dataset.id;
const rel = this.value, oldRel = states[state].diplomacy[sel];
states[state].diplomacy[sel] = rel;
this.parentNode.dataset.relations = rel;
const statusTo = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
states[sel].diplomacy[state] = statusTo;
// update relation history
const change = [`Relations change`, `${states[sel].name}-${trimVowels(states[state].name)}ian relations changed to ${rel}`];
const vassal = [`Vassalization`, `${states[state].name} became a vassal of ${states[sel].name}`];
const vassalized = [`Vassalization`, `${states[state].name} vassalized ${states[sel].name}`];
const war = [`War declaration`, `${states[sel].name} declared a war on its enemy ${states[state].name}`];
const peace = [`War termination`, `${states[sel].name} and ${states[state].name} agreed to cease fire and signed a peace treaty`];
peace.push(rel === "Vassal" ? vassal[1] : rel === "Suzerain" ? vassalized[1] : change[1]);
if (oldRel === "Enemy") states[0].diplomacy.push(peace);
else states[0].diplomacy.push(rel === "Vassal" ? vassal : rel === "Suzerain" ? vassalized : rel === "Enemy" ? war : change);
const color = colors[statuses.indexOf(rel)];
this.parentNode.querySelector("input.stateColor").value = color;
showStateRelations();
}
function regenerateRelations() {
BurgsAndStates.generateDiplomacy();
refreshDiplomacyEditor();
}
function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
let message = `<div>`;
chronicle.forEach(e => {
message += `<div style="margin: 0.5em 0">`;
e.forEach((l, i) => message += `<div${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
message += `</div>`;
});
alertMessage.innerHTML = message + `</div>`;
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
buttons: {
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");}
}
});
}
function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
let message = `<table class="matrix-table"><tr><th></th>`;
message += states.map(s => `<th>${s.name}</th>`).join("") + `</tr>`; // headers
states.forEach(s => {
message += `<tr><th>${s.name}</th>` + s.diplomacy.filter((v, i) => valid.includes(i)).map(r => `<td class='${r}'>${r}</td>`).join("") + "</tr>";
});
message += `</table>`;
console.log(alertMessage.innerHTML)
console.log(message)
alertMessage.innerHTML = message;
console.log(alertMessage.innerHTML)
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
}
function downloadDiplomacyData() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
states.forEach(s => {
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
data += s.name + "," + rels.join(",") + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "state_relations_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeDiplomacyEditor() {
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
debug.selectAll(".highlight").remove();
}
}

View file

@ -10,6 +10,7 @@ function restoreDefaultEvents() {
.on(".drag", null)
.on("click", clicked)
.on("touchmove mousemove", moved);
legend.call(d3.drag().on("start", dragLegendBox));
}
// on viewbox click event - run function based on target
@ -19,7 +20,7 @@ function clicked() {
const parent = el.parentElement, grand = parent.parentElement;
if (parent.id === "rivers") editRiver(); else
if (grand.id === "routes") editRoute(); else
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel(); else
if (grand.id === "burgLabels") editBurg(); else
if (grand.id === "burgIcons") editBurg(); else
if (parent.id === "terrain") editReliefIcon(); else
@ -65,63 +66,37 @@ function fitContent() {
return !window.chrome ? "-moz-max-content" : "fit-content";
}
// DOM elements sorting on header click
$(".sortable").on("click", function() {
const el = $(this);
// remove sorting for all siblings except of clicked element
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
const type = el.hasClass("alphabetically") ? "name" : "number";
let state = "no";
if (el.is("[class*='down']")) state = "asc";
if (el.is("[class*='up']")) state = "desc";
const sortby = el.attr("data-sortby");
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
const lines = list.children("div"); // get list elements
if (state === "no" || state === "asc") { // sort desc
el.removeClass("icon-sort-" + type + "-down");
el.addClass("icon-sort-" + type + "-up");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an > bn) {return 1;}
if (an < bn) {return -1;}
return 0;
});
}
if (state === "desc") { // sort asc
el.removeClass("icon-sort-" + type + "-up");
el.addClass("icon-sort-" + type + "-down");
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
if (an === "bottom") {return 1;}
let bn = b.getAttribute("data-" + sortby);
if (bn === "bottom") {return -1;}
if (type === "number") {an = +an; bn = +bn;}
if (an < bn) {return 1;}
if (an > bn) {return -1;}
return 0;
});
}
lines.detach().appendTo(list);
// apply sorting behaviour for lines on Editor header click
document.querySelectorAll(".sortable").forEach(function(e) {
e.addEventListener("click", function(e) {sortLines(this);});
});
function sortLines(header) {
const type = header.classList.contains("alphabetically") ? "name" : "number";
let order = header.className.includes("-down") ? "-up" : "-down";
if (!header.className.includes("icon-sort") && type === "name") order = "-up";
const headers = header.parentNode;
headers.querySelectorAll("div.sortable").forEach(e => {
e.classList.forEach(c => {if(c.includes("icon-sort")) e.classList.remove(c);});
});
header.classList.add("icon-sort-" + type + order);
applySorting(headers);
}
function applySorting(headers) {
const header = headers.querySelector("[class*='icon-sort']");
const header = headers.querySelector("div[class*='icon-sort']");
if (!header) return;
const sortby = header.dataset.sortby;
const type = header.classList.contains("alphabetically") ? "name" : "number";
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
const name = header.classList.contains("alphabetically");
const desc = header.className.includes("-down") ? -1 : 1;
const list = headers.nextElementSibling;
const lines = Array.from(list.children);
lines.sort(function(a, b) {
let an = a.getAttribute("data-" + sortby);
let bn = b.getAttribute("data-" + sortby);
if (type === "number") {an = +an; bn = +bn;}
return (an - bn) * desc;
lines.sort((a, b) => {
const an = name ? a.dataset[sortby] : +a.dataset[sortby];
const bn = name ? b.dataset[sortby] : +b.dataset[sortby];
return (an > bn ? 1 : an < bn ? -1 : 0) * desc;
}).forEach(line => list.appendChild(line));
}
@ -187,4 +162,250 @@ function removeBurg(id) {
pack.burgs[id].removed = true;
const cell = pack.burgs[id].cell;
pack.cells.burg[cell] = 0;
}
// draw legend box
function drawLegend(name, data) {
legend.selectAll("*").remove(); // fully redraw every time
legend.attr("data", data.join("|")); // store data
const itemsInCol = +styleLegendColItems.value;
const fontSize = +legend.attr("font-size");
const backClr = styleLegendBack.value;
const opacity = +styleLegendOpacity.value;
const lineHeight = Math.round(fontSize * 1.7);
const colorBoxSize = Math.round(fontSize / 1.7);
const colOffset = fontSize;
const vOffset = fontSize / 2;
// append items
const boxes = legend.append("g").attr("stroke-width", .5).attr("stroke", "#111111").attr("stroke-dasharray", "none");
const labels = legend.append("g").attr("fill", "#000000").attr("stroke", "none");
const columns = Math.ceil(data.length / itemsInCol);
for (let column=0, i=0; column < columns; column++) {
const linesInColumn = Math.ceil(data.length / columns);
const offset = column ? colOffset * 2 + legend.node().getBBox().width : colOffset;
for (let l=0; l < linesInColumn && data[i]; l++, i++) {
boxes.append("rect").attr("fill", data[i][1])
.attr("x", offset).attr("y", lineHeight + l*lineHeight + vOffset)
.attr("width", colorBoxSize).attr("height", colorBoxSize);
labels.append("text").text(data[i][2])
.attr("x", offset + colorBoxSize * 1.6).attr("y", fontSize/1.6 + lineHeight + l*lineHeight + vOffset);
}
}
// append label
const offset = colOffset + legend.node().getBBox().width / 2;
labels.append("text")
.attr("text-anchor", "middle").attr("font-weight", "bold").attr("font-size", "1.2em")
.attr("id", "legendLabel").text(name).attr("x", offset).attr("y", fontSize * 1.1 + vOffset / 2);
// append box
const bbox = legend.node().getBBox();
const width = bbox.width + colOffset * 2;
const height = bbox.height + colOffset / 2 + vOffset;
legend.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height)
.attr("fill", backClr).attr("fill-opacity", opacity);
fitLegendBox();
}
// fit Legend box to map size
function fitLegendBox() {
if (!legend.selectAll("*").size()) return;
const px = isNaN(+legend.attr("data-x")) ? 99 : legend.attr("data-x") / 100;
const py = isNaN(+legend.attr("data-y")) ? 93 : legend.attr("data-y") / 100;
const bbox = legend.node().getBBox();
const x = rn(svgWidth * px - bbox.width), y = rn(svgHeight * py - bbox.height);
legend.attr("transform", `translate(${x},${y})`);
}
// draw legend with the same data, but using different settings
function redrawLegend() {
const name = legend.select("#legendLabel").text();
const data = legend.attr("data").split("|").map(l => l.split(","));
drawLegend(name, data);
}
function dragLegendBox() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
const bbox = legend.node().getBBox();
d3.event.on("drag", function() {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
legend.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
function clearLegend() {
legend.selectAll("*").remove();
legend.attr("data", null);
}
// draw color (fill) picker
function createPicker() {
const contaiter = d3.select("body").append("svg").attr("id", "pickerContainer").attr("width", "100%").attr("height", "100%");
const curtain = contaiter.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("opacity", .2);
curtain.on("click", () => contaiter.style("display", "none")).on("mousemove", () => tip("Click to close the picker"));
const picker = contaiter.append("g").attr("id", "picker").call(d3.drag().on("start", dragPicker));
const controls = picker.append("g").attr("id", "pickerControls");
const h = controls.append("g");
h.append("text").attr("x", 4).attr("y", 14).text("H:");
h.append("line").attr("x1", 18).attr("y1", 10).attr("x2", 107).attr("y2", 10);
h.append("circle").attr("cx", 75).attr("cy", 10).attr("r", 5).attr("id", "pickerH");
h.on("mousemove", () => tip("Set palette hue"));
const s = controls.append("g");
s.append("text").attr("x", 113).attr("y", 14).text("S:");
s.append("line").attr("x1", 124).attr("y1", 10).attr("x2", 206).attr("y2", 10)
s.append("circle").attr("cx", 181.4).attr("cy", 10).attr("r", 5).attr("id", "pickerS");
s.on("mousemove", () => tip("Set palette saturation"));
const l = controls.append("g");
l.append("text").attr("x", 213).attr("y", 14).text("L:");
l.append("line").attr("x1", 226).attr("y1", 10).attr("x2", 306).attr("y2", 10);
l.append("circle").attr("cx", 282).attr("cy", 10).attr("r", 5).attr("id", "pickerL");
l.on("mousemove", () => tip("Set palette lightness"));
controls.selectAll("line").on("click", clickPickerControl);
controls.selectAll("circle").call(d3.drag().on("start", dragPickerControl));
const colors = picker.append("g").attr("id", "pickerColors").attr("stroke", "#333333");
const hatches = picker.append("g").attr("id", "pickerHatches").attr("stroke", "#333333");
const hatching = d3.selectAll("g#hatching > pattern");
const number = hatching.size();
const clr = d3.range(number).map(i => d3.hsl(i/number*360, .7, .7).hex());
clr.forEach(function(d, i) {
colors.append("rect").attr("id", "picker_" + d).attr("fill", d).attr("class", i?"":"selected")
.attr("x", i*22+4).attr("y", 20).attr("width", 16).attr("height", 16);
});
hatching.each(function(d, i) {
hatches.append("rect").attr("id", "picker_" + this.id).attr("fill", "url(#" + this.id + ")")
.attr("x", i*22+4).attr("y", 41).attr("width", 16).attr("height", 16);
});
colors.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the color"));
hatches.selectAll("rect").on("click", pickerFillClicked).on("mousemove", () => tip("Click to fill with the hatching"));
// append box
const bbox = picker.node().getBBox();
const width = bbox.width + 8;
const height = bbox.height + 9;
const pos = () => tip("Drag to change the picker position");
picker.insert("rect", ":first-child").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "#ffffff").on("mousemove", pos);
picker.insert("text", ":first-child").attr("x", 12).attr("y", -10).attr("id", "pickerLabel").text("Color Picker").on("mousemove", pos);
picker.insert("rect", ":first-child").attr("x", 0).attr("y", -30).attr("width", width).attr("height", 30).attr("id", "pickerHeader").on("mousemove", pos);
picker.attr("transform", `translate(${(svgWidth-width)/2},${(svgHeight-height)/2})`);
}
function updateSelectedRect(fill) {
document.getElementById("picker").querySelector("rect.selected").classList.remove("selected");
document.getElementById("picker").querySelector("rect[fill='"+fill+"']").classList.add("selected");
}
function updatePickerColors() {
const colors = d3.select("#picker > #pickerColors").selectAll("rect");
const number = colors.size();
const h = getPickerControl(pickerH, 360);
const s = getPickerControl(pickerS, 1);
const l = getPickerControl(pickerL, 1);
colors.each(function(d, i) {
const clr = d3.hsl(i/number*180+h, s, l).hex();
this.setAttribute("id", "picker_" + clr);
this.setAttribute("fill", clr);
});
}
function openPicker(fill, callback) {
const picker = d3.select("#picker");
if (!picker.size()) createPicker();
d3.select("#pickerContainer").style("display", "block");
if (fill[0] === "#") {
const hsl = d3.hsl(fill);
if (!isNaN(hsl.h)) setPickerControl(pickerH, hsl.h, 360);
if (!isNaN(hsl.s)) setPickerControl(pickerS, hsl.s, 1);
if (!isNaN(hsl.l)) setPickerControl(pickerL, hsl.l, 1);
updatePickerColors();
}
updateSelectedRect(fill);
openPicker.updateFill = function() {
const selected = document.getElementById("picker").querySelector("rect.selected");
if (!selected) return;
callback(selected.getAttribute("fill"));
}
}
function setPickerControl(control, value, max) {
const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min;
const percent = value / max;
control.setAttribute("cx", min + delta * percent);
}
function getPickerControl(control, max) {
const min = +control.previousSibling.getAttribute("x1");
const delta = +control.previousSibling.getAttribute("x2") - min;
const current = +control.getAttribute("cx") - min;
return current / delta * max;
}
function dragPicker() {
const tr = parseTransform(this.getAttribute("transform"));
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
const picker = d3.select("#picker");
const bbox = picker.node().getBBox();
d3.event.on("drag", function() {
const px = rn((x + d3.event.x + bbox.width) / svgWidth * 100, 2);
const py = rn((y + d3.event.y + bbox.height) / svgHeight * 100, 2);
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
picker.attr("transform", transform).attr("data-x", px).attr("data-y", py);
});
}
function pickerFillClicked() {
updateSelectedRect(this.getAttribute("fill"));
openPicker.updateFill();
}
function clickPickerControl() {
const min = this.getScreenCTM().e;
this.nextSibling.setAttribute("cx", d3.event.x - min);
updatePickerColors();
openPicker.updateFill();
}
function dragPickerControl() {
const min = +this.previousSibling.getAttribute("x1");
const max = +this.previousSibling.getAttribute("x2");
d3.event.on("drag", function() {
const x = Math.max(Math.min(d3.event.x, max), min);
this.setAttribute("cx", x);
updatePickerColors();
openPicker.updateFill();
});
}
// remove all fogging
function unfog() {
defs.select("#fog").selectAll("path").remove();
fogging.selectAll("path").remove();
fogging.attr("display", "none");
}

View file

@ -46,27 +46,27 @@ function moved() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack ell id
if (i === undefined) return;
showLegend(d3.event, i);
showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
}
// show legend on hover (if any)
function showLegend(e, i) {
let id = e.target.id || e.target.parentNode.id;
// show note box on hover (if any)
function showNotes(e, i) {
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("legend").style.display = "block";
document.getElementById("legendHeader").innerHTML = note.name;
document.getElementById("legendBody").innerHTML = note.legend;
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
} else {
document.getElementById("legend").style.display = "none";
document.getElementById("legendHeader").innerHTML = "";
document.getElementById("legendBody").innerHTML = "";
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
}
}
@ -97,13 +97,24 @@ function showMapTooltip(e, i, g) {
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
if (group === "zones") {tip(path[path.length-8].dataset.description); return;}
// covering elements
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
if (layerIsOn("toggleReligions") && pack.cells.religion[i]) {
const religion = pack.religions[pack.cells.religion[i]];
const type = religion.type === "Cult" || religion.type == "Heresy" ? religion.type : religion.type + " religion";
tip(type + ": " + religion.name);
} else
if (pack.cells.state[i] && (layerIsOn("toggleProvinces") || layerIsOn("toggleStates"))) {
const state = pack.states[pack.cells.state[i]].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ", " : "";
tip(prov + state);
} else
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
}
@ -114,13 +125,15 @@ function updateCellInfo(point, i, g) {
infoX.innerHTML = rn(point[0]);
infoY.innerHTML = rn(point[1]);
infoCell.innerHTML = i;
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : "n/a";
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoState.innerHTML = cells.h[i] >= 20 ? cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)" : "no";
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
const f = cells.f[i];
@ -128,13 +141,6 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
function ifDefined(v, r = "no", d) {
if (v === null || v === undefined) return r;
if (d) return v.toFixed(d);
return v;
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(h) {
const unit = heightUnit.value;
@ -143,7 +149,7 @@ function getFriendlyHeight(h) {
else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
return rn(height * unitRatio) + " " + unit;
@ -162,7 +168,7 @@ function getFriendlyPopulation(i) {
return si(rural+urban);
}
// assign lock behavior
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function(e) {
e.addEventListener("mouseover", function(event) {
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
@ -180,13 +186,13 @@ document.querySelectorAll("[data-locked]").forEach(function(e) {
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='"+id+"']");
if (input) localStorage.setItem(id, input.value);
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById("lock_" + id);
if(!el) return;
el.dataset.locked = 1;
el.className = "icon-lock";
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
@ -202,12 +208,24 @@ function locked(id) {
return lockEl.dataset.locked == 1;
}
// check if option is stored in localStorage
function stored(option) {
return localStorage.getItem(option);
}
// apply drop-down menu option. If the value is not in options, add it
function applyOption(select, option) {
const custom = !Array.from(select.options).some(o => o.value == option);
if (custom) select.options.add(new Option(option, option));
select.value = option;
}
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener("keydown", function(event) {
const active = document.activeElement.tagName;
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
if (key === 118) regenerateMap(); // "F7" for new map
if (key === 118) regeneratePrompt(); // "F7" for new map
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
@ -220,6 +238,7 @@ document.addEventListener("keydown", function(event) {
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
else if (shift && key === 82) console.table(pack.religions); // Shift + "R" to log religions data
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
@ -230,10 +249,13 @@ document.addEventListener("keydown", function(event) {
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
else if (key === 70) toggleRelief(); // "F" to toggle Relief icons layer
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
else if (key === 83) toggleStates(); // "S" to toggle States layer
else if (key === 78) toggleProvinces(); // "N" to toggle Provinces layer
else if (key === 90) toggleZones(); // "Z" to toggle Zones
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
else if (key === 82) toggleReligions(); // "R" to toggle Religions layer
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
@ -248,8 +270,7 @@ document.addEventListener("keydown", function(event) {
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
else if (key === 107 || key === 109) pressNumpadSign(key); // Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
@ -263,4 +284,35 @@ document.addEventListener("keydown", function(event) {
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById("brushRadius"); else
if (biomesManuallyBrush.offsetParent) brush = document.getElementById("biomesManuallyBrush"); else
if (statesManuallyBrush.offsetParent) brush = document.getElementById("statesManuallyBrush"); else
if (provincesManuallyBrush.offsetParent) brush = document.getElementById("provincesManuallyBrush"); else
if (culturesManuallyBrush.offsetParent) brush = document.getElementById("culturesManuallyBrush"); else
if (zonesBrush.offsetParent) brush = document.getElementById("zonesBrush"); else
if (religionsManuallyBrush.offsetParent) brush = document.getElementById("religionsManuallyBrush");
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id+"Number").value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : .8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed");
}
}

View file

@ -6,11 +6,12 @@ function editHeightmap() {
alertMessage.innerHTML = `<p>Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.</p>
<p>You can also <i>keep</i> all the data as is, but you won't be able to change the coastline.</p>
<p>You can also <i>keep</i> all the data, but you won't be able to change the coastline.</p>
<p>If you need to change the coastline and keep the data, you may try the <i>risk</i> edit option.
The secondary data will be kept with burgs placed on water being removed,
but the landmass change can cause unexpected data fluctuation and errors.</p>`;
The data will be restored as much as possible, but the coastline change can cause unexpected fluctuations and errors.</p>
<p>Check out <a href="https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization" target="_blank">wiki</a> for guidance.</p>`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: 300,
buttons: {
@ -40,7 +41,9 @@ function editHeightmap() {
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
function enterHeightmapEditMode(type) {
editHeightmap.layers = getLayersState();
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
editHeightmap.layers.forEach(l => document.getElementById(l).click()); // turn off all layers
customization = 1;
closeDialogs();
tip('Heightmap edit mode is active. Click on "Exit Customization" to finalize the heightmap', true);
@ -74,15 +77,6 @@ function editHeightmap() {
viewbox.on("touchmove mousemove", moveCursor);
}
function getLayersState() {
const layers = [];
mapLayers.querySelectorAll("li").forEach(l => {
if (l.id === "toggleScaleBar") return;
if (!l.classList.contains("buttonoff")) {layers.push(l.id); l.click();}
});
return layers;
}
function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]);
@ -108,6 +102,7 @@ function editHeightmap() {
customization = 0;
customizationMenu.style.display = "none";
toolsContent.style.display = "block";
layersPreset.disabled = false;
restoreDefaultEvents();
clearMainTip();
closeDialogs();
@ -121,11 +116,14 @@ function editHeightmap() {
else if (mode === "keep") restoreKeptData();
else if (mode === "risk") restoreRiskedData();
// restore initial layers
terrs.selectAll("*").remove();
turnButtonOff("toggleHeight");
changePreset("landmass");
editHeightmap.layers.forEach(l => document.getElementById(l).click());
layersPreset.disabled = false;
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
getCurrentPreset();
}
function regenerateErasedData() {
@ -158,7 +156,12 @@ function editHeightmap() {
Cultures.generate();
Cultures.expand();
BurgsAndStates.generate();
Religions.generate();
drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
addZone();
addMarkers();
console.timeEnd("regenerateErasedData");
console.groupEnd("Edit Heightmap");
}
@ -180,14 +183,17 @@ function editHeightmap() {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const conf = new Uint8Array(l);
const culture = new Int8Array(l);
const fl = new Uint16Array(l);
const pop = new Uint16Array(l);
const r = new Uint16Array(l);
const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const s = new Uint16Array(l);
const state = new Uint8Array(l);
const burg = new Uint8Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
const province = new Uint16Array(l);
const culture = new Uint16Array(l);
const religion = new Uint16Array(l);
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
@ -198,9 +204,12 @@ function editHeightmap() {
pop[g] = pack.cells.pop[i];
r[g] = pack.cells.r[i];
road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
burg[g] = pack.cells.burg[i];
religion[g] = pack.cells.religion[i];
}
// do not allow to remove land with burgs
@ -224,12 +233,15 @@ function editHeightmap() {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.burg = new Uint16Array(n);
pack.cells.culture = new Int8Array(n);
pack.cells.pop = new Uint16Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.s = new Uint16Array(n);
pack.cells.state = new Uint8Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
pack.cells.province = new Uint16Array(n);
pack.cells.culture = new Uint16Array(n);
pack.cells.religion = new Uint16Array(n);
if (!change) {
pack.cells.r = new Uint16Array(n);
@ -255,12 +267,15 @@ function editHeightmap() {
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];
pack.cells.religion[i] = religion[g];
}
for (const b of pack.burgs) {
if (!b.i) continue;
if (!b.i || b.removed) continue;
b.cell = findCell(b.x, b.y);
b.feature = pack.cells.f[b.cell];
pack.cells.burg[b.cell] = b.i;
@ -268,6 +283,26 @@ function editHeightmap() {
if (b.capital) pack.states[b.state].center = b.cell;
}
for (const p of pack.provinces) {
if (!p.i || p.removed) continue;
const provCells = pack.cells.i.filter(i => pack.cells.province[i] === p.i);
if (!provCells.length) {
const state = p.state;
const stateProvs = pack.states[state].provinces;
if (stateProvs.includes(p.i)) pack.states[state].provinces.splice(stateProvs.indexOf(p), 1);
p.removed = true;
continue;
}
if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell;
else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];}
}
BurgsAndStates.drawStateLabels();
drawStates();
drawBorders();
console.timeEnd("restoreRiskedData");
console.groupEnd("Edit Heightmap");
}
@ -417,9 +452,11 @@ function editHeightmap() {
d3.event.on("drag", () => {
const p = d3.mouse(this);
moveCircle(p[0], p[1], r, "#333");
if (~~d3.event.sourceEvent.timeStamp % 5 != 0) return; // slow down the edit
const inRadius = findGridAll(p[0], p[1], r);
const selection = changeOnlyLand.checked ? inRadius.filter(i => grid.cells.h[i] >= 20) : inRadius;
if (selection && selection.length) changeHeightForSelection(selection, start);
if (selection && selection.length) changeHeightForSelection(selection, start);
});
d3.event.on("end", updateHeightmap);
@ -497,6 +534,8 @@ function editHeightmap() {
function openTemplateEditor() {
if ($("#templateEditor").is(":visible")) return;
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
@ -505,21 +544,40 @@ function editHeightmap() {
if (modules.openTemplateEditor) return;
modules.openTemplateEditor = true;
$("#templateBody").sortable({items: "div:not(.elType)"});
$("#templateBody").sortable({items: "div", handle: ".icon-resize-vertical", containment: "parent", axis: "y"});
// add listeners
body.addEventListener("click", function(ev) {
const el = ev.target;
if (el.classList.contains("icon-check")) {
el.classList.remove("icon-check");
el.classList.add("icon-check-empty");
el.parentElement.style.opacity = .5;
body.dataset.changed = 1;
return;
}
if (el.classList.contains("icon-check-empty")) {
el.classList.add("icon-check");
el.classList.remove("icon-check-empty");
el.parentElement.style.opacity = 1;
return;
}
if (el.classList.contains("icon-trash-empty")) {
el.parentElement.remove(); return;
}
});
document.getElementById("templateTools").addEventListener("click", e => addStepOnClick(e));
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
document.getElementById("templateSelect").addEventListener("change", e => selectTemplate(e));
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", e => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", uploadTemplate);
function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return;
const type = e.target.id.replace("template", "");
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 1);
document.getElementById("templateBody").dataset.changed = 1;
addStep(type);
}
@ -540,19 +598,22 @@ function editHeightmap() {
}
function getStepHTML(type, count, arg3, arg4, arg5) {
const Trash = `<i class="icon-trash-empty pointer" data-tip="Remove the step" onclick="this.parentElement.remove()"></i>`;
const Trash = `<i class="icon-trash-empty pointer" data-tip="Click to remove the step"></i>`;
const Hide = `<div class="icon-check" data-tip="Click to skip the step"></div>`;
const Reorder = `<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>`;
const common = `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5||"20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4||"15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3||"40-50"}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count||"1-2"}></span>`;
const Type = `<div class="elType">${type}</div>`;
const blob = `<div data-type="${type}">${Type}${Trash}${TempY}${TempX}${Height}${Count}</div>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `<div data-type="${type}">${Type}${Trash}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `<div data-type="${type}">${Type}${Trash}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `<div data-type="${type}">${Type}${Trash}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
}
function setRange(event) {
@ -569,7 +630,7 @@ function editHeightmap() {
const body = document.getElementById("templateBody");
const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed");
const template = e.target.value;
const template = e.target.value;
if (!steps || !changed) {changeTemplate(template); return;}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
@ -722,6 +783,7 @@ function editHeightmap() {
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || "";
@ -752,12 +814,13 @@ function editHeightmap() {
function downloadTemplate() {
const body = document.getElementById("templateBody");
body.setAttribute("data-changed", 0);
body.dataset.changed = 0;
const steps = body.querySelectorAll("#templateBody > div");
if (!steps.length) return;
let stepsData = "";
for (const s of steps) {
if (s.style.opacity == .5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0";

View file

@ -1,23 +1,24 @@
"use strict";
function editLabel() {
if (customization) return;
closeDialogs(".stable");
closeDialogs();
if (!layerIsOn("toggleLabels")) toggleLabels();
const node = d3.event.target;
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
const tspan = d3.event.target;
const textPath = tspan.parentNode;
const text = textPath.parentNode;
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label: " + node.innerHTML, resizable: false,
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
title: "Edit Label", resizable: false,
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
close: closeLabelEditor
});
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
drawControlPointsAndLine();
selectLabelGroup(node);
updateValues(node);
selectLabelGroup(text);
updateValues(textPath);
if (modules.editLabel) return;
modules.editLabel = true;
@ -40,20 +41,21 @@ function editLabel() {
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
}
}
function selectLabelGroup(node) {
const group = node.parentNode.parentNode.id;
function selectLabelGroup(text) {
const group = text.parentNode.id;
const select = document.getElementById("labelGroupSelect");
select.options.length = 0; // remove all options
@ -63,23 +65,26 @@ function editLabel() {
});
}
function updateValues(node) {
document.getElementById("labelText").value = node.innerHTML;
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
function updateValues(textPath) {
document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
}
function drawControlPointsAndLine() {
debug.select("#controlPoints").remove();
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
const path = document.getElementById("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength();
const increment = l / Math.max(Math.ceil(l / 100), 2);
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
.attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -103,7 +108,7 @@ function editLabel() {
}
function clickControlPoint() {
this.remove();
this.remove();
redrawLabelPath();
}
@ -127,7 +132,7 @@ function editLabel() {
const before = ":nth-child(" + (index + 2) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -240,12 +245,24 @@ function editLabel() {
}
function changeText() {
const text = document.getElementById("labelText").value;
elSelected.select("textPath").text(text);
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
pack.states[id].name = text;
}
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode)
.append("text").attr("x", 0).attr("x", 0)
.attr("font-size", el.getAttribute("font-size")).node();
const lines = input.split("|");
const top = (lines.length - 1) / -2; // y offset
const inner = lines.map((l, d) => {
example.innerHTML = l;
const left = example.getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d?1:top}em">${l}</tspan>`;
}).join("");
el.innerHTML = inner;
example.remove();
if (elSelected.attr("id").slice(0,10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {
@ -282,12 +299,21 @@ function editLabel() {
function changeRelativeSize() {
elSelected.select("textPath").attr("font-size", this.value + "%");
tip("Label relative size: " + this.value + "%");
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
const path = defs.select("#textPath_" + elSelected.attr("id"));
path.attr("d", `M${c[0]-bbox.width},${c[1]}h${bbox.width*2}`);
drawControlPointsAndLine();
}
function editLabelLegend() {
const id = elSelected.attr("id");
const name = elSelected.text();
editLegends(id, name);
editNotes(id, name);
}
function removeLabel() {

View file

@ -13,34 +13,83 @@ function restoreLayers() {
if (layerIsOn("togglePopulation")) drawPopulation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleRelief")) ReliefIcons();
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
if (layerIsOn("toggleCultures")) drawCultures();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleReligions")) drawReligions();
// states are getting rendered each time, if it's not required than layers should be hidden
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
}
// layers to be turned on; changable by user
let presets = {
"political": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar", "toggleStates"],
"cultural": ["toggleBorders", "toggleCultures", "toggleIcons", "toggleLabels", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"religions": ["toggleBorders", "toggleIcons", "toggleLabels", "toggleReligions", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"provinces": ["toggleBorders", "toggleIcons", "toggleProvinces", "toggleRivers", "toggleScaleBar"],
"biomes": ["toggleBiomes", "toggleRivers", "toggleScaleBar"],
"heightmap": ["toggleHeight", "toggleRivers", "toggleScaleBar"],
"poi": ["toggleBorders", "toggleHeight", "toggleIcons", "toggleMarkers", "toggleRivers", "toggleRoutes", "toggleScaleBar"],
"landmass": ["toggleScaleBar"]
}
restoreLayers(); // run on-load
restoreCustomPresets(); // run on-load
function restoreCustomPresets() {
const storedPresets = JSON.parse(localStorage.getItem("presets"));
if (!storedPresets) return;
for (const preset in storedPresets) {
if (presets[preset]) continue;
layersPreset.add(new Option(preset, preset));
}
presets = storedPresets;
}
function applyPreset() {
const selected = localStorage.getItem("preset");
if (selected) changePreset(selected);
}
// toggle layers on preset change
function changePreset(preset) {
const layers = getLayers(preset); // layers to be turned on
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
const layers = presets[preset]; // layers to be turned on
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (ignore.includes(e.id)) return; // ignore
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
layersPreset.value = preset;
localStorage.setItem("preset", preset);
}
// retrun list of layers to be turned on
function getLayers(preset) {
switch(preset) {
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
case "heightmap": return ["toggleHeight", "toggleRivers"];
case "biomes": return ["toggleBiomes", "toggleRivers"];
case "landmass": return [];
function savePreset() {
// don't allow if layers should already esist as a preset
if (layersPreset.value !== "custom") {
tip(`Current layers are already saved as a "${layersPreset.selectedOptions[0].label}" preset`, false, "error");
return;
}
// add new preset
const preset = prompt("Please provide a preset name"); // preset name
if (!preset) return;
presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
layersPreset.add(new Option(preset, preset, false, true));
localStorage.setItem("presets", JSON.stringify(presets));
localStorage.setItem("preset", preset);
}
function getCurrentPreset() {
const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)")).map(node => node.id).sort();
for (const preset in presets) {
if (JSON.stringify(presets[preset]) !== JSON.stringify(layers)) continue;
layersPreset.value = preset;
return;
}
layersPreset.value = "custom";
}
function toggleHeight() {
@ -79,7 +128,7 @@ function drawHeightmap() {
if (h > currentLayer) currentLayer += skip;
if (currentLayer > 100) break; // no layers possible with height > 100
if (h < currentLayer) continue;
if (used[i]) continue; // already marked
if (used[i]) continue; // already marked
const onborder = cells.c[i].some(n => cells.h[n] < h);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
@ -271,8 +320,7 @@ function drawBiomes() {
paths.forEach(function(d, i) {
if (d.length < 10) return;
const color = biomesData.color[i];
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
biomes.append("path").attr("d", d).attr("fill", biomesData.color[i]).attr("stroke", biomesData.color[i]).attr("id", "biome"+i);
});
// connect vertices to chain
@ -403,8 +451,8 @@ function drawCultures() {
paths[c] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => cultures[d[1]].color).attr("id", d => "culture"+d[1]);
// connect vertices to chain
function connectVertices(start, t) {
@ -428,31 +476,87 @@ function drawCultures() {
console.timeEnd("drawCultures");
}
function toggleReligions() {
if (!relig.selectAll("path").size()) {
turnButtonOn("toggleReligions");
drawReligions();
} else {
relig.selectAll("path").remove();
turnButtonOff("toggleReligions");
}
}
function drawReligions() {
console.time("drawReligions");
relig.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, religions = pack.religions, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(religions.length).fill("");
for (const i of cells.i) {
if (!cells.religion[i]) continue;
if (used[i]) continue;
used[i] = 1;
const r = cells.religion[i];
const onborder = cells.c[i].some(n => cells.religion[n] !== r);
if (!onborder) continue;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.religion[i] !== r));
const chain = connectVertices(vertex, r);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v]);
paths[r] += "M" + points.join("L") + "Z";
}
const data = paths.map((p, i) => [p, i]).filter(d => d[0].length > 10);
relig.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => religions[d[1]].color).attr("id", d => "religion"+d[1]);
// connect vertices to chain
function connectVertices(start, t) {
const chain = []; // vertices chain to form a path
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1]; // previous vertex in chain
chain.push(current); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.religion[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.religion[c[0]] !== t;
const c1 = c[1] >= n || cells.religion[c[1]] !== t;
const c2 = c[2] >= n || cells.religion[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) current = v[0];
else if (v[1] !== prev && c1 !== c2) current = v[1];
else if (v[2] !== prev && c0 !== c2) current = v[2];
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
}
return chain;
}
console.timeEnd("drawReligions");
}
function toggleStates() {
if (!layerIsOn("toggleStates")) {
turnButtonOn("toggleStates");
regions.attr("display", null);
drawStatesWithBorders();
drawStates();
} else {
regions.attr("display", "none").selectAll("path").remove();
turnButtonOff("toggleStates");
}
}
function drawStatesWithBorders() {
console.time("drawStatesWithBorders");
// draw states
function drawStates() {
console.time("drawStates");
regions.selectAll("path").remove();
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(states.length); // store vertices array
const body = new Array(states.length).fill(""); // store path around each state
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
for (const i of cells.i) {
if (!cells.state[i] || used[i]) continue;
used[i] = 1;
const s = cells.state[i];
const onborder = cells.c[i].some(n => cells.state[n] !== s);
if (!onborder) continue;
@ -461,17 +565,19 @@ function drawStatesWithBorders() {
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
const chain = connectVertices(vertex, s, borderWith);
if (chain.length < 3) continue;
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
const points = chain.map(v => vertices.p[v[0]]);
if (!vArray[s]) vArray[s] = [];
vArray[s].push(points);
body[s] += "M" + points.join("L");
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
// const p = chain.map(v => vertices.p[v[0]])
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
// const poly = polylabel([p], 1.0); // pole of inaccessibility
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
}
// find state visual center
vArray.forEach((ar, i) => {
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
states[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
@ -479,10 +585,9 @@ function drawStatesWithBorders() {
defs.select("#statePaths").selectAll("clipPath").remove();
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
statesHalo.selectAll(".path").data(bodyData).enter().append("path")
.attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666")
.attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
// connect vertices to chain
function connectVertices(start, t, state) {
@ -507,7 +612,106 @@ function drawStatesWithBorders() {
chain.push([start, state, land]); // add starting vertex to sequence to close the path
return chain;
}
console.timeEnd("drawStatesWithBorders");
invokeActiveZooming();
console.timeEnd("drawStates");
}
// draw state and province borders
function drawBorders() {
console.time("drawBorders");
borders.selectAll("path").remove();
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
const sPath = [], pPath = [];
const sUsed = new Array(pack.states.length).fill("").map(a => []);
const pUsed = new Array(pack.provinces.length).fill("").map(a => []);
for (let i=0; i < cells.i.length; i++) {
if (!cells.state[i]) continue;
const p = cells.province[i];
const s = cells.state[i];
// if cell is on province border
const provToCell = cells.c[i].find(n => cells.state[n] === s && p > cells.province[n] && pUsed[p][n] !== cells.province[n]);
if (provToCell) {
const provTo = cells.province[provToCell];
pUsed[p][provToCell] = provTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === provTo));
const chain = connectVertices(vertex, p, cells.province, provTo, pUsed);
if (chain.length > 1) {
pPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
// if cell is on state border
const stateToCell = cells.c[i].find(n => cells.h[n] >= 20 && s > cells.state[n] && sUsed[s][n] !== cells.state[n]);
if (stateToCell !== undefined) {
const stateTo = cells.state[stateToCell];
sUsed[s][stateToCell] = stateTo;
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] >= 20 && cells.state[i] === stateTo));
const chain = connectVertices(vertex, s, cells.state, stateTo, sUsed);
if (chain.length > 1) {
sPath.push("M" + chain.map(c => vertices.p[c]).join(" "));
i--;
continue;
}
}
}
stateBorders.append("path").attr("d", sPath.join(" "));
provinceBorders.append("path").attr("d", pPath.join(" "));
// connect vertices to chain
function connectVertices(current, f, array, t, used) {
let chain = [];
const checkCell = c => c >= n || array[c] !== f;
const checkVertex = v => vertices.c[v].some(c => array[c] === f) && vertices.c[v].some(c => array[c] === t && cells.h[c] >= 20);
// find starting vertex
for (let i=0; i < 1000; i++) {
if (i === 999) console.error("Find starting vertex: limit is reached", current, f, t);
const p = chain[chain.length-2] || -1; // previous vertex
const v = vertices.v[current], c = vertices.c[current];
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
if (v0 + v1 + v2 === 1) break;
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === chain[0]) break;
if (current === p) return [];
chain.push(current);
}
chain = [current]; // vertices chain to form a path
// find path
for (let i=0; i < 1000; i++) {
if (i === 999) console.error("Find path: limit is reached", current, f, t);
const p = chain[chain.length-2] || -1; // previous vertex
const v = vertices.v[current], c = vertices.c[current];
c.filter(c => array[c] === t).forEach(c => used[f][c] = t);
const v0 = checkCell(c[0]) !== checkCell(c[1]) && checkVertex(v[0]);
const v1 = checkCell(c[1]) !== checkCell(c[2]) && checkVertex(v[1]);
const v2 = checkCell(c[0]) !== checkCell(c[2]) && checkVertex(v[2]);
current = v0 && p !== v[0] ? v[0] : v1 && p !== v[1] ? v[1] : v[2];
if (current === p) break;
if (current === chain[chain.length-1]) break;
if (chain.length > 1 && v0 + v1 + v2 < 2) break;
chain.push(current);
if (current === chain[0]) break;
}
return chain;
}
console.timeEnd("drawBorders");
}
function toggleBorders() {
@ -520,6 +724,89 @@ function toggleBorders() {
}
}
function toggleProvinces() {
if (!layerIsOn("toggleProvinces")) {
turnButtonOn("toggleProvinces");
drawProvinces();
} else {
provs.selectAll("*").remove();
turnButtonOff("toggleProvinces");
}
}
function drawProvinces() {
console.time("drawProvinces");
const labelsOn = provs.attr("data-labels") == 1;
provs.selectAll("*").remove();
const cells = pack.cells, vertices = pack.vertices, provinces = pack.provinces, n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const vArray = new Array(provinces.length); // store vertices array
const body = new Array(provinces.length).fill(""); // store path around each province
const gap = new Array(provinces.length).fill(""); // store path along water for each province to fill the gaps
for (const i of cells.i) {
if (!cells.province[i] || used[i]) continue;
const p = cells.province[i];
const onborder = cells.c[i].some(n => cells.province[n] !== p);
if (!onborder) continue;
const borderWith = cells.c[i].map(c => cells.province[c]).find(n => n !== p);
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.province[i] === borderWith));
const chain = connectVertices(vertex, p, borderWith);
if (chain.length < 3) continue;
const points = chain.map(v => vertices.p[v[0]]);
if (!vArray[p]) vArray[p] = [];
vArray[p].push(points);
body[p] += "M" + points.join("L");
gap[p] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
}
// find state visual center
vArray.forEach((ar, i) => {
const sorted = ar.sort((a, b) => b.length - a.length); // sort by points number
provinces[i].pole = polylabel(sorted, 1.0); // pole of inaccessibility
});
const g = provs.append("g").attr("id", "provincesBody");
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
g.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "province"+d[1]);
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, provinces[i].color]).filter(d => d[0]);
g.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "province-gap"+d[1]);
const labels = provs.append("g").attr("id", "provinceLabels");
labels.style("display", `${labelsOn ? "block" : "none"}`);
const labelData = provinces.filter(p => p.i && !p.removed);
labels.selectAll(".path").data(labelData).enter().append("text")
.attr("x", d => d.pole[0]).attr("y", d => d.pole[1])
.attr("id", d => "provinceLabel"+d.i).text(d => d.name);
// connect vertices to chain
function connectVertices(start, t, province) {
const chain = []; // vertices chain to form a path
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.province[c] !== t);
function check(i) {province = cells.province[i]; land = cells.h[i] >= 20;}
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
chain.push([current, province, land]); // add current vertex to sequence
const c = vertices.c[current]; // cells adjacent to vertex
c.filter(c => cells.province[c] === t).forEach(c => used[c] = 1);
const c0 = c[0] >= n || cells.province[c[0]] !== t;
const c1 = c[1] >= n || cells.province[c[1]] !== t;
const c2 = c[2] >= n || cells.province[c[2]] !== t;
const v = vertices.v[current]; // neighboring vertices
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
if (current === chain[chain.length-1][0]) {console.error("Next vertex is not found"); break;}
}
chain.push([start, province, land]); // add starting vertex to sequence to close the path
return chain;
}
console.timeEnd("drawProvinces");
}
function toggleGrid() {
if (!gridOverlay.selectAll("*").size()) {
turnButtonOn("toggleGrid");
@ -592,49 +879,32 @@ function toggleCoordinates() {
function drawCoordinates() {
if (!layerIsOn("toggleCoordinates")) return;
coordinates.selectAll("*").remove(); // remove every time
const eqY = +document.getElementById("equatorOutput").value;
const eqD = +document.getElementById("equidistanceOutput").value;
const merX = svgWidth / 2; // x of zero meridian
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
const goal = merX / eqD / scale ** 0.4 * 12;
const goal = mapCoordinates.lonT / scale / 10;
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
const desired = +coordinates.attr("data-size")
const size = Math.max(desired + 1 - scale, 2);
coordinates.attr("font-size", size);
// map coordinates extent
const extent = getViewBoxExtent();
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
const desired = +coordinates.attr("data-size"); // desired label size
coordinates.attr("font-size", Math.max(rn(desired / scale ** .8, 2), .1)); // actual label size
const graticule = d3.geoGraticule().extent([[mapCoordinates.lonW, mapCoordinates.latN], [mapCoordinates.lonE, mapCoordinates.latS]])
.stepMajor([400, 400]).stepMinor([step, step]);
const projection = d3.geoEquirectangular().fitSize([graphWidth, graphHeight], graticule());
const grid = coordinates.append("g").attr("id", "coordinateGrid");
const lalitude = coordinates.append("g").attr("id", "lalitude");
const longitude = coordinates.append("g").attr("id", "longitude");
const grid = coordinates.append("g").attr("id", "coordinateGrid");
const labels = coordinates.append("g").attr("id", "coordinateLabels");
// rander lalitude lines
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
const c = eqY - l / 90 * eqD;
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
if (nearBorder || !Number.isInteger(l)) return;
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
const p = getViewPoint(scale + desired + 2, scale + desired / 2); // on border point on viexBox
const data = graticule.lines().map(d => {
const lat = d.coordinates[0][1] === d.coordinates[1][1]; // check if line is latitude or longitude
const c = d.coordinates[0], pos = projection(c); // map coordinates
const [x, y] = lat ? [rn(p.x, 2), rn(pos[1], 2)] : [rn(pos[0], 2), rn(p.y, 2)]; // labels position
const v = lat ? c[1] : c[0]; // label
const text = !v ? v : Number.isInteger(v) ? lat ? c[1] < 0 ? -c[1] + "°S" : c[1] + "°N" : c[0] < 0 ? -c[0] + "°W" : c[0] + "°E" : "";
return {lat, x, y, text};
});
// rander longitude lines
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
const c = merX + l / 90 * eqD;
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
if (nearBorder || !Number.isInteger(l)) return;
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
});
function nextStep(v) {return (v / step | 0) * step;}
const d = round(d3.geoPath(projection)(graticule()));
grid.append("path").attr("d", d).attr("vector-effect", "non-scaling-stroke");
labels.selectAll('text').data(data).enter().append("text").attr("x", d => d.x).attr("y", d => d.y).text(d => d.text);
}
// conver svg point into viewBox point
@ -677,9 +947,8 @@ function toggleTexture() {
turnButtonOn("toggleTexture");
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll("*").size()) {
const link = getAbsolutePath(styleTextureInput.value);
texture.append("image").attr("x", 0).attr("y", 0).attr("width", graphWidth).attr("height", graphHeight)
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
.attr('xlink:href', getDefaultTexture()).attr('preserveAspectRatio', "xMidYMid slice");
}
$('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
@ -723,6 +992,7 @@ function toggleLabels() {
if (!layerIsOn("toggleLabels")) {
turnButtonOn("toggleLabels");
$('#labels').fadeIn();
invokeActiveZooming();
} else {
turnButtonOff("toggleLabels");
$('#labels').fadeOut();
@ -759,6 +1029,16 @@ function toggleScaleBar() {
}
}
function toggleZones() {
if (!layerIsOn("toggleZones")) {
turnButtonOn("toggleZones");
$('#zones').fadeIn();
} else {
turnButtonOff("toggleZones");
$('#zones').fadeOut();
}
}
function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
return !buttonoff;
@ -766,23 +1046,22 @@ function layerIsOn(el) {
function turnButtonOff(el) {
document.getElementById(el).classList.add("buttonoff");
layersPreset.value = "custom";
getCurrentPreset();
}
function turnButtonOn(el) {
document.getElementById(el).classList.remove("buttonoff");
layersPreset.value = "custom";
getCurrentPreset();
}
// move layers on mapLayers dragging (jquery sortable)
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
$("#mapLayers").sortable({items: "li:not(.solid)", containment: "parent", cancel: ".solid", update: moveLayer});
function moveLayer(event, ui) {
const el = getLayer(ui.item.attr("id"));
if (el) {
const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
}
if (!el) return;
const prev = getLayer(ui.item.prev().attr("id"));
const next = getLayer(ui.item.next().attr("id"));
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
}
// define connection between option layer buttons and actual svg groups to move the element
@ -797,6 +1076,7 @@ function getLayer(id) {
if (id === "toggleRelief") return $("#terrain");
if (id === "toggleCultures") return $("#cults");
if (id === "toggleStates") return $("#regions");
if (id === "toggleProvinces") return $("#provs");
if (id === "toggleBorders") return $("#borders");
if (id === "toggleRoutes") return $("#routes");
if (id === "toggleTemp") return $("#temperature");

View file

@ -181,6 +181,8 @@ function editMarker() {
["1F3AA", "🎪", "Tent"],
["1F3E8", "🏨", "Hotel"],
["1F4B0", "💰", "Money bag"],
["1F6A8", "🚨", "Revolving Light"],
["1F309", "🌉", "Bridge at Night"],
["1F4A8", "💨", "Dashing away"],
["1F334", "🌴", "Palm"],
["1F335", "🌵", "Cactus"],
@ -217,6 +219,7 @@ function editMarker() {
["1F352", "🍒", "Cherries"],
["1F36F", "🍯", "Honey pot"],
["1F37A", "🍺", "Beer"],
["1F37B", "🍻", "Beers"],
["1F377", "🍷", "Wine glass"],
["1F3BB", "🎻", "Violin"],
["1F3B8", "🎸", "Guitar"],
@ -248,6 +251,7 @@ function editMarker() {
["2317", "⌗", "Hash"],
["2318", "⌘", "POI"],
["2307", "⌇", "Wavy"],
["27F1", "⟱", "Downwards Quadruple"],
["21E6", "⇦", "Left arrow"],
["21E7", "⇧", "Top arrow"],
["21E8", "⇨", "Right arrow"],
@ -442,7 +446,7 @@ function editMarker() {
function editMarkerLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function toggleAddMarker() {

View file

@ -5,7 +5,7 @@
function addRuler(x1, y1, x2, y2) {
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
const dash = rn(30 / distanceScaleInput.value, 2);
// body
const rulerNew = ruler.append("g").attr("class", "ruler").call(d3.drag().on("start", dragRuler));
@ -18,7 +18,7 @@ function addRuler(x1, y1, x2, y2) {
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
const rotate = `rotate(${angle} ${cx} ${cy})`;
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
}
@ -46,7 +46,7 @@ function dragRulerEdge() {
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
const dist = Math.hypot(x0 - x, y0 - y);
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
const label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
const angle = rn(atan * 180 / Math.PI, 3);
const rotate = `rotate(${angle} ${cx} ${cy})`;
@ -76,7 +76,7 @@ function rulerCenterDrag() {
// change first part
let dist = rn(Math.hypot(x1 - x, y1 - y));
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
let label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
@ -86,7 +86,7 @@ function rulerCenterDrag() {
// change second (new) part
dist = rn(Math.hypot(x2 - x, y2 - y));
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
label = rn(dist * distanceScaleInput.value) + " " + distanceUnitInput.value;
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
@ -110,7 +110,7 @@ function rulerCenterDrag() {
function drawOpisometer() {
lineGen.curve(d3.curveBasis);
const size = rn(1 / scale ** .3 * 2, 1);
const dash = rn(30 / distanceScale.value, 2);
const dash = rn(30 / distanceScaleInput.value, 2);
const p0 = d3.mouse(this);
const points = [[p0[0], p0[1]]];
let length = 0;
@ -131,7 +131,7 @@ function drawOpisometer() {
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
text.attr("x", p[0]).attr("y", p[1]).text(label);
});
@ -176,7 +176,7 @@ function dragOpisometerEnd() {
curve.attr("d", path);
curveGray.attr("d", path);
length = curve.node().getTotalLength();
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
const label = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
text.text(label);
});
@ -215,20 +215,20 @@ function drawPlanimeter() {
addPlanimeter.classList.remove("pressed");
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + " " + unit;
const c = polylabel([points], 1.0); // pole of inaccessibility
text.attr("x", c[0]).attr("y", c[1]).text(area);
});
}
// draw default scale bar
// draw scale bar
function drawScaleBar() {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
const dScale = distanceScale.value;
const unit = distanceUnit.value;
const dScale = distanceScaleInput.value;
const unit = distanceUnitInput.value;
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
@ -269,8 +269,8 @@ function drawScaleBar() {
// fit ScaleBar to map size
function fitScaleBar() {
if (!scaleBar.select("rect").size()) return;
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
const px = isNaN(+barPosX.value) ? 99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 99 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);

View file

@ -163,7 +163,7 @@ function editNamesbase() {
nameBases = [], nameBase = [];
data.forEach(d => {
const e = d.split("|");
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:e[4]});
nameBase.push(e[5].split(","));
});

View file

@ -1,7 +1,7 @@
"use strict";
function editLegends(id, name) {
function editNotes(id, name) {
// update list of objects
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
for (let i = select.options.length; i < notes.length; i++) {
select.options.add(new Option(notes[i].id, notes[i].id));
}
@ -16,54 +16,59 @@ function editLegends(id, name) {
select.options.add(new Option(id, id));
}
select.value = id;
legendName.value = note.name;
legendText.value = note.legend;
notesName.value = note.name;
notesText.value = note.legend;
} else {
if (!notes.length) {
const value = "There are no added notes. Click on element (e.g. label) and add a free text note";
document.getElementById("notesText").value = value;
}
}
// open a dialog
$("#legendEditor").dialog({
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
$("#notesEditor").dialog({
title: "Notes Editor", minWidth: Math.min(svgWidth, 400),
position: {my: "center", at: "center", of: "svg"}
});
if (modules.editLegends) return;
modules.editLegends = true;
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById("legendSelect").addEventListener("change", changeObject);
document.getElementById("legendName").addEventListener("input", changeName);
document.getElementById("legendText").addEventListener("input", changeText);
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("notesSelect").addEventListener("change", changeObject);
document.getElementById("notesName").addEventListener("input", changeName);
document.getElementById("notesText").addEventListener("input", changeText);
document.getElementById("notesFocus").addEventListener("click", validateHighlightElement);
document.getElementById("notesDownload").addEventListener("click", downloadLegends);
document.getElementById("notesUpload").addEventListener("click", () => legendsToLoad.click());
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
document.getElementById("notesRemove").addEventListener("click", triggernotesRemove);
function changeObject() {
const note = notes.find(note => note.id === this.value);
legendName.value = note.name;
legendText.value = note.legend;
notesName.value = note.name;
notesText.value = note.legend;
}
function changeName() {
const id = document.getElementById("legendSelect").value;
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
note.name = this.value;
}
function changeText() {
const id = document.getElementById("legendSelect").value;
const id = document.getElementById("notesSelect").value;
const note = notes.find(note => note.id === id);
note.legend = this.value;
}
function validateHighlightElement() {
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
const element = document.getElementById(select.value);
// if element is not found
if (element === null) {
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note?";
$("#alert").dialog({resizable: false, title: "Element not found",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
@ -103,7 +108,7 @@ function editLegends(id, name) {
const dataBlob = new Blob([legendString],{type:"text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
link.download = "legends" + Date.now() + ".txt";
link.download = "notes" + Date.now() + ".txt";
link.href = url;
link.click();
}
@ -116,8 +121,8 @@ function editLegends(id, name) {
const dataLoaded = fileLoadedEvent.target.result;
if (dataLoaded) {
notes = JSON.parse(dataLoaded);
document.getElementById("legendSelect").options.length = 0;
editLegends(notes[0].id, notes[0].name);
document.getElementById("notesSelect").options.length = 0;
editNotes(notes[0].id, notes[0].name);
} else {
tip("Cannot load a file. Please check the data format", false, "error")
}
@ -125,9 +130,9 @@ function editLegends(id, name) {
fileReader.readAsText(fileToLoad, "UTF-8");
}
function triggerLegendRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
$("#alert").dialog({resizable: false, title: "Remove legend element",
function triggernotesRemove() {
alertMessage.innerHTML = "Are you sure you want to remove the selected note?";
$("#alert").dialog({resizable: false, title: "Remove note",
buttons: {
Remove: function() {$(this).dialog("close"); removeLegend();},
Keep: function() {$(this).dialog("close");}
@ -136,12 +141,12 @@ function editLegends(id, name) {
}
function removeLegend() {
const select = document.getElementById("legendSelect");
const select = document.getElementById("notesSelect");
const index = notes.findIndex(n => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
editLegends(notes[0].id, notes[0].name);
if (!notes.length) {$("#notesEditor").dialog("close"); return;}
editNotes(notes[0].id, notes[0].name);
}
}

View file

@ -67,8 +67,8 @@ options.querySelector("div.tab").addEventListener("click", function(event) {
if (id === "styleTab") styleContent.style.display = "block"; else
if (id === "optionsTab") optionsContent.style.display = "block"; else
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
if (id === "toolsTab" && (!customization || customization === 10)) toolsContent.style.display = "block"; else
if (id === "toolsTab" && customization && customization !== 10) customizationMenu.style.display = "block"; else
if (id === "aboutTab") aboutContent.style.display = "block";
});
@ -90,7 +90,7 @@ function collapse(e) {
styleElementSelect.addEventListener("change", selectStyleElement);
function selectStyleElement() {
const sel = styleElementSelect.value;
let el = viewbox.select("#"+sel);
let el = d3.select("#"+sel);
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
@ -102,14 +102,14 @@ function selectStyleElement() {
// active group element
const group = styleGroupSelect.value;
if (sel == "ocean") el = oceanLayers.select("rect");
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons" || sel == "borders") {
el = d3.select("#"+sel).select("g#"+group).size()
? d3.select("#"+sel).select("g#"+group)
: d3.select("#"+sel).select("g");
}
if (sel !== "landmass") {
// opacity
if (sel !== "landmass" && sel !== "legend") {
// opacity
styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
@ -120,28 +120,34 @@ function selectStyleElement() {
}
// fill
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec" || sel === "fogging") {
styleFill.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill");
}
// stroke color and width
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "relig" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates"|| sel === "zones") {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
// stroke width
if (sel === "fogging") {
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
// stroke dash
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "legend" || sel === "population" || sel === "coordinates"|| sel === "zones") {
styleStrokeDash.style.display = "block";
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
}
// clipping
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes"|| sel === "zones") {
styleClipping.style.display = "block";
styleClippingInput.value = el.attr("mask") || "";
}
@ -167,7 +173,7 @@ function selectStyleElement() {
if (sel === "gridOverlay") styleGrid.style.display = "block";
if (sel === "terrain") styleRelief.style.display = "block";
if (sel === "texture") styleTexture.style.display = "block";
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") styleGroup.style.display = "block";
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes" || sel === "borders") styleGroup.style.display = "block";
if (sel === "markers") styleMarkers.style.display = "block";
if (sel === "population") {
@ -178,7 +184,7 @@ function selectStyleElement() {
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
}
if (sel === "statesBody") {
if (sel === "regions") {
styleStates.style.display = "block";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("stroke-width");
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity");
@ -226,6 +232,22 @@ function selectStyleElement() {
styleIconSizeInput.value = el.attr("size") || 2;
}
if (sel === "legend") {
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
loadDefaultFonts();
styleFont.style.display = "block";
styleSize.style.display = "block";
styleLegend.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#111111";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .5;
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
styleInputFont.style.display = "none";
styleInputFont.value = "";
styleFontSize.value = el.attr("data-size");
}
if (sel === "ocean") {
styleOcean.style.display = "block";
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
@ -253,7 +275,7 @@ function selectStyleElement() {
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons" || sel === "borders") {
document.getElementById(sel).querySelectorAll("g").forEach(el => {
if (el.id === "burgLabels") return;
const count = el.childElementCount;
@ -308,7 +330,9 @@ styleFilterInput.addEventListener("change", function() {
});
styleTextureInput.addEventListener("change", function() {
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
if (this.value === "none") texture.select("image").attr("xlink:href", ""); else
if (this.value === "default") texture.select("image").attr("xlink:href", getDefaultTexture()); else
setBase64Texture(this.value);
});
styleTextureShiftX.addEventListener("input", function() {
@ -335,7 +359,7 @@ styleGridSize.addEventListener("input", function() {
function calculateFriendlyGridSize() {
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
const friendly = "(" + rn(size * distanceScaleInput.value) + " " + distanceUnitInput.value + ")";
styleGridSizeFriendly.value = friendly;
}
@ -367,6 +391,11 @@ outlineLayersInput.addEventListener("change", function() {
OceanLayers();
});
styleReliefSet.addEventListener("change", function() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSizeInput.addEventListener("input", function() {
styleReliefSizeOutput.value = this.value;
const size = +this.value;
@ -386,6 +415,7 @@ styleReliefSizeInput.addEventListener("input", function() {
styleReliefDensityInput.addEventListener("input", function() {
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleTemperatureFillOpacityInput.addEventListener("input", function() {
@ -426,11 +456,26 @@ function shiftCompass() {
d3.select("#rose").attr("transform", tr);
}
styleLegendColItems.addEventListener("input", function() {
styleLegendColItemsOutput.value = this.value;
redrawLegend();
});
styleLegendBack.addEventListener("input", function() {
legend.select("rect").attr("fill", this.value)
});
styleLegendOpacity.addEventListener("input", function() {
styleLegendOpacityOutput.value = this.value;
legend.select("rect").attr("fill-opacity", this.value)
});
styleSelectFont.addEventListener("change", changeFont);
function changeFont() {
const value = styleSelectFont.value;
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
getEl().attr("font-family", font).attr("data-font", fonts[value]);
if (styleElementSelect.value === "legend") redrawLegend();
}
styleFontAdd.addEventListener("click", function() {
@ -471,8 +516,13 @@ styleFontMinus.addEventListener("click", function() {
});
function changeFontSize(size) {
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
const legend = styleElementSelect.value === "legend";
const coords = styleElementSelect.value === "coordinates";
const desSize = legend ? size : coords ? rn(size / scale ** .8, 2) : rn(size + (size / scale));
getEl().attr("data-size", size).attr("font-size", desSize);
styleFontSize.value = size;
if (legend) redrawLegend();
}
styleRadiusInput.addEventListener("change", function() {
@ -568,7 +618,7 @@ function textureProvideURL() {
opt.text = name.slice(0, 20);
styleTextureInput.add(opt);
styleTextureInput.value = textureURL.value;
texture.select("image").attr('xlink:href', textureURL.value);
setBase64Texture(textureURL.value);
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
$(this).dialog("close");
},
@ -577,6 +627,20 @@ function textureProvideURL() {
});
}
function setBase64Texture(url) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var reader = new FileReader();
reader.onloadend = function() {
texture.select("image").attr("xlink:href", reader.result);
}
reader.readAsDataURL(xhr.response);
};
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.send();
};
function fetchTextureURL(url) {
console.log("Provided URL is", url);
const img = new Image();
@ -610,11 +674,14 @@ optionsContent.addEventListener("input", function(event) {
else if (id === "culturesInput") culturesOutput.value = value;
else if (id === "culturesOutput") culturesInput.value = value;
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "powerInput") powerOutput.value = value;
else if (id === "provincesInput") provincesOutput.value = value;
else if (id === "provincesOutput") provincesOutput.value = value;
else if (id === "provincesOutput") powerOutput.value = value;
else if (id === "powerOutput") powerInput.value = value;
else if (id === "neutralInput") neutralOutput.value = value;
else if (id === "neutralOutput") neutralInput.value = value;
else if (id === "manorsInput") changeBurgsNumberSlider(value);
else if (id === "religionsInput") religionsOutput.value = value;
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "transparencyInput") changeDialogsTransparency(value);
@ -655,6 +722,7 @@ function changeMapSize() {
oceanPattern.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
oceanLayers.select("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
fitScaleBar();
fitLegendBox();
}
// just apply map size that was already set, apply graph size!
@ -764,66 +832,67 @@ function changeDialogsTransparency(value) {
}
function changeZoomExtent(value) {
const min = +zoomExtentMin.value;
zoom.scaleExtent([min, +zoomExtentMax.value]);
zoom.scaleTo(svg, +value);
const min = Math.max(+zoomExtentMin.value, .01), max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const scale = Math.max(Math.min(+value, 200), .01);
zoom.scaleTo(svg, scale);
}
// control sroted options
function applyStoredOptions() {
for(let i=0; i < localStorage.length; i++){
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
}
if (localStorage.getItem("distanceUnit")) applyOption(distanceUnitInput, localStorage.getItem("distanceUnit"));
if (localStorage.getItem("heightUnit")) applyOption(heightUnit, localStorage.getItem("heightUnit"));
for (let i=0; i < localStorage.length; i++) {
const stored = localStorage.key(i), value = localStorage.getItem(stored);
const input = document.getElementById(stored+"Input");
const input = document.getElementById(stored+"Input") || document.getElementById(stored);
const output = document.getElementById(stored+"Output");
if (input) input.value = value;
if (output) output.value = value;
lock(stored);
}
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
}
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
changeDialogsTransparency(localStorage.getItem("transparency") || 15);
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
if (localStorage.getItem("equator")) {
const eqY = +equatorInput.value;
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
}
}
// randomize options if randomization is allowed in option
// randomize options if randomization is allowed (not locked)
function randomizeOptions() {
Math.seedrandom(seed); // reset seed to initial one
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
// 'Options' settings
if (!locked("regions")) regionsInput.value = regionsOutput.value = gauss(15, 3, 2, 30);
if (!locked("provinces")) provincesInput.value = provincesOutput.value = gauss(40, 20, 20, 100);
if (!locked("manors")) {manorsInput.value = 1000; manorsOutput.value = "auto";}
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
if (!locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 2, 3, 20);
if (!locked("power")) powerInput.value = powerOutput.value = gauss(3, 2, 0, 10);
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (!locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
// 'Configure World' settings
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
}
if (!locked("mapSize")) mapSizeOutput.value = mapSizeInput.value = gauss(50, 20, 15, 100);
if (!locked("latitude")) latitudeOutput.value = latitudeInput.value = gauss(50, 20, 15, 100);
// define world size
function randomizeWorldSize() {
const eq = document.getElementById("equatorInput");
const eqDI = document.getElementById("equidistanceInput");
const eqDO = document.getElementById("equidistanceOutput");
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
eqDO.max = eqDI.max = eqDO.min * 10;
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
// 'Units Editor' settings
const US = navigator.language === "en-US";
const UK = navigator.language === "en-GB";
if (!locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = distanceUnitOutput.value = US || UK ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US || UK ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
}
// remove all saved data from LocalStorage and reload the page
@ -832,9 +901,7 @@ function restoreDefaultOptions() {
location.reload();
}
// FONTS
// fetch default fonts if not done before
function loadDefaultFonts() {
if (!$('link[href="fonts.css"]').length) {

View file

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

View file

@ -12,8 +12,8 @@ function editReliefIcon() {
updateReliefSizeInput();
$("#reliefEditor").dialog({
title: "Edit Relief Icons", resizable: false,
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
title: "Edit Relief Icons", resizable: false, width: 294,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeReliefEditor
});
@ -27,6 +27,7 @@ function editReliefIcon() {
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
document.getElementById("reliefEditorSet").addEventListener("change", changeIconsSet);
reliefIconsDiv.querySelectorAll("svg").forEach(el => el.addEventListener("click", changeIcon));
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
@ -53,8 +54,13 @@ function editReliefIcon() {
function updateReliefIconSelected() {
const type = elSelected.attr("data-type");
const button = reliefIconsDiv.querySelector("svg[data-type='"+type+"']");
reliefIconsDiv.querySelectorAll("svg.pressed").forEach(b => b.classList.remove("pressed"));
reliefIconsDiv.querySelector("svg[data-type='"+type+"']").classList.add("pressed");
button.classList.add("pressed");
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
button.parentNode.style.display = "block";
reliefEditorSet.value = button.parentNode.dataset.type;
}
function updateReliefSizeInput() {
@ -196,6 +202,12 @@ function editReliefIcon() {
elSelected.attr("x", x-shift).attr("y", y-shift);
}
function changeIconsSet() {
const set = reliefEditorSet.value;
reliefIconsDiv.querySelectorAll("div").forEach(b => b.style.display = "none");
reliefIconsDiv.querySelector("div[data-type='" + set + "']").style.display = "block";
}
function changeIcon() {
if (this.classList.contains("pressed")) return;

View file

@ -0,0 +1,423 @@
"use strict";
function editReligions() {
if (customization) return;
closeDialogs("#religionsEditor, .stable");
if (!layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleProvinces")) toggleProvinces();
const body = document.getElementById("religionsBody");
const animate = d3.transition().duration(1500).ease(d3.easeSinIn);
drawReligionCenters();
refreshReligionsEditor();
if (modules.editReligions) return;
modules.editReligions = true;
$("#religionsEditor").dialog({
title: "Religions Editor", resizable: false, width: fitContent(), close: closeReligionsEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
// add listeners
document.getElementById("religionsEditorRefresh").addEventListener("click", refreshReligionsEditor);
document.getElementById("religionsLegend").addEventListener("click", toggleLegend);
document.getElementById("religionsPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("religionsManually").addEventListener("click", enterReligionsManualAssignent);
document.getElementById("religionsManuallyApply").addEventListener("click", applyReligionsManualAssignent);
document.getElementById("religionsManuallyCancel").addEventListener("click", () => exitReligionsManualAssignment());
document.getElementById("religionsAdd").addEventListener("click", enterAddReligionMode);
document.getElementById("religionsExport").addEventListener("click", downloadReligionsData);
function refreshReligionsEditor() {
religionsCollectStatistics();
religionsEditorAddLines();
}
function religionsCollectStatistics() {
const cells = pack.cells, religions = pack.religions;
religions.forEach(r => r.area = r.rural = r.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const r = cells.religion[i];
religions[r].area += cells.area[i];
religions[r].rural += cells.pop[i];
if (cells.burg[i]) religions[r].urban += pack.burgs[cells.burg[i]].population;
}
}
// add line for each religion
function religionsEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "", totalArea = 0, totalPopulation = 0;
for (const r of pack.religions) {
if (r.removed) continue;
const area = r.area * (distanceScaleInput.value ** 2);
const rural = r.rural * populationRate.value;
const urban = r.urban * populationRate.value * urbanization.value;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
if (r.i) {
lines += `<div class="states religions" 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?r.deity:''}" data-expansionism=${r.expansionism}>
<svg data-tip="Religion fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${r.color}" class="zoneFill"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm hide" 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="religionDeidy hide" value="${r.deity?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="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Remove religion" class="icon-trash-empty hide"></span>
</div>`;
} else {
// No religion (neutral) line
lines += `<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="9" height="9" class="placeholder"></svg>
<input data-tip="Religion name. Click and type to change" class="religionName italic" value="${r.name}" autocorrect="off" spellcheck="false">
<select data-tip="Religion type" class="religionType placeholder">${getTypeOptions(r.type)}</select>
<input data-tip="Religion form" class="religionForm placeholder hide" 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="religionDeidy placeholder hide" 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="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
</div>`;
}
}
body.innerHTML = lines;
// update footer
religionsFooterNumber.innerHTML = pack.religions.filter(r => r.i && !r.removed).length;
religionsFooterArea.innerHTML = si(totalArea) + unit;
religionsFooterPopulation.innerHTML = si(totalPopulation);
religionsFooterArea.dataset.area = totalArea;
religionsFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseenter", ev => religionHighlightOn(ev)));
body.querySelectorAll("div.religions").forEach(el => el.addEventListener("mouseleave", ev => religionHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectReligionOnLineClick));
body.querySelectorAll("rect.zoneFill").forEach(el => el.addEventListener("click", religionChangeColor));
body.querySelectorAll("div > input.religionName").forEach(el => el.addEventListener("input", religionChangeName));
body.querySelectorAll("div > select.religionType").forEach(el => el.addEventListener("change", religionChangeType));
body.querySelectorAll("div > select.religionForm").forEach(el => el.addEventListener("change", religionChangeForm));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", regenerateDeity));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", religionRemove));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(religionsHeader);
$("#religionsEditor").dialog();
}
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;
}
function religionHighlightOn(event) {
if (!layerIsOn("toggleReligions")) return;
if (customization) return;
const religion = +event.target.dataset.id;
relig.select("#religion"+religion).raise().transition(animate).attr("stroke-width", 2.5).attr("stroke", "#c13119");
debug.select("#cultureCenter"+religion).raise().transition(animate).attr("r", 8).attr("stroke", "#c13119");
}
function religionHighlightOff(event) {
if (!layerIsOn("toggleReligions")) return;
const religion = +event.target.dataset.id;
relig.select("#religion"+religion).transition().attr("stroke-width", null).attr("stroke", null);
debug.select("#cultureCenter"+religion).transition().attr("r", 6).attr("stroke", null);
}
function religionChangeColor() {
const el = this;
const currentFill = el.getAttribute("fill");
const religion = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.religions[religion].color = fill;
relig.select("#religion"+religion).attr("fill", fill);
debug.select("#cultureCenter"+religion).attr("fill", fill);
}
openPicker(currentFill, callback);
}
function religionChangeName() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.religions[religion].name = this.value;
}
function religionChangeType() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.religions[religion].type = this.value;
}
function religionChangeForm() {
const religion = +this.parentNode.dataset.id;
this.parentNode.dataset.form = this.value;
pack.religions[religion].form = this.value;
}
function regenerateDeity() {
const religion = +this.parentNode.dataset.id;
const culture = pack.religions[religion].culture;
const deity = Religions.getDeityName(culture);
this.parentNode.dataset.deity = deity;
pack.religions[religion].deity = deity;
this.nextElementSibling.value = deity;
}
function religionRemove() {
if (customization) return;
const religion = +this.parentNode.dataset.id;
relig.select("#religion"+religion).remove();
debug.select("#cultureCenter"+religion).remove();
pack.cells.religion.forEach((r, i) => {if(r === religion) pack.cells.religion[i] = 0;});
pack.religions[religion].removed = true;
refreshReligionsEditor();
}
function drawReligionCenters() {
const tooltip = "Drag to move the religion center";
debug.select("#religionCenters").remove();
const religionCenters = debug.append("g").attr("id", "religionCenters")
.attr("stroke-width", 2).attr("stroke", "#444444").style("cursor", "move");
const data = pack.religions.filter(r => r.center && !r.removed);
religionCenters.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 => pack.religions[d.i].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"); religionHighlightOn(event);})
.on("mouseleave", d => {tip('', true); body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); religionHighlightOff(event);})
.call(d3.drag().on("start", religionCenterDrag));
}
function religionCenterDrag() {
const el = d3.select(this);
const r = +this.dataset.id;
d3.event.on("drag", () => {
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
const cell = findCell(d3.event.x, d3.event.y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.religions[r].center = cell;
});
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // 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 = +religionsFooterArea.dataset.area;
const totalPopulation = +religionsFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
});
} else {
body.dataset.type = "absolute";
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");
document.getElementById("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 r = +religionsManuallyBrushNumber.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) changeReligionForSelection(selection);
});
}
// change religion within selection
function changeReligionForSelection(selection) {
const temp = relig.select("#temp");
const selected = body.querySelector("div.selected");
const r = +selected.dataset.id; // religionNew
const color = pack.religions[r].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 (r === religionOld) return;
// change of append new element
if (exists.size()) exists.attr("data-religion", r).attr("fill", color);
else temp.append("polygon").attr("data-cell", i).attr("data-religion", r).attr("points", getPackPolygon(i)).attr("fill", color);
});
}
function moveReligionBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +religionsManuallyBrushNumber.value;
moveCircle(point[0], point[1], 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();
}
exitReligionsManualAssignment();
}
function exitReligionsManualAssignment(close) {
customization = 0;
relig.select("#temp").remove();
removeCircle();
document.querySelectorAll("#religionsBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("religionsManuallyButtons").style.display = "none";
religionsEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
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")) {exitAddReligionMode(); return;};
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 point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {tip("You cannot place religion center into the water. Please click on a land cell", false, "error"); return;}
const occupied = pack.religions.some(r => !r.removed && r.center === center);
if (occupied) {tip("This cell is already a religion center. Please select a different cell", false, "error"); return;}
if (d3.event.shiftKey === false) exitAddReligionMode();
Religions.add(center);
drawReligionCenters();
religionsEditorAddLines();
}
function downloadReligionsData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Religion,Color,Type,Form,Deity,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.type + ",";
data += el.dataset.form + ",";
data += el.dataset.deity.replace(",", " -") + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "religions_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeReligionsEditor() {
debug.select("#religionCenters").remove();
exitReligionsManualAssignment("close");
exitAddReligionMode();
}
}

View file

@ -68,7 +68,7 @@ function editRiver() {
function drawControlPoints(node) {
const l = node.getTotalLength() / 2;
const segments = Math.ceil(l / 5);
const segments = Math.ceil(l / 8);
const increment = rn(l / segments * 1e5);
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i / 1e5);
@ -80,7 +80,7 @@ function editRiver() {
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -106,7 +106,7 @@ function editRiver() {
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
const tr = parseTransform(elSelected.attr("transform"));
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
riverLength.innerHTML = rn(l * tr[5] * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function clickControlPoint() {
@ -134,7 +134,7 @@ function editRiver() {
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -250,7 +250,7 @@ function editRiver() {
function editRiverLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function removeRiver() {

View file

@ -44,14 +44,14 @@ function editRoute(onClick) {
function drawControlPoints(node) {
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 5);
const increment = l / Math.ceil(l / 8);
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function addControlPoint(point) {
debug.select("#controlPoints").append("circle")
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
.attr("cx", point.x).attr("cy", point.y).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
}
@ -76,7 +76,7 @@ function editRoute(onClick) {
const before = ":nth-child(" + (index + 1) + ")";
debug.select("#controlPoints").insert("circle", before)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .8)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
@ -98,7 +98,7 @@ function editRoute(onClick) {
elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
}
function showGroupSection() {
@ -256,7 +256,7 @@ function editRoute(onClick) {
function editRouteLegend() {
const id = elSelected.attr("id");
editLegends(id, id);
editNotes(id, id);
}
function removeRoute() {

View file

@ -6,6 +6,8 @@ function editStates() {
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleTexture")) toggleTexture();
const body = document.getElementById("statesBodySection");
refreshStatesEditor();
@ -14,109 +16,120 @@ function editStates() {
modules.editStates = true;
$("#statesEditor").dialog({
title: "States Editor", width: fitContent(), close: closeStatesEditor,
title: "States Editor", resizable: false, width: fitContent(), close: closeStatesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
document.getElementById("statesLegend").addEventListener("click", toggleLegend);
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
document.getElementById("statesJustify").addEventListener("click", justifyStates);
document.getElementById("statesRecalculate").addEventListener("click", () => recalculateStates(true));
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
document.getElementById("statesNeutral").addEventListener("input", () => recalculateStates(false));
document.getElementById("statesNeutralNumber").addEventListener("change", () => recalculateStates(false));
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
document.getElementById("statesManuallyCancel").addEventListener("click", () => exitStatesManualAssignment());
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("zoneFill")) stateChangeFill(el); else
if (cl.contains("icon-fleur")) stateOpenCOA(state); else
if (cl.contains("icon-star-empty")) stateCapitalZoomIn(state); else
if (cl.contains("icon-pin")) focusOnState(state, cl); else
if (cl.contains("icon-trash-empty")) stateRemove(state); else
if (cl.contains("hoverButton") && cl.contains("stateName")) regenerateName(state, line); else
if (cl.contains("hoverButton") && cl.contains("stateForm")) regenerateForm(state, line);
});
body.addEventListener("input", function(ev) {
const el = ev.target, cl = el.classList, line = el.parentNode, state = +line.dataset.id;
if (cl.contains("stateName")) stateChangeName(state, line, el.value); else
if (cl.contains("stateForm")) stateChangeForm(state, line, el.value); else
if (cl.contains("stateCapital")) stateChangeCapitalName(state, line, el.value); else
if (cl.contains("cultureType")) stateChangeType(state, line, el.value); else
if (cl.contains("stateCulture")) stateChangeCulture(state, line, el.value); else
if (cl.contains("statePower")) stateChangeExpansionism(state, line, el.value);
});
function refreshStatesEditor() {
statesCollectStatistics();
BurgsAndStates.collectStatistics();
statesEditorAddLines();
}
function statesCollectStatistics() {
const cells = pack.cells, states = pack.states;
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const s = cells.state[i];
states[s].cells += 1;
states[s].area += cells.area[i];
states[s].rural += cells.pop[i];
if (cells.burg[i]) {
states[s].urban += pack.burgs[cells.burg[i]].population;
states[s].burgs++;
}
}
}
// add line for each state
function statesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const hidden = statesRegenerateButtons.style.display === "block" ? "" : "hidden"; // show/hide regenerate columns
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
for (const s of pack.states) {
if (s.removed) continue;
const area = s.area * (distanceScale.value ** 2);
const area = s.area * (distanceScaleInput.value ** 2);
const rural = s.rural * populationRate.value;
const urban = s.urban * populationRate.value * urbanization.value;
const population = rural + urban;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
totalArea += area;
totalPopulation += population;
totalBurgs += s.burgs;
const focused = defs.select("#fog #focusState"+s.i).size();
if (!s.i) {
// Neutral line
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
data-population=${population} data-burgs=${s.burgs} data-color="" data-capital="" data-culture="" data-type="" data-expansionism="">
<input class="stateColor placeholder" type="color">
data-population=${population} data-burgs=${s.burgs} data-color="" data-form="" data-capital="" data-culture="" data-type="" data-expansionism="">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
<span class="icon-star-empty placeholder"></span>
<input class="stateCapital placeholder">
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
<span class="icon-resize-full ${hidden} placeholder"></span>
<input class="statePower ${hidden} placeholder" type="number" value=0>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span class="icon-fleur placeholder hide"></span>
<input class="stateForm placeholder" value="none">
<span class="icon-star-empty placeholder hide"></span>
<input class="stateCapital placeholder hide">
<select class="stateCulture placeholder hide">${getCultureOptions(0)}</select>
<select class="cultureType ${hidden} placeholder show hide">${getTypeOptions(0)}</select>
<span class="icon-resize-full ${hidden} placeholder show hide"></span>
<input class="statePower ${hidden} placeholder show hide" type="number" value=0>
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
</div>`;
continue;
}
const capital = pack.burgs[s.capital].name;
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-form="${s.formName}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
<svg data-tip="State fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${s.color}" class="zoneFill"></svg>
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty"></span>
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male"></span>
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
<span data-tip="Remove state" class="icon-trash-empty"></span>
<span data-tip="Click to re-generate name" class="icon-arrows-cw stateName hoverButton placeholder"></span>
<span data-tip="Click to open state COA in the Iron Arachne Heraldry Generator" class="icon-fleur pointer hide"></span>
<input data-tip="State form name. Click and type to change" class="stateForm" value="${s.formName}" autocorrect="off" spellcheck="false">
<span data-tip="Click to re-generate form name" class="icon-arrows-cw stateForm hoverButton placeholder"></span>
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer hide"></span>
<input data-tip="Capital name. Click and type to rename" class="stateCapital hide" value="${capital}" autocorrect="off" spellcheck="false"/>
<select data-tip="Dominant culture. Click to change" class="stateCulture hide">${getCultureOptions(s.culture)}</select>
<select data-tip="State type. Click to change" class="cultureType ${hidden} show hide">${getTypeOptions(s.type)}</select>
<span data-tip="State expansionism" class="icon-resize-full ${hidden} show hide"></span>
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden} show hide" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
<span data-tip="Cells count" class="icon-check-empty ${hidden} show hide"></span>
<div data-tip="Cells count" class="stateCells ${hidden} show hide">${s.cells}</div>
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled hide"></span>
<div data-tip="Burgs count" class="stateBurgs hide">${s.burgs}</div>
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="State area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Toggle state focus" class="icon-pin ${focused?'':' inactive'} hide"></span>
<span data-tip="Remove the state" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
@ -130,18 +143,11 @@ function editStates() {
statesFooterArea.dataset.area = totalArea;
statesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", stateChangeCulture));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectStateOnLineClick);
el.addEventListener("mouseenter", ev => stateHighlightOn(ev));
el.addEventListener("mouseleave", ev => stateHighlightOff(ev));
});
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
applySorting(statesHeader);
@ -162,10 +168,11 @@ function editStates() {
}
function stateHighlightOn(event) {
if (!customization) event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.remove("placeholder"));
if (!layerIsOn("toggleStates")) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const path = regions.select("#state"+state).attr("d");
const path = statesBody.select("#state"+state).attr("d");
debug.append("path").attr("class", "highlight").attr("d", path)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)").call(transition);
@ -187,83 +194,179 @@ function editStates() {
}
function stateHighlightOff() {
event.target.querySelectorAll(".hoverButton").forEach(el => el.classList.add("placeholder"));
debug.selectAll(".highlight").each(function(el) {
d3.select(this).call(removePath);
});
}
function stateChangeColor() {
const state = +this.parentNode.dataset.id;
pack.states[state].color = this.value;
regions.select("#state"+state).attr("fill", this.value);
regions.select("#state-gap"+state).attr("stroke", this.value);
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
function stateChangeFill(el) {
const currentFill = el.getAttribute("fill");
const state = +el.parentNode.parentNode.dataset.id;
const callback = function(fill) {
el.setAttribute("fill", fill);
pack.states[state].color = fill;
statesBody.select("#state"+state).attr("fill", fill);
statesBody.select("#state-gap"+state).attr("stroke", fill);
const halo = d3.color(fill) ? d3.color(fill).darker().hex() : "#666666";
statesHalo.select("#state-border"+state).attr("stroke", halo);
}
openPicker(currentFill, callback);
}
function stateChangeName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.states[state].name = this.value;
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
function stateChangeName(state, line, value) {
const oldName = pack.states[state].name;
pack.states[state].name = line.dataset.name = value;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldName, value);
}
function stateChangeCapitalName() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.capital = this.value;
function regenerateName(state, line) {
const culture = pack.states[state].culture;
const oldName = pack.states[state].name;
const newName = Names.getState(Names.getCultureShort(culture), culture);
pack.states[state].name = line.dataset.name = line.querySelector(".stateName").value = newName;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldName, newName);
}
function stateChangeForm(state, line, value) {
const oldForm = pack.states[state].formName;
pack.states[state].formName = line.dataset.form = value;
pack.states[state].fullName = BurgsAndStates.getFullName(pack.states[state]);
changeLabel(state, oldForm, value, true);
}
function regenerateForm(state, line) {
const oldForm = pack.states[state].formName;
let newForm = oldForm;
for (let i=0; newForm === oldForm && i < 50; i++) {
BurgsAndStates.defineStateForms([state]);
newForm = pack.states[state].formName;
}
line.dataset.form = line.querySelector(".stateForm").value = newForm;
changeLabel(state, oldForm, newForm, true);
}
function changeLabel(state, oldName, newName, form) {
const label = document.getElementById("stateLabel"+state);
if (!label) return;
const tspan = Array.from(label.querySelectorAll('tspan'));
const tspanAdj = !form && oldName && newName && pack.states[state].formName ? tspan.find(el => el.textContent.includes(getAdjective(oldName))) : null;
const tspanName = tspanAdj || !oldName || !newName ? null : tspan.find(el => el.textContent.includes(oldName));
if (tspanAdj) {
tspanAdj.textContent = tspanAdj.textContent.replace(getAdjective(oldName), getAdjective(newName));
const l = tspanAdj.getComputedTextLength();
tspanAdj.setAttribute("x", l / -2);
} if (tspanName) {
tspanName.textContent = tspanName.textContent.replace(oldName, newName);
const l = tspanName.getComputedTextLength();
tspanName.setAttribute("x", l / -2);
} else {
BurgsAndStates.drawStateLabels([state]);
}
tip("State label is automatically changed. To make a custom change click on a label and edit the text there", false, "warn");
}
function stateChangeCapitalName(state, line, value) {
line.dataset.capital = value;
const capital = pack.states[state].capital;
if (!capital) return;
pack.burgs[capital].name = this.value;
document.querySelector("#burgLabel"+capital).textContent = this.value;
pack.burgs[capital].name = value;
document.querySelector("#burgLabel"+capital).textContent = value;
}
function stateCapitalZoomIn() {
const state = +this.parentNode.dataset.id;
function stateOpenCOA(state) {
const url = `https://ironarachne.com/heraldry/${seed}-s${state}`;
window.open(url, '_blank');
}
function stateCapitalZoomIn(state) {
const capital = pack.states[state].capital;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr("x"), y = +l.attr("y");
zoomTo(x, y, 8, 2000);
}
function stateChangeCulture() {
const state = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.states[state].culture = v;
function stateChangeCulture(state, line, value) {
line.dataset.base = pack.states[state].culture = +value;
}
function stateChangeType() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.states[state].type = this.value;
function stateChangeType(state, line, value) {
line.dataset.type = pack.states[state].type = value;
recalculateStates();
}
function stateChangeExpansionism() {
const state = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.states[state].expansionism = +this.value;
function stateChangeExpansionism(state, line, value) {
line.dataset.expansionism = pack.states[state].expansionism = value;
recalculateStates();
}
function stateRemove() {
function focusOnState(state, cl) {
if (customization) return;
const state = +this.parentNode.dataset.id;
regions.select("#state"+state).remove();
regions.select("#state-gap"+state).remove();
regions.select("#state-border"+state).remove();
document.querySelector("#stateLabel"+state+" > textPath").remove();
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focusState"+state).size()) return;
fogging.attr("display", "block");
const path = statesBody.select("#state"+state).attr("d");
defs.select("#fog").append("path").attr("d", path).attr("fill", "black").attr("id", "focusState"+state);
fogging.append("path").attr("d", path).attr("id", "focusStateHalo"+state)
.attr("fill", "none").attr("stroke", pack.states[state].color).attr("filter", "url(#blur5)");
} else unfocus(state);
}
function unfocus(s) {
defs.select("#focusState"+s).remove();
fogging.select("#focusStateHalo"+s).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all items are de-focused
}
function stateRemove(state) {
if (customization) return;
statesBody.select("#state"+state).remove();
statesBody.select("#state-gap"+state).remove();
statesHalo.select("#state-border"+state).remove();
unfocus(state);
const label = document.querySelector("#stateLabel"+state);
if (label) label.remove();
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
pack.states[state].removed = true;
// remove provinces
pack.states[state].provinces.forEach(p => {
pack.provinces[p].removed = true;
pack.cells.province.forEach((pr, i) => {if(pr === p) pack.cells.province[i] = 0;});
});
const capital = pack.states[state].capital;
pack.burgs[capital].capital = false;
pack.burgs[capital].state = 0;
moveBurgToGroup(capital, "towns");
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
debug.selectAll(".highlight").remove();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
refreshStatesEditor();
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = pack.states.filter(s => s.i && !s.removed && s.cells).sort((a, b) => b.area - a.area).map(s => [s.i, s.color, s.name]);
drawLegend("States", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
@ -284,75 +387,68 @@ function editStates() {
}
}
function regenerateNames() {
body.querySelectorAll(":scope > div").forEach(function(el) {
const state = +el.dataset.id;
if (!state) return;
const culture = pack.states[state].culture;
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
el.querySelector(".stateName").value = name;
pack.states[state].name = el.dataset.name = name;
labels.select("#stateLabel"+state+" > textPath").text(name);
});
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
}
function openRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
statesRegenerateButtons.style.display = "block";
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
statesEditor.querySelectorAll(".show").forEach(el => el.classList.remove("hidden"));
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
}
function recalculateStates() {
function recalculateStates(must) {
if (!must && !statesAutoChange.checked) return;
BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function justifyStates() {
BurgsAndStates.normalizeStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
BurgsAndStates.generateProvinces();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
}
function randomizeStatesExpansion() {
pack.states.slice(1).forEach(s => {
pack.states.forEach(s => {
if (!s.i || s.removed) return;
const expansionism = rn(Math.random() * 4 + 1, 1);
s.expansionism = expansionism;
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
});
recalculateStates();
recalculateStates(true, true);
}
function exitRegenerationMenu() {
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
statesRegenerateButtons.style.display = "none";
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
statesEditor.querySelectorAll(".show").forEach(el => el.classList.add("hidden"));
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
}
function enterStatesManualAssignent() {
if (!layerIsOn("toggleStates")) toggleStates();
customization = 2;
regions.append("g").attr("id", "temp");
statesBody.append("g").attr("id", "temp");
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("statesManuallyButtons").style.display = "inline-block";
document.getElementById("statesHalo").style.display = "none";
statesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
statesFooter.style.display = "none";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
$("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click on state to select, drag the circle to change state", true);
viewbox.style("cursor", "crosshair").call(d3.drag()
.on("drag", dragStateBrush))
viewbox.style("cursor", "crosshair")
.on("click", selectStateOnMapClick)
.call(d3.drag().on("start", dragStateBrush))
.on("touchmove mousemove", moveStateBrush);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelector("div").classList.add("selected");
}
function selectStateOnLineClick(i) {
function selectStateOnLineClick() {
if (customization !== 2) return;
if (this.parentNode.id !== "statesBodySection") return;
body.querySelector("div.selected").classList.remove("selected");
this.classList.add("selected");
}
@ -362,7 +458,7 @@ function editStates() {
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
const assigned = statesBody.select("#temp").select("polygon[data-cell='"+i+"']");
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
body.querySelector("div.selected").classList.remove("selected");
@ -370,18 +466,22 @@ function editStates() {
}
function dragStateBrush() {
const p = d3.mouse(this);
const r = +statesManuallyBrush.value;
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) changeStateForSelection(selection);
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) changeStateForSelection(selection);
});
}
// change state within selection
function changeStateForSelection(selection) {
const temp = regions.select("#temp");
const temp = statesBody.select("#temp");
const selected = body.querySelector("div.selected");
const stateNew = +selected.dataset.id;
@ -407,44 +507,113 @@ function editStates() {
}
function applyStatesManualAssignent() {
const cells = pack.cells;
const changed = regions.select("#temp").selectAll("polygon");
changed.each(function() {
const cells = pack.cells, affectedStates = [], affectedProvinces = [];
statesBody.select("#temp").selectAll("polygon").each(function() {
const i = +this.dataset.cell;
const c = +this.dataset.state;
affectedStates.push(cells.state[i], c);
affectedProvinces.push(cells.province[i]);
cells.state[i] = c;
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
});
if (changed.size()) {
if (affectedStates.length) {
refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
}
exitStatesManualAssignment();
}
function adjustProvinces(affectedProvinces) {
const cells = pack.cells, provinces = pack.provinces, states = pack.states;
const form = {"Zone":1, "Area":1, "Territory":2, "Province":1};
affectedProvinces.forEach(p => {
// do nothing if neutral lands are captured
if (!p) return;
// remove province from state provinces list
const old = provinces[p].state;
if (states[old].provinces.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
// find states owning at least 1 province cell
const provCells = cells.i.filter(i => cells.province[i] === p);
const provStates = [...new Set(provCells.map(i => cells.state[i]))];
// assign province its center owner; if center is neutral, remove province
const owner = cells.state[provinces[p].center];
if (owner) {
const name = provinces[p].name;
// if province is historical part of abouther state province, unite with old province
const part = states[owner].provinces.find(n => name.includes(provinces[n].name));
if (part) {
provinces[p].removed = true;
provCells.filter(i => cells.state[i] === owner).forEach(i => cells.province[i] = part);
} else {
provinces[p].state = owner;
states[owner].provinces.push(p);
provinces[p].color = getMixedColor(states[owner].color);
}
} else {
provinces[p].removed = true;
provCells.filter(i => !cells.state[i]).forEach(i => cells.province[i] = 0);
}
// create new provinces for non-main part
provStates.filter(s => s && s !== owner).forEach(s => createProvince(p, s, provCells.filter(i => cells.state[i] === s)));
});
function createProvince(initProv, state, provCells) {
const province = provinces.length;
provCells.forEach(i => cells.province[i] = province);
const burgCell = provCells.find(i => cells.burg[i]);
const center = burgCell ? burgCell : provCells[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
const name = burgCell && Math.random() < .7
? getAdjective(pack.burgs[burg].name)
: getAdjective(states[state].name) + " " + provinces[initProv].name.split(" ").slice(-1)[0];
const formName = name.split(" ").length > 1 ? provinces[initProv].formName : rw(form);
const fullName = name + " " + formName;
const color = getMixedColor(states[state].color);
provinces.push({i:province, state, center, burg, name, formName, fullName, color});
}
}
function exitStatesManualAssignment() {
function exitStatesManualAssignment(close) {
customization = 0;
regions.select("#temp").remove();
statesBody.select("#temp").remove();
removeCircle();
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("statesManuallyButtons").style.display = "none";
document.getElementById("statesHalo").style.display = "block";
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
statesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
statesFooter.style.display = "block";
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#statesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function enterAddStateMode() {
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
customization = 3;
this.classList.add("pressed");
tip("Click on the map to create a new capital or promote an existing burg", true);
viewbox.style("cursor", "crosshair").on("click", addState);
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "none");
}
function addState() {
@ -460,35 +629,47 @@ function editStates() {
pack.burgs[burg].state = pack.states.length;
moveBurgToGroup(burg, "cities");
exitAddStateMode();
if (d3.event.shiftKey === false) exitAddStateMode();
const i = pack.states.length;
const culture = pack.cells.culture[center];
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
const name = Names.getState(basename, culture);
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
const diplomacy = pack.states.map(s => s.i ? "Neutral" : "x")
diplomacy.push("x");
pack.states.forEach(s => {if (s.i) {s.diplomacy.push("Neutral");}});
const provinces = [];
const affected = [pack.states.length, pack.cells.state[center]];
pack.cells.state[center] = pack.states.length;
pack.cells.c[center].forEach(c => {
if (pack.cells.h[c] < 20) return;
if (pack.cells.burg[c]) return;
affected.push(pack.cells.state[c]);
pack.cells.state[c] = pack.states.length;
});
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
pack.states.push({i, name, diplomacy, provinces, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([i]);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
refreshStatesEditor();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels(affected);
statesEditorAddLines();
}
function exitAddStateMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
body.querySelectorAll("div > input, select, span, svg").forEach(e => e.style.pointerEvents = "all");
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
}
function downloadStatesData() {
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
@ -512,11 +693,12 @@ function editStates() {
link.download = "states_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function closeStatesEditor() {
if (customization === 2) exitStatesManualAssignment();
if (customization === 2) exitStatesManualAssignment("close");
if (customization === 3) exitAddStateMode();
debug.selectAll(".highlight").remove();
}
}

View file

@ -10,19 +10,37 @@ toolsContent.addEventListener("click", function(event) {
if (button === "editHeightmapButton") editHeightmap(); else
if (button === "editBiomesButton") editBiomes(); else
if (button === "editStatesButton") editStates(); else
if (button === "editProvincesButton") editProvinces(); else
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editBurgsButton") editBurgs(); else
if (button === "editUnitsButton") editUnits();
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
if (button === "editZonesButton") editZones();
// Click to Regenerate buttons
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateStates") regenerateStates();
if (event.target.parentNode.id === "regenerateFeature") {
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(button); return;}
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
$("#alert").dialog({resizable: false, title: "Regenerate element",
buttons: {
Proceed: function() {processFeatureRegeneration(button); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
},
create: function() {
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
$('<input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label>').prependTo(pane);
},
close: function() {
const box = $(this).dialog("widget").find(".checkbox")[0];
if (!box) return;
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
}
});
}
// Click to Add buttons
if (button === "addLabel") toggleAddLabel(); else
@ -32,6 +50,18 @@ toolsContent.addEventListener("click", function(event) {
if (button === "addMarker") toggleAddMarker();
});
function processFeatureRegeneration(button) {
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
if (button === "regenerateRivers") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateStates") regenerateStates(); else
if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateReligions") regenerateReligions();
}
function regenerateRivers() {
const heights = new Uint8Array(pack.cells.h);
Rivers.generate();
@ -45,9 +75,10 @@ function recalculatePopulation() {
if (!b.i || b.removed) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
if (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i]) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
});
}
@ -62,14 +93,14 @@ function regenerateBurgs() {
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 10 / densityInput.value ** .8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
const spacing = (graphWidth + graphHeight) / 200 / (burgsCount / 500); // base min distance between towns
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
const cell = sorted[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const state = cells.state[cell];
@ -96,28 +127,29 @@ function regenerateBurgs() {
BurgsAndStates.drawBurgs();
Routes.regenerate();
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsFilterState").options.length = 0;
document.getElementById("burgsFilterCulture").options.length = 0;
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
function regenerateStates() {
Math.seedrandom(Math.floor(Math.random() * 1e9)); // new random seed
const burgs = pack.burgs.filter(b => b.i && !b.removed), states = pack.states.filter(s => s.i && !s.removed);
// burgs sorted by a bit randomized population:
const sorted = burgs.map(b => [b.i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
const capitalsTree = d3.quadtree();
let spacing = (graphWidth + graphHeight) / 2 / states.length; // min distance between capitals
// turn all old capitals into towns
states.forEach(s => {
moveBurgToGroup(s.capital, "towns");
s.capital = 0;
burgs.filter(b => b.capital).forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = false;
});
states.forEach(s => {
let newCapital = 0, x = 0, y = 0;
while (!newCapital) {
newCapital = burgs[biased(1, burgs.length-1, 3)];
for (let i=0; i < sorted.length && !newCapital; i++) {
newCapital = burgs[sorted[i]];
x = newCapital.x, y = newCapital.y;
if (capitalsTree.find(x, y, spacing) !== undefined) {
spacing -= 1;
@ -127,23 +159,44 @@ function regenerateStates() {
}
capitalsTree.add([x, y]);
newCapital.capital = true;
s.capital = newCapital.i;
s.center = newCapital.cell;
s.culture = newCapital.culture;
s.expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
s.expansionism = rn(Math.random() * powerInput.value + 1, 1);
const basename = newCapital.name.length < 9 && newCapital.cell%5 === 0 ? newCapital.name : Names.getCulture(s.culture, 3, 6, "", 0);
s.name = Names.getState(basename, s.culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[newCapital.cell]);
s.type = nomadic ? "Nomadic" : pack.cultures[s.culture].type === "Nomadic" ? "Generic" : pack.cultures[s.culture].type;
moveBurgToGroup(newCapital.i, "cities");
document.getElementById("statesBodySection").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsBody").innerHTML = "<i>Please refresh the editor!</i>";
document.getElementById("burgsFilterState").options.length = 0;
document.getElementById("burgsFilterCulture").options.length = 0;
});
unfog();
BurgsAndStates.expandStates();
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
BurgsAndStates.normalizeStates();
BurgsAndStates.collectStatistics();
BurgsAndStates.assignColors();
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
BurgsAndStates.drawStateLabels();
if (document.getElementById("burgsEditorRefresh").offsetParent) burgsEditorRefresh.click();
if (document.getElementById("statesEditorRefresh").offsetParent) statesEditorRefresh.click();
}
function regenerateProvinces() {
unfog();
BurgsAndStates.generateProvinces(true);
drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
}
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
}
function unpressClickToAddButton() {
@ -179,12 +232,18 @@ function addLabelOnClick() {
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
.attr("startOffset", "50%").attr("font-size", "100%");
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
const width = example.node().getBBox().width;
const x = width / -2; // x offset;
example.remove();
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
.append("tspan").attr("x", x).text(name);
defs.select("#textPaths")
.append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}

View file

@ -12,31 +12,34 @@ function editUnits() {
});
// add listeners
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("mouseenter", hideDistanceUnitOutput);
document.getElementById("distanceScaleInput").addEventListener("mouseleave", showDistanceUnitOutput);
document.getElementById("areaUnit").addEventListener("change", () => lock("areaUnit"));
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", changeScaleBarSize);
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
document.getElementById("barLabel").addEventListener("input", changeScaleBarLabel);
document.getElementById("barPosX").addEventListener("input", changeScaleBarPosition);
document.getElementById("barPosY").addEventListener("input", changeScaleBarPosition);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
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") {
@ -46,6 +49,7 @@ function editUnits() {
}
document.getElementById("distanceUnitOutput").innerHTML = this.value;
lock("distanceUnit");
drawScaleBar();
calculateFriendlyGridSize();
}
@ -54,13 +58,15 @@ function editUnits() {
const scale = +this.value;
if (!scale || isNaN(scale) || scale < 0) {
tip("Distance scale should be a positive number", false, "error");
this.value = document.getElementById("distanceScale").dataset.value;
this.value = document.getElementById("distanceScaleInput").dataset.value;
return;
}
document.getElementById("distanceScaleSlider").value = scale;
document.getElementById("distanceScale").value = scale;
document.getElementById("distanceScale").dataset.value = scale;
document.getElementById("distanceScaleOutput").value = scale;
document.getElementById("distanceScaleInput").value = scale;
document.getElementById("distanceScaleInput").dataset.value = scale;
lock("distanceScale");
drawScaleBar();
calculateFriendlyGridSize();
}
@ -69,23 +75,54 @@ function editUnits() {
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
function changeHeightUnit() {
if (this.value !== "custom_name") return;
const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft";
if (this.value === "custom_name") {
const custom = prompt("Provide a custom name for height unit");
if (custom) this.options.add(new Option(custom, custom, false, true));
else this.value = "ft";
}
lock("heightUnit");
}
function changeHeightExponent() {
document.getElementById("heightExponent").value = this.value;
document.getElementById("heightExponentSlider").value = this.value;
document.getElementById("heightExponentInput").value = this.value;
document.getElementById("heightExponentOutput").value = this.value;
calculateTemperatures();
if (layerIsOn("toggleTemp")) drawTemp();
lock("heightExponent");
}
function changeTemperatureScale() {
lock("temperatureScale");
if (layerIsOn("toggleTemp")) drawTemp();
}
function changeScaleBarSize() {
document.getElementById("barSize").value = this.value;
document.getElementById("barSizeSlider").value = this.value;
document.getElementById("barSizeOutput").value = this.value;
drawScaleBar();
lock("barSize");
}
function changeScaleBarPosition() {
lock("barPosX");
lock("barPosY");
fitScaleBar();
}
function changeScaleBarLabel() {
lock("barLabel");
drawScaleBar();
}
function changeScaleBarOpacity() {
scaleBar.select("rect").attr("opacity", this.value);
lock("barBackOpacity");
}
function changeScaleBarColor() {
scaleBar.select("rect").attr("fill", this.value);
lock("barBackColor");
}
function changePopulationRate() {
@ -96,9 +133,10 @@ function editUnits() {
return;
}
document.getElementById("populationRateSlider").value = rate;
document.getElementById("populationRateOutput").value = rate;
document.getElementById("populationRate").value = rate;
document.getElementById("populationRate").dataset.value = rate;
lock("populationRate");
}
function changeUrbanizationRate() {
@ -109,15 +147,67 @@ function editUnits() {
return;
}
document.getElementById("urbanizationSlider").value = rate;
document.getElementById("urbanizationOutput").value = rate;
document.getElementById("urbanization").value = rate;
document.getElementById("urbanization").dataset.value = rate;
lock("urbanization");
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById("distanceScaleOutput").value = 3;
document.getElementById("distanceScaleInput").value = 3;
document.getElementById("distanceScaleInput").dataset.value = 3;
unlock("distanceScale");
// units
const US = navigator.language === "en-US";
const UK = navigator.language === "en-GB";
distanceUnitInput.value = distanceUnitOutput.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 = barSize.value = 2;
barLabel.value = "";
barBackOpacity.value = .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();
// population
populationRateOutput.value = populationRate.value = 1000;
urbanizationOutput.value = urbanization.value = 1000;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
}
function addAdditionalRuler() {
if (!layerIsOn("toggleRulers")) toggleRulers();
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
addRuler(graphWidth * .2, y, graphWidth * .8, y);
const x = graphWidth/2, y = graphHeight/2;
const pt = document.getElementById('map').createSVGPoint();
pt.x = x, pt.y = y;
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = rn(graphWidth / 4 / scale), dy = rand(dx / 2, dx * 2) - rand(dx / 2, dx * 2);
addRuler(p.x - dx, p.y + dy, p.x + dx, p.y + dy);
}
function toggleOpisometerMode() {

View file

@ -1,6 +1,21 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: 460,
buttons: {
"Whole World": () => applyPreset(100, 50),
"Northern": () => applyPreset(33, 25),
"Tropical": () => applyPreset(33, 50),
"Southern": () => applyPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button")
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
},
});
const globe = d3.select("#globe");
const clr = d3.scaleSequential(d3.interpolateSpectral);
@ -16,7 +31,6 @@ function editWorld() {
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#restoreWind").on("click", restoreDefaultWinds);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
@ -44,30 +58,26 @@ function editWorld() {
}
function updateGlobePosition() {
const eqY = +document.getElementById("equatorOutput").value;
const equidistance = document.getElementById("equidistanceOutput");
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
equidistance.max = equidistanceInput.max = equidistance.min * 10;
const eqD = +equidistance.value;
const size = +document.getElementById("mapSizeOutput").value;
const eqD = graphHeight / 2 * 100 / size;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScale.value, unit = distanceUnit.value;
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
let kilometers; // value converted to kilometers
if (unit === "km") kilometers = v;
else if (unit === "mi") kilometers = v * 1.60934;
else if (unit === "lg") kilometers = v * 5.556;
else if (unit === "vr") kilometers = v * 1.0668;
else return ""; // do not show as distanceUnit is custom
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
if (unit === "km") return v;
else if (unit === "mi") return v * 1.60934;
else if (unit === "lg") return v * 5.556;
else if (unit === "vr") return v * 1.0668;
return 0; // 0 if distanceUnitInput is a custom unit
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
@ -75,7 +85,7 @@ function editWorld() {
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
@ -113,4 +123,11 @@ function editWorld() {
if (update) updateWorld();
}
function applyPreset(size, lat) {
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
updateWorld();
}
}

364
modules/ui/zones-editor.js Normal file
View file

@ -0,0 +1,364 @@
"use strict";
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
zonesEditorAddLines();
if (modules.editZones) return;
modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor", resizable: false, width: fitContent(), close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
body.addEventListener("click", function(ev) {
const el = ev.target, cl = el.classList, zone = el.parentNode.dataset.id;
if (cl.contains("icon-trash-empty")) {zoneRemove(zone); return;}
if (cl.contains("icon-eye")) {toggleVisibility(el); return;}
if (cl.contains("icon-pin")) {focusOnZone(zone, cl); return;}
if (cl.contains("zoneFill")) {changeFill(el); return;}
if (customization) selectZone(el);
});
body.addEventListener("input", function(ev) {
const el = ev.target, zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#"+zone).attr("data-description", el.value);
});
// add line for each zone
function zonesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
zones.selectAll("g").each(function() {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * (distanceScaleInput.value ** 2);
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate.value;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate.value * urbanization.value;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus"+this.id).size();
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
<svg data-tip="Zone fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${fill}" class="zoneFill"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused?'':' inactive'} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive?' inactive':''} hide ${c.length?'':' placeholder'}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
});
body.innerHTML = lines;
// update footer
const totalArea = zonesFooterArea.dataset.area = graphWidth * graphHeight * (distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization.value) * populationRate.value;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
$("#zonesEditor").dialog();
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", "1px solid red");
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#"+zone).style("outline", null);
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
function movezone(ev, ui) {
const zone = $("#"+ui.item.attr("data-id"));
const prev = $("#"+ui.item.prev().attr("data-id"));
if (prev) {zone.insertAfter(prev); return;}
const next = $("#"+ui.item.next().attr("data-id"));
if (next) zone.insertBefore(next);
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "none");
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "none");
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair")
.on("click", selectZoneOnMapClick)
.call(d3.drag().on("start", dragZoneBrush))
.on("touchmove mousemove", moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function() {this.setAttribute("data-init", this.getAttribute("data-cells"));});
}
function selectZone(el) {
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
}
function dragZoneBrush() {
const r = +zonesBrush.value;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
const selected = body.querySelector("div.selected");
const zone = zones.select("#"+selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const erase = document.getElementById("zonesRemove").classList.contains("pressed");
if (erase) {
// remove
selection.forEach(i => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
cells.splice(index, 1);
});
} else {
// add
selection.forEach(i => {
if (cells.includes(i)) return;
cells.push(i);
zone.append("polygon").attr("points", getPackPolygon(i)).attr("id", base + i);
});
}
zone.attr("data-cells", cells);
});
}
function moveZoneBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +zonesBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function() {
if (this.dataset.cells) return;
// all zone cells are removed
unfocus(this.id);
this.style.display = "block";
});
zonesEditorAddLines();
exitZonesManualAssignment();
}
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function() {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone.selectAll("*").data(cells).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
});
exitZonesManualAssignment();
}
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => el.style.display = "inline-block");
document.getElementById("zonesManuallyButtons").style.display = "none";
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => e.style.pointerEvents = "all");
if(!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function() {this.removeAttribute("data-init");});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function changeFill(el) {
const fill = el.getAttribute("fill");
const callback = function(fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
}
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#"+el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
}
function focusOnZone(zone, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
if (defs.select("#fog #focus"+zone).size()) return;
const dataCells = zones.select("#"+zone).attr("data-cells");
if (!dataCells) return;
const data = dataCells.split(",").map(c => +c);
const g = defs.select("#fog").append("g").attr("fill", "black").attr("stroke", "black").attr("id", "focus"+zone);
g.selectAll("path").data(data).enter().append("path").attr("d", d => "M" + getPackPolygon(d) + "Z");
fogging.attr("display", "block");
} else unfocus(zone);
}
function unfocus(z) {
defs.select("#focus"+z).remove();
if (!defs.selectAll("#fog path").size()) fogging.attr("display", "none"); // all states are de-focused
}
function toggleLegend() {
if (legend.selectAll("*").size()) {clearLegend(); return;}; // hide legend
const data = [];
zones.selectAll("g").each(function() {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
data.push([id, fill, description])
});
drawLegend("Zones", data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalCells = +zonesFooterCells.innerHTML;
const totalArea = +zonesFooterArea.dataset.area;
const totalPopulation = +zonesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function(el) {
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
zonesEditorAddLines();
}
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + id.slice(4)%14 + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<svg data-tip="Zone fill style. Click to change" width="9" height="9" style="margin-bottom:-1px"><rect x="0" y="0" width="9" height="9" fill="${fill}" class="zoneFill"></svg>
<input data-tip="Zone description. Click and type to change" class="religionName" value="${description}" autocorrect="off" spellcheck="false">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">0</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">0 ${unit}</div>
<span class="icon-male hide"></span>
<div class="culturePopulation hide">0</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin inactive hide placeholder"></span>
<span data-tip="Toggle zone visibility" class="icon-eye hide placeholder"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area "+unit+",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function(el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
});
const dataBlob = new Blob([data], {type: "text/plain"});
const url = window.URL.createObjectURL(dataBlob);
const link = document.createElement("a");
document.body.appendChild(link);
link.download = "zones_data" + Date.now() + ".csv";
link.href = url;
link.click();
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
}
function zoneRemove(zone) {
zones.select("#"+zone).remove();
unfocus(zone);
zonesEditorAddLines();
}
}