Merge branch 'Azgaar:master' into mass-burg-assignation

This commit is contained in:
Ángel Montero Lamas 2024-08-20 16:25:33 +02:00 committed by GitHub
commit 9c206a3813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
206 changed files with 34855 additions and 2566 deletions

View file

@ -37,12 +37,22 @@ class Battle {
// add listeners
document.getElementById("battleType").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battleType").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document.getElementById("battleNameShow").addEventListener("click", () => Battle.prototype.context.showNameSection());
document.getElementById("battleNamePlace").addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document
.getElementById("battleType")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changeType(ev));
document
.getElementById("battleNameShow")
.addEventListener("click", () => Battle.prototype.context.showNameSection());
document
.getElementById("battleNamePlace")
.addEventListener("change", ev => (Battle.prototype.context.place = ev.target.value));
document.getElementById("battleNameFull").addEventListener("change", ev => Battle.prototype.context.changeName(ev));
document.getElementById("battleNameCulture").addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document.getElementById("battleNameRandom").addEventListener("click", () => Battle.prototype.context.generateName("random"));
document
.getElementById("battleNameCulture")
.addEventListener("click", () => Battle.prototype.context.generateName("culture"));
document
.getElementById("battleNameRandom")
.addEventListener("click", () => Battle.prototype.context.generateName("random"));
document.getElementById("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
@ -52,11 +62,19 @@ class Battle {
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
document.getElementById("battlePhase_attackers").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_attackers").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document
.getElementById("battlePhase_attackers")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "attackers"));
document.getElementById("battlePhase_defenders").addEventListener("click", ev => this.toggleChange(ev));
document.getElementById("battlePhase_defenders").nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document.getElementById("battleDie_attackers").addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document.getElementById("battleDie_defenders").addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
document
.getElementById("battlePhase_defenders")
.nextElementSibling.addEventListener("click", ev => Battle.prototype.context.changePhase(ev, "defenders"));
document
.getElementById("battleDie_attackers")
.addEventListener("click", () => Battle.prototype.context.rollDie("attackers"));
document
.getElementById("battleDie_defenders")
.addEventListener("click", () => Battle.prototype.context.rollDie("defenders"));
}
defineType() {
@ -82,8 +100,12 @@ class Battle {
document.getElementById("battleType").className = "icon-button-" + this.type;
const sideSpecific = document.getElementById("battlePhases_" + this.type + "_attackers");
const attackers = sideSpecific ? sideSpecific.content : document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific ? document.getElementById("battlePhases_" + this.type + "_defenders").content : attackers;
const attackers = sideSpecific
? sideSpecific.content
: document.getElementById("battlePhases_" + this.type).content;
const defenders = sideSpecific
? document.getElementById("battlePhases_" + this.type + "_defenders").content
: attackers;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
@ -139,26 +161,37 @@ class Battle {
regiment.survivors = Object.assign({}, regiment.u);
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScale) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em; stroke: #333">
<rect x="0" y="0" width="100%" height="100%" fill="${color}"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
const body = `<tbody id="battle${state.i}-${regiment.i}">`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${regiment.name}">${regiment.name.slice(0, 24)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(0, 26)}</td>`;
let initial = `<tr class="battleInitial"><td>${icon}</td><td class="regiment" data-tip="${
regiment.name
}">${regiment.name.slice(0, 24)}</td>`;
let casualties = `<tr class="battleCasualties"><td></td><td data-tip="${state.fullName}">${state.fullName.slice(
0,
26
)}</td>`;
let survivors = `<tr class="battleSurvivors"><td></td><td data-tip="Supply line length, affects morale">Distance to base: ${distance} ${distanceUnitInput.value}</td>`;
for (const u of options.military) {
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.u[u.name] || 0}</td>`;
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${
regiment.u[u.name] || 0
}</td>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.u[u.name] || 0}</td>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.u[u.name] || 0
}</td>`;
}
initial += `<td data-tip="Initial forces" style="width: 2.5em; text-align: center">${regiment.a || 0}</td></tr>`;
casualties += `<td data-tip="Casualties" style="width: 2.5em; text-align: center; color: red">0</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${regiment.a || 0}</td></tr>`;
survivors += `<td data-tip="Survivors" style="width: 2.5em; text-align: center; color: green">${
regiment.a || 0
}</td></tr>`;
const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
@ -173,17 +206,23 @@ class Battle {
.filter(s => s.military && !s.removed)
.map(s => s.military)
.flat();
const distance = reg => rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScaleInput.value) + " " + distanceUnitInput.value;
const isAdded = reg => context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
const distance = reg =>
rn(Math.hypot(context.y - reg.y, context.x - reg.x) * distanceScale) + " " + distanceUnitInput.value;
const isAdded = reg =>
context.defenders.regiments.some(r => r === reg) || context.attackers.regiments.some(r => r === reg);
body.innerHTML = regiments
.map(r => {
const s = pack.states[r.state],
added = isAdded(r),
dist = added ? "0 " + distanceUnitInput.value : distance(r);
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${s.name} data-regiment=${r.name}
return `<div ${added ? "class='inactive'" : ""} data-s=${s.i} data-i=${r.i} data-state=${
s.name
} data-regiment=${r.name}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" ></svg>
<svg width=".9em" height=".9em" style="margin-bottom:-1px; stroke: #333"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" ></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div>
<div style="width:1.2em">${r.icon}</div>
<div style="width:13em">${r.name.slice(0, 24)}</div>
@ -267,7 +306,10 @@ class Battle {
}
generateName(type) {
const place = type === "culture" ? Names.getCulture(pack.cells.culture[this.cell], null, null, "") : Names.getBase(rand(nameBases.length - 1));
const place =
type === "culture"
? Names.getCulture(pack.cells.culture[this.cell], null, null, "")
: Names.getBase(rand(nameBases.length - 1));
document.getElementById("battleNamePlace").value = this.place = place;
document.getElementById("battleNameFull").value = this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
@ -286,35 +328,161 @@ class Battle {
calculateStrength(side) {
const scheme = {
// field battle phases
skirmish: {melee: 0.2, ranged: 2.4, mounted: 0.1, machinery: 3, naval: 1, armored: 0.2, aviation: 1.8, magical: 1.8}, // ranged excel
skirmish: {
melee: 0.2,
ranged: 2.4,
mounted: 0.1,
machinery: 3,
naval: 1,
armored: 0.2,
aviation: 1.8,
magical: 1.8
}, // ranged excel
melee: {melee: 2, ranged: 1.2, mounted: 1.5, machinery: 0.5, naval: 0.2, armored: 2, aviation: 0.8, magical: 0.8}, // melee excel
pursue: {melee: 1, ranged: 1, mounted: 4, machinery: 0.05, naval: 1, armored: 1, aviation: 1.5, magical: 0.6}, // mounted excel
retreat: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.2, armored: 0.1, aviation: 0.8, magical: 0.05}, // reduced
retreat: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.2,
armored: 0.1,
aviation: 0.8,
magical: 0.05
}, // reduced
// naval battle phases
shelling: {melee: 0, ranged: 0.2, mounted: 0, machinery: 2, naval: 2, armored: 0, aviation: 0.1, magical: 0.5}, // naval and machinery excel
boarding: {melee: 1, ranged: 0.5, mounted: 0.5, machinery: 0, naval: 0.5, armored: 0.4, aviation: 0, magical: 0.2}, // melee excel
boarding: {
melee: 1,
ranged: 0.5,
mounted: 0.5,
machinery: 0,
naval: 0.5,
armored: 0.4,
aviation: 0,
magical: 0.2
}, // melee excel
chase: {melee: 0, ranged: 0.15, mounted: 0, machinery: 1, naval: 1, armored: 0, aviation: 0.15, magical: 0.5}, // reduced
withdrawal: {melee: 0, ranged: 0.02, mounted: 0, machinery: 0.5, naval: 0.1, armored: 0, aviation: 0.1, magical: 0.3}, // reduced
withdrawal: {
melee: 0,
ranged: 0.02,
mounted: 0,
machinery: 0.5,
naval: 0.1,
armored: 0,
aviation: 0.1,
magical: 0.3
}, // reduced
// siege phases
blockade: {melee: 0.25, ranged: 0.25, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
sheltering: {melee: 0.3, ranged: 0.5, mounted: 0.2, machinery: 0.5, naval: 0.2, armored: 0.1, aviation: 0.25, magical: 0.25}, // no active actions
blockade: {
melee: 0.25,
ranged: 0.25,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sheltering: {
melee: 0.3,
ranged: 0.5,
mounted: 0.2,
machinery: 0.5,
naval: 0.2,
armored: 0.1,
aviation: 0.25,
magical: 0.25
}, // no active actions
sortie: {melee: 2, ranged: 0.5, mounted: 1.2, machinery: 0.2, naval: 0.1, armored: 0.5, aviation: 1, magical: 1}, // melee excel
bombardment: {melee: 0.2, ranged: 0.5, mounted: 0.2, machinery: 3, naval: 1, armored: 0.5, aviation: 1, magical: 1}, // machinery excel
storming: {melee: 1, ranged: 0.6, mounted: 0.5, machinery: 1, naval: 0.1, armored: 0.1, aviation: 0.5, magical: 0.5}, // melee excel
bombardment: {
melee: 0.2,
ranged: 0.5,
mounted: 0.2,
machinery: 3,
naval: 1,
armored: 0.5,
aviation: 1,
magical: 1
}, // machinery excel
storming: {
melee: 1,
ranged: 0.6,
mounted: 0.5,
machinery: 1,
naval: 0.1,
armored: 0.1,
aviation: 0.5,
magical: 0.5
}, // melee excel
defense: {melee: 2, ranged: 3, mounted: 1, machinery: 1, naval: 0.1, armored: 1, aviation: 0.5, magical: 1}, // ranged excel
looting: {melee: 1.6, ranged: 1.6, mounted: 0.5, machinery: 0.2, naval: 0.02, armored: 0.2, aviation: 0.1, magical: 0.3}, // melee excel
surrendering: {melee: 0.1, ranged: 0.1, mounted: 0.05, machinery: 0.01, naval: 0.01, armored: 0.02, aviation: 0.01, magical: 0.03}, // reduced
looting: {
melee: 1.6,
ranged: 1.6,
mounted: 0.5,
machinery: 0.2,
naval: 0.02,
armored: 0.2,
aviation: 0.1,
magical: 0.3
}, // melee excel
surrendering: {
melee: 0.1,
ranged: 0.1,
mounted: 0.05,
machinery: 0.01,
naval: 0.01,
armored: 0.02,
aviation: 0.01,
magical: 0.03
}, // reduced
// ambush phases
surprise: {melee: 2, ranged: 2.4, mounted: 1, machinery: 1, naval: 1, armored: 1, aviation: 0.8, magical: 1.2}, // increased
shock: {melee: 0.5, ranged: 0.5, mounted: 0.5, machinery: 0.4, naval: 0.3, armored: 0.1, aviation: 0.4, magical: 0.5}, // reduced
shock: {
melee: 0.5,
ranged: 0.5,
mounted: 0.5,
machinery: 0.4,
naval: 0.3,
armored: 0.1,
aviation: 0.4,
magical: 0.5
}, // reduced
// langing phases
landing: {melee: 0.8, ranged: 0.6, mounted: 0.6, machinery: 0.5, naval: 0.5, armored: 0.5, aviation: 0.5, magical: 0.6}, // reduced
flee: {melee: 0.1, ranged: 0.01, mounted: 0.5, machinery: 0.01, naval: 0.5, armored: 0.1, aviation: 0.2, magical: 0.05}, // reduced
waiting: {melee: 0.05, ranged: 0.5, mounted: 0.05, machinery: 0.5, naval: 2, armored: 0.05, aviation: 0.5, magical: 0.5}, // reduced
landing: {
melee: 0.8,
ranged: 0.6,
mounted: 0.6,
machinery: 0.5,
naval: 0.5,
armored: 0.5,
aviation: 0.5,
magical: 0.6
}, // reduced
flee: {
melee: 0.1,
ranged: 0.01,
mounted: 0.5,
machinery: 0.01,
naval: 0.5,
armored: 0.1,
aviation: 0.2,
magical: 0.05
}, // reduced
waiting: {
melee: 0.05,
ranged: 0.5,
mounted: 0.05,
machinery: 0.5,
naval: 2,
armored: 0.05,
aviation: 0.5,
magical: 0.5
}, // reduced
// air battle phases
maneuvering: {melee: 0, ranged: 0.1, mounted: 0, machinery: 0.2, naval: 0, armored: 0, aviation: 1, magical: 0.2}, // aviation
@ -324,7 +492,8 @@ class Battle {
const forces = this.getJoinedForces(this[side].regiments);
const phase = this[side].phase;
const adjuster = Math.max(populationRate / 10, 10); // population adjuster, by default 100
this[side].power = d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
this[side].power =
d3.sum(options.military.map(u => (forces[u.name] || 0) * u.power * scheme[phase][u.type])) / adjuster;
const UIvalue = this[side].power ? Math.max(this[side].power | 0, 1) : 0;
document.getElementById("battlePower_" + side).innerHTML = UIvalue;
}
@ -723,11 +892,13 @@ class Battle {
const status = battleStatus[+P(0.7)];
const result = `The ${this.getTypeName(this.type)} ended in ${status}`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(this.attackers.regiments, 1)} and ${getSide(
this.defenders.regiments,
0
)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(this.defenders.casualties)}%`;
const legend = `${this.name} took place in ${options.year} ${options.eraShort}. It was fought between ${getSide(
this.attackers.regiments,
1
)} and ${getSide(this.defenders.regiments, 0)}. ${result}.
\r\nAttackers losses: ${getLosses(this.attackers.casualties)}%, defenders losses: ${getLosses(
this.defenders.casualties
)}%`;
notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000);

View file

@ -390,7 +390,7 @@ function editBiomes() {
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 found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection);
});

View file

@ -21,38 +21,37 @@ function editBurg(id) {
modules.editBurg = true;
// add listeners
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
byId("burgGroupShow").addEventListener("click", showGroupSection);
byId("burgGroupHide").addEventListener("click", hideGroupSection);
byId("burgSelectGroup").addEventListener("change", changeGroup);
byId("burgInputGroup").addEventListener("change", createNewGroup);
byId("burgAddGroup").addEventListener("click", toggleNewGroupInput);
byId("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
document.getElementById("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgType").addEventListener("input", changeType);
document.getElementById("burgCulture").addEventListener("input", changeCulture);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
byId("burgName").addEventListener("input", changeName);
byId("burgNameReRandom").addEventListener("click", generateNameRandom);
byId("burgType").addEventListener("input", changeType);
byId("burgCulture").addEventListener("input", changeCulture);
byId("burgNameReCulture").addEventListener("click", generateNameCulture);
byId("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("addCustomMFCGBurgLink").addEventListener("click", addCustomMfcgLink);
byId("burgLinkOpen").addEventListener("click", openBurgLink);
byId("burgLinkEdit").addEventListener("click", changeBurgLink);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
byId("burgStyleShow").addEventListener("click", showStyleSection);
byId("burgStyleHide").addEventListener("click", hideStyleSection);
byId("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
byId("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
byId("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
document.getElementById("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
byId("burgEmblem").addEventListener("click", openEmblemEdit);
byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
byId("burglLegend").addEventListener("click", editBurgLegend);
byId("burgLock").addEventListener("click", toggleBurgLockButton);
byId("burgRemove").addEventListener("click", removeSelectedBurg);
byId("burgTemperatureGraph").addEventListener("click", showTemperatureGraph);
function updateBurgValues() {
const id = +elSelected.attr("data-id");
@ -60,46 +59,46 @@ function editBurg(id) {
const province = pack.cells.province[b.cell];
const provinceName = province ? pack.provinces[province].fullName + ", " : "";
const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
document.getElementById("burgProvinceAndState").innerHTML = provinceName + stateName;
byId("burgProvinceAndState").innerHTML = provinceName + stateName;
document.getElementById("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
byId("burgName").value = b.name;
byId("burgType").value = b.type || "Generic";
byId("burgPopulation").value = rn(b.population * populationRate * urbanization);
byId("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
// update list and select culture
const cultureSelect = document.getElementById("burgCulture");
const cultureSelect = byId("burgCulture");
cultureSelect.options.length = 0;
const cultures = pack.cultures.filter(c => !c.removed);
cultures.forEach(c => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
document.getElementById("burgTemperature").innerHTML = convertTemperature(temperature);
document.getElementById("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
document.getElementById("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
byId("burgTemperature").innerHTML = convertTemperature(temperature);
byId("burgTemperatureLikeIn").innerHTML = getTemperatureLikeness(temperature);
byId("burgElevation").innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features
if (b.capital) document.getElementById("burgCapital").classList.remove("inactive");
else document.getElementById("burgCapital").classList.add("inactive");
if (b.port) document.getElementById("burgPort").classList.remove("inactive");
else document.getElementById("burgPort").classList.add("inactive");
if (b.citadel) document.getElementById("burgCitadel").classList.remove("inactive");
else document.getElementById("burgCitadel").classList.add("inactive");
if (b.walls) document.getElementById("burgWalls").classList.remove("inactive");
else document.getElementById("burgWalls").classList.add("inactive");
if (b.plaza) document.getElementById("burgPlaza").classList.remove("inactive");
else document.getElementById("burgPlaza").classList.add("inactive");
if (b.temple) document.getElementById("burgTemple").classList.remove("inactive");
else document.getElementById("burgTemple").classList.add("inactive");
if (b.shanty) document.getElementById("burgShanty").classList.remove("inactive");
else document.getElementById("burgShanty").classList.add("inactive");
if (b.capital) byId("burgCapital").classList.remove("inactive");
else byId("burgCapital").classList.add("inactive");
if (b.port) byId("burgPort").classList.remove("inactive");
else byId("burgPort").classList.add("inactive");
if (b.citadel) byId("burgCitadel").classList.remove("inactive");
else byId("burgCitadel").classList.add("inactive");
if (b.walls) byId("burgWalls").classList.remove("inactive");
else byId("burgWalls").classList.add("inactive");
if (b.plaza) byId("burgPlaza").classList.remove("inactive");
else byId("burgPlaza").classList.add("inactive");
if (b.temple) byId("burgTemple").classList.remove("inactive");
else byId("burgTemple").classList.add("inactive");
if (b.shanty) byId("burgShanty").classList.remove("inactive");
else byId("burgShanty").classList.add("inactive");
//toggle lock
updateBurgLockIcon();
// select group
const group = elSelected.node().parentNode.id;
const select = document.getElementById("burgSelectGroup");
const select = byId("burgSelectGroup");
select.options.length = 0; // remove all options
burgLabels.selectAll("g").each(function () {
@ -109,68 +108,16 @@ function editBurg(id) {
// set emlem image
const coaID = "burgCOA" + id;
COArenderer.trigger(coaID, b.coa);
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
byId("burgEmblem").setAttribute("href", "#" + coaID);
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
if (b.link) {
document.getElementById("mfcgBurgSeedSection").style.display = "none";
} else {
document.getElementById("mfcgBurgSeedSection").style.display = "inline-block";
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
}
if (options.showBurgPreview) {
byId("burgPreviewSection").style.display = "block";
updateBurgPreview(b);
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
byId("burgPreviewSection").style.display = "none";
}
}
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
function getTemperatureLikeness(temperature) {
if (temperature < -5) return "Yakutsk";
const cities = [
"Snag (Yukon)",
"Yellowknife (Canada)",
"Okhotsk (Russia)",
"Fairbanks (Alaska)",
"Nuuk (Greenland)",
"Murmansk", // -5 - 0
"Arkhangelsk",
"Anchorage",
"Tromsø",
"Reykjavik",
"Riga",
"Stockholm",
"Halifax",
"Prague",
"Copenhagen",
"London", // 1 - 10
"Antwerp",
"Paris",
"Milan",
"Batumi",
"Rome",
"Dubrovnik",
"Lisbon",
"Barcelona",
"Marrakesh",
"Alexandria", // 11 - 20
"Tegucigalpa",
"Guangzhou",
"Rio de Janeiro",
"Dakar",
"Miami",
"Jakarta",
"Mogadishu",
"Bangkok",
"Aden",
"Khartoum"
]; // 21 - 30
if (temperature > 30) return "Mecca";
return cities[temperature + 5] || null;
}
function dragBurgLabel() {
const tr = parseTransform(this.getAttribute("transform"));
const dx = +tr[0] - d3.event.x,
@ -186,15 +133,15 @@ function editBurg(id) {
function showGroupSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("burgGroupSection").style.display = "inline-block";
byId("burgGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("burgGroupSection").style.display = "none";
document.getElementById("burgInputGroup").style.display = "none";
document.getElementById("burgInputGroup").value = "";
document.getElementById("burgSelectGroup").style.display = "inline-block";
byId("burgGroupSection").style.display = "none";
byId("burgInputGroup").style.display = "none";
byId("burgInputGroup").value = "";
byId("burgSelectGroup").style.display = "inline-block";
}
function changeGroup() {
@ -223,7 +170,7 @@ function editBurg(id) {
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
@ -251,10 +198,10 @@ function editBurg(id) {
// just rename if only 1 element left
const count = elSelected.node().parentNode.childElementCount;
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
document.getElementById("burgSelectGroup").selectedOptions[0].remove();
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
byId("burgSelectGroup").selectedOptions[0].remove();
byId("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
byId("burgInputGroup").value = "";
labelG.id = group;
iconG.id = group;
if (anchor) anchorG.id = group;
@ -262,9 +209,9 @@ function editBurg(id) {
}
// create new groups
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
byId("burgSelectGroup").options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById("burgInputGroup").value = "";
byId("burgInputGroup").value = "";
addBurgsGroup(group);
moveBurgToGroup(id, group);
@ -284,7 +231,9 @@ function editBurg(id) {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}?
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${burgsToRemove.length}`;
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}`;
$("#alert").dialog({
resizable: false,
title: "Remove burg group",
@ -343,7 +292,10 @@ function editBurg(id) {
function changePopulation() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
updateBurgPreview(burg);
}
function toggleFeature() {
@ -357,9 +309,9 @@ function editBurg(id) {
if (burg[feature]) this.classList.remove("inactive");
else if (!burg[feature]) this.classList.add("inactive");
if (burg.port) document.getElementById("burgEditAnchorStyle").style.display = "inline-block";
else document.getElementById("burgEditAnchorStyle").style.display = "none";
updateMFCGFrame(burg);
if (burg.port) byId("burgEditAnchorStyle").style.display = "inline-block";
else byId("burgEditAnchorStyle").style.display = "none";
updateBurgPreview(burg);
}
function toggleBurgLockButton() {
@ -374,22 +326,22 @@ function editBurg(id) {
const id = +elSelected.attr("data-id");
const b = pack.burgs[id];
if (b.lock) {
document.getElementById("burgLock").classList.remove("icon-lock-open");
document.getElementById("burgLock").classList.add("icon-lock");
byId("burgLock").classList.remove("icon-lock-open");
byId("burgLock").classList.add("icon-lock");
} else {
document.getElementById("burgLock").classList.remove("icon-lock");
document.getElementById("burgLock").classList.add("icon-lock-open");
byId("burgLock").classList.remove("icon-lock");
byId("burgLock").classList.add("icon-lock-open");
}
}
function showStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("burgStyleSection").style.display = "inline-block";
byId("burgStyleSection").style.display = "inline-block";
}
function hideStyleSection() {
document.querySelectorAll("#burgBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("burgStyleSection").style.display = "none";
byId("burgStyleSection").style.display = "none";
}
function editGroupLabelStyle() {
@ -407,38 +359,38 @@ function editBurg(id) {
editStyle("anchors", g);
}
function updateMFCGFrame(burg) {
const mfcgURL = getMFCGlink(burg);
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL + "&preview=1");
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
function updateBurgPreview(burg) {
const src = getBurgLink(burg) + "&preview=1";
// recreate object to force reload (Chrome bug)
const container = byId("burgPreviewObject");
container.innerHTML = "";
const object = document.createElement("object");
object.style.width = "100%";
object.data = src;
container.insertBefore(object, null);
}
function changeSeed() {
function openBurgLink() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
openURL(getBurgLink(burg));
}
function randomizeSeed() {
function changeBurgLink() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function addCustomMfcgLink() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const message = "Enter custom link to the burg map. It can be a link to Medieval Fantasy City Generator or other tool. Keep empty to use MFCG seed";
prompt(message, {default: burg.link || "", required: false}, link => {
if (link) burg.link = link;
else delete burg.link;
updateMFCGFrame(burg);
});
prompt(
"Provide custom link to the burg map. It can be a link to Medieval Fantasy City Generator, a different tool, or just an image. Leave empty to use the default map",
{default: getBurgLink(burg), required: false},
link => {
if (link) burg.link = link;
else delete burg.link;
updateBurgPreview(burg);
}
);
}
function openEmblemEdit() {
@ -447,16 +399,16 @@ function editBurg(id) {
editEmblem("burg", "burgCOA" + id, burg);
}
function toggleMFCGMap() {
options.showMFCGMap = !options.showMFCGMap;
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
function toggleBurgPreview() {
options.showBurgPreview = !options.showBurgPreview;
byId("burgPreviewSection").style.display = options.showBurgPreview ? "block" : "none";
byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o";
}
function toggleRelocateBurg() {
const toggler = document.getElementById("toggleCells");
document.getElementById("burgRelocate").classList.toggle("pressed");
if (document.getElementById("burgRelocate").classList.contains("pressed")) {
const toggler = byId("toggleCells");
byId("burgRelocate").classList.toggle("pressed");
if (byId("burgRelocate").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
tip("Click on map to relocate burg. Hold Shift for continuous move", true);
if (!layerIsOn("toggleCells")) {
@ -576,8 +528,53 @@ function editBurg(id) {
}
function closeBurgEditor() {
document.getElementById("burgRelocate").classList.remove("pressed");
byId("burgRelocate").classList.remove("pressed");
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
unselect();
}
}
// in °C, array from -1 °C; source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
function getTemperatureLikeness(temperature) {
if (temperature < -5) return "Yakutsk";
const cities = [
"Snag (Yukon)",
"Yellowknife (Canada)",
"Okhotsk (Russia)",
"Fairbanks (Alaska)",
"Nuuk (Greenland)",
"Murmansk", // -5 - 0
"Arkhangelsk",
"Anchorage",
"Tromsø",
"Reykjavik",
"Riga",
"Stockholm",
"Halifax",
"Prague",
"Copenhagen",
"London", // 1 - 10
"Antwerp",
"Paris",
"Milan",
"Batumi",
"Rome",
"Dubrovnik",
"Lisbon",
"Barcelona",
"Marrakesh",
"Alexandria", // 11 - 20
"Tegucigalpa",
"Guangzhou",
"Rio de Janeiro",
"Dakar",
"Miami",
"Jakarta",
"Mogadishu",
"Bangkok",
"Aden",
"Khartoum"
]; // 21 - 30
if (temperature > 30) return "Mecca";
return cities[temperature + 5] || null;
}

View file

@ -1,5 +1,5 @@
"use strict";
function overviewBurgs(options = {stateId: null, cultureId: null}) {
function overviewBurgs(settings = {stateId: null, cultureId: null}) {
if (customization) return;
closeDialogs("#burgsOverview, .stable");
if (!layerIsOn("toggleIcons")) toggleIcons();
@ -37,7 +37,6 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
byId("burgGroupAssign").addEventListener("click", burgGroupAssignInBulk);
byId("burgsLockAll").addEventListener("click", toggleLockAll);
byId("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
byId("burgsInvertLock").addEventListener("click", invertLock);
function refreshBurgsEditor() {
updateFilter();
@ -46,7 +45,7 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
function updateFilter() {
const stateFilter = byId("burgsFilterState");
const selectedState = options.stateId !== null ? options.stateId : stateFilter.value || -1;
const selectedState = settings.stateId !== null ? settings.stateId : stateFilter.value || -1;
stateFilter.options.length = 0; // remove all options
stateFilter.options.add(new Option("all", -1, false, selectedState === -1));
stateFilter.options.add(new Option(pack.states[0].name, 0, false, selectedState === 0));
@ -54,7 +53,7 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
statesSorted.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const cultureFilter = byId("burgsFilterCulture");
const selectedCulture = options.cultureId !== null ? options.cultureId : cultureFilter.value || -1;
const selectedCulture = settings.cultureId !== null ? settings.cultureId : cultureFilter.value || -1;
cultureFilter.options.length = 0; // remove all options
cultureFilter.options.add(new Option(`all`, -1, false, selectedCulture === -1));
cultureFilter.options.add(new Option(pack.cultures[0].name, 0, false, selectedCulture === 0));
@ -280,7 +279,8 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const cell = findCell(...point);
if (pack.cells.h[cell] < 20)
return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell])
@ -481,10 +481,7 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
}
function downloadBurgsData() {
let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,X,Y,Latitude,Longitude,Elevation (${heightUnit.value}),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town`; // headers
if (options.showMFCGMap) data += `,City Generator Link`;
data += "\n";
let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,X,Y,Latitude,Longitude,Elevation (${heightUnit.value}),Temperature,Temperature likeness,Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town,Emblem,City Generator Link\n`; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
valid.forEach(b => {
@ -505,6 +502,9 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
data += getLatitude(b.y, 2) + ",";
data += getLongitude(b.x, 2) + ",";
data += parseInt(getHeight(pack.cells.h[b.cell])) + ",";
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
data += convertTemperature(temperature) + ",";
data += getTemperatureLikeness(temperature) + ",";
// add status data
data += b.capital ? "capital," : ",";
@ -514,7 +514,9 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
data += b.plaza ? "plaza," : ",";
data += b.temple ? "temple," : ",";
data += b.shanty ? "shanty town," : ",";
if (options.showMFCGMap) data += getMFCGlink(b);
data += b.coa ? JSON.stringify(b.coa).replace(/"/g, "").replace(/,/g, ";") + "," : ",";
data += getBurgLink(b);
data += "\n";
});
@ -628,11 +630,6 @@ function overviewBurgs(options = {stateId: null, cultureId: null}) {
burgsOverviewAddLines();
}
function invertLock() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
burgsOverviewAddLines();
}
function toggleLockAll() {
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
const allLocked = activeBurgs.every(burg => burg.lock);

View file

@ -22,7 +22,7 @@ function clicked() {
if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute();
else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
@ -132,27 +132,43 @@ function applySorting(headers) {
}
function addBurg(point) {
const cells = pack.cells;
const x = rn(point[0], 2),
y = rn(point[1], 2);
const cell = findCell(x, point[1]);
const i = pack.burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
const state = cells.state[cell];
const feature = cells.f[cell];
const {cells, states} = pack;
const x = rn(point[0], 2);
const y = rn(point[1], 2);
const temple = pack.states[state].form === "Theocracy";
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cell, false);
const cellId = findCell(x, y);
const i = pack.burgs.length;
const culture = cells.culture[cellId];
const name = Names.getCulture(culture);
const state = cells.state[cellId];
const feature = cells.f[cellId];
const population = Math.max(cells.s[cellId] / 3 + i / 1000 + (cellId % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cellId, false);
// generate emblem
const coa = COA.generate(pack.states[state].coa, 0.25, null, type);
const coa = COA.generate(states[state].coa, 0.25, null, type);
coa.shield = COA.getShield(culture, state);
COArenderer.add("burg", i, coa, x, y);
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population, coa, type});
cells.burg[cell] = i;
const burg = {
name,
cell: cellId,
x,
y,
state,
i,
culture,
feature,
capital: 0,
port: 0,
temple: 0,
population,
coa,
type
};
pack.burgs.push(burg);
cells.burg[cellId] = i;
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgIcons
@ -173,7 +189,17 @@ function addBurg(point) {
.attr("dy", `${townSize * -1.5}px`)
.text(name);
BurgsAndStates.defineBurgFeatures(pack.burgs[i]);
BurgsAndStates.defineBurgFeatures(burg);
const newRoute = Routes.connect(cellId);
if (newRoute && layerIsOn("toggleRoutes")) {
routes
.select("#" + newRoute.group)
.append("path")
.attr("d", Routes.getPath(newRoute))
.attr("id", "route" + newRoute.i);
}
return i;
}
@ -223,18 +249,19 @@ function addBurgsGroup(group) {
}
function removeBurg(id) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (label) label.remove();
if (icon) icon.remove();
if (anchor) anchor.remove();
document.querySelector("#burgLabels [data-id='" + id + "']")?.remove();
document.querySelector("#burgIcons [data-id='" + id + "']")?.remove();
document.querySelector("#anchors [data-id='" + id + "']")?.remove();
const cells = pack.cells;
const burg = pack.burgs[id];
const cells = pack.cells,
burg = pack.burgs[id];
burg.removed = true;
cells.burg[burg.cell] = 0;
const noteId = notes.findIndex(note => note.id === `burg${id}`);
if (noteId !== -1) notes.splice(noteId, 1);
if (burg.coa) {
const coaId = "burgCOA" + id;
if (byId(coaId)) byId(coaId).remove();
@ -243,25 +270,22 @@ function removeBurg(id) {
}
}
function toggleCapital(burg) {
const state = pack.burgs[burg].state;
if (!state) {
tip("Neutral lands cannot have a capital", false, "error");
return;
}
if (pack.burgs[burg].capital) {
tip("To change capital please assign a capital status to another burg of this state", false, "error");
return;
}
const old = pack.states[state].capital;
function toggleCapital(burgId) {
const {burgs, states} = pack;
if (burgs[burgId].capital)
return tip("To change capital please assign a capital status to another burg of this state", false, "error");
// change statuses
pack.states[state].capital = burg;
pack.states[state].center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1;
pack.burgs[old].capital = 0;
moveBurgToGroup(burg, "cities");
moveBurgToGroup(old, "towns");
const stateId = burgs[burgId].state;
if (!stateId) return tip("Neutral lands cannot have a capital", false, "error");
const prevCapitalId = states[stateId].capital;
states[stateId].capital = burgId;
states[stateId].center = burgs[burgId].cell;
burgs[burgId].capital = 1;
burgs[prevCapitalId].capital = 0;
moveBurgToGroup(burgId, "cities");
moveBurgToGroup(prevCapitalId, "towns");
}
function togglePort(burg) {
@ -291,16 +315,20 @@ function togglePort(burg) {
.attr("height", size);
}
function getBurgSeed(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function getMFCGlink(burg) {
function getBurgLink(burg) {
if (burg.link) return burg.link;
const population = burg.population * populationRate * urbanization;
if (population >= options.villageMaxPopulation || burg.citadel || burg.walls || burg.temple || burg.shanty)
return createMfcgLink(burg);
return createVillageGeneratorLink(burg);
}
function createMfcgLink(burg) {
const {cells} = pack;
const {i, name, population: burgPopulation, cell} = burg;
const seed = getBurgSeed(burg);
const burgSeed = burg.MFCG || seed + String(burg.i).padStart(4, 0);
const sizeRaw = 2.13 * Math.pow((burgPopulation * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
@ -308,35 +336,35 @@ function getMFCGlink(burg) {
const river = cells.r[cell] ? 1 : 0;
const coast = Number(burg.port > 0);
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : null;
const sea = (() => {
if (!coast || !cells.haven[cell]) return null;
// calculate see direction: 0 = south, 0.5 = west, 1 = north, 1.5 = east
const p1 = cells.p[cell];
const p2 = cells.p[cells.haven[cell]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
return rn(normalize(deg, 0, 360) * 2, 2);
})();
const biome = cells.biome[cell];
const arableBiomes = river ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
const farms = +arableBiomes.includes(biome);
const farms = +arableBiomes.includes(cells.biome[cell]);
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
const hub = +cells.road[cell] > 50;
const hub = Routes.isCrossroad(cell);
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shantytown = +burg.shanty;
function getSeaDirections(i) {
const p1 = cells.p[i];
const p2 = cells.p[cells.haven[i]];
let deg = (Math.atan2(p2[1] - p1[1], p2[0] - p1[0]) * 180) / Math.PI - 90;
if (deg < 0) deg += 360;
return rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
}
const parameters = {
const url = new URL("https://watabou.github.io/city-generator/");
url.search = new URLSearchParams({
name,
population,
size,
seed,
seed: burgSeed,
river,
coast,
farms,
@ -348,14 +376,62 @@ function getMFCGlink(burg) {
walls,
shantytown,
gates: -1
};
const url = new URL("https://watabou.github.io/city-generator/");
url.search = new URLSearchParams(parameters);
});
if (sea) url.searchParams.append("sea", sea);
return url.toString();
}
function createVillageGeneratorLink(burg) {
const {cells, features} = pack;
const {i, population, cell} = burg;
const pop = rn(population * populationRate * urbanization);
const burgSeed = seed + String(i).padStart(4, 0);
const tags = [];
if (cells.r[cell] && cells.haven[cell]) tags.push("estuary");
else if (cells.haven[cell] && features[cells.f[cell]].cells === 1) tags.push("island,district");
else if (burg.port) tags.push("coast");
else if (cells.conf[cell]) tags.push("confluence");
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const connections = pack.cells.routes[cell] || {};
const roads = Object.values(connections).filter(routeId => {
const route = pack.routes[routeId];
return route.group === "roads" || route.group === "trails";
}).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
if (!arableBiomes.includes(biome)) tags.push("uncultivated");
else if (each(6)(cell)) tags.push("farmland");
const temp = grid.cells.temp[cells.g[cell]];
if (temp <= 0 || temp > 28 || (temp > 25 && each(3)(cell))) tags.push("no orchards");
if (!burg.plaza) tags.push("no square");
if (pop < 100) tags.push("sparse");
else if (pop > 300) tags.push("dense");
const width = (() => {
if (pop > 1500) return 1600;
if (pop > 1000) return 1400;
if (pop > 500) return 1000;
if (pop > 200) return 800;
if (pop > 100) return 600;
return 400;
})();
const height = rn(width / 2.2);
const url = new URL("https://watabou.github.io/village-generator/");
url.search = new URLSearchParams({pop, name: "", seed: burgSeed, width, height, tags});
return url.toString();
}
// draw legend box
function drawLegend(name, data) {
legend.selectAll("*").remove(); // fully redraw every time
@ -1096,12 +1172,12 @@ function selectIcon(initial, callback) {
input.oninput = e => callback(input.value);
table.onclick = e => {
if (e.target.tagName === "TD") {
input.value = e.target.innerHTML;
input.value = e.target.textContent;
callback(input.value);
}
};
table.onmouseover = e => {
if (e.target.tagName === "TD") tip(`Click to select ${e.target.innerHTML} icon`);
if (e.target.tagName === "TD") tip(`Click to select ${e.target.textContent} icon`);
};
$("#iconSelector").dialog({
@ -1125,7 +1201,6 @@ function getAreaUnit(squareMark = "²") {
}
function getArea(rawArea) {
const distanceScale = byId("distanceScaleInput")?.value;
return rawArea * distanceScale ** 2;
}
@ -1176,18 +1251,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.93.10");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.00");
Editor.open();
}
async function editCultures() {
if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.95.04");
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.96.01");
Editor.open();
}
async function editReligions() {
if (customization) return;
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.89.10");
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.96.00");
Editor.open();
}

View file

@ -1,43 +1,14 @@
"use strict";
function showEPForRoute(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const routeLen = node.getTotalLength() * distanceScaleInput.value;
showElevationProfile(points, routeLen, false);
}
function showEPForRiver(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
points.push(i);
});
const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
showElevationProfile(points, riverLen, true);
}
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
function showElevationProfile(data, routeLen, isRiver) {
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
document.getElementById("epScaleRange").addEventListener("change", draw);
document.getElementById("epCurve").addEventListener("change", draw);
document.getElementById("epSave").addEventListener("click", downloadCSV);
byId("epScaleRange").on("change", draw);
byId("epCurve").on("change", draw);
byId("epSave").on("click", downloadCSV);
$("#elevationProfile").dialog({
title: "Elevation profile",
resizable: false,
width: window.width,
close: closeElevationProfile,
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
});
@ -45,18 +16,20 @@ function showElevationProfile(data, routeLen, isRiver) {
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
let slope = 0;
if (isRiver) {
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length - 1]]) {
const firstCellHeight = pack.cells.h[data.at(0)];
const lastCellHeight = pack.cells.h[data.at(-1)];
if (firstCellHeight < lastCellHeight) {
slope = 1; // up-hill
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length - 1]]) {
} else if (firstCellHeight > lastCellHeight) {
slope = -1; // down-hill
}
}
const chartWidth = window.innerWidth - 180,
chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
const xOffset = 80,
yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
const biomesHeight = 40;
const chartWidth = window.innerWidth - 200;
const chartHeight = 300;
const xOffset = 80;
const yOffset = 80;
const biomesHeight = 10;
let lastBurgIndex = 0;
let lastBurgCell = 0;
@ -109,8 +82,8 @@ function showElevationProfile(data, routeLen, isRiver) {
draw();
function downloadCSV() {
let data =
"Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
let csv =
"Id,x,y,lat,lon,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n"; // headers
for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k];
@ -123,35 +96,39 @@ function showElevationProfile(data, routeLen, isRiver) {
let pop = pack.cells.pop[cell];
let h = pack.cells.h[cell];
data += k + 1 + ",";
data += chartData.points[k][0] + ",";
data += chartData.points[k][1] + ",";
data += cell + ",";
data += getHeight(h) + ",";
data += h + ",";
data += rn(pop * populationRate) + ",";
csv += k + 1 + ",";
const [x, y] = pack.cells.p[data[k]];
csv += x + ",";
csv += y + ",";
const lat = getLatitude(y, 2);
const lon = getLongitude(x, 2);
csv += lat + ",";
csv += lon + ",";
csv += cell + ",";
csv += getHeight(h) + ",";
csv += h + ",";
csv += rn(pop * populationRate) + ",";
if (burg) {
data += pack.burgs[burg].name + ",";
data += pack.burgs[burg].population * populationRate * urbanization + ",";
csv += pack.burgs[burg].name + ",";
csv += pack.burgs[burg].population * populationRate * urbanization + ",";
} else {
data += ",0,";
csv += ",0,";
}
data += biomesData.name[biome] + ",";
data += biomesData.color[biome] + ",";
data += pack.cultures[culture].name + ",";
data += pack.cultures[culture].color + ",";
data += pack.religions[religion].name + ",";
data += pack.religions[religion].color + ",";
data += pack.provinces[province].name + ",";
data += pack.provinces[province].color + ",";
data += pack.states[state].name + ",";
data += pack.states[state].color + ",";
data = data + "\n";
csv += biomesData.name[biome] + ",";
csv += biomesData.color[biome] + ",";
csv += pack.cultures[culture].name + ",";
csv += pack.cultures[culture].color + ",";
csv += pack.religions[religion].name + ",";
csv += pack.religions[religion].color + ",";
csv += pack.provinces[province].name + ",";
csv += pack.provinces[province].color + ",";
csv += pack.states[state].name + ",";
csv += pack.states[state].color + ",";
csv += "\n";
}
const name = getFileName("elevation profile") + ".csv";
downloadFile(data, name);
downloadFile(csv, name);
}
function draw() {
@ -170,7 +147,7 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
}
document.getElementById("elevationGraph").innerHTML = "";
byId("elevationGraph").innerHTML = "";
const chart = d3
.select("#elevationGraph")
@ -193,8 +170,15 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("d", "M0,0 V4 L2,2 Z")
.attr("fill", "darkgray");
let colors = getColorScheme(terrs.attr("scheme"));
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
const colors = getColorScheme("natural");
const landdef = chart
.select("defs")
.append("linearGradient")
.attr("id", "landdef")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
if (chartData.mah == chartData.mih) {
landdef
@ -247,7 +231,14 @@ function showElevationProfile(data, routeLen, isRiver) {
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += " L" + parseInt(xscale(0) + +xOffset) + "," + parseInt(yscale(0) + +yOffset);
path += "Z";
chart.append("g").attr("id", "epland").append("path").attr("d", path).attr("stroke", "purple").attr("stroke-width", "0").attr("fill", "url(#landdef)");
chart
.append("g")
.attr("id", "epland")
.append("path")
.attr("d", path)
.attr("stroke", "purple")
.attr("stroke-width", "0")
.attr("fill", "url(#landdef)");
// biome / heights
let g = chart.append("g").attr("id", "epbiomes");
@ -289,7 +280,14 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.cell[k] +
")";
g.append("rect").attr("stroke", c).attr("fill", c).attr("x", x).attr("y", y).attr("width", xscale(1)).attr("height", 15).attr("data-tip", dataTip);
g.append("rect")
.attr("stroke", c)
.attr("fill", c)
.attr("x", x)
.attr("y", y)
.attr("width", xscale(1))
.attr("height", biomesHeight)
.attr("data-tip", dataTip);
}
const xAxis = d3
@ -314,10 +312,7 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "center")
.attr("transform", function (d) {
return "rotate(0)"; // used to rotate labels, - anti-clockwise, + clockwise
});
.style("text-anchor", "center");
chart
.append("g")
@ -366,12 +361,22 @@ function showElevationProfile(data, routeLen, isRiver) {
.attr("x", x1)
.attr("y", y1)
.attr("text-anchor", "middle");
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
byId("ep" + b).innerHTML = pack.burgs[b].name;
// arrow from burg name to graph line
g.append("path")
.attr("id", "eparrow" + b)
.attr("d", "M" + x1.toString() + "," + (y1 + 3).toString() + "L" + x1.toString() + "," + parseInt(chartData.points[k][1] - 3).toString())
.attr(
"d",
"M" +
x1.toString() +
"," +
(y1 + 3).toString() +
"L" +
x1.toString() +
"," +
parseInt(chartData.points[k][1] - 3).toString()
)
.attr("stroke", "darkgray")
.attr("fill", "lightgray")
.attr("stroke-width", "1")
@ -381,10 +386,10 @@ function showElevationProfile(data, routeLen, isRiver) {
}
function closeElevationProfile() {
document.getElementById("epScaleRange").removeEventListener("change", draw);
document.getElementById("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = "";
byId("epScaleRange").removeEventListener("change", draw);
byId("epCurve").removeEventListener("change", draw);
byId("epSave").removeEventListener("click", downloadCSV);
byId("elevationGraph").innerHTML = "";
modules.elevation = false;
}
}

View file

@ -67,9 +67,9 @@ function showDataTip(event) {
function showElementLockTip(event) {
const locked = event?.target?.classList?.contains("icon-lock");
if (locked) {
tip("Click to unlock the element and allow it to be changed by regeneration tools");
tip("Locked. Click to unlock the element and allow it to be changed by regeneration tools");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
tip("Unlocked. Click to lock the element and prevent changes to it by regeneration tools");
}
}
@ -151,7 +151,12 @@ function showMapTooltip(point, e, i, g) {
return;
}
if (group === "routes") return tip("Click to edit the Route");
if (group === "routes") {
const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name;
if (name) return tip(`${name}. Click to edit the Route`);
return tip("Click to edit the Route");
}
if (group === "terrain") return tip("Click to edit the Relief Icon");
@ -285,7 +290,7 @@ function toDMS(coord, c) {
const minutes = Math.floor(minutesNotTruncated);
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
const cardinal = c === "lat" ? (coord >= 0 ? "N" : "S") : coord >= 0 ? "E" : "W";
return degrees + " + minutes + " " + seconds + "″ " + cardinal;
return degrees + " + minutes + "" + seconds + "″" + cardinal;
}
// get surface elevation

View file

@ -246,6 +246,7 @@ function editHeightmap(options) {
Cultures.expand();
BurgsAndStates.generate();
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
@ -281,8 +282,7 @@ function editHeightmap(options) {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const pop = new Uint16Array(l);
const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const routes = {};
const s = new Uint16Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
@ -300,8 +300,7 @@ function editHeightmap(options) {
biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i];
road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
routes[g] = pack.cells.routes[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
@ -353,8 +352,7 @@ function editHeightmap(options) {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.routes = {};
pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
@ -389,8 +387,7 @@ function editHeightmap(options) {
if (!isLand) continue;
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.routes[i] = routes[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];

View file

@ -50,6 +50,7 @@ function handleKeyup(event) {
else if (shift && code === "KeyO") editNotes();
else if (shift && code === "KeyA") overviewCharts();
else if (shift && code === "KeyT") overviewBurgs();
else if (shift && code === "KeyU") overviewRoutes();
else if (shift && code === "KeyV") overviewRivers();
else if (shift && code === "KeyM") overviewMilitary();
else if (shift && code === "KeyK") overviewMarkers();
@ -57,7 +58,7 @@ function handleKeyup(event) {
else if (key === "!") toggleAddBurg();
else if (key === "@") toggleAddLabel();
else if (key === "#") toggleAddRiver();
else if (key === "$") toggleAddRoute();
else if (key === "$") createRoute();
else if (key === "%") toggleAddMarker();
else if (alt && code === "KeyB") console.table(pack.burgs);
else if (alt && code === "KeyS") console.table(pack.states);

View file

@ -48,8 +48,7 @@ function editLake() {
document.getElementById("lakeArea").value = si(getArea(l.area)) + " " + getAreaUnit();
const length = d3.polygonLength(l.vertices.map(v => pack.vertices.p[v]));
document.getElementById("lakeShoreLength").value =
si(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
document.getElementById("lakeShoreLength").value = si(length * distanceScale) + " " + distanceUnitInput.value;
const lakeCells = Array.from(cells.i.filter(i => cells.f[i] === l.i));
const heights = lakeCells.map(i => cells.h[i]);

View file

@ -169,6 +169,7 @@ function restoreLayers() {
if (layerIsOn("toggleGrid")) drawGrid();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.style("display", "block");
if (layerIsOn("toggleRoutes")) drawRoutes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("togglePopulation")) drawPopulation();
@ -188,92 +189,135 @@ function restoreLayers() {
}
function toggleHeight(event) {
if (customization === 1) {
tip("You cannot turn off the layer when heightmap is in edit mode", false, "error");
return;
}
if (customization === 1) return tip("You cannot turn off the layer when heightmap is in edit mode", false, "error");
if (!terrs.selectAll("*").size()) {
const children = terrs.selectAll("#oceanHeights > *, #landHeights > *");
if (!children.size()) {
turnButtonOn("toggleHeight");
drawHeightmap();
if (event && isCtrlClick(event)) editStyle("terrs");
} else {
if (event && isCtrlClick(event)) {
editStyle("terrs");
return;
}
if (event && isCtrlClick(event)) return editStyle("terrs");
turnButtonOff("toggleHeight");
terrs.selectAll("*").remove();
children.remove();
}
}
function drawHeightmap() {
TIME && console.time("drawHeightmap");
terrs.selectAll("*").remove();
const {cells, vertices} = pack;
const n = cells.i.length;
const used = new Uint8Array(cells.i.length);
const paths = new Array(101).fill("");
const ocean = terrs.select("#oceanHeights");
const land = terrs.select("#landHeights");
const scheme = getColorScheme(terrs.attr("scheme"));
const terracing = terrs.attr("terracing") / 10; // add additional shifted darker layer for pseudo-3d effect
const skip = +terrs.attr("skip") + 1;
const simplification = +terrs.attr("relax");
ocean.selectAll("*").remove();
land.selectAll("*").remove();
switch (+terrs.attr("curve")) {
case 0:
lineGen.curve(d3.curveBasisClosed);
break;
case 1:
lineGen.curve(d3.curveLinear);
break;
case 2:
lineGen.curve(d3.curveStep);
break;
default:
lineGen.curve(d3.curveBasisClosed);
const paths = new Array(101);
// ocean cells
const renderOceanCells = Boolean(+ocean.attr("data-render"));
if (renderOceanCells) {
const {cells, vertices} = grid;
const used = new Uint8Array(cells.i.length);
const skip = +ocean.attr("skip") + 1 || 1;
const relax = +ocean.attr("relax") || 0;
lineGen.curve(d3[ocean.attr("curve") || "curveBasisClosed"]);
let currentLayer = 0;
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer >= 20) break;
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));
const chain = connectVertices(cells, vertices, vertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
let currentLayer = 20;
const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
for (const i of heights) {
const h = cells.h[i];
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
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));
const chain = connectVertices(vertex, h);
if (chain.length < 3) continue;
const points = simplifyLine(chain).map(v => vertices.p[v]);
paths[h] += round(lineGen(points));
// land cells
{
const {cells, vertices} = pack;
const used = new Uint8Array(cells.i.length);
const skip = +land.attr("skip") + 1 || 1;
const relax = +land.attr("relax") || 0;
lineGen.curve(d3[land.attr("curve") || "curveBasisClosed"]);
let currentLayer = 20;
const heights = Array.from(cells.i).sort((a, b) => cells.h[a] - cells.h[b]);
for (const i of heights) {
const h = cells.h[i];
if (h > currentLayer) currentLayer += skip;
if (h < currentLayer) continue;
if (currentLayer > 100) break; // no layers possible with height > 100
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));
const chain = connectVertices(cells, vertices, vertex, h, used);
if (chain.length < 3) continue;
const points = simplifyLine(chain, relax).map(v => vertices.p[v]);
if (!paths[h]) paths[h] = "";
paths[h] += round(lineGen(points));
}
}
terrs
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(0.8)); // draw base layer
for (const i of d3.range(20, 101)) {
if (paths[i].length < 10) continue;
const color = getColor(i, scheme);
if (terracing)
terrs
.append("path")
.attr("d", paths[i])
.attr("transform", "translate(.7,1.4)")
.attr("fill", d3.color(color).darker(terracing))
.attr("data-height", i);
terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
// render paths
for (const height of d3.range(0, 101)) {
const group = height < 20 ? ocean : land;
const scheme = getColorScheme(group.attr("scheme"));
if (height === 0 && renderOceanCells) {
// draw base ocean layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(1));
}
if (height === 20) {
// draw base land layer
group
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", graphWidth)
.attr("height", graphHeight)
.attr("fill", scheme(0.8));
}
if (paths[height] && paths[height].length >= 10) {
const terracing = group.attr("terracing") / 10 || 0;
const color = getColor(height, scheme);
if (terracing) {
group
.append("path")
.attr("d", paths[height])
.attr("transform", "translate(.7,1.4)")
.attr("fill", d3.color(color).darker(terracing))
.attr("data-height", height);
}
group.append("path").attr("d", paths[height]).attr("fill", color).attr("data-height", height);
}
}
// connect vertices to chain
function connectVertices(start, h) {
function connectVertices(cells, vertices, start, h, used) {
const n = cells.i.length;
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
@ -295,7 +339,7 @@ function drawHeightmap() {
return chain;
}
function simplifyLine(chain) {
function simplifyLine(chain, simplification) {
if (!simplification) return chain;
const n = simplification + 1; // filter each nth element
return chain.filter((d, i) => i % n === 0);
@ -349,7 +393,6 @@ function drawTemp() {
const start = findStart(i, t);
if (!start) continue;
used[i] = 1;
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
const chain = connectVertices(start, t); // vertices chain to form a path
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
@ -1027,27 +1070,29 @@ function drawStates() {
const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const bodyString = bodyData.map(d => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join("");
const gapString = gapData.map(d => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join("");
const clipString = bodyData
.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`)
.join("");
const haloString = haloData
.map(
d =>
`<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${
d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666"
}"/>`
)
.join("");
statesBody.html(bodyString + gapString);
defs.select("#statePaths").html(clipString);
statesHalo.html(haloString);
// connect vertices to chain
const isOptimized = shapeRendering.value === "optimizeSpeed";
if (!isOptimized) {
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const haloString = haloData
.map(d => {
const stroke = d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666";
return `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${stroke}"/>`;
})
.join("");
statesHalo.html(haloString);
const clipString = bodyData
.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`)
.join("");
defs.select("#statePaths").html(clipString);
}
function connectVertices(start, state) {
const chain = []; // vertices chain to form a path
const getType = c => {
@ -1482,10 +1527,6 @@ function toggleCompass(event) {
if (!layerIsOn("toggleCompass")) {
turnButtonOn("toggleCompass");
$("#compass").fadeIn();
if (!compass.selectAll("*").size()) {
compass.append("use").attr("xlink:href", "#rose");
shiftCompass();
}
if (event && isCtrlClick(event)) editStyle("compass");
} else {
if (event && isCtrlClick(event)) {
@ -1581,18 +1622,33 @@ function drawRivers() {
function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes");
$("#routes").fadeIn();
drawRoutes();
if (event && isCtrlClick(event)) editStyle("routes");
} else {
if (event && isCtrlClick(event)) {
editStyle("routes");
return;
}
$("#routes").fadeOut();
if (event && isCtrlClick(event)) return editStyle("routes");
routes.selectAll("path").remove();
turnButtonOff("toggleRoutes");
}
}
function drawRoutes() {
TIME && console.time("drawRoutes");
const routePaths = {};
for (const route of pack.routes) {
const {i, group} = route;
if (!routePaths[group]) routePaths[group] = [];
routePaths[group].push(`<path id="route${i}" d="${Routes.getPath(route)}"/>`);
}
routes.selectAll("path").remove();
for (const group in routePaths) {
routes.select("#" + group).html(routePaths[group].join(""));
}
TIME && console.timeEnd("drawRoutes");
}
function toggleMilitary() {
if (!layerIsOn("toggleMilitary")) {
turnButtonOn("toggleMilitary");
@ -1670,10 +1726,7 @@ function toggleLabels(event) {
invokeActiveZooming();
if (event && isCtrlClick(event)) editStyle("labels");
} else {
if (event && isCtrlClick(event)) {
editStyle("labels");
return;
}
if (event && isCtrlClick(event)) return editStyle("labels");
turnButtonOff("toggleLabels");
labels.style("display", "none");
}
@ -1685,10 +1738,7 @@ function toggleIcons(event) {
$("#icons").fadeIn();
if (event && isCtrlClick(event)) editStyle("burgIcons");
} else {
if (event && isCtrlClick(event)) {
editStyle("burgIcons");
return;
}
if (event && isCtrlClick(event)) return editStyle("burgIcons");
turnButtonOff("toggleIcons");
$("#icons").fadeOut();
}
@ -1701,10 +1751,7 @@ function toggleRulers(event) {
rulers.draw();
ruler.style("display", null);
} else {
if (event && isCtrlClick(event)) {
editStyle("ruler");
return;
}
if (event && isCtrlClick(event)) return editStyle("ruler");
turnButtonOff("toggleRulers");
ruler.selectAll("*").remove();
ruler.style("display", "none");
@ -1715,17 +1762,112 @@ function toggleScaleBar(event) {
if (!layerIsOn("toggleScaleBar")) {
turnButtonOn("toggleScaleBar");
$("#scaleBar").fadeIn();
if (event && isCtrlClick(event)) editUnits();
if (event && isCtrlClick(event)) editStyle("scaleBar");
} else {
if (event && isCtrlClick(event)) {
editUnits();
return;
}
if (event && isCtrlClick(event)) return editStyle("scaleBar");
$("#scaleBar").fadeOut();
turnButtonOff("toggleScaleBar");
}
}
function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
const unit = distanceUnitInput.value;
const size = +scaleBar.attr("data-bar-size");
const length = (function () {
const init = 100;
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
if (val > 900) val = rn(val, -3); // round to 1000
else if (val > 90) val = rn(val, -2); // round to 100
else if (val > 9) val = rn(val, -1); // round to 10
else val = rn(val); // round to 1
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
return length;
})();
scaleBar.select("#scaleBarContent").remove(); // redraw content every time
const content = scaleBar.append("g").attr("id", "scaleBarContent");
const lines = content.append("g");
lines
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", length + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
lines
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", length + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
lines
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", length + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", size + " " + rn(length / 5 - size, 2))
.attr("stroke", "#3d3d3d");
const texts = content.append("g").attr("text-anchor", "middle").attr("font-family", "var(--serif)");
texts
.selectAll("text")
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.6em")
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
const label = scaleBar.attr("data-label");
if (label) {
texts
.append("text")
.attr("x", (length + 1) / 2)
.attr("dy", ".6em")
.attr("dominant-baseline", "text-before-edge")
.text(label);
}
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (scaleBarBack.size()) {
const bbox = content.node().getBBox();
const paddingTop = +scaleBarBack.attr("data-top") || 0;
const paddingLeft = +scaleBarBack.attr("data-left") || 0;
const paddingRight = +scaleBarBack.attr("data-right") || 0;
const paddingBottom = +scaleBarBack.attr("data-bottom") || 0;
scaleBar
.select("#scaleBarBack")
.attr("x", -paddingLeft)
.attr("y", -paddingTop)
.attr("width", bbox.width + paddingRight)
.attr("height", bbox.height + paddingBottom);
}
}
// fit ScaleBar to screen size
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
const posX = +scaleBar.attr("data-x") || 99;
const posY = +scaleBar.attr("data-y") || 99;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn((fullWidth * posX) / 100 - bbox.width + 10);
const y = rn((fullHeight * posY) / 100 - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}
function toggleZones(event) {
if (!layerIsOn("toggleZones")) {
turnButtonOn("toggleZones");
@ -1748,10 +1890,7 @@ function toggleEmblems(event) {
$("#emblems").fadeIn();
if (event && isCtrlClick(event)) editStyle("emblems");
} else {
if (event && isCtrlClick(event)) {
editStyle("emblems");
return;
}
if (event && isCtrlClick(event)) return editStyle("emblems");
$("#emblems").fadeOut();
turnButtonOff("toggleEmblems");
}

View file

@ -66,7 +66,7 @@ class Measurer {
}
getDash() {
return rn(30 / distanceScaleInput.value, 2);
return rn(30 / distanceScale, 2);
}
drag() {
@ -205,7 +205,7 @@ class Ruler extends Measurer {
updateLabel() {
const length = this.getLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -337,7 +337,7 @@ class Opisometer extends Measurer {
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -475,7 +475,7 @@ class RouteOpisometer extends Measurer {
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -486,9 +486,7 @@ class RouteOpisometer extends Measurer {
const cells = pack.cells;
const c = findCell(mousePoint[0], mousePoint[1]);
if (!cells.road[c] && !d3.event.sourceEvent.shiftKey) {
return;
}
if (!Routes.isConnected(c) && !d3.event.sourceEvent.shiftKey) return;
context.trackCell(c, rigth);
});
@ -532,101 +530,3 @@ class Planimeter extends Measurer {
this.el.select("text").attr("x", c[0]).attr("y", c[1]).text(area);
}
}
// Scale bar
function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
scaleBar.selectAll("*").remove(); // fully redraw every time
const distanceScale = +distanceScaleInput.value;
const unit = distanceUnitInput.value;
const size = +barSizeInput.value;
// calculate size
const init = 100;
let val = (init * size * distanceScale) / scaleLevel; // bar length in distance unit
if (val > 900) val = rn(val, -3);
// round to 1000
else if (val > 90) val = rn(val, -2);
// round to 100
else if (val > 9) val = rn(val, -1);
// round to 10
else val = rn(val); // round to 1
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
scaleBar
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", length + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", size)
.attr("x2", length + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
const dash = size + " " + rn(length / 5 - size, 2);
scaleBar
.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", length + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", dash)
.attr("stroke", "#3d3d3d");
const fontSize = rn(5 * size, 1);
scaleBar
.selectAll("text")
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * length) / 5, 2))
.attr("y", 0)
.attr("dy", "-.6em")
.attr("font-size", fontSize)
.text(d => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? "" : " " + unit));
if (barLabel.value !== "") {
scaleBar
.append("text")
.attr("x", (length + 1) / 2)
.attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize)
.text(barLabel.value);
}
const bbox = scaleBar.node().getBBox();
// append backbround rectangle
scaleBar
.insert("rect", ":first-child")
.attr("x", -10)
.attr("y", -20)
.attr("width", bbox.width + 10)
.attr("height", bbox.height + 15)
.attr("stroke-width", size)
.attr("stroke", "none")
.attr("filter", "url(#blur5)")
.attr("fill", barBackColor.value)
.attr("opacity", +barBackOpacity.value);
}
// fit ScaleBar to screen size
function fitScaleBar(scaleBar, fullWidth, fullHeight) {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
const px = isNaN(+barPosX.value) ? 0.99 : barPosX.value / 100;
const py = isNaN(+barPosY.value) ? 0.99 : barPosY.value / 100;
const bbox = scaleBar.select("rect").node().getBBox();
const x = rn(fullWidth * px - bbox.width + 10);
const y = rn(fullHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
}

View file

@ -54,7 +54,11 @@ function overviewMilitary() {
const insert = html => document.getElementById("militaryTotal").insertAdjacentHTML("beforebegin", html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " "));
insert(`<div data-tip="State ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
insert(
`<div data-tip="State ${
u.name
} units number. Click to sort" class="sortable removable" data-sortby="${u.name.toLowerCase()}">${label}&nbsp;</div>`
);
}
header.querySelectorAll(".removable").forEach(function (e) {
e.addEventListener("click", function () {
@ -75,8 +79,10 @@ function overviewMilitary() {
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = (total / population) * 100;
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`).join(" ");
const sortData = options.military.map(u => `data-${u.name.toLowerCase()}="${getForces(u)}"`).join(" ");
const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`)
.join(" ");
lines += /* html */ `<div
class="states"
@ -91,9 +97,14 @@ function overviewMilitary() {
<fill-box data-tip="${s.fullName}" fill="${s.color}" disabled></fill-box>
<input data-tip="${s.fullName}" style="width:6em" value="${s.name}" readonly />
${lineData}
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(total)}</div>
<div data-type="total" data-tip="Total state military personnel (considering crew)" style="font-weight: bold">${si(
total
)}</div>
<div data-type="population" data-tip="State population">${si(population)}</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(rate, 2)}%</div>
<div data-type="rate" data-tip="Military personnel rate (% of state population). Depends on war alert">${rn(
rate,
2
)}%</div>
<input
data-tip="War Alert. Editable modifier to military forces number, depends of political situation"
style="width:4.1em"
@ -131,7 +142,9 @@ function overviewMilitary() {
});
const getForces = u => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
options.military.forEach(u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u)));
options.military.forEach(
u => (line.dataset[u.name] = line.querySelector(`div[data-type='${u.name}']`).innerHTML = getForces(u))
);
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const total = (line.dataset.total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0));
@ -237,7 +250,16 @@ function overviewMilitary() {
position: {my: "center", at: "center", of: "svg"},
buttons: {
Apply: applyMilitaryOptions,
Add: () => addUnitLine({icon: "🛡️", name: "custom" + militaryOptionsTable.rows.length, rural: 0.2, urban: 0.5, crew: 1, power: 1, type: "melee"}),
Add: () =>
addUnitLine({
icon: "🛡️",
name: "custom" + militaryOptionsTable.rows.length,
rural: 0.2,
urban: 0.5,
crew: 1,
power: 1,
type: "melee"
}),
Restore: restoreDefaultUnits,
Cancel: function () {
$(this).dialog("close");
@ -262,7 +284,7 @@ function overviewMilitary() {
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
if (type === "icon") return selectIcon(el.textContent, v => (el.textContent = v));
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
@ -294,7 +316,9 @@ function overviewMilitary() {
function addUnitLine(unit) {
const {type, icon, name, rural, urban, power, crew, separate} = unit;
const row = document.createElement("tr");
const typeOptions = types.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ");
const typeOptions = types
.map(t => `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`)
.join(" ");
const getLimitButton = attr =>
`<button
@ -305,7 +329,9 @@ function overviewMilitary() {
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${icon || " "}</button></td>
row.innerHTML = /* html */ `<td><button data-type="icon" data-tip="Click to select unit icon">${
icon || " "
}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${name}" /></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
@ -344,7 +370,9 @@ function overviewMilitary() {
const lines = filtered.map(
({i, name, fullName, color}) =>
`<tr data-tip="${name}"><td><span style="color:${color}">⬤</span></td>
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${!initial.length || initial.includes(i) ? "checked" : ""} >
<td><input data-i="${i}" id="el${i}" type="checkbox" class="checkbox" ${
!initial.length || initial.includes(i) ? "checked" : ""
} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
@ -387,22 +415,21 @@ function overviewMilitary() {
function applyMilitaryOptions() {
const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
if (new Set(names).size !== names.length) {
tip("All units should have unique names", false, "error");
return;
}
if (new Set(names).size !== names.length) return tip("All units should have unique names", false, "error");
$("#militaryOptions").dialog("close");
options.military = unitLines.map((r, i) => {
const elements = Array.from(r.querySelectorAll("input, button, select"));
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] = elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.innerHTML || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;
return el.value;
});
const [icon, name, biomes, states, cultures, religions, rural, urban, crew, power, type, separate] =
elements.map(el => {
const {type, value} = el.dataset || {};
if (type === "icon") return el.textContent || "";
if (type) return value ? value.split(",").map(v => parseInt(v)) : null;
if (el.type === "number") return +el.value || 0;
if (el.type === "checkbox") return +el.checked || 0;
return el.value;
});
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
@ -419,7 +446,8 @@ function overviewMilitary() {
}
function militaryRecalculate() {
alertMessage.innerHTML = "Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated";
alertMessage.innerHTML =
"Are you sure you want to recalculate military forces for all states?<br>Regiments for all states will be regenerated";
$("#alert").dialog({
resizable: false,
title: "Remove regiment",
@ -443,7 +471,7 @@ function overviewMilitary() {
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.state + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += units.map(u => el.dataset[u.toLowerCase()]).join(",") + ",";
data += el.dataset.total + ",";
data += el.dataset.population + ",";
data += rn(el.dataset.rate, 2) + "%,";

View file

@ -64,7 +64,7 @@ function editNotes(id, name) {
async function initEditor() {
if (!window.tinymce) {
const url = "https://cdn.tiny.cloud/1/4i6a79ymt2y0cagke174jp3meoi28vyecrch12e5puyw3p9a/tinymce/5/tinymce.min.js";
const url = "https://azgaar.github.io/Fantasy-Map-Generator/libs/tinymce/tinymce.min.js";
try {
await import(url);
} catch (error) {
@ -79,11 +79,13 @@ function editNotes(id, name) {
}
if (window.tinymce) {
window.tinymce._setBaseUrl("https://azgaar.github.io/Fantasy-Map-Generator/libs/tinymce");
tinymce.init({
license_key: "gpl",
selector: "#notesLegend",
height: "90%",
menubar: false,
plugins: `autolink lists link charmap print code fullscreen image link media table paste hr wordcount`,
plugins: `autolink lists link charmap code fullscreen image link media table wordcount`,
toolbar: `code | undo redo | removeformat | bold italic strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media table | fontselect fontsizeselect | blockquote hr charmap | print fullscreen`,
media_alt_source: false,
media_poster: false,

View file

@ -76,7 +76,7 @@ document
// show popup with a list of Patreon supportes (updated manually)
async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=1.93.08");
const {supporters} = await import("../dynamic/supporters.js?v=1.97.14");
const list = supporters.split("\n").sort();
const columns = window.innerWidth < 800 ? 2 : 5;
@ -119,9 +119,9 @@ function updateOutputToFollowInput(ev) {
// Option listeners
const optionsContent = byId("optionsContent");
optionsContent.addEventListener("input", function (event) {
const id = event.target.id;
const value = event.target.value;
optionsContent.addEventListener("input", event => {
const {id, value} = event.target;
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
else if (id === "pointsInput") changeCellsDensity(+value);
else if (id === "culturesSet") changeCultureSet();
@ -133,10 +133,8 @@ optionsContent.addEventListener("input", function (event) {
else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
});
optionsContent.addEventListener("change", function (event) {
const id = event.target.id;
const value = event.target.value;
optionsContent.addEventListener("change", event => {
const {id, value} = event.target;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUiSize(value);
@ -146,8 +144,8 @@ optionsContent.addEventListener("change", function (event) {
else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
});
optionsContent.addEventListener("click", function (event) {
const id = event.target.id;
optionsContent.addEventListener("click", event => {
const {id} = event.target;
if (id === "restoreDefaultCanvasSize") restoreDefaultCanvasSize();
else if (id === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "optionsCopySeed") copyMapURL();
@ -327,6 +325,7 @@ const cellsDensityMap = {
};
function changeCellsDensity(value) {
pointsInput.value = value;
const cells = cellsDensityMap[value] || 1000;
pointsInput.dataset.cells = cells;
pointsOutputFormatted.value = getCellsDensityValue(cells);
@ -536,6 +535,7 @@ function applyStoredOptions() {
const key = localStorage.key(i);
if (key === "speakerVoice") continue;
const input = byId(key + "Input") || byId(key);
const output = byId(key + "Output");
@ -544,6 +544,9 @@ function applyStoredOptions() {
if (output) output.value = value;
lock(key);
if (key === "points") changeCellsDensity(+value);
if (key === "distanceScale") distanceScale = +value;
// add saved style presets to options
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
}
@ -581,6 +584,7 @@ function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
// 'Options' settings
if (randomize || !locked("points")) changeCellsDensity(4); // reset to default, no need to randomize
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
@ -602,7 +606,8 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === "en-US";
if (randomize || !locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (randomize || !locked("distanceScale"))
distanceScale = distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
@ -641,17 +646,16 @@ function randomizeCultureSet() {
function setRendering(value) {
viewbox.attr("shape-rendering", value);
// if (value === "optimizeSpeed") {
// // block some styles
// coastline.select("#sea_island").style("filter", "none");
// statesHalo.style("display", "none");
// emblems.style("opacity", 1);
// } else {
// // remove style block
// coastline.select("#sea_island").style("filter", null);
// statesHalo.style("display", null);
// emblems.style("opacity", null);
// }
if (value === "optimizeSpeed") {
// block some styles
coastline.select("#sea_island").style("filter", "none");
statesHalo.style("display", "none");
} else {
// remove style block
coastline.select("#sea_island").style("filter", null);
statesHalo.style("display", null);
if (pack.cells && statesHalo.selectAll("*").size() === 0) drawStates();
}
}
// generate current year and era name
@ -691,7 +695,7 @@ function changeEra() {
}
async function openTemplateSelectionDialog() {
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.93.12");
const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.96.00");
HeightmapSelectionDialog.open();
}
@ -774,7 +778,7 @@ function showExportPane() {
}
async function exportToJson(type) {
const {exportToJson} = await import("../dynamic/export-json.js?v=1.93.03");
const {exportToJson} = await import("../dynamic/export-json.js?v=1.97.08");
exportToJson(type);
}
@ -867,11 +871,9 @@ byId("mapToLoad").addEventListener("change", function () {
});
function openExportToPngTiles() {
byId("tileStatus").innerHTML = "";
closeDialogs();
updateTilesOptions();
const status = byId("tileStatus");
status.innerHTML = "";
let loading = null;
const inputs = byId("exportToPngTilesScreen").querySelectorAll("input");
inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
@ -881,16 +883,7 @@ function openExportToPngTiles() {
title: "Download tiles",
width: "23em",
buttons: {
Download: function () {
status.innerHTML = "Preparing for download...";
setTimeout(() => (status.innerHTML = "Downloading. It may take some time."), 1000);
loading = setInterval(() => (status.innerHTML += "."), 1000);
exportToPngTiles().then(() => {
clearInterval(loading);
status.innerHTML = /* html */ `Done. Check file in "Downloads" (crtl + J)`;
setTimeout(() => (status.innerHTML = ""), 8000);
});
},
Download: () => exportToPngTiles(),
Cancel: function () {
$(this).dialog("close");
}
@ -898,7 +891,6 @@ function openExportToPngTiles() {
close: () => {
inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
debug.selectAll("*").remove();
clearInterval(loading);
}
});
}
@ -911,9 +903,9 @@ function updateTilesOptions() {
}
const tileSize = byId("tileSize");
const tilesX = +byId("tileColsOutput").value;
const tilesY = +byId("tileRowsOutput").value;
const scale = +byId("tileScaleOutput").value;
const tilesX = +byId("tileColsOutput").value || 2;
const tilesY = +byId("tileRowsOutput").value || 2;
const scale = +byId("tileScaleOutput").value || 1;
// calculate size
const sizeX = graphWidth * scale * tilesX;
@ -928,18 +920,27 @@ function updateTilesOptions() {
const labels = [];
const tileW = (graphWidth / tilesX) | 0;
const tileH = (graphHeight / tilesY) | 0;
for (let y = 0, i = 0; y + tileH <= graphHeight; y += tileH) {
for (let x = 0; x + tileW <= graphWidth; x += tileW, i++) {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
function getRowLabel(row) {
const first = row >= alphabet.length ? alphabet[Math.floor(row / alphabet.length) - 1] : "";
const last = alphabet[row % alphabet.length];
return first + last;
}
for (let y = 0, row = 0; y + tileH <= graphHeight; y += tileH, row++) {
for (let x = 0, column = 1; x + tileW <= graphWidth; x += tileW, column++) {
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${i}</text>`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${getRowLabel(row)}${column}</text>`);
}
}
const rectsG = "<g fill='none' stroke='#000'>" + rects.join("") + "</g>";
const labelsG =
"<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='24px'>" +
labels.join("") +
"</g>";
debug.html(rectsG + labelsG);
debug.html(`
<g fill='none' stroke='#000'>${rects.join("")}</g>
<g fill='#000' stroke='none' text-anchor='middle' dominant-baseline='central' font-size='18px'>${labels.join(
""
)}</g>
`);
}
// View mode

View file

@ -44,12 +44,14 @@ function editProvinces() {
cl = el.classList,
line = el.parentNode,
p = +line.dataset.id;
const stateId = pack.provinces[p].state;
if (el.tagName === "FILL-BOX") changeFill(el);
else if (cl.contains("name")) editProvinceName(p);
else if (cl.contains("coaIcon")) editEmblem("province", "provinceCOA" + p, pack.provinces[p]);
else if (cl.contains("icon-star-empty")) capitalZoomIn(p);
else if (cl.contains("icon-flag-empty")) triggerIndependencePromps(p);
else if (cl.contains("icon-dot-circled")) overviewBurgs({stateId});
else if (cl.contains("culturePopulation")) changePopulation(p);
else if (cl.contains("icon-pin")) toggleFog(p, cl);
else if (cl.contains("icon-trash-empty")) removeProvince(p);
@ -71,9 +73,8 @@ function editProvinces() {
}
function collectStatistics() {
const cells = pack.cells,
provinces = pack.provinces,
burgs = pack.burgs;
const {cells, provinces, burgs} = pack;
provinces.forEach(p => {
if (!p.i || p.removed) return;
p.area = p.rural = p.urban = 0;
@ -107,16 +108,18 @@ function editProvinces() {
statesSorted.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
}
// add line for each state
// add line for each province
function provincesEditorAddLines() {
const unit = " " + getAreaUnit();
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;
let lines = "";
let totalArea = 0;
let totalPopulation = 0;
let totalBurgs = 0;
for (const p of filtered) {
const area = getArea(p.area);
@ -128,6 +131,7 @@ function editProvinces() {
rural
)}; Urban population: ${si(urban)}`;
totalPopulation += population;
totalBurgs += p.burgs.length;
const stateName = pack.states[p.state].name;
const capital = p.burg ? pack.burgs[p.burg].name : "";
@ -144,6 +148,7 @@ function editProvinces() {
data-state="${stateName}"
data-area=${area}
data-population=${population}
data-burgs=${p.burgs.length}
>
<fill-box fill="${p.color}"></fill-box>
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly />
@ -163,6 +168,8 @@ function editProvinces() {
${p.burgs.length ? getCapitalOptions(p.burgs, p.burg) : ""}
</select>
<input data-tip="Province owner" class="provinceOwner" value="${stateName}" disabled">
<span data-tip="Click to overview province burgs" style="padding-right: 1px" class="icon-dot-circled pointer hide"></span>
<div data-tip="Burgs count" class="provinceBurgs hide">${p.burgs.length}</div>
<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>
@ -179,11 +186,12 @@ function editProvinces() {
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;
byId("provincesFooterNumber").innerHTML = filtered.length;
byId("provincesFooterBurgs").innerHTML = totalBurgs;
byId("provincesFooterArea").innerHTML = filtered.length ? si(totalArea / filtered.length) + unit : 0 + unit;
byId("provincesFooterPopulation").innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
byId("provincesFooterArea").dataset.area = totalArea;
byId("provincesFooterPopulation").dataset.population = totalPopulation;
body.querySelectorAll("div.states").forEach(el => {
el.addEventListener("click", selectProvinceOnLineClick);
@ -294,7 +302,7 @@ function editProvinces() {
// move all burgs to a new state
province.burgs.forEach(b => (burgs[b].state = newStateId));
// difine new state attributes
// define new state attributes
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = province.coa;
@ -501,6 +509,9 @@ function editProvinces() {
applyOption(provinceNameEditorSelectForm, p.formName);
document.getElementById("provinceNameEditorFull").value = p.fullName;
const cultureId = pack.cells.culture[p.center];
document.getElementById("provinceCultureDisplay").innerText = pack.cultures[cultureId].name;
$("#provinceNameEditor").dialog({
resizable: false,
title: "Change province name",
@ -520,12 +531,12 @@ function editProvinces() {
modules.editProvinceName = true;
// add listeners
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCuture);
document.getElementById("provinceNameEditorShortCulture").addEventListener("click", regenerateShortNameCulture);
document.getElementById("provinceNameEditorShortRandom").addEventListener("click", regenerateShortNameRandom);
document.getElementById("provinceNameEditorAddForm").addEventListener("click", addCustomForm);
document.getElementById("provinceNameEditorFullRegenerate").addEventListener("click", regenerateFullName);
function regenerateShortNameCuture() {
function regenerateShortNameCulture() {
const province = +provinceNameEditor.dataset.province;
const culture = pack.cells.culture[pack.provinces[province].center];
const name = Names.getState(Names.getCultureShort(culture), culture);
@ -576,12 +587,15 @@ function editProvinces() {
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const totalBurgs = +byId("provincesFooterBurgs").innerText;
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) + "%";
const {cells, burgs, area, population} = el.dataset;
el.querySelector(".provinceBurgs").innerText = rn((+burgs / totalBurgs) * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+area / totalArea) * 100) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+population / totalPopulation) * 100) + "%";
});
} else {
body.dataset.type = "absolute";
@ -872,7 +886,7 @@ function editProvinces() {
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 found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand);
if (selection) changeForSelection(selection);
});
@ -1064,10 +1078,7 @@ function editProvinces() {
function downloadProvincesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data =
"Id,Province,Full Name,Form,State,Color,Capital,Area " +
unit +
",Total Population,Rural Population,Urban Population\n"; // headers
let data = `Id,Province,Full Name,Form,State,Color,Capital,Area ${unit},Total Population,Rural Population,Urban Population,Burgs\n`; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
const key = parseInt(el.dataset.id);
@ -1081,8 +1092,9 @@ function editProvinces() {
data += el.dataset.capital + ",";
data += el.dataset.area + ",";
data += el.dataset.population + ",";
data += `${Math.round(provincePack.rural * populationRate)},`;
data += `${Math.round(provincePack.urban * populationRate * urbanization)}\n`;
data += Math.round(provincePack.rural * populationRate) + ",";
data += Math.round(provincePack.urban * populationRate * urbanization) + ",";
data += el.dataset.burgs + "\n";
});
const name = getFileName("Provinces") + ".csv";

View file

@ -8,9 +8,10 @@ function editRegiment(selector) {
armies.selectAll(":scope > g > g").call(d3.drag().on("drag", dragRegiment));
elSelected = selector ? document.querySelector(selector) : d3.event.target.parentElement; // select g element
if (!pack.states[elSelected.dataset.state]) return;
if (!regiment()) return;
updateRegimentData(regiment());
if (!getRegiment()) return;
updateRegimentData(getRegiment());
drawBase();
drawRotationControl();
$("#regimentEditor").dialog({
title: "Edit Regiment",
@ -37,8 +38,8 @@ function editRegiment(selector) {
document.getElementById("regimentRemove").addEventListener("click", removeRegiment);
// get regiment data element
function regiment() {
return pack.states[elSelected.dataset.state].military.find(r => r.i == elSelected.dataset.id);
function getRegiment() {
return pack.states[elSelected.dataset.state]?.military.find(r => r.i == elSelected.dataset.id);
}
function updateRegimentData(regiment) {
@ -60,7 +61,7 @@ function editRegiment(selector) {
}
function drawBase() {
const reg = regiment();
const reg = getRegiment();
const clr = pack.states[elSelected.dataset.state].color;
const base = viewbox
.insert("g", "g#armies")
@ -69,12 +70,8 @@ function editRegiment(selector) {
.attr("stroke", "#000")
.attr("cursor", "move");
base
.on("mouseenter", () => {
tip("Regiment base. Drag to re-base the regiment", true);
})
.on("mouseleave", () => {
tip("", true);
});
.on("mouseenter", () => tip("Regiment base. Drag to re-base the regiment", true))
.on("mouseleave", () => tip("", true));
base
.append("line")
@ -92,8 +89,42 @@ function editRegiment(selector) {
.call(d3.drag().on("drag", dragBase));
}
function drawRotationControl() {
const reg = getRegiment();
const {x, width, y, height} = elSelected.getBBox();
debug
.append("circle")
.attr("id", "rotationControl")
.attr("cx", x + width)
.attr("cy", y + height / 2)
.attr("r", 1)
.attr("opacity", 1)
.attr("fill", "yellow")
.attr("stroke-width", 0.3)
.attr("stroke", "black")
.attr("cursor", "alias")
.attr("transform", `rotate(${reg.angle || 0})`)
.attr("transform-origin", `${reg.x}px ${reg.y}px`)
.on("mouseenter", () => tip("Drag to rotate the regiment", true))
.on("mouseleave", () => tip("", true))
.call(d3.drag().on("start", rotateRegiment));
}
function rotateRegiment() {
const reg = getRegiment();
d3.event.on("drag", function () {
const {x, y} = d3.event;
const angle = rn(Math.atan2(y - reg.y, x - reg.x) * (180 / Math.PI), 2);
elSelected.setAttribute("transform", `rotate(${angle})`);
this.setAttribute("transform", `rotate(${angle})`);
reg.angle = rn(angle, 2);
});
}
function changeType() {
const reg = regiment();
const reg = getRegiment();
reg.n = +!reg.n;
document.getElementById("regimentType").className = reg.n ? "icon-anchor" : "icon-users";
@ -110,11 +141,11 @@ function editRegiment(selector) {
}
function changeName() {
elSelected.dataset.name = regiment().name = this.value;
elSelected.dataset.name = getRegiment().name = this.value;
}
function restoreName() {
const reg = regiment(),
const reg = getRegiment(),
regs = pack.states[elSelected.dataset.state].military;
const name = Military.getName(reg, regs);
elSelected.dataset.name = reg.name = document.getElementById("regimentName").value = name;
@ -129,12 +160,12 @@ function editRegiment(selector) {
function changeEmblem() {
const emblem = document.getElementById("regimentEmblem").value;
regiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
getRegiment().icon = elSelected.querySelector(".regimentIcon").innerHTML = emblem;
}
function changeUnit() {
const u = this.dataset.u;
const reg = regiment();
const reg = getRegiment();
reg.u[u] = +this.value || 0;
reg.a = d3.sum(Object.values(reg.u));
elSelected.querySelector("text").innerHTML = Military.getTotal(reg);
@ -143,7 +174,7 @@ function editRegiment(selector) {
}
function splitRegiment() {
const reg = regiment(),
const reg = getRegiment(),
u1 = reg.u;
const state = +elSelected.dataset.state,
military = pack.states[state].military;
@ -206,8 +237,7 @@ function editRegiment(selector) {
function addRegimentOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const x = pack.cells.p[cell][0],
y = pack.cells.p[cell][1];
const [x, y] = pack.cells.p[cell];
const state = +elSelected.dataset.state,
military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
@ -254,7 +284,7 @@ function editRegiment(selector) {
return;
}
const attacker = regiment();
const attacker = getRegiment();
const defender = pack.states[regSelected.dataset.state].military.find(r => r.i == regSelected.dataset.id);
if (!attacker.a || !defender.a) {
tip("Regiment has no troops to battle", false, "error");
@ -322,7 +352,7 @@ function editRegiment(selector) {
return;
}
const reg = regiment(); // reg to be attached
const reg = getRegiment(); // reg to be attached
const sel = pack.states[newState].military.find(r => r.i == regSelected.dataset.id); // reg to attach to
for (const unit of options.military) {
@ -349,11 +379,11 @@ function editRegiment(selector) {
if (index != -1) notes.splice(index, 1);
const s = pack.states[elSelected.dataset.state];
Military.generateNote(regiment(), s);
Military.generateNote(getRegiment(), s);
}
function editLegend() {
editNotes(elSelected.id, regiment().name);
editNotes(elSelected.id, getRegiment().name);
}
function removeRegiment() {
@ -365,7 +395,7 @@ function editRegiment(selector) {
Remove: function () {
$(this).dialog("close");
const military = pack.states[elSelected.dataset.state].military;
const regIndex = military.indexOf(regiment());
const regIndex = military.indexOf(getRegiment());
if (regIndex === -1) return;
military.splice(regIndex, 1);
@ -392,8 +422,6 @@ function editRegiment(selector) {
const size = +armies.attr("box-size");
const w = reg.n ? size * 4 : size * 6;
const h = size * 2;
const x1 = x => rn(x - w / 2, 2);
const y1 = y => rn(y - size, 2);
const baseRect = this.querySelector("rect");
const text = this.querySelector("text");
@ -402,26 +430,37 @@ function editRegiment(selector) {
const self = elSelected === this;
const baseLine = viewbox.select("g#regimentBase > line");
const rotationControl = debug.select("#rotationControl");
d3.event.on("drag", function () {
const x = (reg.x = d3.event.x),
y = (reg.y = d3.event.y);
const {x, y} = d3.event;
reg.x = x;
reg.y = y;
const x1 = rn(x - w / 2, 2);
const y1 = rn(y - size, 2);
baseRect.setAttribute("x", x1(x));
baseRect.setAttribute("y", y1(y));
this.setAttribute("transform-origin", `${x}px ${y}px`);
baseRect.setAttribute("x", x1);
baseRect.setAttribute("y", y1);
text.setAttribute("x", x);
text.setAttribute("y", y);
iconRect.setAttribute("x", x1(x) - h);
iconRect.setAttribute("y", y1(y));
icon.setAttribute("x", x1(x) - size);
iconRect.setAttribute("x", x1 - h);
iconRect.setAttribute("y", y1);
icon.setAttribute("x", x1 - size);
icon.setAttribute("y", y);
if (self) baseLine.attr("x2", x).attr("y2", y);
if (self) {
baseLine.attr("x2", x).attr("y2", y);
rotationControl
.attr("cx", x1 + w)
.attr("cy", y)
.attr("transform-origin", `${x}px ${y}px`);
}
});
}
function dragBase() {
const baseLine = viewbox.select("g#regimentBase > line");
const reg = regiment();
const reg = getRegiment();
d3.event.on("drag", function () {
this.setAttribute("cx", d3.event.x);
@ -436,9 +475,10 @@ function editRegiment(selector) {
}
function closeEditor() {
debug.selectAll("*").remove();
viewbox.selectAll("g#regimentBase").remove();
armies.selectAll(":scope > g").classed("draggable", false);
armies.selectAll("g>g").call(d3.drag().on("drag", null));
viewbox.selectAll("g#regimentBase").remove();
document.getElementById("regimentAdd").classList.remove("pressed");
document.getElementById("regimentAttack").classList.remove("pressed");
document.getElementById("regimentAttach").classList.remove("pressed");

View file

@ -5,12 +5,15 @@ function editRiver(id) {
closeDialogs(".stable");
if (!layerIsOn("toggleRivers")) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip("Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead", true);
tip(
"Drag control points to change the river course. Click on point to remove it. Click on river to add additional control point. For major changes please create a new river instead",
true
);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
@ -33,18 +36,18 @@ function editRiver(id) {
modules.editRiver = true;
// add listeners
document.getElementById("riverCreateSelectingCells").addEventListener("click", createRiver);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
document.getElementById("riverRemove").addEventListener("click", removeRiver);
document.getElementById("riverName").addEventListener("input", changeName);
document.getElementById("riverType").addEventListener("input", changeType);
document.getElementById("riverNameCulture").addEventListener("click", generateNameCulture);
document.getElementById("riverNameRandom").addEventListener("click", generateNameRandom);
document.getElementById("riverMainstem").addEventListener("change", changeParent);
document.getElementById("riverSourceWidth").addEventListener("input", changeSourceWidth);
document.getElementById("riverWidthFactor").addEventListener("input", changeWidthFactor);
byId("riverCreateSelectingCells").on("click", createRiver);
byId("riverEditStyle").on("click", () => editStyle("rivers"));
byId("riverElevationProfile").on("click", showRiverElevationProfile);
byId("riverLegend").on("click", editRiverLegend);
byId("riverRemove").on("click", removeRiver);
byId("riverName").on("input", changeName);
byId("riverType").on("input", changeType);
byId("riverNameCulture").on("click", generateNameCulture);
byId("riverNameRandom").on("click", generateNameRandom);
byId("riverMainstem").on("change", changeParent);
byId("riverSourceWidth").on("input", changeSourceWidth);
byId("riverWidthFactor").on("input", changeWidthFactor);
function getRiver() {
const riverId = +elSelected.attr("id").slice(5);
@ -55,10 +58,10 @@ function editRiver(id) {
function updateRiverData() {
const r = getRiver();
document.getElementById("riverName").value = r.name;
document.getElementById("riverType").value = r.type;
byId("riverName").value = r.name;
byId("riverType").value = r.type;
const parentSelect = document.getElementById("riverMainstem");
const parentSelect = byId("riverMainstem");
parentSelect.options.length = 0;
const parent = r.parent || r.i;
const sortedRivers = pack.rivers.slice().sort((a, b) => (a.name > b.name ? 1 : -1));
@ -66,11 +69,11 @@ function editRiver(id) {
const opt = new Option(river.name, river.i, false, river.i === parent);
parentSelect.options.add(opt);
});
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
byId("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
document.getElementById("riverDischarge").value = r.discharge + " m³/s";
document.getElementById("riverSourceWidth").value = r.sourceWidth;
document.getElementById("riverWidthFactor").value = r.widthFactor;
byId("riverDischarge").value = r.discharge + " m³/s";
byId("riverSourceWidth").value = r.sourceWidth;
byId("riverWidthFactor").value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
@ -78,8 +81,8 @@ function editRiver(id) {
function updateRiverLength(river) {
river.length = rn(elSelected.node().getTotalLength() / 2, 2);
const lengthUI = `${rn(river.length * distanceScaleInput.value)} ${distanceUnitInput.value}`;
document.getElementById("riverLength").value = lengthUI;
const lengthUI = `${rn(river.length * distanceScale)} ${distanceUnitInput.value}`;
byId("riverLength").value = lengthUI;
}
function updateRiverWidth(river) {
@ -88,8 +91,8 @@ function editRiver(id) {
const meanderedPoints = addMeandering(cells);
river.width = getWidth(getOffset(discharge, meanderedPoints.length, widthFactor, sourceWidth));
const width = `${rn(river.width * distanceScaleInput.value, 3)} ${distanceUnitInput.value}`;
document.getElementById("riverWidth").value = width;
const width = `${rn(river.width * distanceScale, 3)} ${distanceUnitInput.value}`;
byId("riverWidth").value = width;
}
function drawControlPoints(points) {
@ -163,7 +166,7 @@ function editRiver(id) {
elSelected.attr("d", path);
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
if (byId("elevationProfile").offsetParent) showRiverElevationProfile();
}
function addControlPoint() {
@ -209,7 +212,7 @@ function editRiver(id) {
const r = getRiver();
r.parent = +this.value;
r.basin = pack.rivers.find(river => river.i === r.parent).basin;
document.getElementById("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
byId("riverBasin").value = pack.rivers.find(river => river.i === r.basin).name;
}
function changeSourceWidth() {
@ -226,9 +229,14 @@ function editRiver(id) {
redrawRiver();
}
function showElevationProfile() {
modules.elevation = true;
showEPForRiver(elSelected.node());
function showRiverElevationProfile() {
const points = debug
.selectAll("#controlPoints > *")
.data()
.map(([x, y]) => findCell(x, y));
const river = getRiver();
const riverLen = rn(river.length * distanceScale);
showElevationProfile(points, riverLen, true);
}
function editRiverLegend() {
@ -266,8 +274,8 @@ function editRiver(id) {
unselect();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,4 +1,5 @@
"use strict";
function overviewRivers() {
if (customization) return;
closeDialogs("#riversOverview, .stable");
@ -34,8 +35,8 @@ function overviewRivers() {
for (const r of pack.rivers) {
const discharge = r.discharge + " m³/s";
const length = rn(r.length * distanceScaleInput.value) + " " + unit;
const width = rn(r.width * distanceScaleInput.value, 3) + " " + unit;
const length = rn(r.length * distanceScale) + " " + unit;
const width = rn(r.width * distanceScale, 3) + " " + unit;
const basin = pack.rivers.find(river => river.i === r.basin)?.name;
lines += /* html */ `<div
@ -49,7 +50,7 @@ function overviewRivers() {
data-basin="${basin}"
>
<span data-tip="Click to focus on river" class="icon-dot-circled pointer"></span>
<div data-tip="River name" class="riverName">${r.name}</div>
<div data-tip="River name" style="margin-left: 0.4em;" class="riverName">${r.name}</div>
<div data-tip="River type name" class="riverType">${r.type}</div>
<div data-tip="River discharge (flux power)" class="biomeArea">${discharge}</div>
<div data-tip="River length from source to mouth" class="biomeArea">${length}</div>
@ -66,16 +67,18 @@ function overviewRivers() {
const averageDischarge = rn(d3.mean(pack.rivers.map(r => r.discharge)));
riversFooterDischarge.innerHTML = averageDischarge + " m³/s";
const averageLength = rn(d3.mean(pack.rivers.map(r => r.length)));
riversFooterLength.innerHTML = averageLength * distanceScaleInput.value + " " + unit;
riversFooterLength.innerHTML = averageLength * distanceScale + " " + unit;
const averageWidth = rn(d3.mean(pack.rivers.map(r => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 3) + " " + unit;
riversFooterWidth.innerHTML = rn(averageWidth * distanceScale, 3) + " " + unit;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => riverHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => riverHighlightOff(ev)));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomToRiver));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openRiverEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerRiverRemove));
body
.querySelectorAll("div > span.icon-trash-empty")
.forEach(el => el.addEventListener("click", triggerRiverRemove));
applySorting(riversHeader);
}
@ -110,7 +113,18 @@ function overviewRivers() {
} else {
rivers.attr("data-basin", "hightlighted");
const basins = [...new Set(pack.rivers.map(r => r.basin))];
const colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"];
const colors = [
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf"
];
basins.forEach((b, i) => {
const color = colors[i % colors.length];
@ -129,8 +143,8 @@ function overviewRivers() {
body.querySelectorAll(":scope > div").forEach(function (el) {
const d = el.dataset;
const discharge = d.discharge + " m³/s";
const length = rn(d.length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const width = rn(d.width * distanceScaleInput.value, 3) + " " + distanceUnitInput.value;
const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
const width = rn(d.width * distanceScale, 3) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(",") + "\n";
});

View file

@ -0,0 +1,85 @@
"use strict";
function editRouteGroups() {
if (customization) return;
if (!layerIsOn("toggleRoutes")) toggleRoutes();
addLines();
$("#routeGroupsEditor").dialog({
title: "Edit Route groups",
resizable: false,
position: {my: "left top", at: "left+10 top+140", of: "#map"}
});
if (modules.editRouteGroups) return;
modules.editRouteGroups = true;
// add listeners
byId("routeGroupsEditorAdd").addEventListener("click", addGroup);
byId("routeGroupsEditorBody").on("click", ev => {
const group = ev.target.parentNode.dataset.id;
if (ev.target.classList.contains("editStyle")) editStyle("routes", group);
else if (ev.target.classList.contains("removeGroup")) removeGroup(group);
});
function addLines() {
byId("routeGroupsEditorBody").innerHTML = "";
const lines = Array.from(routes.selectAll("g")._groups[0]).map(el => {
const count = el.children.length;
return /* html */ `<div data-id="${el.id}" class="states" style="display: flex; justify-content: space-between;">
<span>${el.id} (${count})</span>
<div style="width: auto; display: flex; gap: 0.4em;">
<span data-tip="Edit style" class="editStyle icon-brush pointer" style="font-size: smaller;"></span>
<span data-tip="Remove group" class="removeGroup icon-trash pointer"></span>
</div>
</div>`;
});
byId("routeGroupsEditorBody").innerHTML = lines.join("");
}
const DEFAULT_GROUPS = ["roads", "trails", "searoutes"];
function addGroup() {
prompt("Type group name", {default: "route-group-new"}, v => {
let group = v
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (!group) return tip("Invalid group name", false, "error");
if (!group.startsWith("route-")) group = "route-" + group;
if (byId(group)) return tip("Element with this name already exists. Provide a unique name", false, "error");
if (Number.isFinite(+group.charAt(0))) return tip("Group name should start with a letter", false, "error");
routes
.append("g")
.attr("id", group)
.attr("stroke", "#000000")
.attr("stroke-width", 0.5)
.attr("stroke-dasharray", "1 0.5")
.attr("stroke-linecap", "butt");
byId("routeGroup")?.options.add(new Option(group, group));
addLines();
byId("routeCreatorGroupSelect").options.add(new Option(group, group));
});
}
function removeGroup(group) {
confirmationDialog({
title: "Remove route group",
message:
"Are you sure you want to remove the entire route group? All routes in this group will be removed. This action can't be reverted.",
confirm: "Remove",
onConfirm: () => {
const routes = pack.routes.filter(r => r.group === group);
routes.forEach(r => Routes.remove(r));
if (DEFAULT_GROUPS.includes(group)) routes.select(`#${group}`).remove();
addLines();
}
});
}
}

View file

@ -0,0 +1,140 @@
"use strict";
function createRoute(defaultGroup) {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRoutes")) toggleRoutes();
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
tip("Click to add route point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
viewbox.style("cursor", "crosshair").on("click", onClick);
createRoute.points = [];
const body = byId("routeCreatorBody");
// update route groups
byId("routeCreatorGroupSelect").innerHTML = Array.from(routes.selectAll("g")._groups[0]).map(el => {
const selected = defaultGroup || "roads";
return `<option value="${el.id}" ${el.id === selected ? "selected" : ""}>${el.id}</option>`;
});
$("#routeCreator").dialog({
title: "Create Route",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRouteCreator
});
if (modules.createRoute) return;
modules.createRoute = true;
// add listeners
byId("routeCreatorGroupSelect").on("change", () => drawRoute(createRoute.points));
byId("routeCreatorGroupEdit").on("click", editRouteGroups);
byId("routeCreatorComplete").on("click", completeCreation);
byId("routeCreatorCancel").on("click", () => $("#routeCreator").dialog("close"));
body.on("click", ev => {
if (ev.target.classList.contains("icon-trash-empty")) removePoint(ev.target.parentNode.dataset.point);
});
function onClick() {
const [x, y] = d3.mouse(this);
const cellId = findCell(x, y);
const point = [rn(x, 2), rn(y, 2), cellId];
createRoute.points.push(point);
drawRoute(createRoute.points);
body.innerHTML += `<div class="editorLine" style="display: grid; grid-template-columns: 1fr 1fr 1fr auto; gap: 1em;" data-point="${point.join(
"-"
)}">
<span><b>Cell</b>: ${cellId}</span>
<span><b>X</b>: ${point[0]}</span>
<span><b>Y</b>: ${point[1]}</span>
<span data-tip="Remove the point" class="icon-trash-empty pointer"></span>
</div>`;
}
function removePoint(pointString) {
createRoute.points = createRoute.points.filter(p => p.join("-") !== pointString);
drawRoute(createRoute.points);
body.querySelector(`[data-point='${pointString}']`)?.remove();
}
function drawRoute(points) {
debug
.select("#controlCells")
.selectAll("polygon")
.data(points)
.join("polygon")
.attr("points", p => getPackPolygon(p[2]))
.attr("class", "current");
debug
.select("#controlPoints")
.selectAll("circle")
.data(points)
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6);
const group = byId("routeCreatorGroupSelect").value;
routes.select("#routeTemp").remove();
routes
.select("#" + group)
.append("path")
.attr("d", Routes.getPath({group, points}))
.attr("id", "routeTemp");
}
function completeCreation() {
const points = createRoute.points;
if (points.length < 2) return tip("Add at least 2 points", false, "error");
const routeId = Routes.getNextId();
const group = byId("routeCreatorGroupSelect").value;
const feature = pack.cells.f[points[0][2]];
const route = {points, group, feature, i: routeId};
pack.routes.push(route);
const links = pack.cells.routes;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const nextPoint = points[i + 1];
if (nextPoint) {
const cellId = point[2];
const nextId = nextPoint[2];
if (!links[cellId]) links[cellId] = {};
links[cellId][nextId] = routeId;
if (!links[nextId]) links[nextId] = {};
links[nextId][cellId] = routeId;
}
}
routes.select("#routeTemp").attr("id", "route" + routeId);
editRoute("route" + routeId);
}
function closeRouteCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
debug.select("#controlPoints").remove();
routes.select("#routeTemp").remove();
restoreDefaultEvents();
clearMainTip();
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -1,308 +1,403 @@
"use strict";
const CONTROL_POINST_DISTANCE = 10;
function editRoute(onClick) {
function editRoute(id) {
if (customization) return;
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
if (elSelected && id === elSelected.attr("id")) return;
closeDialogs(".stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes();
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
elSelected = d3.select("#" + id).on("click", addControlPoint);
tip(
"Drag control points to change the route. Click on point to remove it. Click on the route to add additional control point. For major changes please create a new route instead",
true
);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
{
const route = getRoute();
updateRouteData(route);
drawControlPoints(route.points);
drawCells(route.points);
updateLockIcon();
}
$("#routeEditor").dialog({
title: "Edit Route",
resizable: false,
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"},
close: closeRoutesEditor
position: {my: "left top", at: "left+10 top+10", of: "#map"},
close: closeRouteEditor
});
debug.append("g").attr("id", "controlPoints");
const node = onClick ? elSelected.node() : d3.event.target;
elSelected = d3.select(node).on("click", addInterimControlPoint);
drawControlPoints(node);
selectRouteGroup(node);
viewbox.on("touchmove mousemove", showEditorTips);
if (onClick) toggleRouteCreationMode();
if (modules.editRoute) return;
modules.editRoute = true;
// add listeners
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup);
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput);
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile);
byId("routeCreateSelectingCells").on("click", showCreationDialog);
byId("routeSplit").on("click", togglePressed);
byId("routeJoin").on("click", openJoinRoutesDialog);
byId("routeElevationProfile").on("click", showRouteElevationProfile);
byId("routeLegend").on("click", editRouteLegend);
byId("routeLock").on("click", toggleLockButton);
byId("routeRemove").on("click", removeRoute);
byId("routeName").on("input", changeName);
byId("routeGroup").on("input", changeGroup);
byId("routeGroupEdit").on("click", editRouteGroups);
byId("routeEditStyle").on("click", editRouteGroupStyle);
byId("routeGenerateName").on("click", generateName);
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
document.getElementById("routeRemove").addEventListener("click", removeRoute);
function showEditorTips() {
showMainTip();
if (routeNew.classList.contains("pressed")) return;
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point");
else if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
function getRoute() {
const routeId = +elSelected.attr("id").slice(5);
return pack.routes.find(route => route.i === routeId);
}
function drawControlPoints(node) {
const totalLength = node.getTotalLength();
const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE);
for (let i = 0; i <= totalLength; i += increment) {
const point = node.getPointAtLength(i);
addControlPoint([point.x, point.y]);
}
routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value;
function updateRouteData(route) {
route.name = route.name || Routes.generateName(route);
byId("routeName").value = route.name;
const routeGroup = byId("routeGroup");
routeGroup.options.length = 0;
routes.selectAll("g").each(function () {
routeGroup.options.add(new Option(this.id, this.id, false, this.id === route.group));
});
updateRouteLength(route);
const isWater = route.points.some(([x, y, cellId]) => pack.cells.h[cellId] < 20);
byId("routeElevationProfile").style.display = isWater ? "none" : "inline-block";
}
function addControlPoint(point, before = null) {
debug
.select("#controlPoints")
.insert("circle", before)
.attr("cx", point[0])
.attr("cy", point[1])
.attr("r", 0.6)
.call(d3.drag().on("drag", dragControlPoint))
.on("click", clickControlPoint);
function updateRouteLength(route) {
route.length = Routes.getLength(route.i);
byId("routeLength").value = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const controls = document.getElementById("controlPoints").querySelectorAll("circle");
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]);
const index = getSegmentId(points, point, 2);
addControlPoint(point, ":nth-child(" + (index + 1) + ")");
redrawRoute();
}
function dragControlPoint() {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
redrawRoute();
}
function redrawRoute() {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const points = [];
function drawControlPoints(points) {
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
});
elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
if (modules.elevation) showEPForRoute(elSelected.node());
.data(points)
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.call(d3.drag().on("start", dragControlPoint))
.on("click", handleControlPointClick);
}
function showElevationProfile() {
modules.elevation = true;
showEPForRoute(elSelected.node());
function drawCells(points) {
debug
.select("#controlCells")
.selectAll("polygon")
.data(points)
.join("polygon")
.attr("points", p => getPackPolygon(p[2]));
}
function showGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("routeGroupsSelection").style.display = "inline-block";
}
function dragControlPoint() {
const route = getRoute();
const initCell = d3.event.subject[2];
const pointIndex = route.points.indexOf(d3.event.subject);
function hideGroupSection() {
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("routeGroupsSelection").style.display = "none";
document.getElementById("routeGroupName").style.display = "none";
document.getElementById("routeGroupName").value = "";
document.getElementById("routeGroup").style.display = "inline-block";
}
d3.event.on("drag", function () {
this.setAttribute("cx", d3.event.x);
this.setAttribute("cy", d3.event.y);
function selectRouteGroup(node) {
const group = node.parentNode.id;
const select = document.getElementById("routeGroup");
select.options.length = 0; // remove all options
const x = rn(d3.event.x, 2);
const y = rn(d3.event.y, 2);
const cellId = findCell(x, y);
routes.selectAll("g").each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
this.__data__ = route.points[pointIndex] = [x, y, cellId];
redrawRoute(route);
drawCells(route.points);
});
}
function changeRouteGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
}
d3.event.on("end", () => {
const movedToCell = findCell(d3.event.x, d3.event.y);
function toggleNewGroupInput() {
if (routeGroupName.style.display === "none") {
routeGroupName.style.display = "inline-block";
routeGroupName.focus();
routeGroup.style.display = "none";
} else {
routeGroupName.style.display = "none";
routeGroup.style.display = "inline-block";
}
}
if (movedToCell !== initCell) {
const prev = route.points[pointIndex - 1];
if (prev) {
removeConnection(initCell, prev[2]);
addConnection(movedToCell, prev[2], route.i);
}
function createNewGroup() {
if (!this.value) {
tip("Please provide a valid group name");
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip("Group name should start with a letter", false, "error");
return;
}
// just rename if only 1 element left
const oldGroup = elSelected.node().parentNode;
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
if (!basic && oldGroup.childElementCount === 1) {
document.getElementById("routeGroup").selectedOptions[0].remove();
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("routes").appendChild(newGroup);
newGroup.id = group;
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("routeGroupName").value = "";
}
function removeRouteGroup() {
const group = elSelected.node().parentNode.id;
const basic = ["roads", "trails", "searoutes"].includes(group);
const count = elSelected.node().parentNode.childElementCount;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic ? "all elements in the group" : "the entire route group"
}? <br /><br />Routes to be
removed: ${count}`;
$("#alert").dialog({
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#routeEditor").dialog("close");
hideGroupSection();
if (basic)
routes
.select("#" + group)
.selectAll("path")
.remove();
else routes.select("#" + group).remove();
},
Cancel: function () {
$(this).dialog("close");
const next = route.points[pointIndex + 1];
if (next) {
removeConnection(initCell, next[2]);
addConnection(movedToCell, next[2], route.i);
}
}
});
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle("routes", g);
function redrawRoute(route) {
elSelected.attr("d", Routes.getPath(route));
updateRouteLength(route);
if (byId("elevationProfile").offsetParent) showRouteElevationProfile();
}
function toggleRouteSplitMode() {
document.getElementById("routeNew").classList.remove("pressed");
function addControlPoint() {
const route = getRoute();
const [x, y] = d3.mouse(this);
const cellId = findCell(x, y);
const point = [rn(x, 2), rn(y, 2), cellId];
const isNewCell = !route.points.some(p => p[2] === cellId);
const index = getSegmentId(route.points, point, 2);
route.points.splice(index, 0, point);
// check if added point is in new cell
if (isNewCell) {
const prev = route.points[index - 1];
const next = route.points[index + 1];
if (!prev) ERROR && console.error("Can't add control point to the start of the route");
if (!next) ERROR && console.error("Can't add control point to the end of the route");
if (!prev || !next) return;
removeConnection(prev[2], next[2]);
addConnection(prev[2], cellId, route.i);
addConnection(cellId, next[2], route.i);
drawCells(route.points);
}
drawControlPoints(route.points);
redrawRoute(route);
}
function handleControlPointClick() {
const controlPoint = d3.select(this);
const point = controlPoint.datum();
const route = getRoute();
const index = route.points.indexOf(point);
const isSplitMode = byId("routeSplit").classList.contains("pressed");
return isSplitMode ? splitRoute() : removeControlPoint(controlPoint);
function splitRoute() {
const oldRoutePoints = route.points.slice(0, index + 1);
const newRoutePoints = route.points.slice(index);
// update old route
route.points = oldRoutePoints;
drawControlPoints(route.points);
drawCells(route.points);
redrawRoute(route);
// create new route
const newRoute = {
i: Routes.getNextId(),
group: route.group,
feature: route.feature,
name: route.name,
points: newRoutePoints
};
pack.routes.push(newRoute);
for (let i = 0; i < newRoute.points.length; i++) {
const cellId = newRoute.points[i][2];
const nextPoint = newRoute.points[i + 1];
if (nextPoint) addConnection(cellId, nextPoint[2], newRoute.i);
}
routes
.select("#" + newRoute.group)
.append("path")
.attr("d", Routes.getPath(newRoute))
.attr("id", "route" + newRoute.i);
byId("routeSplit").classList.remove("pressed");
}
function removeControlPoint(controlPoint) {
const isOnlyPointInCell = route.points.filter(p => p[2] === point[2]).length === 1;
if (isOnlyPointInCell) {
const prev = route.points[index - 1];
const next = route.points[index + 1];
if (prev) removeConnection(prev[2], point[2]);
if (next) removeConnection(point[2], next[2]);
if (prev && next) addConnection(prev[2], next[2], route.i);
}
controlPoint.remove();
route.points = route.points.filter(p => p !== point);
drawCells(route.points);
redrawRoute(route);
}
}
function openJoinRoutesDialog() {
const route = getRoute();
const firstCell = route.points.at(0)[2];
const lastCell = route.points.at(-1)[2];
const candidateRoutes = pack.routes.filter(r => {
if (r.i === route.i) return false;
if (r.group !== route.group) return false;
if (r.points.at(0)[2] === lastCell) return true;
if (r.points.at(-1)[2] === firstCell) return true;
if (r.points.at(0)[2] === firstCell) return true;
if (r.points.at(-1)[2] === lastCell) return true;
return false;
});
if (candidateRoutes.length) {
const options = candidateRoutes.map(r => {
r.name = r.name || Routes.generateName(r);
r.length = r.length || Routes.getLength(r.i);
const length = rn(r.length * distanceScale) + " " + distanceUnitInput.value;
return `<option value="${r.i}">${r.name} (${length})</option>`;
});
alertMessage.innerHTML = /* html */ `<div>Route to join with:
<select>${options.join("")}</select>
</div>`;
$("#alert").dialog({
title: "Join routes",
width: fitContent(),
position: {my: "left top", at: "left+10 top+150", of: "#map"},
buttons: {
Cancel: () => {
$("#alert").dialog("close");
},
Join: () => {
const selectedRouteId = +alertMessage.querySelector("select").value;
const selectedRoute = pack.routes.find(r => r.i === selectedRouteId);
joinRoutes(route, selectedRoute);
tip("Routes joined", false, "success", 5000);
$("#alert").dialog("close");
}
}
});
} else {
tip("No routes to join with. Route must start or end at current route's start or end cell", false, "error", 4000);
}
}
function joinRoutes(route, joinedRoute) {
if (route.points.at(-1)[2] === joinedRoute.points.at(0)[2]) {
// joinedRoute starts at the end of current route
route.points = [...route.points, ...joinedRoute.points.slice(1)];
} else if (route.points.at(0)[2] === joinedRoute.points.at(-1)[2]) {
// joinedRoute ends at the start of current route
route.points = [...joinedRoute.points, ...route.points.slice(1)];
} else if (route.points.at(0)[2] === joinedRoute.points.at(0)[2]) {
// joinedRoute and current route both start at the same cell
route.points = [...route.points.reverse(), ...joinedRoute.points.slice(1)];
} else if (route.points.at(-1)[2] === joinedRoute.points.at(-1)[2]) {
// joinedRoute and current route both end at the same cell
route.points = [...route.points, ...joinedRoute.points.reverse().slice(1)];
}
for (let i = 0; i < route.points.length; i++) {
const point = route.points[i];
const nextPoint = route.points[i + 1];
if (nextPoint) addConnection(point[2], nextPoint[2], route.i);
}
Routes.remove(joinedRoute);
drawControlPoints(route.points);
redrawRoute(route);
drawCells(route.points);
}
function showCreationDialog() {
const route = getRoute();
createRoute(route.group);
}
function togglePressed() {
this.classList.toggle("pressed");
}
function clickControlPoint() {
if (routeSplit.classList.contains("pressed")) splitRoute(this);
else {
this.remove();
redrawRoute();
}
function removeConnection(from, to) {
const routes = pack.cells.routes;
if (routes[from]) delete routes[from][to];
if (routes[to]) delete routes[to][from];
}
function splitRoute(clicked) {
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const group = d3.select(elSelected.node().parentNode);
routeSplit.classList.remove("pressed");
function addConnection(from, to, routeId) {
const routes = pack.cells.routes;
const points1 = [],
points2 = [];
let points = points1;
debug
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
if (this === clicked) {
points = points2;
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
}
this.remove();
});
if (!routes[from]) routes[from] = {};
routes[from][to] = routeId;
elSelected.attr("d", round(lineGen(points1)));
const id = getNextId("route");
group.append("path").attr("id", id).attr("d", lineGen(points2));
debug.select("#controlPoints").selectAll("circle").remove();
drawControlPoints(elSelected.node());
if (!routes[to]) routes[to] = {};
routes[to][from] = routeId;
}
function toggleRouteCreationMode() {
document.getElementById("routeSplit").classList.remove("pressed");
document.getElementById("routeNew").classList.toggle("pressed");
if (document.getElementById("routeNew").classList.contains("pressed")) {
tip("Click on map to add control points", true);
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
elSelected.on("click", null);
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
}
function changeName() {
getRoute().name = this.value;
}
function addPointOnClick() {
// create new route
if (!elSelected.attr("data-new")) {
debug.select("#controlPoints").selectAll("circle").remove();
const parent = elSelected.node().parentNode;
const id = getNextId("route");
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
}
function changeGroup() {
const group = this.value;
byId(group).appendChild(elSelected.node());
getRoute().group = group;
}
addControlPoint(d3.mouse(this));
redrawRoute();
function generateName() {
const route = getRoute();
route.name = routeName.value = Routes.generateName(route);
}
function showRouteElevationProfile() {
const route = getRoute();
const length = rn(route.length * distanceScale);
showElevationProfile(
route.points.map(p => p[2]),
length,
false
);
}
function editRouteLegend() {
const id = elSelected.attr("id");
editNotes(id, id);
const route = getRoute();
editNotes(id, route.name);
}
function editRouteGroupStyle() {
const {group} = getRoute();
editStyle("routes", group);
}
function toggleLockButton() {
const route = getRoute();
route.lock = !route.lock;
updateLockIcon();
}
function updateLockIcon() {
const route = getRoute();
if (route.lock) {
byId("routeLock").classList.remove("icon-lock-open");
byId("routeLock").classList.add("icon-lock");
} else {
byId("routeLock").classList.remove("icon-lock");
byId("routeLock").classList.add("icon-lock-open");
}
}
function removeRoute() {
alertMessage.innerHTML = "Are you sure you want to remove the route?";
alertMessage.innerHTML = "Are you sure you want to remove the route";
$("#alert").dialog({
resizable: false,
width: "22em",
title: "Remove route",
buttons: {
Remove: function () {
Routes.remove(getRoute());
$(this).dialog("close");
elSelected.remove();
$("#routeEditor").dialog("close");
},
Cancel: function () {
@ -312,12 +407,16 @@ function editRoute(onClick) {
});
}
function closeRoutesEditor() {
elSelected.attr("data-new", null).on("click", null);
clearMainTip();
routeSplit.classList.remove("pressed");
routeNew.classList.remove("pressed");
function closeRouteEditor() {
debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
unselect();
clearMainTip();
const forced = +byId("toggleCells").dataset.forced;
byId("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
}
}

View file

@ -0,0 +1,187 @@
"use strict";
function overviewRoutes() {
if (customization) return;
closeDialogs("#routesOverview, .stable");
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const body = byId("routesBody");
routesOverviewAddLines();
$("#routesOverview").dialog();
if (modules.overviewRoutes) return;
modules.overviewRoutes = true;
$("#routesOverview").dialog({
title: "Routes Overview",
resizable: false,
width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
byId("routesOverviewRefresh").on("click", routesOverviewAddLines);
byId("routesCreateNew").on("click", createRoute);
byId("routesExport").on("click", downloadRoutesData);
byId("routesLockAll").on("click", toggleLockAll);
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
// add line for each route
function routesOverviewAddLines() {
body.innerHTML = "";
let lines = "";
for (const route of pack.routes) {
route.name = route.name || Routes.generateName(route);
route.length = route.length || Routes.getLength(route.i);
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
lines += /* html */ `<div
class="states"
data-id="${route.i}"
data-name="${route.name}"
data-group="${route.group}"
data-length="${route.length}"
>
<span data-tip="Click to focus on route" class="icon-dot-circled pointer"></span>
<div data-tip="Route name" style="width: 15em; margin-left: 0.4em;">${route.name}</div>
<div data-tip="Route group" style="width: 8em;">${route.group}</div>
<div data-tip="Route length" style="width: 6em;">${length}</div>
<span data-tip="Edit route" class="icon-pencil"></span>
<span class="locks pointer ${
route.lock ? "icon-lock" : "icon-lock-open inactive"
}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove route" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML("beforeend", lines);
// update footer
routesFooterNumber.innerHTML = pack.routes.length;
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)) || 0);
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", routeHighlightOn));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove));
applySorting(routesHeader);
}
function routeHighlightOn(event) {
if (!layerIsOn("toggleRoutes")) toggleRoutes();
const routeId = +event.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "none");
}
function routeHighlightOff(e) {
const routeId = +e.target.dataset.id;
routes
.select("#route" + routeId)
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null);
}
function zoomToRoute() {
const r = +this.parentNode.dataset.id;
const route = routes.select("#route" + r).node();
highlightElement(route, 3);
}
function downloadRoutesData() {
let data = "Id,Route,Group,Length\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
const d = el.dataset;
const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
data += [d.id, d.name, d.group, length].join(",") + "\n";
});
const name = getFileName("Routes") + ".csv";
downloadFile(data, name);
}
function openRouteEditor() {
const id = "route" + this.parentNode.dataset.id;
editRoute(id);
}
function toggleLockStatus() {
const routeId = +this.parentNode.dataset.id;
const route = pack.routes[routeId];
route.lock = !route.lock;
if (this.classList.contains("icon-lock")) {
this.classList.remove("icon-lock");
this.classList.add("icon-lock-open");
this.classList.add("inactive");
} else {
this.classList.remove("icon-lock-open");
this.classList.add("icon-lock");
this.classList.remove("inactive");
}
}
function toggleLockAll() {
const allLocked = pack.routes.every(route => route.lock);
pack.routes.forEach(route => {
route.lock = !allLocked;
});
routesOverviewAddLines();
byId("routesLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
}
function triggerRouteRemove() {
const routeId = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
$("#alert").dialog({
resizable: false,
width: "22em",
title: "Remove route",
buttons: {
Remove: function () {
const route = pack.routes.find(r => r.i === routeId);
Routes.remove(route);
routesOverviewAddLines();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function triggerAllRoutesRemove() {
alertMessage.innerHTML = /* html */ `Are you sure you want to remove all routes? This action can't be undone`;
$("#alert").dialog({
resizable: false,
title: "Remove all routes",
buttons: {
Remove: function () {
pack.cells.routes = {};
pack.routes = [];
routes.selectAll("path").remove();
routesOverviewAddLines();
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
}

View file

@ -14,6 +14,7 @@
byId("styleFilterInput").innerHTML = allOptions;
byId("styleStatesBodyFilter").innerHTML = allOptions;
byId("styleScaleBarBackgroundFilter").innerHTML = allOptions;
}
// store some style inputs as options
@ -31,6 +32,7 @@ function editStyle(element, group) {
styleElementSelect.classList.add("glow");
if (group) styleGroupSelect.classList.add("glow");
setTimeout(() => {
styleElementSelect.classList.remove("glow");
if (group) styleGroupSelect.classList.remove("glow");
@ -81,10 +83,10 @@ function selectStyleElement() {
styleIsOff.style.display = isLayerOff ? "block" : "none";
// active group element
const group = styleGroupSelect.value;
if (["routes", "labels", "coastline", "lakes", "anchors", "burgIcons", "borders"].includes(styleElement)) {
const gEl = group && el.select("#" + group);
el = group && gEl.size() ? gEl : el.select("g");
if (["routes", "labels", "coastline", "lakes", "anchors", "burgIcons", "borders", "terrs"].includes(styleElement)) {
const group = styleGroupSelect.value;
const defaultGroupSelector = styleElement === "terrs" ? "#landHeights" : "g";
el = group && el.select("#" + group).size() ? el.select("#" + group) : el.select(defaultGroupSelector);
}
// opacity
@ -94,13 +96,13 @@ function selectStyleElement() {
}
// filter
if (!["landmass", "legend", "regions"].includes(styleElement)) {
if (!["landmass", "legend", "regions", "scaleBar"].includes(styleElement)) {
styleFilter.style.display = "block";
styleFilterInput.value = el.attr("filter") || "";
}
// fill
if (["rivers", "lakes", "landmass", "prec", "ice", "fogging", "vignette"].includes(styleElement)) {
if (["rivers", "lakes", "landmass", "prec", "ice", "fogging", "scaleBar", "vignette"].includes(styleElement)) {
styleFill.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill");
}
@ -170,11 +172,14 @@ function selectStyleElement() {
if (styleElement === "terrs") {
styleHeightmap.style.display = "block";
styleHeightmapScheme.value = terrs.attr("scheme");
styleHeightmapTerracingInput.value = styleHeightmapTerracingOutput.value = terrs.attr("terracing");
styleHeightmapSkipInput.value = styleHeightmapSkipOutput.value = terrs.attr("skip");
styleHeightmapSimplificationInput.value = styleHeightmapSimplificationOutput.value = terrs.attr("relax");
styleHeightmapCurve.value = terrs.attr("curve");
styleHeightmapRenderOceanOption.style.display = el.attr("id") === "oceanHeights" ? "block" : "none";
styleHeightmapRenderOcean.checked = +el.attr("data-render");
styleHeightmapScheme.value = el.attr("scheme");
styleHeightmapTerracingInput.value = styleHeightmapTerracingOutput.value = el.attr("terracing");
styleHeightmapSkipInput.value = styleHeightmapSkipOutput.value = el.attr("skip");
styleHeightmapSimplificationInput.value = styleHeightmapSimplificationOutput.value = el.attr("relax");
styleHeightmapCurve.value = el.attr("curve");
}
if (styleElement === "markers") {
@ -336,7 +341,7 @@ function selectStyleElement() {
// update group options
styleGroupSelect.options.length = 0; // remove all options
if (["routes", "labels", "coastline", "lakes", "anchors", "burgIcons", "borders"].includes(styleElement)) {
if (["routes", "labels", "coastline", "lakes", "anchors", "burgIcons", "borders", "terrs"].includes(styleElement)) {
const groups = byId(styleElement).querySelectorAll("g");
groups.forEach(el => {
if (el.id === "burgLabels") return;
@ -356,6 +361,31 @@ function selectStyleElement() {
if (auto) styleFilter.style.display = "none";
}
if (styleElement === "scaleBar") {
styleScaleBar.style.display = "block";
styleScaleBarSize.value = el.attr("data-bar-size");
styleScaleBarFontSize.value = el.attr("font-size");
styleScaleBarPositionX.value = el.attr("data-x") || "99";
styleScaleBarPositionY.value = el.attr("data-y") || "99";
styleScaleBarLabel.value = el.attr("data-label") || "";
const scaleBarBack = el.select("#scaleBarBack");
if (scaleBarBack.size()) {
styleScaleBarBackgroundOpacityInput.value = styleScaleBarBackgroundOpacityOutput.value =
scaleBarBack.attr("opacity");
styleScaleBarBackgroundFillInput.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundStrokeInput.value = styleScaleBarBackgroundStrokeOutput.value =
scaleBarBack.attr("stroke");
styleScaleBarBackgroundStrokeWidth.value = scaleBarBack.attr("stroke-width");
styleScaleBarBackgroundFilter.value = scaleBarBack.attr("filter");
styleScaleBarBackgroundPaddingTop.value = scaleBarBack.attr("data-top");
styleScaleBarBackgroundPaddingRight.value = scaleBarBack.attr("data-right");
styleScaleBarBackgroundPaddingBottom.value = scaleBarBack.attr("data-bottom");
styleScaleBarBackgroundPaddingLeft.value = scaleBarBack.attr("data-left");
}
}
if (styleElement === "vignette") {
styleVignette.style.display = "block";
@ -473,7 +503,7 @@ styleGridScale.addEventListener("input", function () {
function calculateFriendlyGridSize() {
const size = styleGridScale.value * 25;
const friendly = `${rn(size * distanceScaleInput.value, 2)} ${distanceUnitInput.value}`;
const friendly = `${rn(size * distanceScale, 2)} ${distanceUnitInput.value}`;
styleGridSizeFriendly.value = friendly;
}
@ -519,18 +549,16 @@ outlineLayers.addEventListener("change", function () {
});
styleHeightmapScheme.addEventListener("change", function () {
terrs.attr("scheme", this.value);
getEl().attr("scheme", this.value);
drawHeightmap();
});
openCreateHeightmapSchemeButton.addEventListener("click", function () {
// start with current scheme
this.dataset.stops = terrs.attr("scheme").startsWith("#")
? terrs.attr("scheme")
: (function () {
const scheme = heightmapColorSchemes[terrs.attr("scheme")];
return [0, 0.25, 0.5, 0.75, 1].map(scheme).map(toHEX).join(",");
})();
const scheme = getEl().attr("scheme");
this.dataset.stops = scheme.startsWith("#")
? scheme
: (() => [0, 0.25, 0.5, 0.75, 1].map(heightmapColorSchemes[scheme]).map(toHEX).join(","))();
// render dialog base structure
alertMessage.innerHTML = /* html */ `<div>
@ -622,7 +650,7 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () {
if (stops in heightmapColorSchemes) return tip("This scheme already exists", false, "error");
addCustomColorScheme(stops);
terrs.attr("scheme", stops);
getEl().attr("scheme", stops);
drawHeightmap();
handleClose();
@ -644,23 +672,28 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () {
});
});
styleHeightmapRenderOcean.addEventListener("change", function () {
getEl().attr("data-render", +this.checked);
drawHeightmap();
});
styleHeightmapTerracingInput.addEventListener("input", function () {
terrs.attr("terracing", this.value);
getEl().attr("terracing", this.value);
drawHeightmap();
});
styleHeightmapSkipInput.addEventListener("input", function () {
terrs.attr("skip", this.value);
getEl().attr("skip", this.value);
drawHeightmap();
});
styleHeightmapSimplificationInput.addEventListener("input", function () {
terrs.attr("relax", this.value);
getEl().attr("relax", this.value);
drawHeightmap();
});
styleHeightmapCurve.addEventListener("change", function () {
terrs.attr("curve", this.value);
getEl().attr("curve", this.value);
drawHeightmap();
});
@ -957,7 +990,7 @@ function textureProvideURL() {
}
function fetchTextureURL(url) {
INFO && console.log("Provided URL is", url);
INFO && console.info("Provided URL is", url);
const img = new Image();
img.onload = function () {
const canvas = byId("texturePreview");
@ -1043,6 +1076,44 @@ styleVignetteBlur.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("filter", `blur(${this.value}px)`);
});
styleScaleBar.addEventListener("input", function (event) {
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (!scaleBarBack.size()) return;
const {id, value} = event.target;
if (id === "styleScaleBarSize") scaleBar.attr("data-bar-size", value);
else if (id === "styleScaleBarFontSize") scaleBar.attr("font-size", value);
else if (id === "styleScaleBarPositionX") scaleBar.attr("data-x", value);
else if (id === "styleScaleBarPositionY") scaleBar.attr("data-y", value);
else if (id === "styleScaleBarLabel") scaleBar.attr("data-label", value);
else if (id === "styleScaleBarBackgroundOpacityInput") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFillInput") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStrokeInput") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundStrokeWidth") scaleBarBack.attr("stroke-width", value);
else if (id === "styleScaleBarBackgroundFilter") scaleBarBack.attr("filter", value);
else if (id === "styleScaleBarBackgroundPaddingTop") scaleBarBack.attr("data-top", value);
else if (id === "styleScaleBarBackgroundPaddingRight") scaleBarBack.attr("data-right", value);
else if (id === "styleScaleBarBackgroundPaddingBottom") scaleBarBack.attr("data-bottom", value);
else if (id === "styleScaleBarBackgroundPaddingLeft") scaleBarBack.attr("data-left", value);
if (
[
"styleScaleBarSize",
"styleScaleBarPositionX",
"styleScaleBarPositionY",
"styleScaleBarLabel",
"styleScaleBarBackgroundPaddingLeft",
"styleScaleBarBackgroundPaddingTop",
"styleScaleBarBackgroundPaddingRight",
"styleScaleBarBackgroundPaddingBottom"
].includes(id)
) {
drawScaleBar(scaleBar, scale);
fitScaleBar(scaleBar, svgWidth, svgHeight);
}
});
function updateElements() {
// burgIcons to desired size
burgIcons.selectAll("g").each(function () {

View file

@ -10,6 +10,7 @@ const systemPresets = [
"watercolor",
"clean",
"atlas",
"darkSeas",
"cyberpunk",
"night",
"monochrome"
@ -63,7 +64,7 @@ async function getStylePreset(desiredPreset) {
async function fetchSystemPreset(preset) {
try {
const res = await fetch(`./styles/${preset}.json`);
const res = await fetch(`./styles/${preset}.json?v=${version}`);
return await res.json();
} catch (err) {
throw new Error("Cannot fetch style preset", preset);
@ -140,6 +141,9 @@ function applyStyleWithUiRefresh(style) {
invokeActiveZooming();
setPresetRemoveButtonVisibiliy();
drawScaleBar(scaleBar, scale);
fitScaleBar(scaleBar, svgWidth, svgHeight);
}
function addStylePreset() {
@ -195,7 +199,7 @@ function addStylePreset() {
"mask"
],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"],
"#compass > use": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
"#cults": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#landmass": ["opacity", "fill", "filter"],
@ -239,7 +243,18 @@ function addStylePreset() {
"#oceanLayers": ["filter", "layers"],
"#oceanBase": ["fill"],
"#oceanicPattern": ["href", "opacity"],
"#terrs": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
"#terrs #oceanHeights": [
"data-render",
"opacity",
"scheme",
"terracing",
"skip",
"relax",
"curve",
"filter",
"mask"
],
"#terrs #landHeights": ["opacity", "scheme", "terracing", "skip", "relax", "curve", "filter", "mask"],
"#legend": [
"data-size",
"font-size",
@ -301,7 +316,19 @@ function addStylePreset() {
],
"#fogging": ["opacity", "fill", "filter"],
"#vignette": ["opacity", "fill", "filter"],
"#vignette-rect": ["x", "y", "width", "height", "rx", "ry", "filter"]
"#vignette-rect": ["x", "y", "width", "height", "rx", "ry", "filter"],
"#scaleBar": ["opacity", "fill", "font-size", "data-bar-size", "data-x", "data-y", "data-label"],
"#scaleBarBack": [
"opacity",
"fill",
"stroke",
"stroke-width",
"filter",
"data-top",
"data-right",
"data-bottom",
"data-left"
]
};
for (const selector in attributes) {

View file

@ -258,11 +258,16 @@ window.UISubmap = (function () {
byId("latitudeInput").value = latitudeOutput.value;
// fix scale
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2);
distanceScale =
distanceScaleInput.value =
distanceScaleOutput.value =
rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn(
(populationRate = populationRateOutput.value / scale),
2
);
customization = 0;
startResample(options);
}, 1000);

View file

@ -22,6 +22,7 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "editZonesButton") editZones();
else if (button === "overviewChartsButton") overviewCharts();
else if (button === "overviewBurgsButton") overviewBurgs();
else if (button === "overviewRoutesButton") overviewRoutes();
else if (button === "overviewRiversButton") overviewRivers();
else if (button === "overviewMilitaryButton") overviewMilitary();
else if (button === "overviewMarkersButton") overviewMarkers();
@ -66,7 +67,7 @@ toolsContent.addEventListener("click", function (event) {
if (button === "addLabel") toggleAddLabel();
else if (button === "addBurgTool") toggleAddBurg();
else if (button === "addRiver") toggleAddRiver();
else if (button === "addRoute") toggleAddRoute();
else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
@ -79,7 +80,7 @@ function processFeatureRegeneration(event, button) {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
Routes.regenerate();
regenerateRoutes();
if (!layerIsOn("toggleRoutes")) toggleRoutes();
} else if (button === "regenerateRivers") regenerateRivers();
else if (button === "regeneratePopulation") recalculatePopulation();
@ -115,6 +116,14 @@ async function openEmblemEditor() {
editEmblem(type, id, el);
}
function regenerateRoutes() {
const locked = pack.routes.filter(route => route.lock).map((route, index) => ({...route, i: index}));
Routes.generate(locked);
routes.selectAll("path").remove();
if (layerIsOn("toggleRoutes")) drawRoutes();
}
function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
@ -129,7 +138,7 @@ function recalculatePopulation() {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.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, 0.6, 20, 3), 3);
@ -247,13 +256,16 @@ function recreateStates() {
capitalsTree.add([x, y]);
// update label id reference
labels
.select("#states")
.select(`#stateLabel${state.i}`)
.attr("id", `stateLabel${newId}`)
.select("textPath")
.attr("xlink:href", `#textPath_stateLabel${newId}`);
defs.select("#textPaths").select(`#textPath_stateLabel${state.i}`).attr("id", `textPath_stateLabel${newId}`);
byId(`textPath_stateLabel${state.i}`)?.setAttribute("id", `textPath_stateLabel${newId}`);
const $label = byId(`stateLabel${state.i}`);
if ($label) {
$label.setAttribute("id", `stateLabel${newId}`);
const $textPath = $label.querySelector("textPath");
if ($textPath) {
$textPath.removeAttribute("href");
$textPath.setAttribute("href", `#textPath_stateLabel${newId}`);
}
}
// update emblem id reference
byId(`stateCOA${state.i}`)?.setAttribute("id", `stateCOA${newId}`);
@ -332,29 +344,44 @@ function regenerateProvinces() {
}
function regenerateBurgs() {
const {cells, states} = pack;
const lockedburgs = pack.burgs.filter(b => b.i && !b.removed && b.lock);
const {cells, features, burgs, states, provinces} = pack;
rankCells();
cells.burg = new Uint16Array(cells.i.length);
const burgs = (pack.burgs = [0]); // clear burgs array
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
pack.provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
// remove notes for unlocked burgs
notes = notes.filter(note => {
if (note.id.startsWith("burg")) {
const burgId = +note.id.slice(4);
return burgs[burgId]?.lock;
}
return true;
});
const newBurgs = [0]; // new burgs array
const burgsTree = d3.quadtree();
// add locked burgs
cells.burg = new Uint16Array(cells.i.length); // clear cells burg data
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
// readd locked burgs
const lockedburgs = burgs.filter(burg => burg.i && !burg.removed && burg.lock);
for (let j = 0; j < lockedburgs.length; j++) {
const id = burgs.length;
const lockedBurg = lockedburgs[j];
lockedBurg.i = id;
burgs.push(lockedBurg);
const newId = newBurgs.length;
const noteIndex = notes.findIndex(note => note.id === `burg${lockedBurg.i}`);
if (noteIndex !== -1) notes[noteIndex].id = `burg${newId}`;
lockedBurg.i = newId;
newBurgs.push(lockedBurg);
burgsTree.add([lockedBurg.x, lockedBurg.y]);
cells.burg[lockedBurg.cell] = id;
cells.burg[lockedBurg.cell] = newId;
if (lockedBurg.capital) {
const stateId = lockedBurg.state;
states[stateId].capital = id;
states[stateId].capital = newId;
states[stateId].center = lockedBurg.cell;
}
}
@ -367,8 +394,8 @@ function regenerateBurgs() {
existingStatesCount;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
for (let i = 0; i < sorted.length && newBurgs.length < burgsCount; i++) {
const id = newBurgs.length;
const cell = sorted[i];
const [x, y] = cells.p[cell];
@ -384,31 +411,34 @@ function regenerateBurgs() {
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
burgs.push({cell, x, y, state: stateId, i: id, culture, name, capital, feature: cells.f[cell]});
newBurgs.push({cell, x, y, state: stateId, i: id, culture, name, capital, feature: cells.f[cell]});
burgsTree.add([x, y]);
cells.burg[cell] = id;
}
pack.burgs = newBurgs; // assign new burgs array
// add a capital at former place for states without added capitals
states
.filter(s => s.i && !s.removed && !s.capital)
.forEach(s => {
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
s.capital = burg;
s.center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1;
pack.burgs[burg].state = s.i;
moveBurgToGroup(burg, "cities");
const [x, y] = cells.p[s.center];
const burgId = addBurg([x, y]);
s.capital = burgId;
s.center = pack.burgs[burgId].cell;
pack.burgs[burgId].capital = 1;
pack.burgs[burgId].state = s.i;
moveBurgToGroup(burgId, "cities");
});
pack.features.forEach(f => {
features.forEach(f => {
if (f.port) f.port = 0; // reset features ports counter
});
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
Routes.regenerate();
regenerateRoutes();
// remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
@ -771,34 +801,6 @@ function addRiverOnClick() {
}
}
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRoute.classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
tip("Click on map to add a first control point", true);
if (!layerIsOn("toggleRoutes")) toggleRoutes();
}
function addRouteOnClick() {
unpressClickToAddButton();
const point = d3.mouse(this);
const id = getNextId("route");
elSelected = routes
.select("g")
.append("path")
.attr("id", id)
.attr("data-new", 1)
.attr("d", `M${point[0]},${point[1]}`);
editRoute(true);
}
function toggleAddMarker() {
const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
if (pressed) {
@ -939,6 +941,6 @@ function viewCellDetails() {
}
async function overviewCharts() {
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.89.24");
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.99.00");
Overview.open();
}

View file

@ -24,13 +24,6 @@ function editUnits() {
byId("heightExponentInput").addEventListener("input", changeHeightExponent);
byId("heightExponentOutput").addEventListener("input", changeHeightExponent);
byId("temperatureScale").addEventListener("change", changeTemperatureScale);
byId("barSizeOutput").addEventListener("input", renderScaleBar);
byId("barSizeInput").addEventListener("input", renderScaleBar);
byId("barLabel").addEventListener("input", renderScaleBar);
byId("barPosX").addEventListener("input", fitScaleBar);
byId("barPosY").addEventListener("input", fitScaleBar);
byId("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
byId("barBackColor").addEventListener("input", changeScaleBarColor);
byId("populationRateOutput").addEventListener("input", changePopulationRate);
byId("populationRateInput").addEventListener("change", changePopulationRate);
@ -62,6 +55,7 @@ function editUnits() {
}
function changeDistanceScale() {
distanceScale = +this.value;
renderScaleBar();
calculateFriendlyGridSize();
}
@ -84,14 +78,6 @@ function editUnits() {
if (layerIsOn("toggleTemp")) drawTemp();
}
function changeScaleBarOpacity() {
scaleBar.select("rect").attr("opacity", this.value);
}
function changeScaleBarColor() {
scaleBar.select("rect").attr("fill", this.value);
}
function changePopulationRate() {
populationRate = +this.value;
}
@ -105,10 +91,9 @@ function editUnits() {
}
function restoreDefaultUnits() {
// distanceScale
distanceScale = 3;
byId("distanceScaleOutput").value = 3;
byId("distanceScaleInput").value = 3;
byId("distanceScaleOutput").value = distanceScale;
byId("distanceScaleInput").value = distanceScale;
unlock("distanceScale");
// units
@ -129,19 +114,6 @@ function editUnits() {
localStorage.removeItem("heightExponent");
calculateTemperatures();
// scale bar
barSizeOutput.value = barSizeInput.value = 2;
barLabel.value = "";
barBackOpacity.value = 0.2;
barBackColor.value = "#ffffff";
barPosX.value = barPosY.value = 99;
localStorage.removeItem("barSize");
localStorage.removeItem("barLabel");
localStorage.removeItem("barBackOpacity");
localStorage.removeItem("barBackColor");
localStorage.removeItem("barPosX");
localStorage.removeItem("barPosY");
renderScaleBar();
// population
@ -207,13 +179,15 @@ function editUnits() {
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(
d3.drag().on("start", function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
@ -222,7 +196,7 @@ function editUnits() {
d3.event.on("drag", function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});

View file

@ -5,18 +5,17 @@ function editWorld() {
title: "Configure World",
resizable: false,
width: "minmax(40em, 85vw)",
buttons: {
"Whole World": () => applyWorldPreset(100, 50),
Northern: () => applyWorldPreset(33, 25),
Tropical: () => applyWorldPreset(33, 50),
Southern: () => applyWorldPreset(33, 75)
},
buttons: {"Update world": updateWorld},
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"));
const checkbox = /* html */ `<div class="dontAsk" data-tip="Automatically update world on input changes and button clicks">
<input id="wcAutoChange" class="checkbox" type="checkbox" checked />
<label for="wcAutoChange" class="checkbox-label"><i>auto-apply changes</i></label>
</div>`;
const pane = this.parentElement.querySelector(".ui-dialog-buttonpane");
pane.insertAdjacentHTML("afterbegin", checkbox);
const button = this.parentElement.querySelector(".ui-dialog-buttonset > button");
button.on("mousemove", () => tip("Apply curreny settings to the map"));
},
close: function () {
$(this).dialog("destroy");
@ -34,12 +33,17 @@ function editWorld() {
if (modules.editWorld) return;
modules.editWorld = true;
byId("worldControls").addEventListener("input", e => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
const graticule = d3.geoGraticule();
globe.select("#globeWindArrows").on("click", handleWindChange);
globe.select("#globeGraticule").attr("d", round(path(graticule()))); // globe graticule
updateWindDirections();
byId("restoreWinds").addEventListener("click", restoreDefaultWinds);
byId("worldControls").on("input", handleControlsChange);
byId("restoreWinds").on("click", restoreDefaultWinds);
byId("wcWholeWorld").on("click", () => applyWorldPreset(100, 50));
byId("wcNorthern").on("click", () => applyWorldPreset(33, 25));
byId("wcTropical").on("click", () => applyWorldPreset(33, 50));
byId("wcSouthern").on("click", () => applyWorldPreset(33, 75));
function updateInputValues() {
byId("temperatureEquatorInput").value = options.temperatureEquator;
@ -55,27 +59,27 @@ function editWorld() {
byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
}
function updateWorld(el) {
if (el?.dataset.stored) {
const stored = el.dataset.stored;
byId(stored + "Input").value = el.value;
byId(stored + "Output").value = el.value;
lock(el.dataset.stored);
function handleControlsChange({target}) {
const stored = target.dataset.stored;
byId(stored + "Input").value = target.value;
byId(stored + "Output").value = target.value;
lock(stored);
if (stored === "temperatureEquator") {
options.temperatureEquator = Number(el.value);
byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
}
if (stored === "temperatureNorthPole") {
options.temperatureNorthPole = Number(el.value);
byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
}
if (stored === "temperatureSouthPole") {
options.temperatureSouthPole = Number(el.value);
byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
}
if (stored === "temperatureEquator") {
options.temperatureEquator = Number(target.value);
byId("temperatureEquatorF").innerText = convertTemperature(options.temperatureEquator, "°F");
} else if (stored === "temperatureNorthPole") {
options.temperatureNorthPole = Number(target.value);
byId("temperatureNorthPoleF").innerText = convertTemperature(options.temperatureNorthPole, "°F");
} else if (stored === "temperatureSouthPole") {
options.temperatureSouthPole = Number(target.value);
byId("temperatureSouthPoleF").innerText = convertTemperature(options.temperatureSouthPole, "°F");
}
if (byId("wcAutoChange").checked) updateWorld();
}
function updateWorld() {
updateGlobeTemperature();
updateGlobePosition();
calculateTemperatures();
@ -101,21 +105,22 @@ function editWorld() {
calculateMapCoordinates();
const mc = mapCoordinates;
const scale = +distanceScaleInput.value;
const unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
const meridian = toKilometer(eqD * 2 * distanceScale);
byId("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
byId("mapSizeFriendly").innerHTML = `${rn(graphWidth * distanceScale)}x${rn(graphHeight * distanceScale)} ${unit}`;
byId("meridianLength").innerHTML = rn(eqD * 2);
byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
byId("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * distanceScale)} ${unit}`;
byId("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
byId("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
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;
if (unit === "mi") return v * 1.60934;
if (unit === "lg") return v * 4.828;
if (unit === "vr") return v * 1.0668;
if (unit === "nmi") return v * 1.852;
if (unit === "nlg") return v * 5.556;
return 0; // 0 if distanceUnitInput is a custom unit
}
@ -128,6 +133,7 @@ function editWorld() {
[mc.lonW, mc.latN],
[mc.lonE, mc.latS]
]);
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
}
@ -161,21 +167,22 @@ function editWorld() {
});
}
function changeWind() {
function handleWindChange() {
const arrow = d3.event.target.nextElementSibling;
const tier = +arrow.dataset.tier;
options.winds[tier] = (options.winds[tier] + 45) % 360;
const tr = parseTransform(arrow.getAttribute("transform"));
arrow.setAttribute("transform", `rotate(${options.winds[tier]} ${tr[1]} ${tr[2]})`);
localStorage.setItem("winds", options.winds);
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
if (mapTiers.includes(tier)) updateWorld();
if (byId("wcAutoChange").checked && mapTiers.includes(tier)) updateWorld();
}
function restoreDefaultWinds() {
const defaultWinds = [225, 45, 225, 315, 135, 315];
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => ((90 - c) / 30) | 0);
const update = mapTiers.some(t => options.winds[t] != defaultWinds[t]);
const update = byId("wcAutoChange").checked && mapTiers.some(t => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
@ -186,6 +193,6 @@ function editWorld() {
byId("latitudeInput").value = byId("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
updateWorld();
if (byId("wcAutoChange").checked) updateWorld();
}
}

View file

@ -3,7 +3,7 @@
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
const body = byId("zonesBodySection");
updateFilters();
zonesEditorAddLines();
@ -20,20 +20,20 @@ function editZones() {
});
// add listeners
document.getElementById("zonesFilterType").addEventListener("click", updateFilters);
document.getElementById("zonesFilterType").addEventListener("change", filterZonesByType);
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
byId("zonesFilterType").on("click", updateFilters);
byId("zonesFilterType").on("change", filterZonesByType);
byId("zonesEditorRefresh").on("click", zonesEditorAddLines);
byId("zonesEditStyle").on("click", () => editStyle("zones"));
byId("zonesLegend").on("click", toggleLegend);
byId("zonesPercentage").on("click", togglePercentageMode);
byId("zonesManually").on("click", enterZonesManualAssignent);
byId("zonesManuallyApply").on("click", applyZonesManualAssignent);
byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent);
byId("zonesAdd").on("click", addZonesLayer);
byId("zonesExport").on("click", downloadZonesData);
byId("zonesRemove").on("click", toggleEraseMode);
body.addEventListener("click", function (ev) {
body.on("click", function (ev) {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
@ -45,7 +45,7 @@ function editZones() {
if (customization) selectZone(el);
});
body.addEventListener("input", function (ev) {
body.on("input", function (ev) {
const el = ev.target;
const zone = zones.select("#" + el.parentNode.dataset.id);
@ -58,10 +58,11 @@ function editZones() {
const zones = Array.from(document.querySelectorAll("#zones > g"));
const types = unique(zones.map(zone => zone.dataset.type));
const filterSelect = document.getElementById("zonesFilterType");
const filterSelect = byId("zonesFilterType");
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
filterSelect.innerHTML = "<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.innerHTML =
"<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.value = typeToFilterBy;
}
@ -69,7 +70,7 @@ function editZones() {
function zonesEditorAddLines() {
const unit = " " + getAreaUnit();
const typeToFilterBy = document.getElementById("zonesFilterType").value;
const typeToFilterBy = byId("zonesFilterType").value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy);
@ -80,9 +81,12 @@ function editZones() {
const fill = zoneEl.getAttribute("fill");
const area = getArea(d3.sum(c.map(i => pack.cells.area[i])));
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const urban =
d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
rural
)}; Urban population: ${si(urban)}. Click to change`;
const inactive = zoneEl.style.display === "none";
const focused = defs.select("#fog #focus" + zoneEl.id).size();
@ -98,8 +102,12 @@ function editZones() {
<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="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>`;
});
@ -109,7 +117,9 @@ function editZones() {
// update footer
const totalArea = getArea(graphWidth * graphHeight);
zonesFooterArea.dataset.area = totalArea;
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
const totalPop =
(d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) *
populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length;
@ -117,8 +127,8 @@ function editZones() {
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)));
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", ev => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
@ -150,7 +160,13 @@ function editZones() {
zonesEditorAddLines();
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
$(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"));
@ -166,7 +182,7 @@ function editZones() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
byId("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
@ -174,7 +190,11 @@ function editZones() {
$("#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);
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 () {
@ -195,24 +215,27 @@ function editZones() {
}
function dragZoneBrush() {
const r = +zonesBrush.value;
const radius = +byId("zonesBrush").value;
const eraseMode = byId("zonesRemove").classList.contains("pressed");
const landOnly = byId("zonesBrushLandOnly").checked;
const selected = body.querySelector("div.selected");
const zone = zones.select("#" + selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const [x, y] = d3.mouse(this);
moveCircle(x, y, radius);
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y, radius)];
if (landOnly) selection = selection.filter(i => pack.cells.h[i] >= 20);
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) {
if (eraseMode) {
// remove
selection.forEach(i => {
const index = cells.indexOf(i);
@ -280,12 +303,13 @@ function editZones() {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
byId("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"}});
if (!close)
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
@ -300,7 +324,7 @@ function editZones() {
const fill = el.getAttribute("fill");
const callback = newFill => {
el.fill = newFill;
document.getElementById(el.parentNode.dataset.id).setAttribute("fill", newFill);
byId(el.parentNode.dataset.id).setAttribute("fill", newFill);
};
openPicker(fill, callback);
@ -356,7 +380,8 @@ function editZones() {
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) + "%";
el.querySelector(".culturePopulation").innerHTML =
rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
@ -369,7 +394,13 @@ function editZones() {
const description = "Unknown zone";
const type = "Unknown";
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-type", type).attr("data-cells", "").attr("fill", fill);
zones
.append("g")
.attr("id", id)
.attr("data-description", description)
.attr("data-type", type)
.attr("data-cells", "")
.attr("fill", fill);
zonesEditorAddLines();
}
@ -411,13 +442,19 @@ function editZones() {
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const rural = rn(d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate);
const urban = rn(d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization);
const urban = rn(
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
alertMessage.innerHTML = /* html */ `Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"} />
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
burgs.length ? "" : "disabled"
} />
<p>Total population: ${l(total)} <span id="totalPop">${l(
total
)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;