merge completed... now to fix all the bugs...

This commit is contained in:
howlingsails 2021-12-12 23:02:38 -08:00
commit 87c4d80fbc
3472 changed files with 466748 additions and 6517 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,8 @@
"use strict";
'use strict';
class Battle {
constructor(attacker, defender) {
if (customization) return;
closeDialogs(".stable");
closeDialogs('.stable');
customization = 13; // enter customization to avoid unwanted dialog closing
Battle.prototype.context = this; // store context
@ -14,21 +14,21 @@ class Battle {
this.defenders = {regiments: [], distances: [], morale: 100, casualties: 0, power: 0};
this.addHeaders();
this.addRegiment("attackers", attacker);
this.addRegiment("defenders", defender);
this.addRegiment('attackers', attacker);
this.addRegiment('defenders', defender);
this.place = this.definePlace();
this.defineType();
this.name = this.defineName();
this.randomize();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
this.getInitialMorale();
$("#battleScreen").dialog({
$('#battleScreen').dialog({
title: this.name,
resizable: false,
width: fitContent(),
position: {my: "center", at: "center", of: "#map"},
position: {my: 'center', at: 'center', of: '#map'},
close: () => Battle.prototype.context.cancelResults()
});
@ -36,42 +36,42 @@ class Battle {
modules.Battle = true;
// 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("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("battleNameHide").addEventListener("click", this.hideNameSection);
document.getElementById("battleAddRegiment").addEventListener("click", this.addSide);
document.getElementById("battleRoll").addEventListener("click", () => Battle.prototype.context.randomize());
document.getElementById("battleRun").addEventListener("click", () => Battle.prototype.context.run());
document.getElementById("battleApply").addEventListener("click", () => Battle.prototype.context.applyResults());
document.getElementById("battleCancel").addEventListener("click", () => Battle.prototype.context.cancelResults());
document.getElementById("battleWiki").addEventListener("click", () => wiki("Battle-Simulator"));
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('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('battleNameHide').addEventListener('click', this.hideNameSection);
document.getElementById('battleAddRegiment').addEventListener('click', this.addSide);
document.getElementById('battleRoll').addEventListener('click', () => Battle.prototype.context.randomize());
document.getElementById('battleRun').addEventListener('click', () => Battle.prototype.context.run());
document.getElementById('battleApply').addEventListener('click', () => Battle.prototype.context.applyResults());
document.getElementById('battleCancel').addEventListener('click', () => Battle.prototype.context.cancelResults());
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_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_attackers').addEventListener('click', (ev) => this.toggleChange(ev));
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'));
}
defineType() {
const attacker = this.attackers.regiments[0];
const defender = this.defenders.regiments[0];
const getType = () => {
const typesA = Object.keys(attacker.u).map(name => options.military.find(u => u.name === name).type);
const typesD = Object.keys(defender.u).map(name => options.military.find(u => u.name === name).type);
const typesA = Object.keys(attacker.u).map((name) => options.military.find((u) => u.name === name).type);
const typesD = Object.keys(defender.u).map((name) => options.military.find((u) => u.name === name).type);
if (attacker.n && defender.n) return "naval"; // attacker and defender are navals
if (typesA.every(t => t === "aviation") && typesD.every(t => t === "aviation")) return "air"; // if attackers and defender have only aviation units
if (attacker.n && !defender.n && typesA.some(t => t !== "naval")) return "landing"; // if attacked is naval with non-naval units and defender is not naval
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return "siege"; // defender is in walled town
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return "ambush"; // 20% if defenders are in forest or marshes
return "field";
if (attacker.n && defender.n) return 'naval'; // attacker and defender are navals
if (typesA.every((t) => t === 'aviation') && typesD.every((t) => t === 'aviation')) return 'air'; // if attackers and defender have only aviation units
if (attacker.n && !defender.n && typesA.some((t) => t !== 'naval')) return 'landing'; // if attacked is naval with non-naval units and defender is not naval
if (!defender.n && pack.burgs[pack.cells.burg[this.cell]].walls) return 'siege'; // defender is in walled town
if (P(0.1) && [5, 6, 7, 8, 9, 12].includes(pack.cells.biome[this.cell])) return 'ambush'; // 20% if defenders are in forest or marshes
return 'field';
};
this.type = getType();
@ -79,25 +79,25 @@ class Battle {
}
setType() {
document.getElementById("battleType").className = "icon-button-" + this.type;
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 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;
document.getElementById("battlePhase_attackers").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_defenders").nextElementSibling.innerHTML = "";
document.getElementById("battlePhase_attackers").nextElementSibling.append(attackers.cloneNode(true));
document.getElementById("battlePhase_defenders").nextElementSibling.append(defenders.cloneNode(true));
document.getElementById('battlePhase_attackers').nextElementSibling.innerHTML = '';
document.getElementById('battlePhase_defenders').nextElementSibling.innerHTML = '';
document.getElementById('battlePhase_attackers').nextElementSibling.append(attackers.cloneNode(true));
document.getElementById('battlePhase_defenders').nextElementSibling.append(defenders.cloneNode(true));
}
definePlace() {
const cells = pack.cells,
i = this.cell;
const burg = cells.burg[i] ? pack.burgs[cells.burg[i]].name : null;
const getRiver = i => {
const river = pack.rivers.find(r => r.i === i);
return river.name + " " + river.type;
const getRiver = (i) => {
const river = pack.rivers.find((r) => r.i === i);
return river.name + ' ' + river.type;
};
const river = !burg && cells.r[i] ? getRiver(cells.r[i]) : null;
const proper = burg || river ? null : Names.getCulture(cells.culture[this.cell]);
@ -105,28 +105,28 @@ class Battle {
}
defineName() {
if (this.type === "field") return "Battle of " + this.place;
if (this.type === "naval") return "Naval Battle of " + this.place;
if (this.type === "siege") return "Siege of " + this.place;
if (this.type === "ambush") return this.place + " Ambush";
if (this.type === "landing") return this.place + " Landing";
if (this.type === "air") return `${this.place} ${P(0.8) ? "Air Battle" : "Dogfight"}`;
if (this.type === 'field') return 'Battle of ' + this.place;
if (this.type === 'naval') return 'Naval Battle of ' + this.place;
if (this.type === 'siege') return 'Siege of ' + this.place;
if (this.type === 'ambush') return this.place + ' Ambush';
if (this.type === 'landing') return this.place + ' Landing';
if (this.type === 'air') return `${this.place} ${P(0.8) ? 'Air Battle' : 'Dogfight'}`;
}
getTypeName() {
if (this.type === "field") return "field battle";
if (this.type === "naval") return "naval battle";
if (this.type === "siege") return "siege";
if (this.type === "ambush") return "ambush";
if (this.type === "landing") return "landing";
if (this.type === "air") return "battle";
if (this.type === 'field') return 'field battle';
if (this.type === 'naval') return 'naval battle';
if (this.type === 'siege') return 'siege';
if (this.type === 'ambush') return 'ambush';
if (this.type === 'landing') return 'landing';
if (this.type === 'air') return 'battle';
}
addHeaders() {
let headers = "<thead><tr><th></th><th></th>";
let headers = '<thead><tr><th></th><th></th>';
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, " "));
const label = capitalize(u.name.replace(/_/g, ' '));
headers += `<th data-tip="${label}">${u.icon}</th>`;
}
@ -140,7 +140,7 @@ class Battle {
const state = pack.states[regiment.state];
const distance = (Math.hypot(this.y - regiment.by, this.x - regiment.bx) * distanceScaleInput.value) | 0; // distance between regiment and its base
const color = state.color[0] === "#" ? state.color : "#999";
const color = state.color[0] === '#' ? state.color : '#999';
const icon = `<svg width="1.4em" height="1.4em" style="margin-bottom: -.6em">
<rect x="0" y="0" width="100%" height="100%" fill="${color}" class="fillRect"></rect>
<text x="0" y="1.04em" style="">${regiment.icon}</text></svg>`;
@ -160,28 +160,28 @@ class Battle {
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>`;
const div = side === "attackers" ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + "</tbody>";
const div = side === 'attackers' ? battleAttackers : battleDefenders;
div.innerHTML += body + initial + casualties + survivors + '</tbody>';
this[side].regiments.push(regiment);
this[side].distances.push(distance);
}
addSide() {
const body = document.getElementById("regimentSelectorBody");
const body = document.getElementById('regimentSelectorBody');
const context = Battle.prototype.context;
const regiments = pack.states
.filter(s => s.military && !s.removed)
.map(s => s.military)
.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) * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const isAdded = (reg) => context.defenders.regiments.some((r) => r === reg) || context.attackers.regiments.some((r) => r === reg);
body.innerHTML = regiments
.map(r => {
.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}
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}
data-total=${r.a} data-distance=${dist} data-tip="Click to select regiment">
<svg width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
<div style="width:6em">${s.name.slice(0, 11)}</div>
@ -191,43 +191,43 @@ class Battle {
<div style="width:4em">${dist}</div>
</div>`;
})
.join("");
.join('');
$("#regimentSelectorScreen").dialog({
$('#regimentSelectorScreen').dialog({
resizable: false,
width: fitContent(),
title: "Add regiment to the battle",
position: {my: "left center", at: "right+10 center", of: "#battleScreen"},
title: 'Add regiment to the battle',
position: {my: 'left center', at: 'right+10 center', of: '#battleScreen'},
close: addSideClosed,
buttons: {
"Add to attackers": () => addSideClicked("attackers"),
"Add to defenders": () => addSideClicked("defenders"),
Cancel: () => $("#regimentSelectorScreen").dialog("close")
'Add to attackers': () => addSideClicked('attackers'),
'Add to defenders': () => addSideClicked('defenders'),
Cancel: () => $('#regimentSelectorScreen').dialog('close')
}
});
applySorting(regimentSelectorHeader);
body.addEventListener("click", selectLine);
body.addEventListener('click', selectLine);
function selectLine(ev) {
if (ev.target.className === "inactive") {
tip("Regiment is already in the battle", false, "error");
if (ev.target.className === 'inactive') {
tip('Regiment is already in the battle', false, 'error');
return;
}
ev.target.classList.toggle("selected");
ev.target.classList.toggle('selected');
}
function addSideClicked(side) {
const selected = body.querySelectorAll(".selected");
const selected = body.querySelectorAll('.selected');
if (!selected.length) {
tip("Please select a regiment first", false, "error");
tip('Please select a regiment first', false, 'error');
return;
}
$("#regimentSelectorScreen").dialog("close");
selected.forEach(line => {
$('#regimentSelectorScreen').dialog('close');
selected.forEach((line) => {
const state = pack.states[line.dataset.s];
const regiment = state.military.find(r => r.i == +line.dataset.i);
const regiment = state.military.find((r) => r.i == +line.dataset.i);
Battle.prototype.addRegiment.call(context, side, regiment);
Battle.prototype.calculateStrength.call(context, side);
Battle.prototype.getInitialMorale.call(context);
@ -235,7 +235,7 @@ class Battle {
// move regiment
const defenders = context.defenders.regiments,
attackers = context.attackers.regiments;
const shift = side === "attackers" ? attackers.length * -8 : (defenders.length - 1) * 8;
const shift = side === 'attackers' ? attackers.length * -8 : (defenders.length - 1) * 8;
regiment.px = regiment.x;
regiment.py = regiment.y;
Military.moveRegiment(regiment, defenders[0].x, defenders[0].y + shift);
@ -243,34 +243,34 @@ class Battle {
}
function addSideClosed() {
body.innerHTML = "";
body.removeEventListener("click", selectLine);
body.innerHTML = '';
body.removeEventListener('click', selectLine);
}
}
showNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("battleNameSection").style.display = "inline-block";
document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('battleNameSection').style.display = 'inline-block';
document.getElementById("battleNamePlace").value = this.place;
document.getElementById("battleNameFull").value = this.name;
document.getElementById('battleNamePlace').value = this.place;
document.getElementById('battleNameFull').value = this.name;
}
hideNameSection() {
document.querySelectorAll("#battleBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("battleNameSection").style.display = "none";
document.querySelectorAll('#battleBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('battleNameSection').style.display = 'none';
}
changeName(ev) {
this.name = ev.target.value;
$("#battleScreen").dialog({title: this.name});
$('#battleScreen').dialog({title: this.name});
}
generateName(type) {
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});
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});
}
getJoinedForces(regiments) {
@ -324,38 +324,38 @@ 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;
document.getElementById('battlePower_' + side).innerHTML = UIvalue;
}
getInitialMorale() {
const powerFee = diff => Math.min(Math.max(100 - diff ** 1.5 * 10 + 10, 50), 100);
const distanceFee = dist => Math.min(d3.mean(dist) / 50, 15);
const powerFee = (diff) => minmax(100 - diff ** 1.5 * 10 + 10, 50, 100);
const distanceFee = (dist) => Math.min(d3.mean(dist) / 50, 15);
const powerDiff = this.defenders.power / this.attackers.power;
this.attackers.morale = powerFee(powerDiff) - distanceFee(this.attackers.distances);
this.defenders.morale = powerFee(1 / powerDiff) - distanceFee(this.defenders.distances);
this.updateMorale("attackers");
this.updateMorale("defenders");
this.updateMorale('attackers');
this.updateMorale('defenders');
}
updateMorale(side) {
const morale = document.getElementById("battleMorale_" + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, "");
const morale = document.getElementById('battleMorale_' + side);
morale.dataset.tip = morale.dataset.tip.replace(morale.value, '');
morale.value = this[side].morale | 0;
morale.dataset.tip += morale.value;
}
randomize() {
this.rollDie("attackers");
this.rollDie("defenders");
this.rollDie('attackers');
this.rollDie('defenders');
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
}
rollDie(side) {
const el = document.getElementById("battleDie_" + side);
const el = document.getElementById('battleDie_' + side);
const prev = +el.innerHTML;
do {
el.innerHTML = rand(1, 6);
@ -369,131 +369,131 @@ class Battle {
const powerRatio = this.attackers.power / this.defenders.power;
const getFieldBattlePhase = () => {
const prev = [this.attackers.phase || "skirmish", this.defenders.phase || "skirmish"]; // previous phase
const prev = [this.attackers.phase || 'skirmish', this.defenders.phase || 'skirmish']; // previous phase
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
// skirmish phase continuation depends on ranged forces number
if (prev[0] === "skirmish" && prev[1] === "skirmish") {
if (prev[0] === 'skirmish' && prev[1] === 'skirmish') {
const forces = this.getJoinedForces(this.attackers.regiments.concat(this.defenders.regiments));
const total = d3.sum(Object.values(forces)); // total forces
const ranged =
d3.sum(
options.military
.filter(u => u.type === "ranged")
.map(u => u.name)
.map(u => forces[u])
.filter((u) => u.type === 'ranged')
.map((u) => u.name)
.map((u) => forces[u])
) / total; // ranged units
if (P(ranged) || P(0.8 - i / 10)) return ["skirmish", "skirmish"];
if (P(ranged) || P(0.8 - i / 10)) return ['skirmish', 'skirmish'];
}
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getNavalBattlePhase = () => {
const prev = [this.attackers.phase || "shelling", this.defenders.phase || "shelling"]; // previous phase
const prev = [this.attackers.phase || 'shelling', this.defenders.phase || 'shelling']; // previous phase
if (prev[0] === "withdrawal") return ["withdrawal", "chase"];
if (prev[0] === "chase") return ["chase", "withdrawal"];
if (prev[0] === 'withdrawal') return ['withdrawal', 'chase'];
if (prev[0] === 'chase') return ['chase', 'withdrawal'];
// withdrawal phase when power imbalanced
if (!prev[0] === "boarding") {
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ["withdrawal", "chase"];
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ["chase", "withdrawal"];
if (!prev[0] === 'boarding') {
if (powerRatio < 0.5 || (P(this.attackers.casualties) && powerRatio < 1)) return ['withdrawal', 'chase'];
if (powerRatio > 2 || (P(this.defenders.casualties) && powerRatio > 1)) return ['chase', 'withdrawal'];
}
// boarding phase can start from 2nd iteration
if (prev[0] === "boarding" || P(i / 10 - 0.1)) return ["boarding", "boarding"];
if (prev[0] === 'boarding' || P(i / 10 - 0.1)) return ['boarding', 'boarding'];
return ["shelling", "shelling"]; // default option
return ['shelling', 'shelling']; // default option
};
const getSiegePhase = () => {
const prev = [this.attackers.phase || "blockade", this.defenders.phase || "sheltering"]; // previous phase
let phase = ["blockade", "sheltering"]; // default phase
const prev = [this.attackers.phase || 'blockade', this.defenders.phase || 'sheltering']; // previous phase
let phase = ['blockade', 'sheltering']; // default phase
if (prev[0] === "retreat" || prev[0] === "looting") return prev;
if (prev[0] === 'retreat' || prev[0] === 'looting') return prev;
if (P(1 - morale[0] / 30) && powerRatio < 1) return ["retreat", "pursue"]; // attackers retreat chance if moral < 30
if (P(1 - morale[1] / 15)) return ["looting", "surrendering"]; // defenders surrendering chance if moral < 15
if (P(1 - morale[0] / 30) && powerRatio < 1) return ['retreat', 'pursue']; // attackers retreat chance if moral < 30
if (P(1 - morale[1] / 15)) return ['looting', 'surrendering']; // defenders surrendering chance if moral < 15
if (P((powerRatio - 1) / 2)) return ["storming", "defense"]; // start storm
if (P((powerRatio - 1) / 2)) return ['storming', 'defense']; // start storm
if (prev[0] !== "storming") {
const machinery = options.military.filter(u => u.type === "machinery").map(u => u.name); // machinery units
if (prev[0] !== 'storming') {
const machinery = options.military.filter((u) => u.type === 'machinery').map((u) => u.name); // machinery units
const attackers = this.getJoinedForces(this.attackers.regiments);
const machineryA = d3.sum(machinery.map(u => attackers[u]));
if (i && machineryA && P(0.9)) phase[0] = "bombardment";
const machineryA = d3.sum(machinery.map((u) => attackers[u]));
if (i && machineryA && P(0.9)) phase[0] = 'bombardment';
const defenders = this.getJoinedForces(this.defenders.regiments);
const machineryD = d3.sum(machinery.map(u => defenders[u]));
if (machineryD && P(0.9)) phase[1] = "bombardment";
const machineryD = d3.sum(machinery.map((u) => defenders[u]));
if (machineryD && P(0.9)) phase[1] = 'bombardment';
if (i && prev[1] !== "sortie" && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = "sortie"; // defenders sortie
if (i && prev[1] !== 'sortie' && machineryD < machineryA && P(0.25) && P(morale[1] / 70)) phase[1] = 'sortie'; // defenders sortie
}
return phase;
};
const getAmbushPhase = () => {
const prev = [this.attackers.phase || "shock", this.defenders.phase || "surprise"]; // previous phase
const prev = [this.attackers.phase || 'shock', this.defenders.phase || 'surprise']; // previous phase
if (prev[1] === "surprise" && P(1 - (powerRatio * i) / 5)) return ["shock", "surprise"];
if (prev[1] === 'surprise' && P(1 - (powerRatio * i) / 5)) return ['shock', 'surprise'];
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getLandingPhase = () => {
const prev = [this.attackers.phase || "landing", this.defenders.phase || "defense"]; // previous phase
const prev = [this.attackers.phase || 'landing', this.defenders.phase || 'defense']; // previous phase
if (prev[1] === "waiting") return ["flee", "waiting"];
if (prev[1] === "pursue") return ["flee", P(0.3) ? "pursue" : "waiting"];
if (prev[1] === "retreat") return ["pursue", "retreat"];
if (prev[1] === 'waiting') return ['flee', 'waiting'];
if (prev[1] === 'pursue') return ['flee', P(0.3) ? 'pursue' : 'waiting'];
if (prev[1] === 'retreat') return ['pursue', 'retreat'];
if (prev[0] === "landing") {
const attackers = P(i / 2) ? "melee" : "landing";
const defenders = i ? prev[1] : P(0.5) ? "defense" : "shock";
if (prev[0] === 'landing') {
const attackers = P(i / 2) ? 'melee' : 'landing';
const defenders = i ? prev[1] : P(0.5) ? 'defense' : 'shock';
return [attackers, defenders];
}
if (P(1 - morale[0] / 40)) return ["flee", "pursue"]; // chance if moral < 40
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"]; // chance if moral < 25
if (P(1 - morale[0] / 40)) return ['flee', 'pursue']; // chance if moral < 40
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat']; // chance if moral < 25
return ["melee", "melee"]; // default option
return ['melee', 'melee']; // default option
};
const getAirBattlePhase = () => {
const prev = [this.attackers.phase || "maneuvering", this.defenders.phase || "maneuvering"]; // previous phase
const prev = [this.attackers.phase || 'maneuvering', this.defenders.phase || 'maneuvering']; // previous phase
// chance if moral < 25
if (P(1 - morale[0] / 25)) return ["retreat", "pursue"];
if (P(1 - morale[1] / 25)) return ["pursue", "retreat"];
if (P(1 - morale[0] / 25)) return ['retreat', 'pursue'];
if (P(1 - morale[1] / 25)) return ['pursue', 'retreat'];
if (prev[0] === "maneuvering" && P(1 - i / 10)) return ["maneuvering", "maneuvering"];
if (prev[0] === 'maneuvering' && P(1 - i / 10)) return ['maneuvering', 'maneuvering'];
return ["dogfight", "dogfight"]; // default option
return ['dogfight', 'dogfight']; // default option
};
const phase = (function (type) {
switch (type) {
case "field":
case 'field':
return getFieldBattlePhase();
case "naval":
case 'naval':
return getNavalBattlePhase();
case "siege":
case 'siege':
return getSiegePhase();
case "ambush":
case 'ambush':
return getAmbushPhase();
case "landing":
case 'landing':
return getLandingPhase();
case "air":
case 'air':
return getAirBattlePhase();
default:
getFieldBattlePhase();
@ -503,23 +503,23 @@ class Battle {
this.attackers.phase = phase[0];
this.defenders.phase = phase[1];
const buttonA = document.getElementById("battlePhase_attackers");
buttonA.className = "icon-button-" + this.attackers.phase;
const buttonA = document.getElementById('battlePhase_attackers');
buttonA.className = 'icon-button-' + this.attackers.phase;
buttonA.dataset.tip = buttonA.nextElementSibling.querySelector("[data-phase='" + phase[0] + "']").dataset.tip;
const buttonD = document.getElementById("battlePhase_defenders");
buttonD.className = "icon-button-" + this.defenders.phase;
const buttonD = document.getElementById('battlePhase_defenders');
buttonD.className = 'icon-button-' + this.defenders.phase;
buttonD.dataset.tip = buttonD.nextElementSibling.querySelector("[data-phase='" + phase[1] + "']").dataset.tip;
}
run() {
// validations
if (!this.attackers.power) {
tip("Attackers army destroyed", false, "warn");
tip('Attackers army destroyed', false, 'warn');
return;
}
if (!this.defenders.power) {
tip("Defenders army destroyed", false, "warn");
tip('Defenders army destroyed', false, 'warn');
return;
}
@ -558,8 +558,8 @@ class Battle {
const casualtiesA = (casualties * defense) / (attack + defense); // attackers casualties, ~5% per iteration
const casualtiesD = (casualties * attack) / (attack + defense); // defenders casualties, ~5% per iteration
this.calculateCasualties("attackers", casualtiesA);
this.calculateCasualties("defenders", casualtiesD);
this.calculateCasualties('attackers', casualtiesA);
this.calculateCasualties('defenders', casualtiesD);
this.attackers.casualties += casualtiesA;
this.defenders.casualties += casualtiesD;
@ -568,14 +568,14 @@ class Battle {
this.defenders.morale = Math.max(this.defenders.morale - casualtiesD * 100 - 1, 0);
// update table values
this.updateTable("attackers");
this.updateTable("defenders");
this.updateTable('attackers');
this.updateTable('defenders');
// prepare for next iteration
this.iteration += 1;
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
}
calculateCasualties(side, casualties) {
@ -591,9 +591,9 @@ class Battle {
updateTable(side) {
for (const r of this[side].regiments) {
const tbody = document.getElementById("battle" + r.state + "-" + r.i);
const battleCasualties = tbody.querySelector(".battleCasualties");
const battleSurvivors = tbody.querySelector(".battleSurvivors");
const tbody = document.getElementById('battle' + r.state + '-' + r.i);
const battleCasualties = tbody.querySelector('.battleCasualties');
const battleSurvivors = tbody.querySelector('.battleSurvivors');
let index = 3; // index to find table element easily
for (const u of options.military) {
@ -615,35 +615,35 @@ class Battle {
const hideSection = function () {
button.style.opacity = 1;
div.style.display = "none";
div.style.display = 'none';
};
if (div.style.display === "block") {
if (div.style.display === 'block') {
hideSection();
return;
}
button.style.opacity = 0.5;
div.style.display = "block";
div.style.display = 'block';
document.getElementsByTagName("body")[0].addEventListener("click", hideSection, {once: true});
document.getElementsByTagName('body')[0].addEventListener('click', hideSection, {once: true});
}
changeType(ev) {
if (ev.target.tagName !== "BUTTON") return;
if (ev.target.tagName !== 'BUTTON') return;
this.type = ev.target.dataset.type;
this.setType();
this.selectPhase();
this.calculateStrength("attackers");
this.calculateStrength("defenders");
this.calculateStrength('attackers');
this.calculateStrength('defenders');
this.name = this.defineName();
$("#battleScreen").dialog({title: this.name});
$('#battleScreen').dialog({title: this.name});
}
changePhase(ev, side) {
if (ev.target.tagName !== "BUTTON") return;
if (ev.target.tagName !== 'BUTTON') return;
const phase = (this[side].phase = ev.target.dataset.phase);
const button = document.getElementById("battlePhase_" + side);
button.className = "icon-button-" + phase;
const button = document.getElementById('battlePhase_' + side);
button.className = 'icon-button-' + phase;
button.dataset.tip = ev.target.dataset.tip;
this.calculateStrength(side);
}
@ -654,34 +654,49 @@ class Battle {
const relativeCasualties = this.defenders.casualties / (this.attackers.casualties + this.attackers.casualties);
const battleStatus = getBattleStatus(relativeCasualties, maxCasualties);
function getBattleStatus(relative, max) {
if (isNaN(relative)) return ["standoff", "standoff"]; // if no casualties at all
if (max < 0.05) return ["minor skirmishes", "minor skirmishes"];
if (relative > 95) return ["attackers flawless victory", "disorderly retreat of defenders"];
if (relative > 0.7) return ["attackers decisive victory", "defenders disastrous defeat"];
if (relative > 0.6) return ["attackers victory", "defenders defeat"];
if (relative > 0.4) return ["stalemate", "stalemate"];
if (relative > 0.3) return ["attackers defeat", "defenders victory"];
if (relative > 0.5) return ["attackers disastrous defeat", "decisive victory of defenders"];
if (relative >= 0) return ["attackers disorderly retreat", "flawless victory of defenders"];
return ["stalemate", "stalemate"]; // exception
if (isNaN(relative)) return ['standoff', 'standoff']; // if no casualties at all
if (max < 0.05) return ['minor skirmishes', 'minor skirmishes'];
if (relative > 95) return ['attackers flawless victory', 'disorderly retreat of defenders'];
if (relative > 0.7) return ['attackers decisive victory', 'defenders disastrous defeat'];
if (relative > 0.6) return ['attackers victory', 'defenders defeat'];
if (relative > 0.4) return ['stalemate', 'stalemate'];
if (relative > 0.3) return ['attackers defeat', 'defenders victory'];
if (relative > 0.5) return ['attackers disastrous defeat', 'decisive victory of defenders'];
if (relative >= 0) return ['attackers disorderly retreat', 'flawless victory of defenders'];
return ['stalemate', 'stalemate']; // exception
}
this.attackers.regiments.forEach(r => applyResultForSide(r, "attackers"));
this.defenders.regiments.forEach(r => applyResultForSide(r, "defenders"));
this.attackers.regiments.forEach((r) => applyResultForSide(r, 'attackers'));
this.defenders.regiments.forEach((r) => applyResultForSide(r, 'defenders'));
function applyResultForSide(r, side) {
const id = "regiment" + r.state + "-" + r.i;
const id = 'regiment' + r.state + '-' + r.i;
// add result to regiment note
const note = notes.find(n => n.id === id);
const note = notes.find((n) => n.id === id);
if (note) {
const status = side === "attackers" ? battleStatus[0] : battleStatus[1];
const status = side === 'attackers' ? battleStatus[0] : battleStatus[1];
const losses = r.a ? Math.abs(d3.sum(Object.values(r.casualties))) / r.a : 1;
const regStatus = losses === 1 ? "is destroyed" : losses > 0.8 ? "is almost completely destroyed" : losses > 0.5 ? "suffered terrible losses" : losses > 0.3 ? "suffered severe losses" : losses > 0.2 ? "suffered heavy losses" : losses > 0.05 ? "suffered significant losses" : losses > 0 ? "suffered unsignificant losses" : "left the battle without loss";
const regStatus =
losses === 1
? 'is destroyed'
: losses > 0.8
? 'is almost completely destroyed'
: losses > 0.5
? 'suffered terrible losses'
: losses > 0.3
? 'suffered severe losses'
: losses > 0.2
? 'suffered heavy losses'
: losses > 0.05
? 'suffered significant losses'
: losses > 0
? 'suffered unsignificant losses'
: 'left the battle without loss';
const casualties = Object.keys(r.casualties)
.map(t => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
.filter(c => c);
const casualtiesText = casualties.length ? " Casualties: " + list(casualties) + "." : "";
.map((t) => (r.casualties[t] ? `${Math.abs(r.casualties[t])} ${t}` : null))
.filter((c) => c);
const casualtiesText = casualties.length ? ' Casualties: ' + list(casualties) + '.' : '';
const legend = `\r\n\r\n${battleName} (${options.year} ${options.eraShort}): ${status}. The regiment ${regStatus}.${casualtiesText}`;
note.legend += legend;
}
@ -691,57 +706,47 @@ class Battle {
armies.select(`g#${id} > text`).text(Military.getTotal(r)); // update reg box
}
// append battlefield marker
void (function addMarkerSymbol() {
if (svg.select("#defs-markers").select("#marker_battlefield").size()) return;
const symbol = svg.select("#defs-markers").append("symbol").attr("id", "marker_battlefield").attr("viewBox", "0 0 30 30");
symbol.append("path").attr("d", "M6,19 l9,10 L24,19").attr("fill", "#000000").attr("stroke", "none");
symbol.append("circle").attr("cx", 15).attr("cy", 15).attr("r", 10).attr("fill", "#ffffff").attr("stroke", "#000000").attr("stroke-width", 1);
symbol.append("text").attr("x", "50%").attr("y", "52%").attr("fill", "#000000").attr("stroke", "#3200ff").attr("stroke-width", 0).attr("font-size", "12px").attr("dominant-baseline", "central").text("⚔️");
})();
const i = last(pack.markers)?.i + 1 || 0;
{
// append battlefield marker
const marker = {i, x: this.x, y: this.y, cell: this.cell, icon: '⚔️', type: 'battlefields', dy: 52};
pack.markers.push(marker);
const markerHTML = drawMarker(marker);
document.getElementById('markers').insertAdjacentHTML('beforeend', markerHTML);
}
const getSide = (regs, n) => (regs.length > 1 ? `${n ? "regiments" : "forces"} of ${list([...new Set(regs.map(r => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + " " + regs[0].name);
const getLosses = casualties => Math.min(rn(casualties * 100), 100);
const getSide = (regs, n) =>
regs.length > 1 ? `${n ? 'regiments' : 'forces'} of ${list([...new Set(regs.map((r) => pack.states[r.state].name))])}` : getAdjective(pack.states[regs[0].state].name) + ' ' + regs[0].name;
const getLosses = (casualties) => Math.min(rn(casualties * 100), 100);
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}.
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 id = getNextId("markerElement");
notes.push({id, name: this.name, legend});
notes.push({id: `marker${i}`, name: this.name, legend});
tip(`${this.name} is over. ${result}`, true, "success", 4000);
tip(`${this.name} is over. ${result}`, true, 'success', 4000);
markers
.append("use")
.attr("id", id)
.attr("xlink:href", "#marker_battlefield")
.attr("data-id", "#marker_battlefield")
.attr("data-x", this.x)
.attr("data-y", this.y)
.attr("x", this.x - 15)
.attr("y", this.y - 30)
.attr("data-size", 1)
.attr("width", 30)
.attr("height", 30);
$("#battleScreen").dialog("destroy");
$('#battleScreen').dialog('destroy');
this.cleanData();
}
cancelResults() {
// move regiments back to initial positions
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => Military.moveRegiment(r, r.px, r.py));
$("#battleScreen").dialog("close");
this.attackers.regiments.concat(this.defenders.regiments).forEach((r) => Military.moveRegiment(r, r.px, r.py));
$('#battleScreen').dialog('close');
this.cleanData();
}
cleanData() {
battleAttackers.innerHTML = battleDefenders.innerHTML = ""; // clean DOM
battleAttackers.innerHTML = battleDefenders.innerHTML = ''; // clean DOM
customization = 0; // exit edit mode
// clean temp data
this.attackers.regiments.concat(this.defenders.regiments).forEach(r => {
this.attackers.regiments.concat(this.defenders.regiments).forEach((r) => {
delete r.px;
delete r.py;
delete r.casualties;

View file

@ -1,55 +1,55 @@
"use strict";
'use strict';
function editBiomes() {
if (customization) return;
closeDialogs("#biomesEditor, .stable");
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleStates")) toggleStates();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleReligions")) toggleReligions();
if (layerIsOn("toggleProvinces")) toggleProvinces();
closeDialogs('#biomesEditor, .stable');
if (!layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleStates')) toggleStates();
if (layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleReligions')) toggleReligions();
if (layerIsOn('toggleProvinces')) toggleProvinces();
const body = document.getElementById("biomesBody");
const body = document.getElementById('biomesBody');
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
refreshBiomesEditor();
if (modules.editBiomes) return;
modules.editBiomes = true;
$("#biomesEditor").dialog({
title: "Biomes Editor",
$('#biomesEditor').dialog({
title: 'Biomes Editor',
resizable: false,
width: fitContent(),
close: closeBiomesEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}
});
// add listeners
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
document.getElementById("biomesEditStyle").addEventListener("click", () => editStyle("biomes"));
document.getElementById("biomesLegend").addEventListener("click", toggleLegend);
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
document.getElementById("biomesManuallyCancel").addEventListener("click", () => exitBiomesCustomizationMode());
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
document.getElementById("biomesAdd").addEventListener("click", addCustomBiome);
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
document.getElementById('biomesEditorRefresh').addEventListener('click', refreshBiomesEditor);
document.getElementById('biomesEditStyle').addEventListener('click', () => editStyle('biomes'));
document.getElementById('biomesLegend').addEventListener('click', toggleLegend);
document.getElementById('biomesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('biomesManually').addEventListener('click', enterBiomesCustomizationMode);
document.getElementById('biomesManuallyApply').addEventListener('click', applyBiomesChange);
document.getElementById('biomesManuallyCancel').addEventListener('click', () => exitBiomesCustomizationMode());
document.getElementById('biomesRestore').addEventListener('click', restoreInitialBiomes);
document.getElementById('biomesAdd').addEventListener('click', addCustomBiome);
document.getElementById('biomesRegenerateReliefIcons').addEventListener('click', regenerateIcons);
document.getElementById('biomesExport').addEventListener('click', downloadBiomesData);
body.addEventListener("click", function (ev) {
body.addEventListener('click', function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("fillRect")) biomeChangeColor(el);
else if (cl.contains("icon-info-circled")) openWiki(el);
else if (cl.contains("icon-trash-empty")) removeCustomBiome(el);
if (cl.contains('fillRect')) biomeChangeColor(el);
else if (cl.contains('icon-info-circled')) openWiki(el);
else if (cl.contains('icon-trash-empty')) removeCustomBiome(el);
if (customization === 6) selectBiomeOnLineClick(el);
});
body.addEventListener("change", function (ev) {
body.addEventListener('change', function (ev) {
const el = ev.target,
cl = el.classList;
if (cl.contains("biomeName")) biomeChangeName(el);
else if (cl.contains("biomeHabitability")) biomeChangeHabitability(el);
if (cl.contains('biomeName')) biomeChangeName(el);
else if (cl.contains('biomeHabitability')) biomeChangeHabitability(el);
});
function refreshBiomesEditor() {
@ -76,14 +76,14 @@ function editBiomes() {
}
function biomesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const b = biomesData;
let lines = "",
let lines = '',
totalArea = 0,
totalPopulation = 0;
for (const i of b.i) {
if (!i || biomesData.name[i] === "removed") continue; // ignore water and removed biomes
if (!i || biomesData.name[i] === 'removed') continue; // ignore water and removed biomes
const area = b.area[i] * distanceScaleInput.value ** 2;
const rural = b.rural[i] * populationRate;
const urban = b.urban[i] * populationRate * urbanization;
@ -94,7 +94,9 @@ function editBiomes() {
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
b.color[i]
}" class="fillRect pointer"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
<span data-tip="Biome habitability percent" class="hide">%</span>
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=9999 class="biomeHabitability hide" value=${b.habitability[i]}>
@ -105,40 +107,40 @@ function editBiomes() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="biomePopulation hide">${si(population)}</div>
<span data-tip="Open Wikipedia article about the biome" class="icon-info-circled pointer hide"></span>
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ""}
${i > 12 && !b.cells[i] ? '<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>' : ''}
</div>`;
}
body.innerHTML = lines;
// update footer
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterBiomes.innerHTML = body.querySelectorAll(':scope > div').length;
biomesFooterCells.innerHTML = pack.cells.h.filter((h) => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation);
biomesFooterArea.dataset.area = totalArea;
biomesFooterPopulation.dataset.population = totalPopulation;
// add listeners
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
body.querySelectorAll('div.biomes').forEach((el) => el.addEventListener('mouseenter', (ev) => biomeHighlightOn(ev)));
body.querySelectorAll('div.biomes').forEach((el) => el.addEventListener('mouseleave', (ev) => biomeHighlightOff(ev)));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(biomesHeader);
$("#biomesEditor").dialog({width: fitContent()});
$('#biomesEditor').dialog({width: fitContent()});
}
function biomeHighlightOn(event) {
if (customization === 6) return;
const biome = +event.target.dataset.id;
biomes
.select("#biome" + biome)
.select('#biome' + biome)
.raise()
.transition(animate)
.attr("stroke-width", 2)
.attr("stroke", "#cd4c11");
.attr('stroke-width', 2)
.attr('stroke', '#cd4c11');
}
function biomeHighlightOff(event) {
@ -146,23 +148,23 @@ function editBiomes() {
const biome = +event.target.dataset.id;
const color = biomesData.color[biome];
biomes
.select("#biome" + biome)
.select('#biome' + biome)
.transition()
.attr("stroke-width", 0.7)
.attr("stroke", color);
.attr('stroke-width', 0.7)
.attr('stroke', color);
}
function biomeChangeColor(el) {
const currentFill = el.getAttribute("fill");
const currentFill = el.getAttribute('fill');
const biome = +el.parentNode.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute("fill", fill);
el.setAttribute('fill', fill);
biomesData.color[biome] = fill;
biomes
.select("#biome" + biome)
.attr("fill", fill)
.attr("stroke", fill);
.select('#biome' + biome)
.attr('fill', fill)
.attr('stroke', fill);
};
openPicker(currentFill, callback);
@ -179,7 +181,7 @@ function editBiomes() {
const failed = isNaN(+el.value) || +el.value < 0 || +el.value > 9999;
if (failed) {
el.value = biomesData.habitability[biome];
tip("Please provide a valid number in range 0-9999", false, "error");
tip('Please provide a valid number in range 0-9999', false, 'error');
return;
}
biomesData.habitability[biome] = +el.value;
@ -190,69 +192,69 @@ function editBiomes() {
function openWiki(el) {
const name = el.parentNode.dataset.name;
if (name === "Custom" || !name) {
tip("Please provide a biome name", false, "error");
if (name === 'Custom' || !name) {
tip('Please provide a biome name', false, 'error');
return;
}
const wiki = "https://en.wikipedia.org/wiki/";
const wiki = 'https://en.wikipedia.org/wiki/';
switch (name) {
case "Hot desert":
openURL(wiki + "Desert_climate#Hot_desert_climates");
case "Cold desert":
openURL(wiki + "Desert_climate#Cold_desert_climates");
case "Savanna":
openURL(wiki + "Tropical_and_subtropical_grasslands,_savannas,_and_shrublands");
case "Grassland":
openURL(wiki + "Temperate_grasslands,_savannas,_and_shrublands");
case "Tropical seasonal forest":
openURL(wiki + "Seasonal_tropical_forest");
case "Temperate deciduous forest":
openURL(wiki + "Temperate_deciduous_forest");
case "Tropical rainforest":
openURL(wiki + "Tropical_rainforest");
case "Temperate rainforest":
openURL(wiki + "Temperate_rainforest");
case "Taiga":
openURL(wiki + "Taiga");
case "Tundra":
openURL(wiki + "Tundra");
case "Glacier":
openURL(wiki + "Glacier");
case "Wetland":
openURL(wiki + "Wetland");
case 'Hot desert':
openURL(wiki + 'Desert_climate#Hot_desert_climates');
case 'Cold desert':
openURL(wiki + 'Desert_climate#Cold_desert_climates');
case 'Savanna':
openURL(wiki + 'Tropical_and_subtropical_grasslands,_savannas,_and_shrublands');
case 'Grassland':
openURL(wiki + 'Temperate_grasslands,_savannas,_and_shrublands');
case 'Tropical seasonal forest':
openURL(wiki + 'Seasonal_tropical_forest');
case 'Temperate deciduous forest':
openURL(wiki + 'Temperate_deciduous_forest');
case 'Tropical rainforest':
openURL(wiki + 'Tropical_rainforest');
case 'Temperate rainforest':
openURL(wiki + 'Temperate_rainforest');
case 'Taiga':
openURL(wiki + 'Taiga');
case 'Tundra':
openURL(wiki + 'Tundra');
case 'Glacier':
openURL(wiki + 'Glacier');
case 'Wetland':
openURL(wiki + 'Wetland');
default:
openURL(`https://en.wikipedia.org/w/index.php?search=${name}`);
}
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const d = biomesData;
const data = Array.from(d.i)
.filter(i => d.cells[i])
.filter((i) => d.cells[i])
.sort((a, b) => d.area[b] - d.area[a])
.map(i => [i, d.color[i], d.name[i]]);
drawLegend("Biomes", data);
.map((i) => [i, d.color[i], d.name[i]]);
drawLegend('Biomes', data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +biomesFooterCells.innerHTML;
const totalArea = +biomesFooterArea.dataset.area;
const totalPopulation = +biomesFooterPopulation.dataset.population;
body.querySelectorAll(":scope> div").forEach(function (el) {
el.querySelector(".biomeCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%";
body.querySelectorAll(':scope> div').forEach(function (el) {
el.querySelector('.biomeCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100) + '%';
el.querySelector('.biomePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + '%';
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
biomesEditorAddLines();
}
}
@ -261,14 +263,14 @@ function editBiomes() {
const b = biomesData,
i = biomesData.i.length;
if (i > 254) {
tip("Maximum number of biomes reached (255), data cleansing is required", false, "error");
tip('Maximum number of biomes reached (255), data cleansing is required', false, 'error');
return;
}
b.i.push(i);
b.color.push(getRandomColor());
b.habitability.push(50);
b.name.push("Custom");
b.name.push('Custom');
b.iconsDensity.push(0);
b.icons.push([]);
b.cost.push(50);
@ -278,7 +280,7 @@ function editBiomes() {
b.cells.push(0);
b.area.push(0);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const line = `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability=${b.habitability[i]} data-cells=0 data-area=0 data-population=0 data-color=${b.color[i]}>
<svg data-tip="Biomes fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${b.color[i]}" class="fillRect pointer"></svg>
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
@ -293,84 +295,84 @@ function editBiomes() {
<span data-tip="Remove the custom biome" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
$("#biomesEditor").dialog({width: fitContent()});
body.insertAdjacentHTML('beforeend', line);
biomesFooterBiomes.innerHTML = body.querySelectorAll(':scope > div').length;
$('#biomesEditor').dialog({width: fitContent()});
}
function removeCustomBiome(el) {
const biome = +el.parentNode.dataset.id;
el.parentNode.remove();
biomesData.name[biome] = "removed";
biomesData.name[biome] = 'removed';
biomesFooterBiomes.innerHTML = +biomesFooterBiomes.innerHTML - 1;
}
function regenerateIcons() {
ReliefIcons();
if (!layerIsOn("toggleRelief")) toggleRelief();
if (!layerIsOn('toggleRelief')) toggleRelief();
}
function downloadBiomesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Biome,Color,Habitability,Cells,Area " + unit + ",Population\n"; // headers
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Biome,Color,Habitability,Cells,Area ' + unit + ',Population\n'; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += el.dataset.color + ",";
data += el.dataset.habitability + "%,";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += el.dataset.color + ',';
data += el.dataset.habitability + '%,';
data += el.dataset.cells + ',';
data += el.dataset.area + ',';
data += el.dataset.population + '\n';
});
const name = getFileName("Biomes") + ".csv";
const name = getFileName('Biomes') + '.csv';
downloadFile(data, name);
}
function enterBiomesCustomizationMode() {
if (!layerIsOn("toggleBiomes")) toggleBiomes();
if (!layerIsOn('toggleBiomes')) toggleBiomes();
customization = 6;
biomes.append("g").attr("id", "temp");
biomes.append('g').attr('id', 'temp');
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "none"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "block"));
body.querySelector("div.biomes").classList.add("selected");
document.querySelectorAll('#biomesBottom > button').forEach((el) => (el.style.display = 'none'));
document.querySelectorAll('#biomesBottom > div').forEach((el) => (el.style.display = 'block'));
body.querySelector('div.biomes').classList.add('selected');
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "none"));
biomesFooter.style.display = "none";
$("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
biomesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
biomesFooter.style.display = 'none';
$('#biomesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
tip("Click on biome to select, drag the circle to change biome", true);
viewbox.style("cursor", "crosshair").on("click", selectBiomeOnMapClick).call(d3.drag().on("start", dragBiomeBrush)).on("touchmove mousemove", moveBiomeBrush);
tip('Click on biome to select, drag the circle to change biome', true);
viewbox.style('cursor', 'crosshair').on('click', selectBiomeOnMapClick).call(d3.drag().on('start', dragBiomeBrush)).on('touchmove mousemove', moveBiomeBrush);
}
function selectBiomeOnLineClick(line) {
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
line.classList.add("selected");
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
line.classList.add('selected');
}
function selectBiomeOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) {
tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error");
tip('You cannot reassign water via biomes. Please edit the Heightmap to change water', false, 'error');
return;
}
const assigned = biomes.select("#temp").select("polygon[data-cell='" + i + "']");
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
const assigned = biomes.select('#temp').select("polygon[data-cell='" + i + "']");
const biome = assigned.size() ? +assigned.attr('data-biome') : pack.cells.biome[i];
body.querySelector("div.selected").classList.remove("selected");
body.querySelector("div[data-id='" + biome + "']").classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
body.querySelector("div[data-id='" + biome + "']").classList.add('selected');
}
function dragBiomeBrush() {
const r = +biomesManuallyBrush.value;
d3.event.on("drag", () => {
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -383,20 +385,20 @@ function editBiomes() {
// change region within selection
function changeBiomeForSelection(selection) {
const temp = biomes.select("#temp");
const selected = body.querySelector("div.selected");
const temp = biomes.select('#temp');
const selected = body.querySelector('div.selected');
const biomeNew = selected.dataset.id;
const color = biomesData.color[biomeNew];
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
const biomeOld = exists.size() ? +exists.attr('data-biome') : pack.cells.biome[i];
if (biomeNew === biomeOld) return;
// change of append new element
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
if (exists.size()) exists.attr('data-biome', biomeNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-biome', biomeNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
});
}
@ -408,7 +410,7 @@ function editBiomes() {
}
function applyBiomesChange() {
const changed = biomes.select("#temp").selectAll("polygon");
const changed = biomes.select('#temp').selectAll('polygon');
changed.each(function () {
const i = +this.dataset.cell;
const b = +this.dataset.biome;
@ -424,21 +426,21 @@ function editBiomes() {
function exitBiomesCustomizationMode(close) {
customization = 0;
biomes.select("#temp").remove();
biomes.select('#temp').remove();
removeCircle();
document.querySelectorAll("#biomesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.querySelectorAll("#biomesBottom > div").forEach(el => (el.style.display = "none"));
document.querySelectorAll('#biomesBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.querySelectorAll('#biomesBottom > div').forEach((el) => (el.style.display = 'none'));
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
biomesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden"));
biomesFooter.style.display = "block";
if (!close) $("#biomesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg"}});
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
biomesEditor.querySelectorAll('.hide').forEach((el) => el.classList.remove('hidden'));
biomesFooter.style.display = 'block';
if (!close) $('#biomesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
restoreDefaultEvents();
clearMainTip();
const selected = document.querySelector("#biomesBody > div.selected");
if (selected) selected.classList.remove("selected");
const selected = document.querySelector('#biomesBody > div.selected');
if (selected) selected.classList.remove('selected');
}
function restoreInitialBiomes() {
@ -450,6 +452,6 @@ function editBiomes() {
}
function closeBiomesEditor() {
exitBiomesCustomizationMode("close");
exitBiomesCustomizationMode('close');
}
}

View file

@ -10,15 +10,14 @@ function editBurg(id) {
burgLabels.selectAll('text').call(d3.drag().on('start', dragBurgLabel)).classed('draggable', true);
updateBurgValues();
const my = id || d3.event.target.tagName === 'text' ? 'center bottom-40' : 'center top+40';
const at = id ? 'center' : d3.event.target.tagName === 'text' ? 'top' : 'bottom';
const of = id ? 'svg' : d3.event.target;
const my = id || d3.event.target.tagName === "text" ? "center bottom-20" : "center top+20";
const at = id ? "center" : d3.event.target.tagName === "text" ? "top" : "bottom";
const of = id ? "svg" : d3.event.target;
$('#burgEditor').dialog({
title: 'Edit Burg',
resizable: false,
close: closeBurgEditor,
position: {my, at, of, collision: 'fit'}
position: {my, at, of, collision: "fit"}
});
if (modules.editBurg) return;
@ -39,6 +38,8 @@ function editBurg(id) {
document.getElementById('burgNameReCulture').addEventListener('click', generateNameCulture);
document.getElementById('burgPopulation').addEventListener('change', changePopulation);
burgBody.querySelectorAll('.burgFeature').forEach((el) => el.addEventListener('click', toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById('burgStyleShow').addEventListener('click', showStyleSection);
document.getElementById('burgStyleHide').addEventListener('click', hideStyleSection);
@ -48,6 +49,8 @@ function editBurg(id) {
document.getElementById('burgSeeInMFCG').addEventListener('click', openInMFCG);
document.getElementById('burgEditEmblem').addEventListener('click', openEmblemEdit);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById('burgRelocate').addEventListener('click', toggleRelocateBurg);
document.getElementById('burglLegend').addEventListener('click', editBurgLegend);
document.getElementById('burgLock').addEventListener('click', toggleBurgLockButton);
@ -68,7 +71,14 @@ function editBurg(id) {
document.getElementById('burgState').innerHTML = stateName;
document.getElementById('burgProvince').innerHTML = provinceName;
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
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";
document.getElementById("burgName").value = b.name;
document.getElementById("burgType").value = b.type || "Generic";
document.getElementById("burgPopulation").value = rn(b.population * populationRate * urbanization);
document.getElementById("burgEditAnchorStyle").style.display = +b.port ? "inline-block" : "none";
// update list and select culture
const cultureSelect = document.getElementById('burgCulture');
@ -119,6 +129,14 @@ function editBurg(id) {
const coaID = 'burgCOA' + id;
COArenderer.trigger(coaID, b.coa);
document.getElementById('burgEmblem').setAttribute('href', '#' + coaID);
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
}
function getProduction(pool) {
@ -411,11 +429,7 @@ function editBurg(id) {
}
}
function showBurgELockTip() {
const id = +elSelected.attr('data-id');
showBurgLockTip(id);
}
function showStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgStyleSection').style.display = 'inline-block';
@ -443,57 +457,62 @@ function editBurg(id) {
function openInMFCG(event) {
const id = elSelected.attr('data-id');
const burg = pack.burgs[id];
const defSeed = +(seed + id.padStart(4, 0));
if (isCtrlClick(event)) {
prompt(
`Please provide a Medieval Fantasy City Generator seed.
Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
{default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
(v) => {
burg.MFCG = v;
openMFCG(v);
}
);
} else openMFCG();
}
function getBurgSeed(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {
openURL(burg.MFCGlink);
return;
}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
const population = rn(burg.population * populationRate * urbanization);
function getMFCGlink(burg) {
const {cells} = pack;
const {name, population, cell} = burg;
const burgSeed = getBurgSeed(burg);
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
const people = rn(population * populationRate * urbanization);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : '';
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;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return '&sea=' + norm;
}
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
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;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea=" + norm;
}
const site = 'http://fantasycities.watabou.ru/?random=0&continuous=0';
const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
}
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
return url;
}
function changeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
}
function randomizeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function openEmblemEdit() {
@ -502,6 +521,12 @@ 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 toggleRelocateBurg() {
const toggler = document.getElementById('toggleCells');
document.getElementById('burgRelocate').classList.toggle('pressed');

View file

@ -0,0 +1,739 @@
'use strict';
function editBurg(id) {
if (customization) return;
closeDialogs('.stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = id || d3.event.target.dataset.id;
elSelected = burgLabels.select("[data-id='" + burg + "']");
burgLabels.selectAll('text').call(d3.drag().on('start', dragBurgLabel)).classed('draggable', true);
updateBurgValues();
<<<<<<< HEAD
const my = id || d3.event.target.tagName === 'text' ? 'center bottom-40' : 'center top+40';
const at = id ? 'center' : d3.event.target.tagName === 'text' ? 'top' : 'bottom';
const of = id ? 'svg' : d3.event.target;
$('#burgEditor').dialog({
title: 'Edit Burg',
resizable: false,
close: closeBurgEditor,
position: {my, at, of, collision: 'fit'}
=======
$("#burgEditor").dialog({
title: "Edit Burg",
resizable: false,
close: closeBurgEditor,
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"}
>>>>>>> master
});
if (modules.editBurg) return;
modules.editBurg = true;
// add listeners
<<<<<<< HEAD
document.getElementById('burgGroupShow').addEventListener('click', showGroupSection);
document.getElementById('burgGroupHide').addEventListener('click', hideGroupSection);
document.getElementById('burgSelectGroup').addEventListener('change', changeGroup);
document.getElementById('burgInputGroup').addEventListener('change', createNewGroup);
document.getElementById('burgAddGroup').addEventListener('click', toggleNewGroupInput);
document.getElementById('burgRemoveGroup').addEventListener('click', removeBurgsGroup);
document.getElementById('burgName').addEventListener('input', changeName);
document.getElementById('burgNameReRandom').addEventListener('click', generateNameRandom);
document.getElementById('burgType').addEventListener('input', changeType);
document.getElementById('burgCulture').addEventListener('input', changeCulture);
document.getElementById('burgNameReCulture').addEventListener('click', generateNameCulture);
document.getElementById('burgPopulation').addEventListener('change', changePopulation);
burgBody.querySelectorAll('.burgFeature').forEach((el) => el.addEventListener('click', toggleFeature));
document.getElementById('burgStyleShow').addEventListener('click', showStyleSection);
document.getElementById('burgStyleHide').addEventListener('click', hideStyleSection);
document.getElementById('burgEditLabelStyle').addEventListener('click', editGroupLabelStyle);
document.getElementById('burgEditIconStyle').addEventListener('click', editGroupIconStyle);
document.getElementById('burgEditAnchorStyle').addEventListener('click', editGroupAnchorStyle);
document.getElementById('burgSeeInMFCG').addEventListener('click', openInMFCG);
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('burgLock').addEventListener('mouseover', showBurgELockTip);
document.getElementById('burgRemove').addEventListener('click', removeSelectedBurg);
=======
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
document.getElementById("burgName").addEventListener("input", changeName);
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
document.getElementById("burgType").addEventListener("input", changeType);
document.getElementById("burgCulture").addEventListener("input", changeCulture);
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
document.getElementById("burgPopulation").addEventListener("change", changePopulation);
burgBody.querySelectorAll(".burgFeature").forEach(el => el.addEventListener("click", toggleFeature));
document.getElementById("mfcgBurgSeed").addEventListener("change", changeSeed);
document.getElementById("regenerateMFCGBurgSeed").addEventListener("click", randomizeSeed);
document.getElementById("burgStyleShow").addEventListener("click", showStyleSection);
document.getElementById("burgStyleHide").addEventListener("click", hideStyleSection);
document.getElementById("burgEditLabelStyle").addEventListener("click", editGroupLabelStyle);
document.getElementById("burgEditIconStyle").addEventListener("click", editGroupIconStyle);
document.getElementById("burgEditAnchorStyle").addEventListener("click", editGroupAnchorStyle);
document.getElementById("burgEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgToggleMFCGMap").addEventListener("click", toggleMFCGMap);
document.getElementById("burgEditEmblem").addEventListener("click", openEmblemEdit);
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
document.getElementById("burgLock").addEventListener("click", toggleBurgLockButton);
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
>>>>>>> master
function updateBurgValues() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
document.getElementById('burgName').value = b.name;
document.getElementById('burgType').value = b.type || 'Generic';
document.getElementById('burgPopulation').value = rn(b.population * populationRate * urbanization);
const stateName = pack.states[b.state].fullName || pack.states[b.state].name;
const province = pack.cells.province[b.cell];
const provinceName = province ? pack.provinces[province].fullName : '';
document.getElementById('burgState').innerHTML = stateName;
document.getElementById('burgProvince').innerHTML = provinceName;
document.getElementById('burgEditAnchorStyle').style.display = +b.port ? 'inline-block' : 'none';
// update list and select culture
const cultureSelect = document.getElementById('burgCulture');
cultureSelect.options.length = 0;
const cultures = pack.cultures.filter((c) => !c.removed);
cultures.forEach((c) => cultureSelect.options.add(new Option(c.name, c.i, false, c.i === b.culture)));
const temperature = grid.cells.temp[pack.cells.g[b.cell]];
document.getElementById('burgTemperature').innerHTML = convertTemperature(temperature);
document.getElementById('burgTemperatureLike').innerHTML = getTemperatureLikeness(temperature);
document.getElementById('burgElevation').innerHTML = getHeight(pack.cells.h[b.cell]);
// toggle features
if (b.capital) document.getElementById('burgCapital').classList.remove('inactive');
else document.getElementById('burgCapital').classList.add('inactive');
if (b.port) document.getElementById('burgPort').classList.remove('inactive');
else document.getElementById('burgPort').classList.add('inactive');
if (b.citadel) document.getElementById('burgCitadel').classList.remove('inactive');
else document.getElementById('burgCitadel').classList.add('inactive');
if (b.walls) document.getElementById('burgWalls').classList.remove('inactive');
else document.getElementById('burgWalls').classList.add('inactive');
if (b.plaza) document.getElementById('burgPlaza').classList.remove('inactive');
else document.getElementById('burgPlaza').classList.add('inactive');
if (b.temple) document.getElementById('burgTemple').classList.remove('inactive');
else document.getElementById('burgTemple').classList.add('inactive');
if (b.shanty) document.getElementById('burgShanty').classList.remove('inactive');
else document.getElementById('burgShanty').classList.add('inactive');
// economics block
document.getElementById('burgProduction').innerHTML = getProduction(b.produced);
const deals = pack.trade.deals;
document.getElementById('burgExport').innerHTML = getExport(deals.filter((deal) => deal.exporter === b.i));
document.getElementById('burgImport').innerHTML = '';
//toggle lock
updateBurgLockIcon();
// select group
const group = elSelected.node().parentNode.id;
const select = document.getElementById('burgSelectGroup');
select.options.length = 0; // remove all options
burgLabels.selectAll('g').each(function () {
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
// set emlem image
const coaID = 'burgCOA' + id;
COArenderer.trigger(coaID, b.coa);
<<<<<<< HEAD
document.getElementById('burgEmblem').setAttribute('href', '#' + coaID);
=======
document.getElementById("burgEmblem").setAttribute("href", "#" + coaID);
if (options.showMFCGMap) {
document.getElementById("mfcgPreviewSection").style.display = "block";
updateMFCGFrame(b);
document.getElementById("mfcgBurgSeed").value = getBurgSeed(b);
} else {
document.getElementById("mfcgPreviewSection").style.display = "none";
}
>>>>>>> master
}
function getProduction(pool) {
let html = '';
for (const resourceId in pool) {
const {name, unit, icon} = Resources.get(+resourceId);
const production = pool[resourceId];
const unitName = production > 1 ? unit + 's' : unit;
html += `<span data-tip="${name}: ${production} ${unitName}">
<svg class="resIcon"><use href="#${icon}"></svg>
<span style="margin: 0 0.2em 0 -0.2em">${production}</span>
</span>`;
}
return html;
}
function getExport(dealsArray) {
if (!dealsArray.length) return 'no';
const totalIncome = rn(d3.sum(dealsArray.map((deal) => deal.burgIncome)));
const exported = dealsArray.map((deal) => {
const {resourceId, quantity, burgIncome} = deal;
const {name, unit, icon} = Resources.get(resourceId);
const unitName = quantity > 1 ? unit + 's' : unit;
return `<span data-tip="${name}: ${quantity} ${unitName}. Income: ${rn(burgIncome)}">
<svg class="resIcon"><use href="#${icon}"></svg>
<span style="margin: 0 0.2em 0 -0.2em">${quantity}</span>
</span>`;
});
return `${totalIncome}: ${exported.join('')}`;
}
// [-1; 31] °C, source: https://en.wikipedia.org/wiki/List_of_cities_by_average_temperature
function getTemperatureLikeness(temperature) {
if (temperature < -15) return 'nowhere in the real-world';
if (temperature < -5) return 'in Yakutsk';
if (temperature > 31) return 'nowhere in the real-world';
const cities = [
'Snag (Yukon)',
'Yellowknife (Canada)',
'Okhotsk (Russia)',
'Fairbanks (Alaska)',
'Nuuk (Greenland)',
'Murmansk', // -5 - 0
'Arkhangelsk',
'Anchorage',
'Tromsø',
'Reykjavik',
'Riga',
'Stockholm',
'Halifax',
'Prague',
'Copenhagen',
'London', // 1 - 10
'Antwerp',
'Paris',
'Milan',
'Batumi',
'Rome',
'Dubrovnik',
'Lisbon',
'Barcelona',
'Marrakesh',
'Alexandria', // 11 - 20
'Tegucigalpa',
'Guangzhou',
'Rio de Janeiro',
'Dakar',
'Miami',
'Jakarta',
'Mogadishu',
'Bangkok',
'Aden',
'Khartoum'
]; // 21 - 30
if (temperature > 30) return 'Mecca';
return cities[temperature + 5] || null;
}
function dragBurgLabel() {
const tr = parseTransform(this.getAttribute('transform'));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on('drag', function () {
const x = d3.event.x,
y = d3.event.y;
this.setAttribute('transform', `translate(${dx + x},${dy + y})`);
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, 'warning');
});
}
function showGroupSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgGroupSection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('burgGroupSection').style.display = 'none';
document.getElementById('burgInputGroup').style.display = 'none';
document.getElementById('burgInputGroup').value = '';
document.getElementById('burgSelectGroup').style.display = 'inline-block';
}
function changeGroup() {
const id = +elSelected.attr('data-id');
moveBurgToGroup(id, this.value);
}
function toggleNewGroupInput() {
if (burgInputGroup.style.display === 'none') {
burgInputGroup.style.display = 'inline-block';
burgInputGroup.focus();
burgSelectGroup.style.display = 'none';
} else {
burgInputGroup.style.display = 'none';
burgSelectGroup.style.display = 'inline-block';
}
}
function createNewGroup() {
if (!this.value) {
tip('Please provide a valid group name', false, 'error');
return;
}
const group = this.value
.toLowerCase()
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (document.getElementById(group)) {
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
if (Number.isFinite(+group.charAt(0))) {
tip('Group name should start with a letter', false, 'error');
return;
}
const id = +elSelected.attr('data-id');
const oldGroup = elSelected.node().parentNode.id;
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (!label || !icon) {
ERROR && console.error('Cannot find label or icon elements');
return;
}
const labelG = document.querySelector('#burgLabels > #' + oldGroup);
const iconG = document.querySelector('#burgIcons > #' + oldGroup);
const anchorG = document.querySelector('#anchors > #' + oldGroup);
// just rename if only 1 element left
const count = elSelected.node().parentNode.childElementCount;
if (oldGroup !== 'cities' && oldGroup !== 'towns' && count === 1) {
document.getElementById('burgSelectGroup').selectedOptions[0].remove();
document.getElementById('burgSelectGroup').options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById('burgInputGroup').value = '';
labelG.id = group;
iconG.id = group;
if (anchor) anchorG.id = group;
return;
}
// create new groups
document.getElementById('burgSelectGroup').options.add(new Option(group, group, false, true));
toggleNewGroupInput();
document.getElementById('burgInputGroup').value = '';
const newLabelG = document.querySelector('#burgLabels').appendChild(labelG.cloneNode(false));
newLabelG.id = group;
const newIconG = document.querySelector('#burgIcons').appendChild(iconG.cloneNode(false));
newIconG.id = group;
if (anchor) {
const newAnchorG = document.querySelector('#anchors').appendChild(anchorG.cloneNode(false));
newAnchorG.id = group;
}
moveBurgToGroup(id, group);
}
function removeBurgsGroup() {
const group = elSelected.node().parentNode;
const basic = group.id === 'cities' || group.id === 'towns';
const burgsInGroup = [];
for (let i = 0; i < group.children.length; i++) {
burgsInGroup.push(+group.children[i].dataset.id);
}
const burgsToRemove = burgsInGroup.filter((b) => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length;
const message = `Are you sure you want to remove
${basic || capital ? 'all unlocked elements in the 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}`;
confirmationDialog({title: 'Remove burg group', message, confirm: 'Remove', onConfirm: removeGroup});
function removeGroup() {
$(this).dialog('close');
$('#burgEditor').dialog('close');
hideGroupSection();
burgsToRemove.forEach((b) => removeBurg(b));
if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector('#burgLabels > #' + group.id);
const iconG = document.querySelector('#burgIcons > #' + group.id);
const anchorG = document.querySelector('#anchors > #' + group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
}
}
function changeName() {
const id = +elSelected.attr('data-id');
pack.burgs[id].name = burgName.value;
elSelected.text(burgName.value);
}
function generateNameRandom() {
const base = rand(nameBases.length - 1);
burgName.value = Names.getBase(base);
changeName();
}
function changeType() {
const id = +elSelected.attr('data-id');
pack.burgs[id].type = this.value;
}
function changeCulture() {
const id = +elSelected.attr('data-id');
pack.burgs[id].culture = +this.value;
}
function generateNameCulture() {
const id = +elSelected.attr('data-id');
const culture = pack.burgs[id].culture;
burgName.value = Names.getCulture(culture);
changeName();
}
function changePopulation() {
const id = +elSelected.attr('data-id');
pack.burgs[id].population = rn(burgPopulation.value / populationRate / urbanization, 4);
}
function toggleFeature() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
const feature = this.dataset.feature;
const turnOn = this.classList.contains('inactive');
if (feature === 'port') togglePort(id);
else if (feature === 'capital') toggleCapital(id);
else b[feature] = +turnOn;
if (b[feature]) this.classList.remove('inactive');
else if (!b[feature]) this.classList.add('inactive');
if (b.port) document.getElementById('burgEditAnchorStyle').style.display = 'inline-block';
else document.getElementById('burgEditAnchorStyle').style.display = 'none';
}
function toggleBurgLockButton() {
const id = +elSelected.attr('data-id');
toggleBurgLock(id);
updateBurgLockIcon();
}
function updateBurgLockIcon() {
const id = +elSelected.attr('data-id');
const b = pack.burgs[id];
if (b.lock) {
document.getElementById('burgLock').classList.remove('icon-lock-open');
document.getElementById('burgLock').classList.add('icon-lock');
} else {
document.getElementById('burgLock').classList.remove('icon-lock');
document.getElementById('burgLock').classList.add('icon-lock-open');
}
}
<<<<<<< HEAD
function showBurgELockTip() {
const id = +elSelected.attr('data-id');
showBurgLockTip(id);
}
=======
>>>>>>> master
function showStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('burgStyleSection').style.display = 'inline-block';
}
function hideStyleSection() {
document.querySelectorAll('#burgBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('burgStyleSection').style.display = 'none';
}
function editGroupLabelStyle() {
const g = elSelected.node().parentNode.id;
editStyle('labels', g);
}
function editGroupIconStyle() {
const g = elSelected.node().parentNode.id;
editStyle('burgIcons', g);
}
function editGroupAnchorStyle() {
const g = elSelected.node().parentNode.id;
editStyle('anchors', g);
}
<<<<<<< HEAD
function openInMFCG(event) {
const id = elSelected.attr('data-id');
const burg = pack.burgs[id];
const defSeed = +(seed + id.padStart(4, 0));
if (isCtrlClick(event)) {
prompt(
`Please provide a Medieval Fantasy City Generator seed.
Seed should be a number. Default seed is FMG map seed + burg id padded to 4 chars with zeros (${defSeed}).
Please note that if seed is custom, "Overworld" button from MFCG will open a different map`,
{default: burg.MFCG || defSeed, step: 1, min: 1, max: 1e13 - 1},
(v) => {
burg.MFCG = v;
openMFCG(v);
}
);
} else openMFCG();
function openMFCG(seed) {
if (!seed && burg.MFCGlink) {
openURL(burg.MFCGlink);
return;
}
const cells = pack.cells;
const name = elSelected.text();
const size = Math.max(Math.min(rn(burg.population), 100), 6); // to be removed once change on MFDC is done
const population = rn(burg.population * populationRate * urbanization);
const s = burg.MFCG || defSeed;
const cell = burg.cell;
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[burg.cell] ? getSeaDirections(burg.cell) : '';
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;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return '&sea=' + norm;
}
const site = 'http://fantasycities.watabou.ru/?random=0&continuous=0';
const url = `${site}&name=${name}&population=${population}&size=${size}&seed=${s}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
openURL(url);
=======
function updateMFCGFrame(burg) {
const mfcgURL = getMFCGlink(burg);
document.getElementById("mfcgPreview").setAttribute("src", mfcgURL);
document.getElementById("mfcgLink").setAttribute("href", mfcgURL);
}
function getBurgSeed(burg) {
return burg.MFCG || Number(`${seed}${String(burg.i).padStart(4, 0)}`);
}
function getMFCGlink(burg) {
const {cells} = pack;
const {name, population, cell} = burg;
const burgSeed = getBurgSeed(burg);
const sizeRaw = 2.13 * Math.pow((population * populationRate) / urbanDensity, 0.385);
const size = minmax(Math.ceil(sizeRaw), 6, 100);
const people = rn(population * populationRate * urbanization);
const hub = +cells.road[cell] > 50;
const river = cells.r[cell] ? 1 : 0;
const coast = +burg.port;
const citadel = +burg.citadel;
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
const shanty = +burg.shanty;
const sea = coast && cells.haven[cell] ? getSeaDirections(cell) : "";
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;
const norm = rn(normalize(deg, 0, 360) * 2, 2); // 0 = south, 0.5 = west, 1 = north, 1.5 = east
return "&sea=" + norm;
>>>>>>> master
}
const baseURL = "https://watabou.github.io/city-generator/?random=0&continuous=0";
const url = `${baseURL}&name=${name}&population=${people}&size=${size}&seed=${burgSeed}&hub=${hub}&river=${river}&coast=${coast}&citadel=${citadel}&plaza=${plaza}&temple=${temple}&walls=${walls}&shantytown=${shanty}${sea}`;
return url;
}
function changeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = +this.value;
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
}
function randomizeSeed() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const burgSeed = rand(1e9 - 1);
burg.MFCG = burgSeed;
updateMFCGFrame(burg);
document.getElementById("mfcgBurgSeed").value = burgSeed;
}
function openEmblemEdit() {
const id = +elSelected.attr('data-id'),
burg = pack.burgs[id];
editEmblem('burg', 'burgCOA' + id, burg);
}
function toggleMFCGMap() {
options.showMFCGMap = !options.showMFCGMap;
document.getElementById("mfcgPreviewSection").style.display = options.showMFCGMap ? "block" : "none";
document.getElementById("burgToggleMFCGMap").className = options.showMFCGMap ? "icon-map" : "icon-map-o";
}
function toggleRelocateBurg() {
const toggler = document.getElementById('toggleCells');
document.getElementById('burgRelocate').classList.toggle('pressed');
if (document.getElementById('burgRelocate').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', relocateBurgOnClick);
tip('Click on map to relocate burg. Hold Shift for continuous move', true);
if (!layerIsOn('toggleCells')) {
toggleCells();
toggler.dataset.forced = true;
}
} else {
clearMainTip();
viewbox.on('click', clicked).style('cursor', 'default');
if (layerIsOn('toggleCells') && toggler.dataset.forced) {
toggleCells();
toggler.dataset.forced = false;
}
}
}
function relocateBurgOnClick() {
const cells = pack.cells;
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const id = +elSelected.attr('data-id');
const burg = pack.burgs[id];
if (cells.h[cell] < 20) {
tip('Cannot place burg into the water! Select a land cell', false, 'error');
return;
}
if (cells.burg[cell] && cells.burg[cell] !== id) {
tip('There is already a burg in this cell. Please select a free cell', false, 'error');
return;
}
const newState = cells.state[cell];
const oldState = burg.state;
if (newState !== oldState && burg.capital) {
tip('Capital cannot be relocated into another state!', false, 'error');
return;
}
// change UI
const x = rn(point[0], 2),
y = rn(point[1], 2);
burgIcons
.select("[data-id='" + id + "']")
.attr('transform', null)
.attr('cx', x)
.attr('cy', y);
burgLabels
.select("text[data-id='" + id + "']")
.attr('transform', null)
.attr('x', x)
.attr('y', y);
const anchor = anchors.select("use[data-id='" + id + "']");
if (anchor.size()) {
const size = anchor.attr('width');
const xa = rn(x - size * 0.47, 2);
const ya = rn(y - size * 0.47, 2);
anchor.attr('transform', null).attr('x', xa).attr('y', ya);
}
// change data
cells.burg[burg.cell] = 0;
cells.burg[cell] = id;
burg.cell = cell;
burg.state = newState;
burg.x = x;
burg.y = y;
if (burg.capital) pack.states[newState].center = burg.cell;
if (d3.event.shiftKey === false) toggleRelocateBurg();
}
function editBurgLegend() {
const id = elSelected.attr('data-id');
const name = elSelected.text();
editNotes('burg' + id, name);
}
function removeSelectedBurg() {
const id = +elSelected.attr('data-id');
if (pack.burgs[id].capital) {
alertMessage.innerHTML = `You cannot remove the burg as it is a state capital.<br><br>
You can change the capital using Burgs Editor (shift + T)`;
$('#alert').dialog({
resizable: false,
title: 'Remove burg',
buttons: {
Ok: function () {
$(this).dialog('close');
}
}
});
} else {
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(id);
$('#burgEditor').dialog('close');
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
}
function closeBurgEditor() {
document.getElementById('burgRelocate').classList.remove('pressed');
burgLabels.selectAll('text').call(d3.drag().on('drag', null)).classed('draggable', false);
unselect();
}
}

View file

@ -79,17 +79,20 @@ function overviewBurgs() {
const province = prov ? pack.provinces[prov].name : '';
const culture = pack.cultures[b.culture].name;
lines += `<div class="states" data-id=${b.i} data-name="${b.name}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Edit burg" class="icon-pencil"></span>
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
lines += `<div class="states" data-id=${b.i} data-name="${
b.name
}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<input data-tip="Burg province" class="burgState" value="${province}" disabled>
<input data-tip="Burg state" class="burgState" value="${state}" disabled>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(b.culture)}</select>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(
b.culture
)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
}"></span>
</div>
<span data-tip="Zoom to burg" class="icon-dot-circled pointer"></span>
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
@ -202,11 +205,6 @@ function overviewBurgs() {
}
}
function showBurgOLockTip() {
const burg = +this.parentNode.dataset.id;
showBurgLockTip(burg);
}
function openBurgEditor() {
const burg = +this.parentNode.dataset.id;
editBurg(burg);
@ -281,6 +279,7 @@ function overviewBurgs() {
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const burgs = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => {
@ -292,6 +291,7 @@ function overviewBurgs() {
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error");
const root = d3
.stratify()
@ -401,6 +401,12 @@ function overviewBurgs() {
const base = this.value === 'states' ? getStatesData() : this.value === 'cultures' ? getCulturesData() : this.value === 'parent' ? getParentData() : getProvincesData();
burgs.forEach((b) => (b.id = b.i + base.length - 1));
? getStatesData()
: this.value === "cultures"
? getCulturesData()
: this.value === "parent"
? getParentData()
: getProvincesData();
const data = base.concat(burgs);
@ -435,6 +441,8 @@ function overviewBurgs() {
function downloadBurgsData() {
let data = 'Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (' + heightUnit.value + '),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n'; // headers
const valid = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
heightUnit.value +
"),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
valid.forEach((b) => {
data += b.i + ',';

View file

@ -0,0 +1,603 @@
'use strict';
function overviewBurgs() {
if (customization) return;
closeDialogs('#burgsOverview, .stable');
if (!layerIsOn('toggleIcons')) toggleIcons();
if (!layerIsOn('toggleLabels')) toggleLabels();
const body = document.getElementById('burgsBody');
updateFilter();
burgsOverviewAddLines();
$('#burgsOverview').dialog();
if (modules.overviewBurgs) return;
modules.overviewBurgs = true;
$('#burgsOverview').dialog({
title: 'Burgs Overview',
resizable: false,
width: fitContent(),
close: exitAddBurgMode,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('burgsOverviewRefresh').addEventListener('click', refreshBurgsEditor);
document.getElementById('burgsChart').addEventListener('click', showBurgsChart);
document.getElementById('burgsFilterState').addEventListener('change', burgsOverviewAddLines);
document.getElementById('burgsFilterCulture').addEventListener('change', burgsOverviewAddLines);
document.getElementById('regenerateBurgNames').addEventListener('click', regenerateNames);
document.getElementById('addNewBurg').addEventListener('click', enterAddBurgMode);
document.getElementById('burgsExport').addEventListener('click', downloadBurgsData);
document.getElementById('burgNamesImport').addEventListener('click', renameBurgsInBulk);
document.getElementById('burgsListToLoad').addEventListener('change', function () {
uploadFile(this, importBurgNames);
});
document.getElementById('burgsRemoveAll').addEventListener('click', triggerAllBurgsRemove);
function refreshBurgsEditor() {
updateFilter();
burgsOverviewAddLines();
}
function updateFilter() {
const stateFilter = document.getElementById('burgsFilterState');
const selectedState = 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));
const statesSorted = pack.states.filter((s) => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach((s) => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
const cultureFilter = document.getElementById('burgsFilterCulture');
const selectedCulture = 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));
const culturesSorted = pack.cultures.filter((c) => c.i && !c.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
culturesSorted.forEach((c) => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
}
// add line for each burg
function burgsOverviewAddLines() {
const selectedState = +document.getElementById('burgsFilterState').value;
const selectedCulture = +document.getElementById('burgsFilterCulture').value;
let filtered = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
if (selectedState != -1) filtered = filtered.filter((b) => b.state === selectedState); // filtered by state
if (selectedCulture != -1) filtered = filtered.filter((b) => b.culture === selectedCulture); // filtered by culture
body.innerHTML = '';
let lines = '',
totalPopulation = 0;
for (const b of filtered) {
const population = b.population * populationRate * urbanization;
totalPopulation += population;
const type = b.capital && b.port ? 'a-capital-port' : b.capital ? 'c-capital' : b.port ? 'p-port' : 'z-burg';
const state = pack.states[b.state].name;
const prov = pack.cells.province[b.cell];
const province = prov ? pack.provinces[prov].name : '';
const culture = pack.cultures[b.culture].name;
<<<<<<< HEAD
lines += `<div class="states" data-id=${b.i} data-name="${b.name}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Edit burg" class="icon-pencil"></span>
=======
lines += `<div class="states" data-id=${b.i} data-name="${
b.name
}" data-state="${state}" data-province="${province}" data-culture="${culture}" data-population=${population} data-type="${type}">
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
>>>>>>> master
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
<input data-tip="Burg province" class="burgState" value="${province}" disabled>
<input data-tip="Burg state" class="burgState" value="${state}" disabled>
<select data-tip="Dominant culture. Click to change burg culture (to change cell cultrure use Cultures Editor)" class="stateCulture">${getCultureOptions(
b.culture
)}</select>
<span data-tip="Burg population" class="icon-male"></span>
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${si(population)}>
<div class="burgType">
<<<<<<< HEAD
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
</div>
<span data-tip="Zoom to burg" class="icon-dot-circled pointer"></span>
<span class="locks pointer ${b.lock ? 'icon-lock' : 'icon-lock-open inactive'}"></span>
=======
<span data-tip="${b.capital ? " This burg is a state capital" : "Click to assign a capital status"}" class="icon-star-empty${
b.capital ? "" : " inactive pointer"
}"></span>
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? "" : " inactive"}" style="font-size:.9em"></span>
</div>
<span data-tip="Edit burg" class="icon-pencil"></span>
<span class="locks pointer ${b.lock ? "icon-lock" : "icon-lock-open inactive"}" onmouseover="showElementLockTip(event)"></span>
>>>>>>> master
<span data-tip="Remove burg" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
// update footer
burgsFooterBurgs.innerHTML = filtered.length;
burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0;
// add listeners
<<<<<<< HEAD
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => burgHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => burgHighlightOff(ev)));
body.querySelectorAll('div > input.burgName').forEach((el) => el.addEventListener('input', changeBurgName));
body.querySelectorAll('div > span.icon-dot-circled').forEach((el) => el.addEventListener('click', zoomIntoBurg));
body.querySelectorAll('div > select.stateCulture').forEach((el) => el.addEventListener('change', changeBurgCulture));
body.querySelectorAll('div > input.burgPopulation').forEach((el) => el.addEventListener('change', changeBurgPopulation));
body.querySelectorAll('div > span.icon-star-empty').forEach((el) => el.addEventListener('click', toggleCapitalStatus));
body.querySelectorAll('div > span.icon-anchor').forEach((el) => el.addEventListener('click', togglePortStatus));
body.querySelectorAll('div > span.locks').forEach((el) => el.addEventListener('click', toggleBurgLockStatus));
body.querySelectorAll('div > span.locks').forEach((el) => el.addEventListener('mouseover', showBurgOLockTip));
body.querySelectorAll('div > span.icon-pencil').forEach((el) => el.addEventListener('click', openBurgEditor));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', triggerBurgRemove));
=======
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev)));
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("change", changeBurgCulture));
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus));
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
>>>>>>> master
applySorting(burgsHeader);
}
function getCultureOptions(culture) {
let options = '';
pack.cultures.filter((c) => !c.removed).forEach((c) => (options += `<option ${c.i === culture ? 'selected' : ''} value="${c.i}">${c.name}</option>`));
return options;
}
function burgHighlightOn(event) {
if (!layerIsOn('toggleLabels')) toggleLabels();
const burg = +event.target.dataset.id;
burgLabels.select("[data-id='" + burg + "']").classed('drag', true);
}
function burgHighlightOff() {
burgLabels.selectAll('text.drag').classed('drag', false);
}
function changeBurgName() {
if (this.value == '') tip('Please provide a name', false, 'error');
const burg = +this.parentNode.dataset.id;
pack.burgs[burg].name = this.value;
this.parentNode.dataset.name = this.value;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
if (label) label.innerHTML = this.value;
}
function zoomIntoBurg() {
const burg = +this.parentNode.dataset.id;
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
const x = +label.getAttribute('x'),
y = +label.getAttribute('y');
zoomTo(x, y, 8, 2000);
}
function changeBurgCulture() {
const burg = +this.parentNode.dataset.id;
const v = +this.value;
pack.burgs[burg].culture = v;
this.parentNode.dataset.culture = pack.cultures[v].name;
}
function changeBurgPopulation() {
const burg = +this.parentNode.dataset.id;
if (this.value == '' || isNaN(+this.value)) {
tip('Please provide an integer number (like 10000, not 10K)', false, 'error');
this.value = si(pack.burgs[burg].population * populationRate * urbanization);
return;
}
pack.burgs[burg].population = this.value / populationRate / urbanization;
this.parentNode.dataset.population = this.value;
this.value = si(this.value);
const population = [];
body.querySelectorAll(':scope > div').forEach((el) => population.push(+getInteger(el.dataset.population)));
burgsFooterPopulation.innerHTML = si(d3.mean(population));
}
function toggleCapitalStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
toggleCapital(burg);
burgsOverviewAddLines();
}
function togglePortStatus() {
const burg = +this.parentNode.parentNode.dataset.id;
togglePort(burg);
if (this.classList.contains('inactive')) this.classList.remove('inactive');
else this.classList.add('inactive');
}
function toggleBurgLockStatus() {
const burg = +this.parentNode.dataset.id;
toggleBurgLock(burg);
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 openBurgEditor() {
const burg = +this.parentNode.dataset.id;
editBurg(burg);
}
function triggerBurgRemove() {
const burg = +this.parentNode.dataset.id;
if (pack.burgs[burg].capital) {
tip('You cannot remove the capital. Please change the capital first', false, 'error');
return;
}
const message = 'Are you sure you want to remove the burg? <br>This action cannot be reverted';
const onConfirm = () => {
removeBurg(burg);
burgsOverviewAddLines();
};
confirmationDialog({title: 'Remove burg', message, confirm: 'Remove', onConfirm});
}
function regenerateNames() {
body.querySelectorAll(':scope > div').forEach(function (el) {
const burg = +el.dataset.id;
//if (pack.burgs[burg].lock) return;
const culture = pack.burgs[burg].culture;
const name = Names.getCulture(culture);
if (!pack.burgs[burg].lock) {
el.querySelector('.burgName').value = name;
pack.burgs[burg].name = el.dataset.name = name;
burgLabels.select("[data-id='" + burg + "']").text(name);
}
});
}
function enterAddBurgMode() {
if (this.classList.contains('pressed')) {
exitAddBurgMode();
return;
}
customization = 3;
this.classList.add('pressed');
tip('Click on the map to create a new burg. Hold Shift to add multiple', true, 'warn');
viewbox.style('cursor', 'crosshair').on('click', addBurgOnClick);
}
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
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]) return tip('There is already a burg in this cell. Please select a free cell', false, 'error');
addBurg(point); // add new burg
if (d3.event.shiftKey === false) {
exitAddBurgMode();
burgsOverviewAddLines();
}
}
function exitAddBurgMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
if (addBurgTool.classList.contains('pressed')) addBurgTool.classList.remove('pressed');
if (addNewBurg.classList.contains('pressed')) addNewBurg.classList.remove('pressed');
}
function showBurgsChart() {
// build hierarchy tree
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const burgs = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => {
const id = b.i + states.length - 1;
const population = b.population;
const capital = b.capital;
const province = pack.cells.province[b.cell];
const parent = province ? province + states.length - 1 : b.state;
return {id, i: b.i, state: b.state, culture: b.culture, province, parent, name: b.name, population, capital, x: b.x, y: b.y};
});
const data = states.concat(burgs);
if (data.length < 2) return tip("No burgs to show", false, "error");
const root = d3
.stratify()
.parentId((d) => d.state)(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value,
height = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: -10, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
const treeLayout = d3.pack().size([w, h]).padding(3);
// prepare svg
alertMessage.innerHTML = `<select id="burgsTreeType" style="display:block; margin-left:13px; font-size:11px">
<option value="states" selected>Group by state</option>
<option value="cultures">Group by culture</option>
<option value="parent">Group by province and state</option>
<option value="provinces">Group by province</option></select>`;
alertMessage.innerHTML += `<div id='burgsInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3
.select('#alertMessage')
.insert('svg', '#burgsInfo')
.attr('id', 'burgsTree')
.attr('width', width)
.attr('height', height - 10)
.attr('stroke-width', 2);
const graph = svg.append('g').attr('transform', `translate(-50, -10)`);
document.getElementById('burgsTreeType').addEventListener('change', updateChart);
treeLayout(root);
const node = graph
.selectAll('circle')
.data(root.leaves())
.join('circle')
.attr('data-id', (d) => d.data.i)
.attr('r', (d) => d.r)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.on('mouseenter', (d) => showInfo(event, d))
.on('mouseleave', (d) => hideInfo(event, d))
.on('click', (d) => zoomTo(d.data.x, d.data.y, 8, 2000));
function showInfo(ev, d) {
d3.select(ev.target).transition().duration(1500).attr('stroke', '#c13119');
const name = d.data.name;
const parent = d.parent.data.name;
const population = si(d.value * populationRate * urbanization);
burgsInfo.innerHTML = `${name}. ${parent}. Population: ${population}`;
burgHighlightOn(ev);
tip('Click to zoom into view');
}
function hideInfo(ev) {
burgHighlightOff(ev);
if (!document.getElementById('burgsInfo')) return;
burgsInfo.innerHTML = '&#8205;';
d3.select(ev.target).transition().attr('stroke', null);
tip('');
}
function updateChart() {
const getStatesData = () =>
pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, state: s.i ? 0 : null, color, name};
});
const getCulturesData = () =>
pack.cultures.map((c) => {
const color = c.color ? c.color : '#ccc';
return {id: c.i, culture: c.i ? 0 : null, color, name: c.name};
});
const getParentData = () => {
const states = pack.states.map((s) => {
const color = s.color ? s.color : '#ccc';
const name = s.fullName ? s.fullName : s.name;
return {id: s.i, parent: s.i ? 0 : null, color, name};
});
const provinces = pack.provinces
.filter((p) => p.i && !p.removed)
.map((p) => {
return {id: p.i + states.length - 1, parent: p.state, color: p.color, name: p.fullName};
});
return states.concat(provinces);
};
const getProvincesData = () =>
pack.provinces.map((p) => {
const color = p.color ? p.color : '#ccc';
const name = p.fullName ? p.fullName : p.name;
return {id: p.i ? p.i : 0, province: p.i ? 0 : null, color, name};
});
const value = (d) => {
if (this.value === 'states') return d.state;
if (this.value === 'cultures') return d.culture;
if (this.value === 'parent') return d.parent;
if (this.value === 'provinces') return d.province;
};
<<<<<<< HEAD
const base = this.value === 'states' ? getStatesData() : this.value === 'cultures' ? getCulturesData() : this.value === 'parent' ? getParentData() : getProvincesData();
burgs.forEach((b) => (b.id = b.i + base.length - 1));
=======
const base =
this.value === "states"
? getStatesData()
: this.value === "cultures"
? getCulturesData()
: this.value === "parent"
? getParentData()
: getProvincesData();
burgs.forEach(b => (b.id = b.i + base.length - 1));
>>>>>>> master
const data = base.concat(burgs);
const root = d3
.stratify()
.parentId((d) => value(d))(data)
.sum((d) => d.population)
.sort((a, b) => b.value - a.value);
node
.data(treeLayout(root).leaves())
.transition()
.duration(2000)
.attr('data-id', (d) => d.data.i)
.attr('fill', (d) => d.parent.data.color)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('r', (d) => d.r);
}
$('#alert').dialog({
title: 'Burgs bubble chart',
width: fitContent(),
position: {my: 'left bottom', at: 'left+10 bottom-10', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
}
function downloadBurgsData() {
<<<<<<< HEAD
let data = 'Id,Burg,Province,State,Culture,Religion,Population,Longitude,Latitude,Elevation (' + heightUnit.value + '),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n'; // headers
const valid = pack.burgs.filter((b) => b.i && !b.removed); // all valid burgs
=======
let data =
"Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,Longitude,Latitude,Elevation (" +
heightUnit.value +
"),Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town\n"; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
>>>>>>> master
valid.forEach((b) => {
data += b.i + ',';
data += b.name + ',';
const province = pack.cells.province[b.cell];
<<<<<<< HEAD
data += province ? pack.provinces[province].fullName + ',' : ',';
data += b.state ? pack.states[b.state].fullName + ',' : pack.states[b.state].name + ',';
data += pack.cultures[b.culture].name + ',';
data += pack.religions[pack.cells.religion[b.cell]].name + ',';
data += rn(b.population * populationRate * urbanization) + ',';
=======
data += province ? pack.provinces[province].name + "," : ",";
data += province ? pack.provinces[province].fullName + "," : ",";
data += pack.states[b.state].name + ",";
data += pack.states[b.state].fullName + ",";
data += pack.cultures[b.culture].name + ",";
data += pack.religions[pack.cells.religion[b.cell]].name + ",";
data += rn(b.population * populationRate * urbanization) + ",";
>>>>>>> master
// add geography data
data += mapCoordinates.lonW + (b.x / graphWidth) * mapCoordinates.lonT + ',';
data += mapCoordinates.latN - (b.y / graphHeight) * mapCoordinates.latT + ','; // this is inverted in QGIS otherwise
data += parseInt(getHeight(pack.cells.h[b.cell])) + ',';
// add status data
data += b.capital ? 'capital,' : ',';
data += b.port ? 'port,' : ',';
data += b.citadel ? 'citadel,' : ',';
data += b.walls ? 'walls,' : ',';
data += b.plaza ? 'plaza,' : ',';
data += b.temple ? 'temple,' : ',';
data += b.shanty ? 'shanty town\n' : '\n';
});
const name = getFileName('Burgs') + '.csv';
downloadFile(data, name);
}
function renameBurgsInBulk() {
const message = `Download burgs list as a text file, make changes and re-upload the file.
If you do not want to change the name, just leave it as is`;
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Download: function () {
const data = pack.burgs
.filter((b) => b.i && !b.removed)
.map((b) => b.name)
.join('\r\n');
const name = getFileName('Burg names') + '.txt';
downloadFile(data, name);
},
Upload: () => burgsListToLoad.click(),
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function importBurgNames(dataLoaded) {
if (!dataLoaded) return tip('Cannot load the file, please check the format', false, 'error');
const data = dataLoaded.split('\r\n');
if (!data.length) return tip('Cannot parse the list, please check the file format', false, 'error');
let change = [],
message = `Burgs will be renamed as below. Please confirm`;
message += `<table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
for (let i = 0; i < data.length && i <= burgs.length; i++) {
const v = data[i];
if (!v || !burgs[i] || v == burgs[i].name) continue;
change.push({id: burgs[i].i, name: v});
message += `<tr><td style="width:20%">${burgs[i].i}</td><td style="width:40%">${burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
}
message += `</tr></table>`;
if (!change.length) message = 'No changes found in the file. Please change some names to get a result';
alertMessage.innerHTML = message;
$('#alert').dialog({
title: 'Burgs bulk renaming',
width: '22em',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Cancel: function () {
$(this).dialog('close');
},
Confirm: function () {
for (let i = 0; i < change.length; i++) {
const id = change[i].id;
pack.burgs[id].name = change[i].name;
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
}
$(this).dialog('close');
burgsOverviewAddLines();
}
}
});
}
function triggerAllBurgsRemove() {
const message = 'Are you sure you want to remove all unlocked burgs except for capitals?<br><i>To remove a capital you have to remove the state first</i>';
confirmationDialog({title: 'Remove all burgs', message, confirm: 'Remove', onConfirm: removeAllBurgs});
}
function removeAllBurgs() {
pack.burgs.filter((b) => b.i && !(b.capital || b.lock)).forEach((b) => removeBurg(b.i));
burgsOverviewAddLines();
}
}

View file

@ -63,8 +63,8 @@ function editCultures() {
function culturesEditorAddLines() {
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '',
totalArea = 0,
totalPopulation = 0;
let totalArea = 0;
let totalPopulation = 0;
const emblemShapeGroup = document.getElementById('emblemShape').selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === 'Diversiform';
@ -84,7 +84,8 @@ function editCultures() {
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<input data-tip="Neutral culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span class="icon-cw placeholder"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
@ -96,17 +97,22 @@ function editCultures() {
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
</div>`;
continue;
}
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
c.color
}" class="fillRect pointer"></svg>
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
</svg>
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
@ -120,7 +126,11 @@ function editCultures() {
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
@ -141,6 +151,7 @@ function editCultures() {
body.querySelectorAll('rect.fillRect').forEach((el) => el.addEventListener('click', cultureChangeColor));
body.querySelectorAll('div > input.cultureName').forEach((el) => el.addEventListener('input', cultureChangeName));
body.querySelectorAll('div > input.statePower').forEach((el) => el.addEventListener('input', cultureChangeExpansionism));
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll('div > select.cultureType').forEach((el) => el.addEventListener('change', cultureChangeType));
body.querySelectorAll('div > select.cultureBase').forEach((el) => el.addEventListener('change', cultureChangeBase));
body.querySelectorAll('div > select.cultureShape').forEach((el) => el.addEventListener('change', cultureChangeShape));
@ -262,6 +273,13 @@ function editCultures() {
);
}
function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture);
this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name;
}
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
@ -534,6 +552,9 @@ function editCultures() {
const graph = svg.append('g').attr('transform', `translate(10, -45)`);
const links = graph.append('g').attr('fill', 'none').attr('stroke', '#aaaaaa');
const nodes = graph.append('g');
.attr("width", width)
.attr("height", height)
.style("text-anchor", "middle");
renderTree();
function renderTree() {
@ -683,6 +704,10 @@ function editCultures() {
tip('Click on culture to select, drag the circle to change culture', true);
viewbox.style('cursor', 'crosshair').on('click', selectCultureOnMapClick).call(d3.drag().on('start', dragCultureBrush)).on('touchmove mousemove', moveCultureBrush);
.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
body.querySelector('div').classList.add('selected');
}
@ -733,7 +758,14 @@ function editCultures() {
// change of append new element
if (exists.size()) exists.attr('data-culture', cultureNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-culture', cultureNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
else
temp
.append("polygon")
.attr("data-cell", i)
.attr("data-culture", cultureNew)
.attr("points", getPackPolygon(i))
.attr("fill", color)
.attr("stroke", color);
});
}

View file

@ -0,0 +1,965 @@
'use strict';
function editCultures() {
if (customization) return;
closeDialogs('#culturesEditor, .stable');
if (!layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleStates')) toggleStates();
if (layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleReligions')) toggleReligions();
if (layerIsOn('toggleProvinces')) toggleProvinces();
const body = document.getElementById('culturesBody');
drawCultureCenters();
refreshCulturesEditor();
if (modules.editCultures) return;
modules.editCultures = true;
$('#culturesEditor').dialog({
title: 'Cultures Editor',
resizable: false,
width: fitContent(),
close: closeCulturesEditor,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}
});
body.focus();
// add listeners
document.getElementById('culturesEditorRefresh').addEventListener('click', refreshCulturesEditor);
document.getElementById('culturesEditStyle').addEventListener('click', () => editStyle('cults'));
document.getElementById('culturesLegend').addEventListener('click', toggleLegend);
document.getElementById('culturesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('culturesHeirarchy').addEventListener('click', showHierarchy);
document.getElementById('culturesRecalculate').addEventListener('click', () => recalculateCultures(true));
document.getElementById('culturesManually').addEventListener('click', enterCultureManualAssignent);
document.getElementById('culturesManuallyApply').addEventListener('click', applyCultureManualAssignent);
document.getElementById('culturesManuallyCancel').addEventListener('click', () => exitCulturesManualAssignment());
document.getElementById('culturesEditNamesBase').addEventListener('click', editNamesbase);
document.getElementById('culturesAdd').addEventListener('click', enterAddCulturesMode);
document.getElementById('culturesExport').addEventListener('click', downloadCulturesData);
function refreshCulturesEditor() {
culturesCollectStatistics();
culturesEditorAddLines();
drawCultureCenters();
}
function culturesCollectStatistics() {
const cells = pack.cells,
cultures = pack.cultures;
cultures.forEach((c) => (c.cells = c.area = c.rural = c.urban = 0));
for (const i of cells.i) {
if (cells.h[i] < 20) continue;
const c = cells.culture[i];
cultures[c].cells += 1;
cultures[c].area += cells.area[i];
cultures[c].rural += cells.pop[i];
if (cells.burg[i]) cultures[c].urban += pack.burgs[cells.burg[i]].population;
}
}
// add line for each culture
function culturesEditorAddLines() {
<<<<<<< HEAD
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '',
totalArea = 0,
totalPopulation = 0;
=======
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
let totalArea = 0;
let totalPopulation = 0;
>>>>>>> master
const emblemShapeGroup = document.getElementById('emblemShape').selectedOptions[0].parentNode.label;
const selectShape = emblemShapeGroup === 'Diversiform';
for (const c of pack.cultures) {
if (c.removed) continue;
const area = c.area * distanceScaleInput.value ** 2;
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to edit`;
totalArea += area;
totalPopulation += population;
if (!c.i) {
// Uncultured (neutral) line
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="" data-emblems="${c.shield}">
<svg width="9" height="9" class="placeholder"></svg>
<input data-tip="Neutral culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
<span class="icon-cw placeholder"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span class="icon-resize-full placeholder hide"></span>
<input class="statePower placeholder hide" type="number">
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
<<<<<<< HEAD
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
=======
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
>>>>>>> master
</div>`;
continue;
}
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism} data-emblems="${c.shield}">
<<<<<<< HEAD
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
c.color
}" class="fillRect pointer"></svg>
=======
<svg data-tip="Culture fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px">
<rect x="0" y="0" width="100%" height="100%" fill="${c.color}" class="fillRect pointer">
</svg>
>>>>>>> master
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
<span data-tip="Regenerate culture name" class="icon-cw hiddenIcon" style="visibility: hidden"></span>
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.cells}</div>
<span data-tip="Culture expansionism. Defines competitive size" class="icon-resize-full hide"></span>
<input data-tip="Culture expansionism. Defines competitive size. Click to change, then click Recalculate to apply change" class="statePower hide" type="number" min=0 max=99 step=.1 value=${
c.expansionism
}>
<select data-tip="Culture type. Defines growth model. Click to change" class="cultureType">${getTypeOptions(c.type)}</select>
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o hide"></span>
<div data-tip="Culture area" class="biomeArea hide">${si(area) + unit}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw hide"></span>
<select data-tip="Culture namesbase. Click to change. Click on arrows to re-generate names" class="cultureBase">${getBaseOptions(c.base)}</select>
<<<<<<< HEAD
${selectShape ? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>` : ''}
=======
${
selectShape
? `<select data-tip="Emblem shape associated with culture. Click to change" class="cultureShape hide">${getShapeOptions(c.shield)}</select>`
: ""
}
>>>>>>> master
<span data-tip="Remove culture" class="icon-trash-empty hide"></span>
</div>`;
}
body.innerHTML = lines;
// update footer
culturesFooterCultures.innerHTML = pack.cultures.filter((c) => c.i && !c.removed).length;
culturesFooterCells.innerHTML = pack.cells.h.filter((h) => h >= 20).length;
culturesFooterArea.innerHTML = si(totalArea) + unit;
culturesFooterPopulation.innerHTML = si(totalPopulation);
culturesFooterArea.dataset.area = totalArea;
culturesFooterPopulation.dataset.population = totalPopulation;
// add listeners
<<<<<<< HEAD
body.querySelectorAll('div.cultures').forEach((el) => el.addEventListener('mouseenter', (ev) => cultureHighlightOn(ev)));
body.querySelectorAll('div.cultures').forEach((el) => el.addEventListener('mouseleave', (ev) => cultureHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('click', selectCultureOnLineClick));
body.querySelectorAll('rect.fillRect').forEach((el) => el.addEventListener('click', cultureChangeColor));
body.querySelectorAll('div > input.cultureName').forEach((el) => el.addEventListener('input', cultureChangeName));
body.querySelectorAll('div > input.statePower').forEach((el) => el.addEventListener('input', cultureChangeExpansionism));
body.querySelectorAll('div > select.cultureType').forEach((el) => el.addEventListener('change', cultureChangeType));
body.querySelectorAll('div > select.cultureBase').forEach((el) => el.addEventListener('change', cultureChangeBase));
body.querySelectorAll('div > select.cultureShape').forEach((el) => el.addEventListener('change', cultureChangeShape));
body.querySelectorAll('div > div.culturePopulation').forEach((el) => el.addEventListener('click', changePopulation));
body.querySelectorAll('div > span.icon-arrows-cw').forEach((el) => el.addEventListener('click', cultureRegenerateBurgs));
body.querySelectorAll('div > span.icon-trash-empty').forEach((el) => el.addEventListener('click', cultureRemove));
culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? 'inline-block' : 'none';
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
=======
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
body.querySelectorAll("rect.fillRect").forEach(el => el.addEventListener("click", cultureChangeColor));
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
body.querySelectorAll("div > span.icon-cw").forEach(el => el.addEventListener("click", cultureRegenerateName));
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("change", cultureChangeBase));
body.querySelectorAll("div > select.cultureShape").forEach(el => el.addEventListener("change", cultureChangeShape));
body.querySelectorAll("div > div.culturePopulation").forEach(el => el.addEventListener("click", changePopulation));
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
culturesHeader.querySelector("div[data-sortby='emblems']").style.display = selectShape ? "inline-block" : "none";
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
>>>>>>> master
togglePercentageMode();
}
applySorting(culturesHeader);
$('#culturesEditor').dialog({width: fitContent()});
}
function getTypeOptions(type) {
let options = '';
const types = ['Generic', 'River', 'Lake', 'Naval', 'Nomadic', 'Hunting', 'Highland'];
types.forEach((t) => (options += `<option ${type === t ? 'selected' : ''} value="${t}">${t}</option>`));
return options;
}
function getBaseOptions(base) {
let options = '';
nameBases.forEach((n, i) => (options += `<option ${base === i ? 'selected' : ''} value="${i}">${n.name}</option>`));
return options;
}
function getShapeOptions(selected) {
const shapes = Object.keys(COA.shields.types)
.map((type) => Object.keys(COA.shields[type]))
.flat();
return shapes.map((shape) => `<option ${shape === selected ? 'selected' : ''} value="${shape}">${capitalize(shape)}</option>`);
}
function cultureHighlightOn(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById('cultureInfo');
if (info) {
d3.select('#hierarchy')
.select("g[data-id='" + culture + "'] > path")
.classed('selected', 1);
const c = pack.cultures[culture];
const rural = c.rural * populationRate;
const urban = c.urban * populationRate * urbanization;
const population = rural + urban > 0 ? si(rn(rural + urban)) + ' people' : 'Extinct';
info.innerHTML = `${c.name} culture. ${c.type}. ${population}`;
tip('Drag to change parent, drag to itself to move to the top level. Hold CTRL and click to change abbreviation');
}
if (!layerIsOn('toggleCultures')) return;
if (customization) return;
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
cults
.select('#culture' + culture)
.raise()
.transition(animate)
.attr('stroke-width', 2.5)
.attr('stroke', '#d0240f');
debug
.select('#cultureCenter' + culture)
.raise()
.transition(animate)
.attr('r', 8)
.attr('stroke', '#d0240f');
}
function cultureHighlightOff(event) {
const culture = +event.target.dataset.id;
const info = document.getElementById('cultureInfo');
if (info) {
d3.select('#hierarchy')
.select("g[data-id='" + culture + "'] > path")
.classed('selected', 0);
info.innerHTML = '&#8205;';
tip('');
}
if (!layerIsOn('toggleCultures')) return;
cults
.select('#culture' + culture)
.transition()
.attr('stroke-width', null)
.attr('stroke', null);
debug
.select('#cultureCenter' + culture)
.transition()
.attr('r', 6)
.attr('stroke', null);
}
function cultureChangeColor() {
const el = this;
const currentFill = el.getAttribute('fill');
const culture = +el.parentNode.parentNode.dataset.id;
const callback = function (fill) {
el.setAttribute('fill', fill);
pack.cultures[culture].color = fill;
cults
.select('#culture' + culture)
.attr('fill', fill)
.attr('stroke', fill);
debug.select('#cultureCenter' + culture).attr('fill', fill);
};
openPicker(currentFill, callback);
}
function cultureChangeName() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.name = this.value;
pack.cultures[culture].name = this.value;
pack.cultures[culture].code = abbreviate(
this.value,
pack.cultures.map((c) => c.code)
);
}
function cultureRegenerateName() {
const culture = +this.parentNode.dataset.id;
const name = Names.getCultureShort(culture);
this.parentNode.querySelector("input.cultureName").value = name;
pack.cultures[culture].name = name;
}
function cultureChangeExpansionism() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.expansionism = this.value;
pack.cultures[culture].expansionism = +this.value;
recalculateCultures();
}
function cultureChangeType() {
const culture = +this.parentNode.dataset.id;
this.parentNode.dataset.type = this.value;
pack.cultures[culture].type = this.value;
recalculateCultures();
}
function cultureChangeBase() {
const culture = +this.parentNode.dataset.id;
const v = +this.value;
this.parentNode.dataset.base = pack.cultures[culture].base = v;
}
function cultureChangeShape() {
const culture = +this.parentNode.dataset.id;
const shape = this.value;
this.parentNode.dataset.emblems = pack.cultures[culture].shield = shape;
const rerenderCOA = (id, coa) => {
const coaEl = document.getElementById(id);
if (!coaEl) return; // not rendered
coaEl.remove();
COArenderer.trigger(id, coa);
};
pack.states.forEach((state) => {
if (state.culture !== culture || !state.i || state.removed || !state.coa || state.coa === 'custom') return;
if (shape === state.coa.shield) return;
state.coa.shield = shape;
rerenderCOA('stateCOA' + state.i, state.coa);
});
pack.provinces.forEach((province) => {
if (pack.cells.culture[province.center] !== culture || !province.i || province.removed || !province.coa || province.coa === 'custom') return;
if (shape === province.coa.shield) return;
province.coa.shield = shape;
rerenderCOA('provinceCOA' + province.i, province.coa);
});
pack.burgs.forEach((burg) => {
if (burg.culture !== culture || !burg.i || burg.removed || !burg.coa || burg.coa === 'custom') return;
if (shape === burg.coa.shield) return;
burg.coa.shield = shape;
rerenderCOA('burgCOA' + burg.i, burg.coa);
});
}
function changePopulation() {
const culture = +this.parentNode.dataset.id;
const c = pack.cultures[culture];
if (!c.cells) {
tip('Culture does not have any cells, cannot change population', false, 'error');
return;
}
const rural = rn(c.rural * populationRate);
const urban = rn(c.urban * populationRate * urbanization);
const total = rural + urban;
const l = (n) => Number(n).toLocaleString();
const burgs = pack.burgs.filter((b) => !b.removed && b.culture === culture);
alertMessage.innerHTML = `
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? '' : 'disabled'}>
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
if (isNaN(totalNew)) return;
totalPop.innerHTML = l(totalNew);
totalPopPerc.innerHTML = rn((totalNew / total) * 100);
};
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$('#alert').dialog({
resizable: false,
title: 'Change culture population',
width: '24em',
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const cells = pack.cells.i.filter((i) => pack.cells.culture[i] === culture);
const pop = rn(points / cells.length);
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach((b) => (b.population = population));
}
refreshCulturesEditor();
}
}
function cultureRegenerateBurgs() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
const cBurgs = pack.burgs.filter((b) => b.culture === culture && !b.lock);
cBurgs.forEach((b) => {
b.name = Names.getCulture(culture);
labels.select("[data-id='" + b.i + "']").text(b.name);
});
tip(`Names for ${cBurgs.length} burgs are regenerated`, false, 'success');
}
function cultureRemove() {
if (customization === 4) return;
const culture = +this.parentNode.dataset.id;
alertMessage.innerHTML = 'Are you sure you want to remove the culture? <br>This action cannot be reverted';
$('#alert').dialog({
resizable: false,
title: 'Remove culture',
buttons: {
Remove: function () {
cults.select('#culture' + culture).remove();
debug.select('#cultureCenter' + culture).remove();
pack.burgs.filter((b) => b.culture == culture).forEach((b) => (b.culture = 0));
pack.states.forEach((s, i) => {
if (s.culture === culture) s.culture = 0;
});
pack.cells.culture.forEach((c, i) => {
if (c === culture) pack.cells.culture[i] = 0;
});
pack.cultures[culture].removed = true;
const origin = pack.cultures[culture].origin;
pack.cultures.forEach((c) => {
if (c.origin === culture) c.origin = origin;
});
refreshCulturesEditor();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function drawCultureCenters() {
const tooltip = 'Drag to move the culture center (ancestral home)';
debug.select('#cultureCenters').remove();
const cultureCenters = debug.append('g').attr('id', 'cultureCenters').attr('stroke-width', 2).attr('stroke', '#444444').style('cursor', 'move');
const data = pack.cultures.filter((c) => c.i && !c.removed);
cultureCenters
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('id', (d) => 'cultureCenter' + d.i)
.attr('data-id', (d) => d.i)
.attr('r', 6)
.attr('fill', (d) => d.color)
.attr('cx', (d) => pack.cells.p[d.center][0])
.attr('cy', (d) => pack.cells.p[d.center][1])
.on('mouseenter', (d) => {
tip(tooltip, true);
body.querySelector(`div[data-id='${d.i}']`).classList.add('selected');
cultureHighlightOn(event);
})
.on('mouseleave', (d) => {
tip('', true);
body.querySelector(`div[data-id='${d.i}']`).classList.remove('selected');
cultureHighlightOff(event);
})
.call(d3.drag().on('start', cultureCenterDrag));
}
function cultureCenterDrag() {
const el = d3.select(this);
const c = +this.id.slice(13);
d3.event.on('drag', () => {
el.attr('cx', d3.event.x).attr('cy', d3.event.y);
const cell = findCell(d3.event.x, d3.event.y);
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
pack.cultures[c].center = cell;
recalculateCultures();
});
}
function toggleLegend() {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const data = pack.cultures
.filter((c) => c.i && !c.removed && c.cells)
.sort((a, b) => b.area - a.area)
.map((c) => [c.i, c.color, c.name]);
drawLegend('Cultures', data);
}
function togglePercentageMode() {
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +culturesFooterCells.innerHTML;
const totalArea = +culturesFooterArea.dataset.area;
const totalPopulation = +culturesFooterPopulation.dataset.population;
body.querySelectorAll(':scope > div').forEach(function (el) {
el.querySelector('.stateCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100) + '%';
el.querySelector('.culturePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + '%';
});
} else {
body.dataset.type = 'absolute';
culturesEditorAddLines();
}
}
function showHierarchy() {
// build hierarchy tree
pack.cultures[0].origin = null;
const cultures = pack.cultures.filter((c) => !c.removed);
if (cultures.length < 3) {
tip('Not enough cultures to show hierarchy', false, 'error');
return;
}
const root = d3
.stratify()
.id((d) => d.i)
.parentId((d) => d.origin)(cultures);
const treeWidth = root.leaves().length;
const treeHeight = root.height;
const width = treeWidth * 40,
height = treeHeight * 60;
const margin = {top: 10, right: 10, bottom: -5, left: 10};
const w = width - margin.left - margin.right;
const h = height + 30 - margin.top - margin.bottom;
const treeLayout = d3.tree().size([w, h]);
// prepare svg
alertMessage.innerHTML = "<div id='cultureInfo' class='chartInfo'>&#8205;</div>";
<<<<<<< HEAD
const svg = d3.select('#alertMessage').insert('svg', '#cultureInfo').attr('id', 'hierarchy').attr('width', width).attr('height', height).style('text-anchor', 'middle');
const graph = svg.append('g').attr('transform', `translate(10, -45)`);
const links = graph.append('g').attr('fill', 'none').attr('stroke', '#aaaaaa');
const nodes = graph.append('g');
=======
const svg = d3
.select("#alertMessage")
.insert("svg", "#cultureInfo")
.attr("id", "hierarchy")
.attr("width", width)
.attr("height", height)
.style("text-anchor", "middle");
const graph = svg.append("g").attr("transform", `translate(10, -45)`);
const links = graph.append("g").attr("fill", "none").attr("stroke", "#aaaaaa");
const nodes = graph.append("g");
>>>>>>> master
renderTree();
function renderTree() {
treeLayout(root);
links
.selectAll('path')
.data(root.links())
.enter()
<<<<<<< HEAD
.append('path')
.attr('d', (d) => {
return (
'M' +
d.source.x +
',' +
d.source.y +
'C' +
d.source.x +
',' +
(d.source.y * 3 + d.target.y) / 4 +
' ' +
d.target.x +
',' +
(d.source.y * 2 + d.target.y) / 3 +
' ' +
d.target.x +
',' +
=======
.append("path")
.attr("d", d => {
return (
"M" +
d.source.x +
"," +
d.source.y +
"C" +
d.source.x +
"," +
(d.source.y * 3 + d.target.y) / 4 +
" " +
d.target.x +
"," +
(d.source.y * 2 + d.target.y) / 3 +
" " +
d.target.x +
"," +
>>>>>>> master
d.target.y
);
});
const node = nodes
.selectAll('g')
.data(root.descendants())
.enter()
.append('g')
.attr('data-id', (d) => d.data.i)
.attr('stroke', '#333333')
.attr('transform', (d) => `translate(${d.x}, ${d.y})`)
.on('mouseenter', () => cultureHighlightOn(event))
.on('mouseleave', () => cultureHighlightOff(event))
.call(d3.drag().on('start', (d) => dragToReorigin(d)));
node
.append('path')
.attr('d', (d) => {
if (!d.data.i) return 'M5,0A5,5,0,1,1,-5,0A5,5,0,1,1,5,0';
// small circle
else if (d.data.type === 'Generic') return 'M11.3,0A11.3,11.3,0,1,1,-11.3,0A11.3,11.3,0,1,1,11.3,0';
// circle
else if (d.data.type === 'River') return 'M0,-14L14,0L0,14L-14,0Z';
// diamond
else if (d.data.type === 'Lake') return 'M-6.5,-11.26l13,0l6.5,11.26l-6.5,11.26l-13,0l-6.5,-11.26Z';
// hexagon
else if (d.data.type === 'Naval') return 'M-11,-11h22v22h-22Z'; // square
if (d.data.type === 'Highland') return 'M-11,-11l11,2l11,-2l-2,11l2,11l-11,-2l-11,2l2,-11Z'; // concave square
if (d.data.type === 'Nomadic') return 'M-4.97,-12.01 l9.95,0 l7.04,7.04 l0,9.95 l-7.04,7.04 l-9.95,0 l-7.04,-7.04 l0,-9.95Z'; // octagon
if (d.data.type === 'Hunting') return 'M0,-14l14,11l-6,14h-16l-6,-14Z'; // pentagon
return 'M-11,-11h22v22h-22Z'; // square
})
.attr('fill', (d) => (d.data.i ? d.data.color : '#ffffff'))
.attr('stroke-dasharray', (d) => (d.data.cells ? 'null' : '1'));
node
.append('text')
.attr('dy', '.35em')
.text((d) => (d.data.i ? d.data.code : ''));
}
$('#alert').dialog({
title: 'Cultures tree',
width: fitContent(),
resizable: false,
position: {my: 'left center', at: 'left+10 center', of: 'svg'},
buttons: {},
close: () => {
alertMessage.innerHTML = '';
}
});
function dragToReorigin(d) {
if (isCtrlClick(d3.event.sourceEvent)) {
changeCode(d);
return;
}
const originLine = graph.append('path').attr('class', 'dragLine').attr('d', `M${d.x},${d.y}L${d.x},${d.y}`);
d3.event.on('drag', () => {
originLine.attr('d', `M${d.x},${d.y}L${d3.event.x},${d3.event.y}`);
});
d3.event.on('end', () => {
originLine.remove();
const selected = graph.select('path.selected');
if (!selected.size()) return;
const culture = d.data.i;
const oldOrigin = d.data.origin;
let newOrigin = selected.datum().data.i;
if (newOrigin == oldOrigin) return; // already a child of the selected node
if (newOrigin == culture) newOrigin = 0; // move to top
if (newOrigin && d.descendants().some((node) => node.id == newOrigin)) return; // cannot be a child of its own child
pack.cultures[culture].origin = d.data.origin = newOrigin; // change data
showHierarchy(); // update hierarchy
});
}
function changeCode(d) {
prompt(`Please provide an abbreviation for culture: ${d.data.name}`, {default: d.data.code}, (v) => {
pack.cultures[d.data.i].code = v;
nodes
.select("g[data-id='" + d.data.i + "']")
.select('text')
.text(v);
});
}
}
function recalculateCultures(must) {
if (!must && !culturesAutoChange.checked) return;
pack.cells.culture = new Uint16Array(pack.cells.i.length);
pack.cultures.forEach(function (c) {
if (!c.i || c.removed) return;
pack.cells.culture[c.center] = c.i;
});
Cultures.expand();
drawCultures();
pack.burgs.forEach((b) => (b.culture = pack.cells.culture[b.cell]));
refreshCulturesEditor();
document.querySelector('input.statePower').focus(); // to not trigger hotkeys
}
function enterCultureManualAssignent() {
if (!layerIsOn('toggleCultures')) toggleCultures();
customization = 4;
cults.append('g').attr('id', 'temp');
document.querySelectorAll('#culturesBottom > *').forEach((el) => (el.style.display = 'none'));
document.getElementById('culturesManuallyButtons').style.display = 'inline-block';
debug.select('#cultureCenters').style('display', 'none');
culturesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
culturesHeader.querySelector("div[data-sortby='type']").style.left = '8.8em';
culturesHeader.querySelector("div[data-sortby='base']").style.left = '13.6em';
culturesFooter.style.display = 'none';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
$('#culturesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
<<<<<<< HEAD
tip('Click on culture to select, drag the circle to change culture', true);
viewbox.style('cursor', 'crosshair').on('click', selectCultureOnMapClick).call(d3.drag().on('start', dragCultureBrush)).on('touchmove mousemove', moveCultureBrush);
=======
tip("Click on culture to select, drag the circle to change culture", true);
viewbox
.style("cursor", "crosshair")
.on("click", selectCultureOnMapClick)
.call(d3.drag().on("start", dragCultureBrush))
.on("touchmove mousemove", moveCultureBrush);
>>>>>>> master
body.querySelector('div').classList.add('selected');
}
function selectCultureOnLineClick(i) {
if (customization !== 4) return;
body.querySelector('div.selected').classList.remove('selected');
this.classList.add('selected');
}
function selectCultureOnMapClick() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]);
if (pack.cells.h[i] < 20) return;
const assigned = cults.select('#temp').select("polygon[data-cell='" + i + "']");
const culture = assigned.size() ? +assigned.attr('data-culture') : pack.cells.culture[i];
body.querySelector('div.selected').classList.remove('selected');
body.querySelector("div[data-id='" + culture + "']").classList.add('selected');
}
function dragCultureBrush() {
const r = +culturesManuallyBrush.value;
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
const selection = found.filter(isLand);
if (selection) changeCultureForSelection(selection);
});
}
function changeCultureForSelection(selection) {
const temp = cults.select('#temp');
const selected = body.querySelector('div.selected');
const cultureNew = +selected.dataset.id;
const color = pack.cultures[cultureNew].color || '#ffffff';
selection.forEach(function (i) {
const exists = temp.select("polygon[data-cell='" + i + "']");
const cultureOld = exists.size() ? +exists.attr('data-culture') : pack.cells.culture[i];
if (cultureNew === cultureOld) return;
// change of append new element
<<<<<<< HEAD
if (exists.size()) exists.attr('data-culture', cultureNew).attr('fill', color).attr('stroke', color);
else temp.append('polygon').attr('data-cell', i).attr('data-culture', cultureNew).attr('points', getPackPolygon(i)).attr('fill', color).attr('stroke', color);
=======
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
else
temp
.append("polygon")
.attr("data-cell", i)
.attr("data-culture", cultureNew)
.attr("points", getPackPolygon(i))
.attr("fill", color)
.attr("stroke", color);
>>>>>>> master
});
}
function moveCultureBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +culturesManuallyBrush.value;
moveCircle(point[0], point[1], radius);
}
function applyCultureManualAssignent() {
const changed = cults.select('#temp').selectAll('polygon');
changed.each(function () {
const i = +this.dataset.cell;
const c = +this.dataset.culture;
pack.cells.culture[i] = c;
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
});
if (changed.size()) {
drawCultures();
refreshCulturesEditor();
}
exitCulturesManualAssignment();
}
function exitCulturesManualAssignment(close) {
customization = 0;
cults.select('#temp').remove();
removeCircle();
document.querySelectorAll('#culturesBottom > *').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('culturesManuallyButtons').style.display = 'none';
culturesEditor.querySelectorAll('.hide').forEach((el) => el.classList.remove('hidden'));
culturesHeader.querySelector("div[data-sortby='type']").style.left = '18.6em';
culturesHeader.querySelector("div[data-sortby='base']").style.left = '35.8em';
culturesFooter.style.display = 'block';
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (!close) $('#culturesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg'}});
debug.select('#cultureCenters').style('display', null);
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
}
function enterAddCulturesMode() {
if (this.classList.contains('pressed')) {
exitAddCultureMode();
return;
}
customization = 9;
this.classList.add('pressed');
tip('Click on the map to add a new culture', true);
viewbox.style('cursor', 'crosshair').on('click', addCulture);
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'none'));
}
function exitAddCultureMode() {
customization = 0;
restoreDefaultEvents();
clearMainTip();
body.querySelectorAll('div > input, select, span, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (culturesAdd.classList.contains('pressed')) culturesAdd.classList.remove('pressed');
}
function addCulture() {
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (pack.cells.h[center] < 20) {
tip('You cannot place culture center into the water. Please click on a land cell', false, 'error');
return;
}
const occupied = pack.cultures.some((c) => !c.removed && c.center === center);
if (occupied) {
tip('This cell is already a culture center. Please select a different cell', false, 'error');
return;
}
if (d3.event.shiftKey === false) exitAddCultureMode();
Cultures.add(center);
drawCultureCenters();
culturesEditorAddLines();
}
function downloadCulturesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Culture,Color,Cells,Expansionism,Type,Area ' + unit + ',Population,Namesbase,Emblems Shape\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += el.dataset.color + ',';
data += el.dataset.cells + ',';
data += el.dataset.expansionism + ',';
data += el.dataset.type + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
const base = +el.dataset.base;
data += nameBases[base].name + ',';
data += el.dataset.emblems + '\n';
});
const name = getFileName('Cultures') + '.csv';
downloadFile(data, name);
}
function closeCulturesEditor() {
debug.select('#cultureCenters').remove();
exitCulturesManualAssignment('close');
exitAddCultureMode();
}
}

View file

@ -1,45 +1,57 @@
"use strict";
'use strict';
function editDiplomacy() {
if (customization) return;
if (pack.states.filter(s => s.i && !s.removed).length < 2) {
tip("There should be at least 2 states to edit the diplomacy", false, "error");
if (pack.states.filter((s) => s.i && !s.removed).length < 2) {
tip('There should be at least 2 states to edit the diplomacy', false, 'error');
return;
}
closeDialogs("#diplomacyEditor, .stable");
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
if (layerIsOn("toggleProvinces")) toggleProvinces();
if (layerIsOn("toggleCultures")) toggleCultures();
if (layerIsOn("toggleBiomes")) toggleBiomes();
if (layerIsOn("toggleReligions")) toggleReligions();
closeDialogs('#diplomacyEditor, .stable');
if (!layerIsOn('toggleStates')) toggleStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
if (layerIsOn('toggleProvinces')) toggleProvinces();
if (layerIsOn('toggleCultures')) toggleCultures();
if (layerIsOn('toggleBiomes')) toggleBiomes();
if (layerIsOn('toggleReligions')) toggleReligions();
const body = document.getElementById("diplomacyBodySection");
const statuses = ["Ally", "Friendly", "Neutral", "Suspicion", "Enemy", "Unknown", "Rival", "Vassal", "Suzerain"];
const description = [" is an ally of ", " is friendly to ", " is neutral to ", " is suspicious of ",
" is at war with ", " does not know about ", " is a rival of ", " is a vassal of ", " is suzerain to "];
const colors = ["#00b300", "#d4f8aa", "#edeee8", "#eeafaa", "#e64b40", "#a9a9a9", "#ad5a1f", "#87CEFA", "#00008B"];
const body = document.getElementById('diplomacyBodySection');
const statuses = ['Ally', 'Friendly', 'Neutral', 'Suspicion', 'Enemy', 'Unknown', 'Rival', 'Vassal', 'Suzerain'];
const description = [
' is an ally of ',
' is friendly to ',
' is neutral to ',
' is suspicious of ',
' is at war with ',
' does not know about ',
' is a rival of ',
' is a vassal of ',
' is suzerain to '
];
const colors = ['#00b300', '#d4f8aa', '#edeee8', '#eeafaa', '#e64b40', '#a9a9a9', '#ad5a1f', '#87CEFA', '#00008B'];
refreshDiplomacyEditor();
tip("Click on a state to see its diplomatic relations", false, "warning");
viewbox.style("cursor", "crosshair").on("click", selectStateOnMapClick);
tip('Click on a state to see its diplomatic relations', false, 'warning');
viewbox.style('cursor', 'crosshair').on('click', selectStateOnMapClick);
if (modules.editDiplomacy) return;
modules.editDiplomacy = true;
$("#diplomacyEditor").dialog({
title: "Diplomacy Editor", resizable: false, width: fitContent(), close: closeDiplomacyEditor,
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#diplomacyEditor').dialog({
title: 'Diplomacy Editor',
resizable: false,
width: fitContent(),
close: closeDiplomacyEditor,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("diplomacyEditorRefresh").addEventListener("click", refreshDiplomacyEditor);
document.getElementById("diplomacyEditStyle").addEventListener("click", () => editStyle("regions"));
document.getElementById("diplomacyRegenerate").addEventListener("click", regenerateRelations);
document.getElementById("diplomacyMatrix").addEventListener("click", showRelationsMatrix);
document.getElementById("diplomacyHistory").addEventListener("click", showRelationsHistory);
document.getElementById("diplomacyExport").addEventListener("click", downloadDiplomacyData);
document.getElementById("diplomacySelect").addEventListener("mouseup", diplomacyChangeRelations);
document.getElementById('diplomacyEditorRefresh').addEventListener('click', refreshDiplomacyEditor);
document.getElementById('diplomacyEditStyle').addEventListener('click', () => editStyle('regions'));
document.getElementById('diplomacyRegenerate').addEventListener('click', regenerateRelations);
document.getElementById('diplomacyMatrix').addEventListener('click', showRelationsMatrix);
document.getElementById('diplomacyHistory').addEventListener('click', showRelationsHistory);
document.getElementById('diplomacyExport').addEventListener('click', downloadDiplomacyData);
document.getElementById('diplomacySelect').addEventListener('mouseup', diplomacyChangeRelations);
function refreshDiplomacyEditor() {
diplomacyEditorAddLines();
@ -49,12 +61,12 @@ function editDiplomacy() {
// add line for each state
function diplomacyEditorAddLines() {
const states = pack.states;
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const selectedLine = body.querySelector('div.Self');
const sel = selectedLine ? +selectedLine.dataset.id : states.find((s) => s.i && !s.removed).i;
const selName = states[sel].fullName;
diplomacySelect.style.display = "none";
diplomacySelect.style.display = 'none';
COArenderer.trigger("stateCOA"+sel, states[sel].coa);
COArenderer.trigger('stateCOA' + sel, states[sel].coa);
let lines = `<div class="states Self" data-id=${sel} data-tip="List below shows relations to ${selName}">
<div style="width: max-content">${selName}</div>
<svg class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${sel}"></use></svg>
@ -68,7 +80,7 @@ function editDiplomacy() {
const tip = s.fullName + description[index] + selName;
const tipSelect = `${tip}. Click to see relations to ${s.name}`;
const tipChange = `${tip}. Click to change relations to ${selName}`;
COArenderer.trigger("stateCOA"+s.i, s.coa);
COArenderer.trigger('stateCOA' + s.i, s.coa);
lines += `<div class="states" data-id=${s.i} data-name="${s.fullName}" data-relations="${relation}">
<svg data-tip="${tipSelect}" class="coaIcon" viewBox="0 0 200 200"><use href="#stateCOA${s.i}"></use></svg>
@ -82,57 +94,61 @@ function editDiplomacy() {
body.innerHTML = lines;
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
body.querySelectorAll(".changeRelations").forEach(el => el.addEventListener("click", toggleDiplomacySelect));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => stateHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => stateHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('click', selectStateOnLineClick));
body.querySelectorAll('.changeRelations').forEach((el) => el.addEventListener('click', toggleDiplomacySelect));
applySorting(diplomacyHeader);
$("#diplomacyEditor").dialog();
$('#diplomacyEditor').dialog();
}
function stateHighlightOn(event) {
if (!layerIsOn("toggleStates")) return;
if (!layerIsOn('toggleStates')) return;
const state = +event.target.dataset.id;
if (customization || !state) return;
const d = regions.select("#state"+state).attr("d");
const d = regions.select('#state' + state).attr('d');
const path = debug.append("path").attr("class", "highlight").attr("d", d)
.attr("fill", "none").attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
.attr("filter", "url(#blur1)");
const path = debug.append('path').attr('class', 'highlight').attr('d', d).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 1).attr('opacity', 1).attr('filter', 'url(#blur1)');
const l = path.node().getTotalLength(), dur = (l + 5000) / 2;
const i = d3.interpolateString("0," + l, l + "," + l);
path.transition().duration(dur).attrTween("stroke-dasharray", function() {return t => i(t)});
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString('0,' + l, l + ',' + l);
path
.transition()
.duration(dur)
.attrTween('stroke-dasharray', function () {
return (t) => i(t);
});
}
function stateHighlightOff(event) {
debug.selectAll(".highlight").each(function() {
d3.select(this).transition().duration(1000).attr("opacity", 0).remove();
debug.selectAll('.highlight').each(function () {
d3.select(this).transition().duration(1000).attr('opacity', 0).remove();
});
}
function showStateRelations() {
const selectedLine = body.querySelector("div.Self");
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find(s => s.i && !s.removed).i;
const selectedLine = body.querySelector('div.Self');
const sel = selectedLine ? +selectedLine.dataset.id : pack.states.find((s) => s.i && !s.removed).i;
if (!sel) return;
if (!layerIsOn("toggleStates")) toggleStates();
if (!layerIsOn('toggleStates')) toggleStates();
statesBody.selectAll("path").each(function() {
if (this.id.slice(0, 9) === "state-gap") return; // exclude state gap element
statesBody.selectAll('path').each(function () {
if (this.id.slice(0, 9) === 'state-gap') return; // exclude state gap element
const id = +this.id.slice(5); // state id
const index = statuses.indexOf(pack.states[id].diplomacy[sel]); // status index
const clr = index !== -1 ? colors[index] : "#4682b4"; // Self (bluish)
this.setAttribute("fill", clr);
statesBody.select("#state-gap"+id).attr("stroke", clr);
statesHalo.select("#state-border"+id).attr("stroke", d3.color(clr).darker().hex());
const clr = index !== -1 ? colors[index] : '#4682b4'; // Self (bluish)
this.setAttribute('fill', clr);
statesBody.select('#state-gap' + id).attr('stroke', clr);
statesHalo.select('#state-border' + id).attr('stroke', d3.color(clr).darker().hex());
});
}
function selectStateOnLineClick() {
if (this.classList.contains("Self")) return;
body.querySelector("div.Self").classList.remove("Self");
this.classList.add("Self");
if (this.classList.contains('Self')) return;
body.querySelector('div.Self').classList.remove('Self');
this.classList.add('Self');
refreshDiplomacyEditor();
}
@ -141,35 +157,39 @@ function editDiplomacy() {
const i = findCell(point[0], point[1]);
const state = pack.cells.state[i];
if (!state) return;
const selectedLine = body.querySelector("div.Self");
const selectedLine = body.querySelector('div.Self');
if (+selectedLine.dataset.id === state) return;
selectedLine.classList.remove("Self");
body.querySelector("div[data-id='"+state+"']").classList.add("Self");
selectedLine.classList.remove('Self');
body.querySelector("div[data-id='" + state + "']").classList.add('Self');
refreshDiplomacyEditor();
}
function toggleDiplomacySelect(event) {
event.stopPropagation();
const select = document.getElementById("diplomacySelect");
const show = select.style.display === "none";
if (!show) {select.style.display = "none"; return;}
select.style.display = "block";
const input = event.target.closest("div").querySelector("input");
select.style.left = input.getBoundingClientRect().left + "px";
select.style.top = input.getBoundingClientRect().bottom + "px";
body.dataset.state = event.target.closest("div.states").dataset.id;
const select = document.getElementById('diplomacySelect');
const show = select.style.display === 'none';
if (!show) {
select.style.display = 'none';
return;
}
select.style.display = 'block';
const input = event.target.closest('div').querySelector('input');
select.style.left = input.getBoundingClientRect().left + 'px';
select.style.top = input.getBoundingClientRect().bottom + 'px';
body.dataset.state = event.target.closest('div.states').dataset.id;
}
function diplomacyChangeRelations(event) {
event.stopPropagation();
diplomacySelect.style.display = "none";
diplomacySelect.style.display = 'none';
const subject = +body.dataset.state;
const rel = event.target.innerHTML;
const states = pack.states, chronicle = states[0].diplomacy;
const selectedLine = body.querySelector("div.Self");
const object = selectedLine ? +selectedLine.dataset.id : states.find(s => s.i && !s.removed).i;
const states = pack.states,
chronicle = states[0].diplomacy;
const selectedLine = body.querySelector('div.Self');
const object = selectedLine ? +selectedLine.dataset.id : states.find((s) => s.i && !s.removed).i;
if (!object) return;
const objectName = states[object].name; // object of relations change
const subjectName = states[subject].name; // subject of relations change - actor
@ -177,7 +197,7 @@ function editDiplomacy() {
const oldRel = states[subject].diplomacy[object];
if (rel === oldRel) return;
states[subject].diplomacy[object] = rel;
states[object].diplomacy[subject] = rel === "Vassal" ? "Suzerain" : rel === "Suzerain" ? "Vassal" : rel;
states[object].diplomacy[subject] = rel === 'Vassal' ? 'Suzerain' : rel === 'Suzerain' ? 'Vassal' : rel;
// update relation history
const change = () => [`Relations change`, `${subjectName}-${getAdjective(objectName)} relations changed to ${rel.toLowerCase()}`];
@ -189,22 +209,18 @@ function editDiplomacy() {
const war = () => [`War declaration`, `${subjectName} declared a war on its enemy ${objectName}`];
const peace = () => {
const treaty = `${subjectName} and ${objectName} agreed to cease fire and signed a peace treaty`;
const changed = rel === "Ally" ? ally()
: rel === "Vassal" ? vassal()
: rel === "Suzerain" ? suzerain()
: rel === "Unknown" ? unknown()
: change();
const changed = rel === 'Ally' ? ally() : rel === 'Vassal' ? vassal() : rel === 'Suzerain' ? suzerain() : rel === 'Unknown' ? unknown() : change();
return [`War termination`, treaty, changed[1]];
}
};
if (oldRel === "Enemy") chronicle.push(peace()); else
if (rel === "Enemy") chronicle.push(war()); else
if (rel === "Vassal") chronicle.push(vassal()); else
if (rel === "Suzerain") chronicle.push(suzerain()); else
if (rel === "Ally") chronicle.push(ally()); else
if (rel === "Unknown") chronicle.push(unknown()); else
if (rel === "Rival") chronicle.push(rival()); else
chronicle.push(change());
if (oldRel === 'Enemy') chronicle.push(peace());
else if (rel === 'Enemy') chronicle.push(war());
else if (rel === 'Vassal') chronicle.push(vassal());
else if (rel === 'Suzerain') chronicle.push(suzerain());
else if (rel === 'Ally') chronicle.push(ally());
else if (rel === 'Unknown') chronicle.push(unknown());
else if (rel === 'Rival') chronicle.push(rival());
else chronicle.push(change());
refreshDiplomacyEditor();
}
@ -216,79 +232,95 @@ function editDiplomacy() {
function showRelationsHistory() {
const chronicle = pack.states[0].diplomacy;
if (!chronicle.length) {tip("Relations history is blank", false, "error"); return;}
if (!chronicle.length) {
tip('Relations history is blank', false, 'error');
return;
}
let message = `<div autocorrect="off" spellcheck="false">`;
chronicle.forEach((e, d) => {
message += `<div>`;
e.forEach((l, i) => message += `<div contenteditable="true" data-id="${d}-${i}"${i ? "" : " style='font-weight:bold'"}>${l}</div>`);
e.forEach((l, i) => (message += `<div contenteditable="true" data-id="${d}-${i}"${i ? '' : " style='font-weight:bold'"}>${l}</div>`));
message += `&#8205;</div>`;
});
alertMessage.innerHTML = message + `</div><i id="info-line">Type to edit. Press Enter to add a new line, empty the element to remove it</i>`;
alertMessage.querySelectorAll("div[contenteditable='true']").forEach(el => el.addEventListener("input", changeReliationsHistory));
alertMessage.querySelectorAll("div[contenteditable='true']").forEach((el) => el.addEventListener('input', changeReliationsHistory));
$("#alert").dialog({title: "Relations history", position: {my: "center", at: "center", of: "svg"},
$('#alert').dialog({
title: 'Relations history',
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Save: function() {
const data = this.querySelector("div").innerText.split("\n").join("\r\n");
const name = getFileName("Relations history") + ".txt";
Save: function () {
const data = this.querySelector('div').innerText.split('\n').join('\r\n');
const name = getFileName('Relations history') + '.txt';
downloadFile(data, name);
},
Clear: function() {pack.states[0].diplomacy = []; $(this).dialog("close");},
Close: function() {$(this).dialog("close");}
Clear: function () {
pack.states[0].diplomacy = [];
$(this).dialog('close');
},
Close: function () {
$(this).dialog('close');
}
}
});
}
function changeReliationsHistory() {
const i = this.dataset.id.split("-");
const i = this.dataset.id.split('-');
const group = pack.states[0].diplomacy[i[0]];
if (this.innerHTML === "") {
if (this.innerHTML === '') {
group.splice(i[1], 1);
this.remove();
} else group[i[1]] = this.innerHTML;
}
function showRelationsMatrix() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
const states = pack.states.filter((s) => s.i && !s.removed);
const valid = states.map((s) => s.i);
let message = `<table class="matrix-table"><tr><th data-tip='&#8205;'></th>`;
message += states.map(s => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join("") + `</tr>`; // headers
states.forEach(s => {
message += `<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` + s.diplomacy
.filter((v, i) => valid.includes(i)).map((r, i) => {
const desc = description[statuses.indexOf(r)];
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;';
return `<td data-tip='${tip}' class='${r}'>${r}</td>`
}).join("") + "</tr>";
message += states.map((s) => `<th data-tip='See relations to ${s.fullName}'>${s.name}</th>`).join('') + `</tr>`; // headers
states.forEach((s) => {
message +=
`<tr><th data-tip='See relations of ${s.fullName}'>${s.name}</th>` +
s.diplomacy
.filter((v, i) => valid.includes(i))
.map((r, i) => {
const desc = description[statuses.indexOf(r)];
const tip = desc ? s.fullName + desc + pack.states[valid[i]].fullName : '&#8205;';
return `<td data-tip='${tip}' class='${r}'>${r}</td>`;
})
.join('') +
'</tr>';
});
message += `</table>`;
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Relations matrix", width: fitContent(), position: {my: "center", at: "center", of: "svg"}, buttons: {}});
$('#alert').dialog({title: 'Relations matrix', width: fitContent(), position: {my: 'center', at: 'center', of: 'svg'}, buttons: {}});
}
function downloadDiplomacyData() {
const states = pack.states.filter(s => s.i && !s.removed);
const valid = states.map(s => s.i);
const states = pack.states.filter((s) => s.i && !s.removed);
const valid = states.map((s) => s.i);
let data = "," + states.map(s => s.name).join(",") + "\n"; // headers
states.forEach(s => {
let data = ',' + states.map((s) => s.name).join(',') + '\n'; // headers
states.forEach((s) => {
const rels = s.diplomacy.filter((v, i) => valid.includes(i));
data += s.name + "," + rels.join(",") + "\n";
data += s.name + ',' + rels.join(',') + '\n';
});
const name = getFileName("Relations") + ".csv";
const name = getFileName('Relations') + '.csv';
downloadFile(data, name);
}
function closeDiplomacyEditor() {
restoreDefaultEvents();
clearMainTip();
const selected = body.querySelector("div.Self");
if (selected) selected.classList.remove("Self");
if (layerIsOn("toggleStates")) drawStates(); else toggleStates();
debug.selectAll(".highlight").remove();
const selected = body.querySelector('div.Self');
if (selected) selected.classList.remove('Self');
if (layerIsOn('toggleStates')) drawStates();
else toggleStates();
debug.selectAll('.highlight').remove();
}
}

View file

@ -1,6 +1,7 @@
// module stub to store common functions for ui editors
'use strict';
modules.editors = true;
restoreDefaultEvents(); // apply default viewbox events on load
// restore default viewbox events
@ -264,15 +265,8 @@ function toggleBurgLock(burg) {
b.lock = b.lock ? 0 : 1;
}
function showBurgLockTip(burg) {
const b = pack.burgs[burg];
if (b.lock) {
tip('Click to unlock burg and allow it to be change by regeneration tools');
} else {
tip('Click to lock burg and prevent changes by regeneration tools');
}
}
// draw legend box
function drawLegend(name, data) {
legend.selectAll('*').remove(); // fully redraw every time
@ -385,6 +379,14 @@ function createPicker() {
const contaiter = d3.select('body').append('svg').attr('id', 'pickerContainer').attr('width', '100%').attr('height', '100%');
contaiter.append('rect').attr('x', 0).attr('y', 0).attr('width', '100%').attr('height', '100%').attr('opacity', 0.2).on('mousemove', cl).on('click', closePicker);
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", "100%")
.attr("height", "100%")
.attr("opacity", 0.2)
.on("mousemove", cl)
.on("click", closePicker);
const picker = contaiter
.append('g')
.attr('id', 'picker')
@ -489,6 +491,17 @@ function createPicker() {
picker.insert('text', ':first-child').attr('x', 12).attr('y', -10).attr('id', 'pickerLabel').text('Color Picker').on('mousemove', pos);
picker.insert('rect', ':first-child').attr('x', 0).attr('y', -30).attr('width', width).attr('height', 30).attr('id', 'pickerHeader').on('mousemove', pos);
picker.attr('transform', `translate(${(svgWidth - width) / 2},${(svgHeight - height) / 2})`);
.attr("fill", "#ffffff")
.attr("stroke", "#5d4651")
.on("mousemove", pos);
.insert("rect", ":first-child")
.attr("x", 288)
.attr("y", -21)
.attr("id", "pickerCloseRect")
.attr("width", 14)
.attr("height", 14)
.on("mousemove", cl)
.on("click", closePicker);
}
function updateSelectedRect(fill) {
@ -693,23 +706,32 @@ function uploadFile(el, callback) {
fileReader.onload = (loaded) => callback(loaded.target.result);
}
function highlightElement(element) {
function getBBox(element) {
if (debug.select('.highlighted').size()) return; // allow only 1 highlight element simultaniosly
const box = element.getBBox();
const y = +element.getAttribute("y");
const transform = element.getAttribute('transform') || null;
const height = +element.getAttribute("height");
return {x, y, width, height};
}
function highlightElement(element, zoom) {
if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaneously
const box = element.tagName === "svg" ? getBBox(element) : element.getBBox();
const enter = d3.transition().duration(1000).ease(d3.easeBounceOut);
const exit = d3.transition().duration(500).ease(d3.easeLinear);
const highlight = debug.append('rect').attr('x', box.x).attr('y', box.y).attr('width', box.width).attr('height', box.height).attr('transform', transform);
highlight.classed("highlighted", 1).attr("transform", transform);
highlight.classed('highlighted', 1).transition(enter).style('outline-offset', '0px').transition(exit).style('outline-color', 'transparent').delay(1000).remove();
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
if (tr[0]) x += tr[0];
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
zoomTo(x, y, scale > 2 ? scale : 3, 1600);
if (zoom) {
const tr = parseTransform(transform);
let x = box.x + box.width / 2;
if (tr[0]) x += tr[0];
let y = box.y + box.height / 2;
if (tr[1]) y += tr[1];
zoomTo(x, y, scale > 2 ? scale : zoom, 1600);
}
}
function selectIcon(initial, callback) {
@ -945,6 +967,37 @@ function selectIcon(initial, callback) {
});
}
function confirmationDialog(options) {
const {
title = "Confirm action",
message = "Are you sure you want to continue? <br>The action cannot be reverted",
cancel = "Cancel",
confirm = "Continue",
onCancel,
onConfirm
} = options;
const buttons = {
[confirm]: function () {
if (onConfirm) onConfirm();
$(this).dialog("close");
},
[cancel]: function () {
if (onCancel) onCancel();
$(this).dialog("close");
}
};
document.getElementById("alertMessage").innerHTML = message;
$("#alert").dialog({resizable: false, title, buttons});
}
// add and register event listeners to clean up on editor closure
function listen(element, event, handler) {
element.addEventListener(event, handler);
return () => element.removeEventListener(event, handler);
}
// Calls the refresh for all currently open editors
function refreshAllEditors() {
TIME && console.time('refreshAllEditors');

1098
modules/ui/editors.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,12 @@
"use strict";
'use strict';
function showEPForRoute(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
@ -17,10 +17,10 @@ function showEPForRoute(node) {
function showEPForRiver(node) {
const points = [];
debug
.select("#controlPoints")
.selectAll("circle")
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
@ -30,16 +30,16 @@ function showEPForRiver(node) {
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);
document.getElementById('epScaleRange').addEventListener('change', draw);
document.getElementById('epCurve').addEventListener('change', draw);
document.getElementById('epSave').addEventListener('click', downloadCSV);
$("#elevationProfile").dialog({
title: "Elevation profile",
$('#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"}
position: {my: 'left top', at: 'left+20 bottom-500', of: window, collision: 'fit'}
});
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
@ -67,7 +67,7 @@ function showElevationProfile(data, routeLen, isRiver) {
let h = pack.cells.h[cell];
if (h < 20) {
const f = pack.features[pack.cells.f[cell]];
if (f.type === "lake") h = f.height;
if (f.type === 'lake') h = f.height;
else h = 20;
}
@ -94,7 +94,7 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.burg[i] = b;
chartData.cell[i] = cell;
let sh = getHeight(h);
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(" ")));
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
chartData.mih = Math.min(chartData.mih, h);
chartData.mah = Math.max(chartData.mah, h);
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
@ -109,7 +109,7 @@ 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 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
for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k];
@ -122,34 +122,34 @@ 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) + ",";
data += k + 1 + ',';
data += chartData.points[k][0] + ',';
data += chartData.points[k][1] + ',';
data += cell + ',';
data += getHeight(h) + ',';
data += h + ',';
data += rn(pop * populationRate) + ',';
if (burg) {
data += pack.burgs[burg].name + ",";
data += pack.burgs[burg].population * populationRate * urbanization + ",";
data += pack.burgs[burg].name + ',';
data += pack.burgs[burg].population * populationRate * urbanization + ',';
} else {
data += ",0,";
data += ',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 += 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";
data = data + '\n';
}
const name = getFileName("elevation profile") + ".csv";
const name = getFileName('elevation profile') + '.csv';
downloadFile(data, name);
}
@ -169,37 +169,48 @@ function showElevationProfile(data, routeLen, isRiver) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
}
document.getElementById("elevationGraph").innerHTML = "";
document.getElementById('elevationGraph').innerHTML = '';
const chart = d3
.select("#elevationGraph")
.append("svg")
.attr("width", chartWidth + 120)
.attr("height", chartHeight + yOffset + biomesHeight)
.attr("id", "elevationSVG")
.attr("class", "epbackground");
.select('#elevationGraph')
.append('svg')
.attr('width', chartWidth + 120)
.attr('height', chartHeight + yOffset + biomesHeight)
.attr('id', 'elevationSVG')
.attr('class', 'epbackground');
// arrow-head definition
chart.append("defs").append("marker").attr("id", "arrowhead").attr("orient", "auto").attr("markerWidth", "2").attr("markerHeight", "4").attr("refX", "0.1").attr("refY", "2").append("path").attr("d", "M0,0 V4 L2,2 Z").attr("fill", "darkgray");
chart
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('orient', 'auto')
.attr('markerWidth', '2')
.attr('markerHeight', '4')
.attr('refX', '0.1')
.attr('refY', '2')
.append('path')
.attr('d', 'M0,0 V4 L2,2 Z')
.attr('fill', 'darkgray');
let colors = getColorScheme();
const landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
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
.append("stop")
.attr("offset", "0%")
.attr("style", "stop-color:" + getColor(chartData.mih, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:' + getColor(chartData.mih, colors) + ';stop-opacity:1');
landdef
.append("stop")
.attr("offset", "100%")
.attr("style", "stop-color:" + getColor(chartData.mah, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:' + getColor(chartData.mah, colors) + ';stop-opacity:1');
} else {
for (let k = chartData.mah; k >= chartData.mih; k--) {
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
landdef
.append("stop")
.attr("offset", perc * 100 + "%")
.attr("style", "stop-color:" + getColor(k, colors) + ";stop-opacity:1");
.append('stop')
.attr('offset', perc * 100 + '%')
.attr('style', 'stop-color:' + getColor(k, colors) + ';stop-opacity:1');
}
}
@ -231,14 +242,14 @@ function showElevationProfile(data, routeLen, isRiver) {
let extra = chartData.points.slice();
let path = curve(extra);
// this completes the right-hand side and bottom of our land "polygon"
path += " L" + parseInt(xscale(extra.length) + +xOffset) + "," + parseInt(extra[extra.length - 1][1]);
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)");
path += ' L' + parseInt(xscale(extra.length) + +xOffset) + ',' + parseInt(extra[extra.length - 1][1]);
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)');
// biome / heights
let g = chart.append("g").attr("id", "epbiomes");
let g = chart.append('g').attr('id', 'epbiomes');
const hu = heightUnit.value;
for (let k = 0; k < chartData.points.length; k++) {
const x = chartData.points[k][0];
@ -257,65 +268,82 @@ function showElevationProfile(data, routeLen, isRiver) {
const populationDesc = rn(pop * populationRate);
const provinceDesc = province ? ", " + pack.provinces[province].name : "";
const dataTip = biomesData.name[chartData.biome[k]] + provinceDesc + ", " + pack.states[state].name + ", " + pack.religions[religion].name + ", " + pack.cultures[culture].name + " (height: " + chartData.height[k] + " " + hu + ", population " + populationDesc + ", cell " + chartData.cell[k] + ")";
const provinceDesc = province ? ', ' + pack.provinces[province].name : '';
const dataTip =
biomesData.name[chartData.biome[k]] +
provinceDesc +
', ' +
pack.states[state].name +
', ' +
pack.religions[religion].name +
', ' +
pack.cultures[culture].name +
' (height: ' +
chartData.height[k] +
' ' +
hu +
', population ' +
populationDesc +
', cell ' +
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', 15).attr('data-tip', dataTip);
}
const xAxis = d3
.axisBottom(xscale)
.ticks(10)
.tickFormat(function (d) {
return rn((d / chartData.points.length) * routeLen) + " " + distanceUnitInput.value;
return rn((d / chartData.points.length) * routeLen) + ' ' + distanceUnitInput.value;
});
const yAxis = d3
.axisLeft(yscale)
.ticks(5)
.tickFormat(function (d) {
return d + " " + hu;
return d + ' ' + hu;
});
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat("");
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat("");
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat('');
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat('');
chart
.append("g")
.attr("id", "epxaxis")
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset + 20) + ")")
.append('g')
.attr('id', 'epxaxis')
.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
.selectAll('text')
.style('text-anchor', 'center')
.attr('transform', function (d) {
return 'rotate(0)'; // used to rotate labels, - anti-clockwise, + clockwise
});
chart
.append("g")
.attr("id", "epyaxis")
.attr("transform", "translate(" + parseInt(+xOffset - 10) + "," + parseInt(+yOffset) + ")")
.append('g')
.attr('id', 'epyaxis')
.attr('transform', 'translate(' + parseInt(+xOffset - 10) + ',' + parseInt(+yOffset) + ')')
.call(yAxis);
// add the X gridlines
chart
.append("g")
.attr("id", "epxgrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
.attr("transform", "translate(" + xOffset + "," + parseInt(chartHeight + +yOffset) + ")")
.append('g')
.attr('id', 'epxgrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + parseInt(chartHeight + +yOffset) + ')')
.call(xGrid);
// add the Y gridlines
chart
.append("g")
.attr("id", "epygrid")
.attr("class", "epgrid")
.attr("stroke-dasharray", "4 1")
.attr("transform", "translate(" + xOffset + "," + yOffset + ")")
.append('g')
.attr('id', 'epygrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + yOffset + ')')
.call(yGrid);
// draw city labels - try to avoid putting labels over one another
g = chart.append("g").attr("id", "epburglabels");
g = chart.append('g').attr('id', 'epburglabels');
let y1 = 0;
const add = 15;
@ -331,31 +359,31 @@ function showElevationProfile(data, routeLen, isRiver) {
if (y1 >= yOffset) y1 = add;
// burg name
g.append("text")
.attr("id", "ep" + b)
.attr("class", "epburglabel")
.attr("x", x1)
.attr("y", y1)
.attr("text-anchor", "middle");
document.getElementById("ep" + b).innerHTML = pack.burgs[b].name;
g.append('text')
.attr('id', 'ep' + b)
.attr('class', 'epburglabel')
.attr('x', x1)
.attr('y', y1)
.attr('text-anchor', 'middle');
document.getElementById('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("stroke", "darkgray")
.attr("fill", "lightgray")
.attr("stroke-width", "1")
.attr("marker-end", "url(#arrowhead)");
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('stroke', 'darkgray')
.attr('fill', 'lightgray')
.attr('stroke-width', '1')
.attr('marker-end', 'url(#arrowhead)');
}
}
}
function closeElevationProfile() {
document.getElementById("epScaleRange").removeEventListener("change", draw);
document.getElementById("epCurve").removeEventListener("change", draw);
document.getElementById("epSave").removeEventListener("click", downloadCSV);
document.getElementById("elevationGraph").innerHTML = "";
document.getElementById('epScaleRange').removeEventListener('change', draw);
document.getElementById('epCurve').removeEventListener('change', draw);
document.getElementById('epSave').removeEventListener('click', downloadCSV);
document.getElementById('elevationGraph').innerHTML = '';
modules.elevation = false;
}
}

View file

@ -1,8 +1,8 @@
// Module to store general UI functions
'use strict';
// Module to store general UI functions
// fit full-screen map if window is resized
$(window).resize(function (e) {
window.addEventListener("resize", function (e) {
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
@ -10,6 +10,7 @@ $(window).resize(function (e) {
});
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
}
// Tooltips
const tooltip = document.getElementById('tooltip');
@ -19,12 +20,6 @@ document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
tooltip.innerHTML = tip;
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
@ -32,11 +27,15 @@ function tip(tip = 'Tip is undefined', main, type, time) {
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
if (main) tooltip.dataset.main = tip; // set main tip
if (main) {
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
tooltip.dataset.color = tooltip.style.background;
}
if (time) setTimeout(() => clearMainTip(), time);
}
function showMainTip() {
tooltip.style.background = tooltip.dataset.color;
tooltip.innerHTML = tooltip.dataset.main;
}
@ -55,6 +54,15 @@ function showDataTip(e) {
tip(dataTip);
}
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");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
}
}
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
@ -79,7 +87,7 @@ function showNotes(e, i) {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
} else if (!options.pinNotes) {
} else if (!options.pinNotes && !markerEditor.offsetParent) {
document.getElementById('notes').style.display = 'none';
document.getElementById('notesHeader').innerHTML = '';
document.getElementById('notesBody').innerHTML = '';
@ -101,6 +109,7 @@ function showMapTooltip(point, e, i, g) {
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
@ -497,229 +506,7 @@ function showInfo() {
});
}
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener('keydown', (event) => {
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keyup', (event) => {
if (!window.closeDialogs) return; // not all modules are loaded
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
const active = document.activeElement.tagName;
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
event.stopPropagation();
const key = event.keyCode;
const ctrl = event.ctrlKey || event.metaKey || key === 17;
const shift = event.shiftKey || key === 16;
const alt = event.altKey || key === 18;
if (key === 112) showInfo();
// "F1" to show info
else if (key === 113) regeneratePrompt();
// "F2" for new map
else if (key === 113) regeneratePrompt();
// "F2" for a new map
else if (key === 117) quickSave();
// "F6" for quick save
else if (key === 120) quickLoad();
// "F9" for quick load
else if (key === 9) toggleOptions(event);
// Tab to toggle options
else if (key === 27) {
closeDialogs();
hideOptions();
} // Escape to close all dialogs
else if (key === 46) removeElementOnKey();
// "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions();
// "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder();
// Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap();
// Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click();
// Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click();
// Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap();
// Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes();
// Shift + "B" to edit Biomes
else if (shift && key === 83) editStates();
// Shift + "S" to edit States
else if (shift && key === 80) editProvinces();
// Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy();
// Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures();
// Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase();
// Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones();
// Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions();
// Shift + "R" to edit Religions
else if (shift && key === 81) editResources();
// Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor();
// Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits();
// Shift + "W" to edit Units
else if (shift && key === 79) editNotes();
// Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs();
// Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers();
// Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary();
// Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails();
// Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg();
// Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel();
// Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver();
// Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute();
// Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker();
// Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs);
// Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states);
// Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures);
// Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions);
// Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features);
// Alt + "F" to log features data
else if (key === 88) toggleTexture();
// "X" to toggle Texture layer
else if (key === 72) toggleHeight();
// "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes();
// "B" to toggle Biomes layer
else if (key === 69) toggleCells();
// "E" to toggle Cells layer
else if (key === 71) toggleGrid();
// "G" to toggle Grid layer
else if (key === 79) toggleCoordinates();
// "O" to toggle Coordinates layer
else if (key === 87) toggleCompass();
// "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers();
// "V" to toggle Rivers layer
else if (key === 70) toggleRelief();
// "F" to toggle Relief icons layer
else if (key === 67) toggleCultures();
// "C" to toggle Cultures layer
else if (key === 83) toggleStates();
// "S" to toggle States layer
else if (key === 80) toggleProvinces();
// "P" to toggle Provinces layer
else if (key === 90) toggleZones();
// "Z" to toggle Zones
else if (key === 68) toggleBorders();
// "D" to toggle Borders layer
else if (key === 82) toggleReligions();
// "R" to toggle Religions layer
else if (key === 85) toggleRoutes();
// "U" to toggle Routes layer
else if (key === 84) toggleTemp();
// "T" to toggle Temperature layer
else if (key === 78) togglePopulation();
// "N" to toggle Population layer
else if (key === 74) toggleIce();
// "J" to toggle Ice layer
else if (key === 65) togglePrec();
// "A" to toggle Precipitation layer
else if (key === 81) toggleResources();
// "Q" to toggle Resources layer
else if (key === 89) toggleEmblems();
// "Y" to toggle Emblems layer
else if (key === 76) toggleLabels();
// "L" to toggle Labels layer
else if (key === 73) toggleIcons();
// "I" to toggle Icons layer
else if (key === 77) toggleMilitary();
// "M" to toggle Military layer
else if (key === 75) toggleMarkers();
// "K" to toggle Markers layer
else if (key === 187) toggleRulers();
// Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar();
// Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0);
// Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0);
// Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10);
// Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10);
// Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key);
// Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000);
// 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
// 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
// 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
// 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
// 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
// 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
// 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
// 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
// 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
// 9 to zoom to 9
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$('.dialog:visible .fastDelete').click();
$("button:visible:contains('Remove')").click();
}
// "Q" to toggle Resources layer

810
modules/ui/general.js.orig Normal file
View file

@ -0,0 +1,810 @@
<<<<<<< HEAD
// Module to store general UI functions
'use strict';
// fit full-screen map if window is resized
$(window).resize(function (e) {
if (localStorage.getItem('mapWidth') && localStorage.getItem('mapHeight')) return;
=======
"use strict";
// Module to store general UI functions
// fit full-screen map if window is resized
window.addEventListener("resize", function (e) {
if (localStorage.getItem("mapWidth") && localStorage.getItem("mapHeight")) return;
>>>>>>> master
mapWidthInput.value = window.innerWidth;
mapHeightInput.value = window.innerHeight;
changeMapSize();
});
<<<<<<< HEAD
window.onbeforeunload = () => 'Are you sure you want to navigate away?';
=======
if (location.hostname && location.hostname !== "localhost" && location.hostname !== "127.0.0.1") {
window.onbeforeunload = () => "Are you sure you want to navigate away?";
}
>>>>>>> master
// Tooltips
const tooltip = document.getElementById('tooltip');
// show tip for non-svg elemets with data-tip
document.getElementById('dialogs').addEventListener('mousemove', showDataTip);
document.getElementById('optionsContainer').addEventListener('mousemove', showDataTip);
document.getElementById('exitCustomization').addEventListener('mousemove', showDataTip);
<<<<<<< HEAD
/**
* @param {string} tip Tooltip text
* @param {boolean} main Show above other tooltips
* @param {string} type Message type (color): error / warn / success
* @param {number} time Timeout to auto hide, ms
*/
function tip(tip = 'Tip is undefined', main, type, time) {
=======
function tip(tip = "Tip is undefined", main, type, time) {
>>>>>>> master
tooltip.innerHTML = tip;
tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #5e5c5c80, #ffffff00)';
if (type === 'error') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #e11d1dcc, #ffffff00)';
else if (type === 'warn') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #be5d08cc, #ffffff00)';
else if (type === 'success') tooltip.style.background = 'linear-gradient(0.1turn, #ffffff00, #127912cc, #ffffff00)';
<<<<<<< HEAD
if (main) tooltip.dataset.main = tip; // set main tip
if (time) setTimeout(() => (tooltip.dataset.main = ''), time); // clear main in some time
=======
if (main) {
tooltip.dataset.main = tip;
tooltip.dataset.color = tooltip.style.background;
}
if (time) setTimeout(() => clearMainTip(), time);
>>>>>>> master
}
function showMainTip() {
tooltip.style.background = tooltip.dataset.color;
tooltip.innerHTML = tooltip.dataset.main;
}
function clearMainTip() {
<<<<<<< HEAD
tooltip.dataset.main = '';
tooltip.innerHTML = '';
=======
tooltip.dataset.color = "";
tooltip.dataset.main = "";
tooltip.innerHTML = "";
>>>>>>> master
}
// show tip at the bottom of the screen, consider possible translation
function showDataTip(e) {
if (!e.target) return;
let dataTip = e.target.dataset.tip;
if (!dataTip && e.target.parentNode.dataset.tip) dataTip = e.target.parentNode.dataset.tip;
if (!dataTip) return;
//const tooltip = lang === "en" ? dataTip : translate(e.target.dataset.t || e.target.parentNode.dataset.t, dataTip);
tip(dataTip);
}
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");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
}
}
const moved = debounce(mouseMove, 100);
function mouseMove() {
const point = d3.mouse(this);
const i = findCell(point[0], point[1]); // pack cell id
if (i === undefined) return;
showNotes(d3.event, i);
const g = findGridCell(point[0], point[1]); // grid cell id
if (tooltip.dataset.main) showMainTip();
else showMapTooltip(point, d3.event, i, g);
if (cellInfo.offsetParent) updateCellInfo(point, i, g);
}
// show note box on hover (if any)
function showNotes(e, i) {
if (notesEditor.offsetParent) return;
let id = e.target.id || e.target.parentNode.id || e.target.parentNode.parentNode.id;
<<<<<<< HEAD
if (e.target.parentNode.parentNode.id === 'burgLabels') id = 'burg' + e.target.dataset.id;
else if (e.target.parentNode.parentNode.id === 'burgIcons') id = 'burg' + e.target.dataset.id;
const note = notes.find((note) => note.id === id);
if (note !== undefined && note.legend !== '') {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
} else if (!options.pinNotes) {
document.getElementById('notes').style.display = 'none';
document.getElementById('notesHeader').innerHTML = '';
document.getElementById('notesBody').innerHTML = '';
=======
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id;
else if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
const note = notes.find(note => note.id === id);
if (note !== undefined && note.legend !== "") {
document.getElementById("notes").style.display = "block";
document.getElementById("notesHeader").innerHTML = note.name;
document.getElementById("notesBody").innerHTML = note.legend;
} else if (!options.pinNotes && !markerEditor.offsetParent) {
document.getElementById("notes").style.display = "none";
document.getElementById("notesHeader").innerHTML = "";
document.getElementById("notesBody").innerHTML = "";
>>>>>>> master
}
}
// show viewbox tooltip if main tooltip is blank
function showMapTooltip(point, e, i, g) {
tip(''); // clear tip
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
if (!path[path.length - 8]) return;
const group = path[path.length - 7].id;
const subgroup = path[path.length - 8].id;
const land = pack.cells.h[i] >= 20;
// specific elements
if (group === 'armies') return tip(e.target.parentNode.dataset.name + '. Click to edit');
if (group === 'emblems' && e.target.tagName === 'use') {
const parent = e.target.parentNode;
<<<<<<< HEAD
const [g, type] = parent.id === 'burgEmblems' ? [pack.burgs, 'burg'] : parent.id === 'provinceEmblems' ? [pack.provinces, 'province'] : [pack.states, 'state'];
=======
const [g, type] =
parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
>>>>>>> master
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
d3.select(e.target).raise();
d3.select(parent).raise();
const name = g[i].fullName || g[i].name;
tip(`${name} ${type} emblem. Click to edit. Hold Shift to show associated area or place`);
return;
}
if (group === 'goods') {
const id = +e.target.dataset.i;
const resource = pack.resources.find((resource) => resource.i === id);
tip('Resource: ' + resource.name);
return;
}
if (group === 'rivers') {
const river = +e.target.id.slice(5);
const r = pack.rivers.find((r) => r.i === river);
const name = r ? r.name + ' ' + r.type : '';
tip(name + '. Click to edit');
if (riversOverview.offsetParent) highlightEditorLine(riversOverview, river, 5000);
return;
}
if (group === 'routes') return tip('Click to edit the Route');
if (group === 'terrain') return tip('Click to edit the Relief Icon');
if (subgroup === 'burgLabels' || subgroup === 'burgIcons') {
const burg = +path[path.length - 10].dataset.id;
const b = pack.burgs[burg];
const population = si(b.population * populationRate * urbanization);
tip(`${b.name}. Population: ${population}. Click to edit`);
if (burgsOverview.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === 'labels') return tip('Click to edit the Label');
<<<<<<< HEAD
if (group === 'markers') return tip('Click to edit the Marker');
=======
if (group === "markers") return tip("Click to edit the Marker and pin the marker note");
>>>>>>> master
if (group === 'ruler') {
const tag = e.target.tagName;
const className = e.target.getAttribute('class');
if (tag === 'circle' && className === 'edge') return tip('Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point');
if (tag === 'circle' && className === 'control') return tip('Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point');
if (tag === 'circle') return tip('Drag to adjust the measurer');
if (tag === 'polyline') return tip('Click on drag to add a control point');
if (tag === 'path') return tip('Drag to move the measurer');
if (tag === 'text') return tip('Drag to move, click to remove the measurer');
}
if (subgroup === 'burgIcons') return tip('Click to edit the Burg');
if (subgroup === 'burgLabels') return tip('Click to edit the Burg');
if (group === 'lakes' && !land) {
const lakeId = +e.target.dataset.f;
const name = pack.features[lakeId]?.name;
const fullName = subgroup === 'freshwater' ? name : name + ' ' + subgroup;
tip(`${fullName} lake. Click to edit`);
return;
}
if (group === 'coastline') return tip('Click to edit the coastline');
if (group === 'zones') {
const zone = path[path.length - 8];
tip(zone.dataset.description);
if (zonesEditor.offsetParent) highlightEditorLine(zonesEditor, zone.id, 5000);
return;
}
if (group === 'ice') return tip('Click to edit the Ice');
// covering elements
if (layerIsOn('togglePrec') && land) tip('Annual Precipitation: ' + getFriendlyPrecipitation(i));
else if (layerIsOn('togglePopulation')) tip(getPopulationTip(i));
else if (layerIsOn('toggleTemp')) tip('Temperature: ' + convertTemperature(grid.cells.temp[g]));
else if (layerIsOn('toggleBiomes') && pack.cells.biome[i]) {
const biome = pack.cells.biome[i];
tip('Biome: ' + biomesData.name[biome]);
if (biomesEditor.offsetParent) highlightEditorLine(biomesEditor, biome);
} else if (layerIsOn('toggleReligions') && pack.cells.religion[i]) {
const religion = pack.cells.religion[i];
const r = pack.religions[religion];
const type = r.type === 'Cult' || r.type == 'Heresy' ? r.type : r.type + ' religion';
tip(type + ': ' + r.name);
if (religionsEditor.offsetParent) highlightEditorLine(religionsEditor, religion);
} else if (pack.cells.state[i] && (layerIsOn('toggleProvinces') || layerIsOn('toggleStates'))) {
const state = pack.cells.state[i];
const stateName = pack.states[state].fullName;
const province = pack.cells.province[i];
const prov = province ? pack.provinces[province].fullName + ', ' : '';
tip(prov + stateName);
if (statesEditor.offsetParent) highlightEditorLine(statesEditor, state);
if (diplomacyEditor.offsetParent) highlightEditorLine(diplomacyEditor, state);
if (militaryOverview.offsetParent) highlightEditorLine(militaryOverview, state);
if (provincesEditor.offsetParent) highlightEditorLine(provincesEditor, province);
} else if (layerIsOn('toggleCultures') && pack.cells.culture[i]) {
const culture = pack.cells.culture[i];
tip('Culture: ' + pack.cultures[culture].name);
if (culturesEditor.offsetParent) highlightEditorLine(culturesEditor, culture);
} else if (layerIsOn('toggleHeight')) tip('Height: ' + getFriendlyHeight(point));
}
function highlightEditorLine(editor, id, timeout = 15000) {
Array.from(editor.getElementsByClassName('states hovered')).forEach((el) => el.classList.remove('hovered')); // clear all hovered
const hovered = Array.from(editor.querySelectorAll('div')).find((el) => el.dataset.id == id);
if (hovered) hovered.classList.add('hovered'); // add hovered class
if (timeout)
setTimeout(() => {
hovered && hovered.classList.remove('hovered');
}, timeout);
}
// get cell info on mouse move
function updateCellInfo(point, i, g) {
const cells = pack.cells;
const x = (infoX.innerHTML = rn(point[0]));
const y = (infoY.innerHTML = rn(point[1]));
const f = cells.f[i];
infoLat.innerHTML = toDMS(mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT, 'lat');
infoLon.innerHTML = toDMS(mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT, 'lon');
infoCell.innerHTML = i;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScaleInput.value ** 2) + unit : 'n/a';
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], pack.cells.h[i], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : 'n/a';
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : 'no';
infoState.innerHTML = cells.h[i] >= 20 ? (cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : 'neutral lands (0)') : 'no';
infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : 'no';
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : 'no';
infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : 'no';
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + ' (' + cells.burg[i] + ')' : 'no';
infoFeature.innerHTML = f ? pack.features[f].group + ' (' + f + ')' : 'n/a';
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
// convert coordinate to DMS format
function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord));
const minutesNotTruncated = (Math.abs(coord) - degrees) * 60;
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;
}
// get surface elevation
function getElevation(f, h) {
if (f.land) return getHeight(h) + ' (' + h + ')'; // land: usual height
if (f.border) return '0 ' + heightUnit.value; // ocean: 0
if (f.type === 'lake') return getHeight(f.height) + ' (' + f.height + ')'; // lake: defined on river generation
}
// get water depth
function getDepth(f, h, p) {
if (f.land) return '0 ' + heightUnit.value; // land: 0
// lake: difference between surface and bottom
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
if (f.type === 'lake') {
const depth = gridH === 19 ? f.height / 2 : gridH;
return getHeight(depth, 'abs');
}
return getHeight(gridH, 'abs'); // ocean: grid height
}
// get user-friendly (real-world) height value from map data
function getFriendlyHeight(p) {
const packH = pack.cells.h[findCell(p[0], p[1])];
const gridH = grid.cells.h[findGridCell(p[0], p[1])];
const h = packH < 20 ? gridH : packH;
return getHeight(h);
}
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === 'm') unitRatio = 1;
// if meter
else if (unit === 'f') unitRatio = 0.5468; // if fathom
let height = -990;
if (h >= 20) height = Math.pow(h - 18, +heightExponentInput.value);
else if (h < 20 && h > 0) height = ((h - 20) / h) * 50;
if (abs) height = Math.abs(height);
return rn(height * unitRatio) + ' ' + unit;
}
// get user-friendly (real-world) precipitation value from map data
function getFriendlyPrecipitation(i) {
const prec = grid.cells.prec[pack.cells.g[i]];
return prec * 100 + ' mm';
}
function getRiverInfo(id) {
const r = pack.rivers.find((r) => r.i == id);
return r ? `${r.name} ${r.type} (${id})` : 'n/a';
}
function getCellPopulation(i) {
const rural = pack.cells.pop[i] * populationRate;
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate * urbanization : 0;
return [rural, urban];
}
// get user-friendly (real-world) population value from map data
function getFriendlyPopulation(i) {
const [rural, urban] = getCellPopulation(i);
return `${si(rural + urban)} (${si(rural)} rural, urban ${si(urban)})`;
}
function getPopulationTip(i) {
const [rural, urban] = getCellPopulation(i);
return `Cell population: ${si(rural + urban)}; Rural: ${si(rural)}; Urban: ${si(urban)}`;
}
function highlightEmblemElement(type, el) {
const i = el.i,
cells = pack.cells;
const animation = d3.transition().duration(1000).ease(d3.easeSinIn);
if (type === 'burg') {
const {x, y} = el;
debug
<<<<<<< HEAD
.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0)
.attr('fill', 'none')
.attr('stroke', '#d0240f')
.attr('stroke-width', 1)
.attr('opacity', 1)
.transition(animation)
.attr('r', 20)
.attr('opacity', 0.1)
.attr('stroke-width', 0)
=======
.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 0)
.attr("fill", "none")
.attr("stroke", "#d0240f")
.attr("stroke-width", 1)
.attr("opacity", 1)
.transition(animation)
.attr("r", 20)
.attr("opacity", 0.1)
.attr("stroke-width", 0)
>>>>>>> master
.remove();
return;
}
const [x, y] = el.pole || pack.cells.p[el.center];
const obj = type === 'state' ? cells.state : cells.province;
const borderCells = cells.i.filter((id) => obj[id] === i && cells.c[id].some((n) => obj[n] !== i));
const data = Array.from(borderCells)
.filter((c, i) => !(i % 2))
.map((i) => cells.p[i])
.map((i) => [i[0], i[1], Math.hypot(i[0] - x, i[1] - y)]);
debug
.selectAll('line')
.data(data)
.enter()
.append('line')
.attr('x1', x)
.attr('y1', y)
.attr('x2', (d) => d[0])
.attr('y2', (d) => d[1])
.attr('stroke', '#d0240f')
.attr('stroke-width', 0.5)
.attr('opacity', 0.2)
.attr('stroke-dashoffset', (d) => d[2])
.attr('stroke-dasharray', (d) => d[2])
.transition(animation)
.attr('stroke-dashoffset', 0)
.attr('opacity', 1)
.transition(animation)
.delay(1000)
.attr('stroke-dashoffset', (d) => d[2])
.attr('opacity', 0)
.remove();
}
// assign lock behavior
document.querySelectorAll('[data-locked]').forEach(function (e) {
e.addEventListener('mouseover', function (event) {
if (this.className === 'icon-lock') tip('Click to unlock the option and allow it to be randomized on new map generation');
else tip('Click to lock the option and always use the current value on new map generation');
event.stopPropagation();
});
e.addEventListener('click', function () {
const id = this.id.slice(5);
if (this.className === 'icon-lock') unlock(id);
else lock(id);
});
});
// lock option
function lock(id) {
const input = document.querySelector("[data-stored='" + id + "']");
if (input) localStorage.setItem(id, input.value);
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 1;
el.className = 'icon-lock';
}
// unlock option
function unlock(id) {
localStorage.removeItem(id);
const el = document.getElementById('lock_' + id);
if (!el) return;
el.dataset.locked = 0;
el.className = 'icon-lock-open';
}
// check if option is locked
function locked(id) {
const lockEl = document.getElementById('lock_' + id);
return lockEl.dataset.locked == 1;
}
// check if option is stored in localStorage
function stored(option) {
return localStorage.getItem(option);
}
// assign skeaker behaviour
Array.from(document.getElementsByClassName('speaker')).forEach((el) => {
const input = el.previousElementSibling;
el.addEventListener('click', () => speak(input.value));
});
function speak(text) {
const speaker = new SpeechSynthesisUtterance(text);
const voices = speechSynthesis.getVoices();
if (voices.length) {
const voiceId = +document.getElementById('speakerVoice').value;
speaker.voice = voices[voiceId];
}
speechSynthesis.speak(speaker);
}
// apply drop-down menu option. If the value is not in options, add it
function applyOption(select, id, name = id) {
const custom = !Array.from(select.options).some((o) => o.value == id);
if (custom) select.options.add(new Option(name, id));
select.value = id;
}
// show info about the generator in a popup
function showInfo() {
const Discord = link('https://discordapp.com/invite/X7E84HU', 'Discord');
const Reddit = link('https://www.reddit.com/r/FantasyMapGenerator', 'Reddit');
const Patreon = link('https://www.patreon.com/azgaar', 'Patreon');
const Trello = link('https://trello.com/b/7x832DG4/fantasy-map-generator', 'Trello');
const Armoria = link('https://azgaar.github.io/Armoria', 'Armoria');
const QuickStart = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial', 'Quick start tutorial');
const QAA = link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A', 'Q&A page');
alertMessage.innerHTML = `
<b>Fantasy Map Generator</b> (FMG) is an open-source application, it means the code is published an anyone can use it.
In case of FMG is also means that you own all created maps and can use them as you wish, you can even sell them.
<p>The development is supported by community, you can donate on ${Patreon}.
You can also help creating overviews, tutorials and spreding the word about the Generator.</p>
<p>The best way to get help is to contact the community on ${Discord} and ${Reddit}.
Before asking questions, please check out the ${QuickStart} and the ${QAA}.</p>
<p>Track the development process on ${Trello}.</p>
<p>Check out our new project: ${Armoria}, heraldry generator and editor.</p>
<b>Links:</b>
<ul style="columns:2">
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator', 'GitHub repository')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/blob/master/LICENSE', 'License')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog', 'Changelog')}</li>
<li>${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys', 'Hotkeys')}</li>
</ul>`;
$('#alert').dialog({
resizable: false,
title: document.title,
width: '28em',
buttons: {
OK: function () {
$(this).dialog('close');
}
},
position: {my: 'center', at: 'center', of: 'svg'}
});
}
<<<<<<< HEAD
// prevent default browser behavior for FMG-used hotkeys
document.addEventListener('keydown', (event) => {
if (event.altKey && event.keyCode !== 18) event.preventDefault(); // disallow alt key combinations
if (event.ctrlKey && event.code === 'KeyS') event.preventDefault(); // disallow CTRL + C
if ([112, 113, 117, 120, 9].includes(event.keyCode)) event.preventDefault(); // F1, F2, F6, F9, Tab
});
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keyup', (event) => {
if (!window.closeDialogs) return; // not all modules are loaded
const canvas3d = document.getElementById('canvas3d'); // check if 3d mode is active
const active = document.activeElement.tagName;
if (active === 'INPUT' || active === 'SELECT' || active === 'TEXTAREA') return; // don't trigger if user inputs a text
if (active === 'DIV' && document.activeElement.contentEditable === 'true') return; // don't trigger if user inputs a text
event.stopPropagation();
const key = event.keyCode;
const ctrl = event.ctrlKey || event.metaKey || key === 17;
const shift = event.shiftKey || key === 16;
const alt = event.altKey || key === 18;
if (key === 112) showInfo();
// "F1" to show info
else if (key === 113) regeneratePrompt();
// "F2" for new map
else if (key === 113) regeneratePrompt();
// "F2" for a new map
else if (key === 117) quickSave();
// "F6" for quick save
else if (key === 120) quickLoad();
// "F9" for quick load
else if (key === 9) toggleOptions(event);
// Tab to toggle options
else if (key === 27) {
closeDialogs();
hideOptions();
} // Escape to close all dialogs
else if (key === 46) removeElementOnKey();
// "Delete" to remove the selected element
else if (key === 79 && canvas3d) toggle3dOptions();
// "O" to toggle 3d options
else if (ctrl && key === 81) toggleSaveReminder();
// Ctrl + "Q" to toggle save reminder
else if (ctrl && key === 83) saveMap();
// Ctrl + "S" to save .map file
else if (undo.offsetParent && ctrl && key === 90) undo.click();
// Ctrl + "Z" to undo
else if (redo.offsetParent && ctrl && key === 89) redo.click();
// Ctrl + "Y" to redo
else if (shift && key === 72) editHeightmap();
// Shift + "H" to edit Heightmap
else if (shift && key === 66) editBiomes();
// Shift + "B" to edit Biomes
else if (shift && key === 83) editStates();
// Shift + "S" to edit States
else if (shift && key === 80) editProvinces();
// Shift + "P" to edit Provinces
else if (shift && key === 68) editDiplomacy();
// Shift + "D" to edit Diplomacy
else if (shift && key === 67) editCultures();
// Shift + "C" to edit Cultures
else if (shift && key === 78) editNamesbase();
// Shift + "N" to edit Namesbase
else if (shift && key === 90) editZones();
// Shift + "Z" to edit Zones
else if (shift && key === 82) editReligions();
// Shift + "R" to edit Religions
else if (shift && key === 81) editResources();
// Shift + "Q" to edit Resources
else if (shift && key === 89) openEmblemEditor();
// Shift + "Y" to edit Emblems
else if (shift && key === 87) editUnits();
// Shift + "W" to edit Units
else if (shift && key === 79) editNotes();
// Shift + "O" to edit Notes
else if (shift && key === 84) overviewBurgs();
// Shift + "T" to open Burgs overview
else if (shift && key === 86) overviewRivers();
// Shift + "V" to open Rivers overview
else if (shift && key === 77) overviewMilitary();
// Shift + "M" to open Military overview
else if (shift && key === 69) viewCellDetails();
// Shift + "E" to open Cell Details
else if (shift && key === 49) toggleAddBurg();
// Shift + "1" to click to add Burg
else if (shift && key === 50) toggleAddLabel();
// Shift + "2" to click to add Label
else if (shift && key === 51) toggleAddRiver();
// Shift + "3" to click to add River
else if (shift && key === 52) toggleAddRoute();
// Shift + "4" to click to add Route
else if (shift && key === 53) toggleAddMarker();
// Shift + "5" to click to add Marker
else if (alt && key === 66) console.table(pack.burgs);
// Alt + "B" to log burgs data
else if (alt && key === 83) console.table(pack.states);
// Alt + "S" to log states data
else if (alt && key === 67) console.table(pack.cultures);
// Alt + "C" to log cultures data
else if (alt && key === 82) console.table(pack.religions);
// Alt + "R" to log religions data
else if (alt && key === 70) console.table(pack.features);
// Alt + "F" to log features data
else if (key === 88) toggleTexture();
// "X" to toggle Texture layer
else if (key === 72) toggleHeight();
// "H" to toggle Heightmap layer
else if (key === 66) toggleBiomes();
// "B" to toggle Biomes layer
else if (key === 69) toggleCells();
// "E" to toggle Cells layer
else if (key === 71) toggleGrid();
// "G" to toggle Grid layer
else if (key === 79) toggleCoordinates();
// "O" to toggle Coordinates layer
else if (key === 87) toggleCompass();
// "W" to toggle Compass Rose layer
else if (key === 86) toggleRivers();
// "V" to toggle Rivers layer
else if (key === 70) toggleRelief();
// "F" to toggle Relief icons layer
else if (key === 67) toggleCultures();
// "C" to toggle Cultures layer
else if (key === 83) toggleStates();
// "S" to toggle States layer
else if (key === 80) toggleProvinces();
// "P" to toggle Provinces layer
else if (key === 90) toggleZones();
// "Z" to toggle Zones
else if (key === 68) toggleBorders();
// "D" to toggle Borders layer
else if (key === 82) toggleReligions();
// "R" to toggle Religions layer
else if (key === 85) toggleRoutes();
// "U" to toggle Routes layer
else if (key === 84) toggleTemp();
// "T" to toggle Temperature layer
else if (key === 78) togglePopulation();
// "N" to toggle Population layer
else if (key === 74) toggleIce();
// "J" to toggle Ice layer
else if (key === 65) togglePrec();
// "A" to toggle Precipitation layer
else if (key === 81) toggleResources();
// "Q" to toggle Resources layer
else if (key === 89) toggleEmblems();
// "Y" to toggle Emblems layer
else if (key === 76) toggleLabels();
// "L" to toggle Labels layer
else if (key === 73) toggleIcons();
// "I" to toggle Icons layer
else if (key === 77) toggleMilitary();
// "M" to toggle Military layer
else if (key === 75) toggleMarkers();
// "K" to toggle Markers layer
else if (key === 187) toggleRulers();
// Equal (=) to toggle Rulers
else if (key === 189) toggleScaleBar();
// Minus (-) to toggle Scale bar
else if (key === 37) zoom.translateBy(svg, 10, 0);
// Left to scroll map left
else if (key === 39) zoom.translateBy(svg, -10, 0);
// Right to scroll map right
else if (key === 38) zoom.translateBy(svg, 0, 10);
// Up to scroll map up
else if (key === 40) zoom.translateBy(svg, 0, -10);
// Up to scroll map up
else if (key === 107 || key === 109) pressNumpadSign(key);
// Numpad Plus/Minus to zoom map or change brush size
else if (key === 48 || key === 96) resetZoom(1000);
// 0 to reset zoom
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1);
// 1 to zoom to 1
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2);
// 2 to zoom to 2
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3);
// 3 to zoom to 3
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4);
// 4 to zoom to 4
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5);
// 5 to zoom to 5
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6);
// 6 to zoom to 6
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7);
// 7 to zoom to 7
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8);
// 8 to zoom to 8
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9);
// 9 to zoom to 9
else if (ctrl) pressControl(); // Control to toggle mode
});
function pressNumpadSign(key) {
// if brush sliders are displayed, decrease brush size
let brush = null;
const d = key === 107 ? 1 : -1;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = Math.max(Math.min(+brush.value + d, +brush.max), +brush.min);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === 107 ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no, zoom map
}
function pressControl() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
// trigger trash button click on "Delete" keypress
function removeElementOnKey() {
$('.dialog:visible .fastDelete').click();
$("button:visible:contains('Remove')").click();
}
=======
>>>>>>> master

View file

@ -7,8 +7,8 @@ function editHeightmap() {
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
<p>You can <i>keep</i> the data, but you won't be able to change the coastline.</p>
<p>Try <i>risk</i> mode to change the coastline and keep the data. The data will be restored as much as possible, but it can cause unpredictable errors.</p>
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Check out ${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization', 'wiki')} for guidance.</p>`;
<p>Please <span class="pseudoLink" onclick=dowloadMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p style="margin-bottom: 0">Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
$('#alert').dialog({
resizable: false,
@ -222,7 +222,7 @@ function editHeightmap() {
Lakes.generateName();
Military.generate();
addMarkers();
Markers.generate();
addZones();
TIME && console.timeEnd('regenerateErasedData');
INFO && console.groupEnd('Edit Heightmap');
@ -334,10 +334,10 @@ function editHeightmap() {
for (const i of pack.cells.i) {
const g = pack.cells.g[i];
const land = pack.cells.h[i] >= 20;
const isLand = pack.cells.h[i] >= 20;
// check biome
pack.cells.biome[i] = land && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], pack.cells.h[i]);
pack.cells.biome[i] = isLand && biome[g] ? biome[g] : getBiomeId(grid.cells.prec[g], grid.cells.temp[g], pack.cells.h[i]);
// rivers data
if (!erosionAllowed) {
@ -346,7 +346,7 @@ function editHeightmap() {
pack.cells.fl[i] = fl[g];
}
if (!land) continue;
if (!isLand) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
@ -614,7 +614,7 @@ function editHeightmap() {
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {
return Math.max(Math.min(v, 100), land ? 20 : 0);
return minmax(v, land ? 20 : 0, 100);
}
const h = grid.cells.h;
@ -626,6 +626,8 @@ function editHeightmap() {
else if (brush === 'brushAlign') s.forEach((i) => (h[i] = lim(h[start])));
else if (brush === 'brushSmooth') s.forEach((i) => (h[i] = rn((d3.mean(grid.cells.c[i].filter((i) => (land ? h[i] >= 20 : 1)).map((c) => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1)));
else if (brush === 'brushDisrupt') s.forEach((i) => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power)));
i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1))
);
mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step
@ -775,6 +777,7 @@ function editHeightmap() {
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4 || '15-85'}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3 || '40-50'}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count || '1-2'}></span>`;
}></span>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === 'Hill' || type === 'Pit' || type === 'Range' || type === 'Trough') return blob;
@ -792,6 +795,8 @@ function editHeightmap() {
} min=0 max=10 step=.1></span></div>`;
if (type === 'Smooth')
return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count || 2}></span></div>`;
count || 2
}></span></div>`;
}
function setRange(event) {
@ -853,31 +858,27 @@ function editHeightmap() {
const steps = body.querySelectorAll('#templateBody > div');
if (!steps.length) return;
const {addHill, addPit, addRange, addTrough, addStrait, modify, smooth} = HeightmapGenerator;
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.dataset.type;
for (const step of steps) {
if (step.style.opacity === "0.5") continue;
const type = step.dataset.type;
const elCount = s.querySelector('.templateCount') || '';
const elHeight = s.querySelector('.templateHeight') || '';
const count = step.querySelector(".templateCount")?.value || "";
const height = step.querySelector(".templateHeight")?.value || "";
const dist = step.querySelector(".templateDist")?.value || null;
const x = step.querySelector(".templateX")?.value || null;
const y = step.querySelector(".templateY")?.value || null;
const elDist = s.querySelector('.templateDist');
const dist = elDist ? elDist.value : null;
const templateX = s.querySelector('.templateX');
const x = templateX ? templateX.value : null;
const templateY = s.querySelector('.templateY');
const y = templateY ? templateY.value : null;
if (type === 'Hill') HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y);
else if (type === 'Pit') HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y);
else if (type === 'Range') HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y);
else if (type === 'Trough') HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y);
else if (type === 'Strait') HeightmapGenerator.addStrait(elCount.value, dist);
else if (type === 'Add') HeightmapGenerator.modify(dist, +elCount.value, 1);
else if (type === 'Multiply') HeightmapGenerator.modify(dist, 0, +elCount.value);
else if (type === 'Smooth') HeightmapGenerator.smooth(+elCount.value);
if (type === "Hill") addHill(count, height, x, y);
else if (type === "Pit") addPit(count, height, x, y);
else if (type === "Range") addRange(count, height, x, y);
else if (type === "Trough") addTrough(count, height, x, y);
else if (type === "Strait") addStrait(count, dist);
else if (type === "Add") modify(dist, +count, 1);
else if (type === "Multiply") modify(dist, 0, +count);
else if (type === "Smooth") smooth(+count);
updateHistory('noStat'); // update history every step
}
@ -896,17 +897,13 @@ function editHeightmap() {
let data = '';
for (const s of steps) {
if (s.style.opacity == 0.5) continue;
const type = s.getAttribute('data-type');
if (s.style.opacity === "0.5") continue;
const elCount = s.querySelector('.templateCount');
const count = elCount ? elCount.value : '0';
const elHeight = s.querySelector('.templateHeight');
const elDist = s.querySelector('.templateDist');
const arg3 = elHeight ? elHeight.value : elDist ? elDist.value : '0';
const templateX = s.querySelector('.templateX');
const x = templateX ? templateX.value : '0';
const templateY = s.querySelector('.templateY');
const y = templateY ? templateY.value : '0';
const count = s.querySelector(".templateCount")?.value || "0";
const arg3 = s.querySelector(".templateHeight")?.value || s.querySelector(".templateDist")?.value || "0";
const x = s.querySelector(".templateX")?.value || "0";
const y = s.querySelector(".templateY")?.value || "0";
data += `${type} ${count} ${arg3} ${x} ${y}\r\n`;
}
@ -1194,10 +1191,14 @@ function editHeightmap() {
}
function setConvertColorsNumber() {
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`, {default: +convertColors.value, step: 1, min: 3, max: 255}, (number) => {
convertColors.value = number;
heightsFromImage(number);
});
prompt(
`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`,
{default: +convertColors.value, step: 1, min: 3, max: 255},
number => {
convertColors.value = number;
heightsFromImage(number);
}
);
}
function setOverlayOpacity(v) {

File diff suppressed because it is too large Load diff

153
modules/ui/hotkeys.js Normal file
View file

@ -0,0 +1,153 @@
'use strict';
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
document.addEventListener('keydown', handleKeydown);
document.addEventListener('keyup', handleKeyup);
function handleKeydown(event) {
const {code, ctrlKey, altKey} = event;
if (altKey && !ctrlKey) event.preventDefault(); // disallow alt key combinations
if (ctrlKey && ['KeyS', 'KeyC'].includes(code)) event.preventDefault(); // disallow CTRL + S and CTRL + C
if (['F1', 'F2', 'F6', 'F9', 'Tab'].includes(code)) event.preventDefault(); // disallow default Fn and Tab
}
function handleKeyup(event) {
if (!modules.editors) return; // if editors are not loaded, do nothing
const {tagName, contentEditable} = document.activeElement;
if (['INPUT', 'SELECT', 'TEXTAREA'].includes(tagName)) return; // don't trigger if user inputs text
if (tagName === 'DIV' && contentEditable === 'true') return; // don't trigger if user inputs a text
if (document.getSelection().toString()) return; // don't trigger if user selects text
event.stopPropagation();
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
const ctrl = ctrlKey || metaKey || key === 'Control';
const shift = shiftKey || key === 'Shift';
const alt = altKey || key === 'Alt';
if (code === 'F1') showInfo();
else if (code === 'F2') regeneratePrompt('hotkey');
else if (code === 'F6') quickSave();
else if (code === 'F9') quickLoad();
else if (code === 'Tab') toggleOptions(event);
else if (code === 'Escape') closeAllDialogs();
else if (code === 'Delete') removeElementOnKey();
else if (code === 'KeyO' && document.getElementById('canvas3d')) toggle3dOptions();
else if (ctrl && code === 'KeyQ') toggleSaveReminder();
else if (ctrl && code === 'KeyS') dowloadMap();
else if (ctrl && code === 'KeyC') saveToDropbox();
else if (ctrl && code === 'KeyZ' && undo.offsetParent) undo.click();
else if (ctrl && code === 'KeyY' && redo.offsetParent) redo.click();
else if (shift && code === 'KeyH') editHeightmap();
else if (shift && code === 'KeyB') editBiomes();
else if (shift && code === 'KeyS') editStates();
else if (shift && code === 'KeyP') editProvinces();
else if (shift && code === 'KeyD') editDiplomacy();
else if (shift && code === 'KeyC') editCultures();
else if (shift && code === 'KeyN') editNamesbase();
else if (shift && code === 'KeyZ') editZones();
else if (shift && code === 'KeyR') editReligions();
else if (shift && code === 'KeyY') openEmblemEditor();
else if (shift && code === 'KeyQ') editUnits();
else if (shift && code === 'KeyO') editNotes();
else if (shift && code === 'KeyT') overviewBurgs();
else if (shift && code === 'KeyV') overviewRivers();
else if (shift && code === 'KeyM') overviewMilitary();
else if (shift && code === 'KeyK') overviewMarkers();
else if (shift && code === 'KeyE') viewCellDetails();
else if (key === '!') toggleAddBurg();
else if (key === '@') toggleAddLabel();
else if (key === '#') toggleAddRiver();
else if (key === '$') toggleAddRoute();
else if (key === '%') toggleAddMarker();
else if (alt && code === 'KeyB') console.table(pack.burgs);
else if (alt && code === 'KeyS') console.table(pack.states);
else if (alt && code === 'KeyC') console.table(pack.cultures);
else if (alt && code === 'KeyR') console.table(pack.religions);
else if (alt && code === 'KeyF') console.table(pack.features);
else if (code === 'KeyX') toggleTexture();
else if (code === 'KeyH') toggleHeight();
else if (code === 'KeyB') toggleBiomes();
else if (code === 'KeyE') toggleCells();
else if (code === 'KeyG') toggleGrid();
else if (code === 'KeyO') toggleCoordinates();
else if (code === 'KeyW') toggleCompass();
else if (code === 'KeyV') toggleRivers();
else if (code === 'KeyF') toggleRelief();
else if (code === 'KeyC') toggleCultures();
else if (code === 'KeyS') toggleStates();
else if (code === 'KeyP') toggleProvinces();
else if (code === 'KeyZ') toggleZones();
else if (code === 'KeyD') toggleBorders();
else if (code === 'KeyR') toggleReligions();
else if (code === 'KeyU') toggleRoutes();
else if (code === 'KeyT') toggleTemp();
else if (code === 'KeyN') togglePopulation();
else if (code === 'KeyJ') toggleIce();
else if (code === 'KeyA') togglePrec();
else if (code === 'KeyY') toggleEmblems();
else if (code === 'KeyL') toggleLabels();
else if (code === 'KeyI') toggleIcons();
else if (code === 'KeyM') toggleMilitary();
else if (code === 'KeyK') toggleMarkers();
else if (code === 'Equal') toggleRulers();
else if (code === 'Slash') toggleScaleBar();
else if (code === 'ArrowLeft') zoom.translateBy(svg, 10, 0);
else if (code === 'ArrowRight') zoom.translateBy(svg, -10, 0);
else if (code === 'ArrowUp') zoom.translateBy(svg, 0, 10);
else if (code === 'ArrowDown') zoom.translateBy(svg, 0, -10);
else if (key === '+' || key === '-') pressNumpadSign(key);
else if (key === '0') resetZoom(1000);
else if (key === '1') zoom.scaleTo(svg, 1);
else if (key === '2') zoom.scaleTo(svg, 2);
else if (key === '3') zoom.scaleTo(svg, 3);
else if (key === '4') zoom.scaleTo(svg, 4);
else if (key === '5') zoom.scaleTo(svg, 5);
else if (key === '6') zoom.scaleTo(svg, 6);
else if (key === '7') zoom.scaleTo(svg, 7);
else if (key === '8') zoom.scaleTo(svg, 8);
else if (key === '9') zoom.scaleTo(svg, 9);
else if (ctrl) toggleMode();
}
function pressNumpadSign(key) {
const change = key === '+' ? 1 : -1;
let brush = null;
if (brushRadius.offsetParent) brush = document.getElementById('brushRadius');
else if (biomesManuallyBrush.offsetParent) brush = document.getElementById('biomesManuallyBrush');
else if (statesManuallyBrush.offsetParent) brush = document.getElementById('statesManuallyBrush');
else if (provincesManuallyBrush.offsetParent) brush = document.getElementById('provincesManuallyBrush');
else if (culturesManuallyBrush.offsetParent) brush = document.getElementById('culturesManuallyBrush');
else if (zonesBrush.offsetParent) brush = document.getElementById('zonesBrush');
else if (religionsManuallyBrush.offsetParent) brush = document.getElementById('religionsManuallyBrush');
if (brush) {
const value = minmax(+brush.value + change, +brush.min, +brush.max);
brush.value = document.getElementById(brush.id + 'Number').value = value;
return;
}
const scaleBy = key === '+' ? 1.2 : 0.8;
zoom.scaleBy(svg, scaleBy); // if no brush elements displayed, zoom map
}
function toggleMode() {
if (zonesRemove.offsetParent) {
zonesRemove.classList.contains('pressed') ? zonesRemove.classList.remove('pressed') : zonesRemove.classList.add('pressed');
}
}
function removeElementOnKey() {
const fastDelete = Array.from(document.querySelectorAll("[role='dialog'] .fastDelete")).find((dialog) => dialog.style.display !== 'none');
if (fastDelete) fastDelete.click();
const visibleDialogs = Array.from(document.querySelectorAll("[role='dialog']")).filter((dialog) => dialog.style.display !== 'none');
if (!visibleDialogs.length) return;
visibleDialogs.forEach((dialog) => dialog.querySelectorAll('button').forEach((button) => button.textContent === 'Remove' && button.click()));
}
function closeAllDialogs() {
closeDialogs();
hideOptions();
}

View file

@ -51,8 +51,8 @@ function editLabel() {
function showEditorTips() {
showMainTip();
if (d3.event.target.parentNode.parentNode.id === elSelected.attr('id')) tip('Drag to shift the label');
else if (d3.event.target.parentNode.id === 'controlPoints') {
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label");
else if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === 'circle') tip('Drag to move, click to delete the control point');
if (d3.event.target.tagName === 'path') tip('Click to add a control point');
}
@ -253,7 +253,13 @@ function editLabel() {
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?<br><br>Labels to be removed: ${count}`;
const onConfirm = () => {
$('#labelEditor').dialog('close');
hideGroupSection();
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
labels
.select('#' + group)
.selectAll('text')
@ -357,10 +363,13 @@ function editLabel() {
const message = 'Are you sure you want to remove the label? <br>This action cannot be reverted';
const onConfirm = () => {
defs.select('#textPath_' + elSelected.attr('id')).remove();
title: "Remove label",
elSelected.remove();
$('#labelEditor').dialog('close');
};
confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm});
$(this).dialog("close");
}
}
function closeLabelEditor() {

View file

@ -0,0 +1,549 @@
'use strict';
function editLabel() {
if (customization) return;
closeDialogs();
if (!layerIsOn('toggleLabels')) toggleLabels();
const tspan = d3.event.target;
const textPath = tspan.parentNode;
const text = textPath.parentNode;
<<<<<<< HEAD
elSelected = d3.select(text).call(d3.drag().on('start', dragLabel)).classed('draggable', true);
viewbox.on('touchmove mousemove', showEditorTips);
$('#labelEditor').dialog({
title: 'Edit Label',
resizable: false,
width: fitContent(),
position: {my: 'center top+10', at: 'bottom', of: text, collision: 'fit'},
=======
elSelected = d3.select(text).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
viewbox.on("touchmove mousemove", showEditorTips);
$("#labelEditor").dialog({
title: "Edit Label",
resizable: false,
width: fitContent(),
position: {my: "center top+10", at: "bottom", of: text, collision: "fit"},
>>>>>>> master
close: closeLabelEditor
});
drawControlPointsAndLine();
selectLabelGroup(text);
updateValues(textPath);
if (modules.editLabel) return;
modules.editLabel = true;
// add listeners
document.getElementById('labelGroupShow').addEventListener('click', showGroupSection);
document.getElementById('labelGroupHide').addEventListener('click', hideGroupSection);
document.getElementById('labelGroupSelect').addEventListener('click', changeGroup);
document.getElementById('labelGroupInput').addEventListener('change', createNewGroup);
document.getElementById('labelGroupNew').addEventListener('click', toggleNewGroupInput);
document.getElementById('labelGroupRemove').addEventListener('click', removeLabelsGroup);
document.getElementById('labelTextShow').addEventListener('click', showTextSection);
document.getElementById('labelTextHide').addEventListener('click', hideTextSection);
document.getElementById('labelText').addEventListener('input', changeText);
document.getElementById('labelTextRandom').addEventListener('click', generateRandomName);
document.getElementById('labelEditStyle').addEventListener('click', editGroupStyle);
document.getElementById('labelSizeShow').addEventListener('click', showSizeSection);
document.getElementById('labelSizeHide').addEventListener('click', hideSizeSection);
document.getElementById('labelStartOffset').addEventListener('input', changeStartOffset);
document.getElementById('labelRelativeSize').addEventListener('input', changeRelativeSize);
document.getElementById('labelAlign').addEventListener('click', editLabelAlign);
document.getElementById('labelLegend').addEventListener('click', editLabelLegend);
document.getElementById('labelRemoveSingle').addEventListener('click', removeLabel);
function showEditorTips() {
showMainTip();
<<<<<<< HEAD
if (d3.event.target.parentNode.parentNode.id === elSelected.attr('id')) tip('Drag to shift the label');
else if (d3.event.target.parentNode.id === 'controlPoints') {
if (d3.event.target.tagName === 'circle') tip('Drag to move, click to delete the control point');
if (d3.event.target.tagName === 'path') tip('Click to add a control point');
=======
if (d3.event.target.parentNode.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label");
else if (d3.event.target.parentNode.id === "controlPoints") {
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
if (d3.event.target.tagName === "path") tip("Click to add a control point");
>>>>>>> master
}
}
function selectLabelGroup(text) {
const group = text.parentNode.id;
const select = document.getElementById('labelGroupSelect');
select.options.length = 0; // remove all options
<<<<<<< HEAD
labels.selectAll(':scope > g').each(function () {
if (this.id === 'burgLabels') return;
=======
labels.selectAll(":scope > g").each(function () {
if (this.id === "burgLabels") return;
>>>>>>> master
select.options.add(new Option(this.id, this.id, false, this.id === group));
});
}
function updateValues(textPath) {
document.getElementById('labelText').value = [...textPath.querySelectorAll('tspan')].map((tspan) => tspan.textContent).join('|');
document.getElementById('labelStartOffset').value = parseFloat(textPath.getAttribute('startOffset'));
document.getElementById('labelRelativeSize').value = parseFloat(textPath.getAttribute('font-size'));
}
function drawControlPointsAndLine() {
debug.select('#controlPoints').remove();
debug.append('g').attr('id', 'controlPoints').attr('transform', elSelected.attr('transform'));
const path = document.getElementById('textPath_' + elSelected.attr('id'));
debug.select('#controlPoints').append('path').attr('d', path.getAttribute('d')).on('click', addInterimControlPoint);
const l = path.getTotalLength();
if (!l) return;
const increment = l / Math.max(Math.ceil(l / 200), 2);
for (let i = 0; i <= l; i += increment) {
addControlPoint(path.getPointAtLength(i));
}
}
function addControlPoint(point) {
<<<<<<< HEAD
debug
.select('#controlPoints')
.append('circle')
.attr('cx', point.x)
.attr('cy', point.y)
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.call(d3.drag().on('drag', dragControlPoint))
.on('click', clickControlPoint);
=======
debug.select("#controlPoints").append("circle").attr("cx", point.x).attr("cy", point.y).attr("r", 2.5).attr("stroke-width", 0.8).call(d3.drag().on("drag", dragControlPoint)).on("click", clickControlPoint);
>>>>>>> master
}
function dragControlPoint() {
this.setAttribute('cx', d3.event.x);
this.setAttribute('cy', d3.event.y);
redrawLabelPath();
}
function redrawLabelPath() {
const path = document.getElementById('textPath_' + elSelected.attr('id'));
lineGen.curve(d3.curveBundle.beta(1));
const points = [];
debug
<<<<<<< HEAD
.select('#controlPoints')
.selectAll('circle')
.each(function () {
points.push([this.getAttribute('cx'), this.getAttribute('cy')]);
=======
.select("#controlPoints")
.selectAll("circle")
.each(function () {
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
>>>>>>> master
});
const d = round(lineGen(points));
path.setAttribute('d', d);
debug.select('#controlPoints > path').attr('d', d);
}
function clickControlPoint() {
this.remove();
redrawLabelPath();
}
function addInterimControlPoint() {
const point = d3.mouse(this);
const dists = [];
debug
<<<<<<< HEAD
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const x = +this.getAttribute('cx');
const y = +this.getAttribute('cy');
=======
.select("#controlPoints")
.selectAll("circle")
.each(function () {
const x = +this.getAttribute("cx");
const y = +this.getAttribute("cy");
>>>>>>> master
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
});
let index = dists.length;
if (dists.length > 1) {
const sorted = dists.slice(0).sort((a, b) => a - b);
const closest = dists.indexOf(sorted[0]);
const next = dists.indexOf(sorted[1]);
if (closest <= next) index = closest + 1;
else index = next + 1;
}
<<<<<<< HEAD
const before = ':nth-child(' + (index + 2) + ')';
debug
.select('#controlPoints')
.insert('circle', before)
.attr('cx', point[0])
.attr('cy', point[1])
.attr('r', 2.5)
.attr('stroke-width', 0.8)
.call(d3.drag().on('drag', dragControlPoint))
.on('click', clickControlPoint);
=======
const before = ":nth-child(" + (index + 2) + ")";
debug.select("#controlPoints").insert("circle", before).attr("cx", point[0]).attr("cy", point[1]).attr("r", 2.5).attr("stroke-width", 0.8).call(d3.drag().on("drag", dragControlPoint)).on("click", clickControlPoint);
>>>>>>> master
redrawLabelPath();
}
function dragLabel() {
<<<<<<< HEAD
const tr = parseTransform(elSelected.attr('transform'));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on('drag', function () {
const x = d3.event.x,
y = d3.event.y;
const transform = `translate(${dx + x},${dy + y})`;
elSelected.attr('transform', transform);
debug.select('#controlPoints').attr('transform', transform);
=======
const tr = parseTransform(elSelected.attr("transform"));
const dx = +tr[0] - d3.event.x,
dy = +tr[1] - d3.event.y;
d3.event.on("drag", function () {
const x = d3.event.x,
y = d3.event.y;
const transform = `translate(${dx + x},${dy + y})`;
elSelected.attr("transform", transform);
debug.select("#controlPoints").attr("transform", transform);
>>>>>>> master
});
}
function showGroupSection() {
<<<<<<< HEAD
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelGroupSection').style.display = 'inline-block';
}
function hideGroupSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelGroupSection').style.display = 'none';
document.getElementById('labelGroupInput').style.display = 'none';
document.getElementById('labelGroupInput').value = '';
document.getElementById('labelGroupSelect').style.display = 'inline-block';
=======
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelGroupSection").style.display = "inline-block";
}
function hideGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelGroupSection").style.display = "none";
document.getElementById("labelGroupInput").style.display = "none";
document.getElementById("labelGroupInput").value = "";
document.getElementById("labelGroupSelect").style.display = "inline-block";
>>>>>>> master
}
function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
if (labelGroupInput.style.display === 'none') {
labelGroupInput.style.display = 'inline-block';
labelGroupInput.focus();
labelGroupSelect.style.display = 'none';
} else {
<<<<<<< HEAD
labelGroupInput.style.display = 'none';
labelGroupSelect.style.display = 'inline-block';
=======
labelGroupInput.style.display = "none";
labelGroupSelect.style.display = "inline-block";
>>>>>>> master
}
}
function createNewGroup() {
if (!this.value) {
<<<<<<< HEAD
tip('Please provide a valid group name');
=======
tip("Please provide a valid group name");
>>>>>>> master
return;
}
const group = this.value
.toLowerCase()
<<<<<<< HEAD
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
=======
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
>>>>>>> master
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;
if (oldGroup !== 'states' && oldGroup !== 'addedLabels' && oldGroup.childElementCount === 1) {
document.getElementById('labelGroupSelect').selectedOptions[0].remove();
document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById('labelGroupInput').value = '';
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById('labels').appendChild(newGroup);
newGroup.id = group;
document.getElementById('labelGroupSelect').options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById('labelGroupInput').value = '';
}
function removeLabelsGroup() {
const group = elSelected.node().parentNode.id;
const basic = group === 'states' || group === 'addedLabels';
const count = elSelected.node().parentNode.childElementCount;
<<<<<<< HEAD
const message = `Are you sure you want to remove ${basic ? 'all elements in the group' : 'the entire label group'}?<br><br>Labels to be removed: ${count}`;
const onConfirm = () => {
$('#labelEditor').dialog('close');
hideGroupSection();
labels
.select('#' + group)
.selectAll('text')
.each(function () {
document.getElementById('textPath_' + this.id).remove();
this.remove();
});
if (!basic) labels.select('#' + group).remove();
};
confirmationDialog({title: 'Remove label group', message, confirm: 'Remove', onConfirm});
}
function showTextSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelTextSection').style.display = 'inline-block';
}
function hideTextSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelTextSection').style.display = 'none';
}
function changeText() {
const input = document.getElementById('labelText').value;
const el = elSelected.select('textPath').node();
const example = d3.select(elSelected.node().parentNode).append('text').attr('x', 0).attr('x', 0).attr('font-size', el.getAttribute('font-size')).node();
=======
alertMessage.innerHTML = `Are you sure you want to remove
${basic ? "all elements in the group" : "the entire label group"}?
<br><br>Labels to be removed: ${count}`;
$("#alert").dialog({
resizable: false,
title: "Remove route group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#labelEditor").dialog("close");
hideGroupSection();
labels
.select("#" + group)
.selectAll("text")
.each(function () {
document.getElementById("textPath_" + this.id).remove();
this.remove();
});
if (!basic) labels.select("#" + group).remove();
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none";
}
function changeText() {
const input = document.getElementById("labelText").value;
const el = elSelected.select("textPath").node();
const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node();
>>>>>>> master
const lines = input.split('|');
const top = (lines.length - 1) / -2; // y offset
const inner = lines
.map((l, d) => {
example.innerHTML = l;
const left = example.getBBox().width / -2; // x offset
return `<tspan x="${left}px" dy="${d ? 1 : top}em">${l}</tspan>`;
})
<<<<<<< HEAD
.join('');
=======
.join("");
>>>>>>> master
el.innerHTML = inner;
example.remove();
<<<<<<< HEAD
if (elSelected.attr('id').slice(0, 10) === 'stateLabel') tip('Use States Editor to change an actual state name, not just a label', false, 'warning');
}
function generateRandomName() {
let name = '';
if (elSelected.attr('id').slice(0, 10) === 'stateLabel') {
const id = +elSelected.attr('id').slice(10);
=======
if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning");
}
function generateRandomName() {
let name = "";
if (elSelected.attr("id").slice(0, 10) === "stateLabel") {
const id = +elSelected.attr("id").slice(10);
>>>>>>> master
const culture = pack.states[id].culture;
name = Names.getState(Names.getCulture(culture, 4, 7, ''), culture);
} else {
const box = elSelected.node().getBBox();
const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2);
const culture = pack.cells.culture[cell];
name = Names.getCulture(culture);
}
document.getElementById('labelText').value = name;
changeText();
}
function editGroupStyle() {
const g = elSelected.node().parentNode.id;
editStyle('labels', g);
}
function showSizeSection() {
<<<<<<< HEAD
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('labelSizeSection').style.display = 'inline-block';
}
function hideSizeSection() {
document.querySelectorAll('#labelEditor > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('labelSizeSection').style.display = 'none';
=======
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelSizeSection").style.display = "none";
>>>>>>> master
}
function changeStartOffset() {
elSelected.select('textPath').attr('startOffset', this.value + '%');
tip('Label offset: ' + this.value + '%');
}
function changeRelativeSize() {
elSelected.select('textPath').attr('font-size', this.value + '%');
tip('Label relative size: ' + this.value + '%');
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
<<<<<<< HEAD
const path = defs.select('#textPath_' + elSelected.attr('id'));
path.attr('d', `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
=======
const path = defs.select("#textPath_" + elSelected.attr("id"));
path.attr("d", `M${c[0] - bbox.width},${c[1]}h${bbox.width * 2}`);
>>>>>>> master
drawControlPointsAndLine();
}
function editLabelLegend() {
const id = elSelected.attr('id');
const name = elSelected.text();
editNotes(id, name);
}
function removeLabel() {
<<<<<<< HEAD
const message = 'Are you sure you want to remove the label? <br>This action cannot be reverted';
const onConfirm = () => {
defs.select('#textPath_' + elSelected.attr('id')).remove();
elSelected.remove();
$('#labelEditor').dialog('close');
};
confirmationDialog({title: 'Remove label', message, confirm: 'Remove', onConfirm});
=======
alertMessage.innerHTML = "Are you sure you want to remove the label?";
$("#alert").dialog({
resizable: false,
title: "Remove label",
buttons: {
Remove: function () {
$(this).dialog("close");
defs.select("#textPath_" + elSelected.attr("id")).remove();
elSelected.remove();
$("#labelEditor").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
>>>>>>> master
}
function closeLabelEditor() {
debug.select('#controlPoints').remove();
unselect();
}
}

View file

@ -48,8 +48,7 @@ function changePreset(preset) {
.querySelectorAll('li')
.forEach(function (e) {
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click();
// turn on
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click();
});
layersPreset.value = preset;
localStorage.setItem('preset', preset);
@ -122,6 +121,7 @@ function restoreLayers() {
if (layerIsOn('toggleReligions')) drawReligions();
if (layerIsOn('toggleIce')) drawIce();
if (layerIsOn('toggleEmblems')) drawEmblems();
if (layerIsOn('toggleMarkers')) drawMarkers();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn('toggleBorders')) borders.selectAll('path').remove();
@ -1419,8 +1419,8 @@ function toggleTexture(event) {
turnButtonOn('toggleTexture');
// append default texture image selected by default. Don't append on load to not harm performance
if (!texture.selectAll('*').size()) {
const x = +styleTextureShiftX.value,
y = +styleTextureShiftY.value;
const x = +styleTextureShiftX.value;
const y = +styleTextureShiftY.value;
const image = texture
.append('image')
.attr('id', 'textureImage')
@ -1430,16 +1430,13 @@ function toggleTexture(event) {
.attr('height', graphHeight - y)
.attr('xlink:href', getDefaultTexture())
.attr('preserveAspectRatio', 'xMidYMid slice');
if (styleTextureInput.value !== 'default') getBase64(styleTextureInput.value, (base64) => image.attr('xlink:href', base64));
getBase64(styleTextureInput.value, (base64) => image.attr('xlink:href', base64));
}
$('#texture').fadeIn();
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
if (event && isCtrlClick(event)) editStyle('texture');
} else {
if (event && isCtrlClick(event)) {
editStyle('texture');
return;
}
if (event && isCtrlClick(event)) return editStyle('texture');
$('#texture').fadeOut();
turnButtonOff('toggleTexture');
}
@ -1459,14 +1456,16 @@ function toggleRivers(event) {
function drawRivers() {
TIME && console.time('drawRivers');
rivers.selectAll('*').remove();
const {addMeandering, getRiverPath} = Rivers;
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const riverPaths = pack.rivers.map((river) => {
const meanderedPoints = addMeandering(river.cells, river.points);
const widthFactor = river.widthFactor || 1;
const startingWidth = river.sourceWidth || 0;
const path = getRiverPath(meanderedPoints, widthFactor, startingWidth);
return `<path id="river${river.i}" d="${path}"/>`;
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
if (!cells || cells.length < 2) return;
const meanderedPoints = addMeandering(cells, points);
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
return `<path id="river${i}" d="${path}"/>`;
});
rivers.html(riverPaths.join(''));
TIME && console.timeEnd('drawRivers');
@ -1505,18 +1504,51 @@ function toggleMilitary() {
function toggleMarkers(event) {
if (!layerIsOn('toggleMarkers')) {
turnButtonOn('toggleMarkers');
$('#markers').fadeIn();
drawMarkers();
if (event && isCtrlClick(event)) editStyle('markers');
} else {
if (event && isCtrlClick(event)) {
editStyle('markers');
return;
}
$('#markers').fadeOut();
if (event && isCtrlClick(event)) return editStyle('markers');
markers.selectAll('*').remove();
turnButtonOff('toggleMarkers');
}
}
function drawMarkers() {
const rescale = +markers.attr('rescale');
const pinned = +markers.attr('pinned');
const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
const html = markersData.map((marker) => drawMarker(marker, rescale));
markers.html(html.join(''));
}
const getPin = (shape = 'bubble', fill = '#fff', stroke = '#000') => {
if (shape === 'bubble') return `<path d="M6,19 l9,10 L24,19" fill="${stroke}" stroke="none" /><circle cx="15" cy="15" r="10" fill="${fill}" stroke="${stroke}"/>`;
if (shape === 'pin') return `<path d="m 15,3 c -5.5,0 -9.7,4.09 -9.7,9.3 0,6.8 9.7,17 9.7,17 0,0 9.7,-10.2 9.7,-17 C 24.7,7.09 20.5,3 15,3 Z" fill="${fill}" stroke="${stroke}"/>`;
if (shape === 'square') return `<path d="m 20,25 -5,4 -5,-4 z" fill="${stroke}"/><path d="M 5,5 H 25 V 25 H 5 Z" fill="${fill}" stroke="${stroke}"/>`;
if (shape === 'squarish') return `<path d="m 5,5 h 20 v 20 h -6 l -4,4 -4,-4 H 5 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'diamond') return `<path d="M 2,15 15,1 28,15 15,29 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'hex') return `<path d="M 15,29 4.61,21 V 9 L 15,3 25.4,9 v 12 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'hexy') return `<path d="M 15,29 6,21 5,8 15,4 25,8 24,21 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'shieldy') return `<path d="M 15,29 6,21 5,7 c 0,0 5,-3 10,-3 5,0 10,3 10,3 l -1,14 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'shield') return `<path d="M 4.6,5.2 H 25 v 6.7 A 20.3,20.4 0 0 1 15,29 20.3,20.4 0 0 1 4.6,11.9 Z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'pentagon') return `<path d="M 4,16 9,4 h 12 l 5,12 -11,13 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'heptagon') return `<path d="M 15,29 6,22 4,12 10,4 h 10 l 6,8 -2,10 z" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'circle') return `<circle cx="15" cy="15" r="11" fill="${fill}" stroke="${stroke}" />`;
if (shape === 'no') return '';
};
function drawMarker(marker, rescale = 1) {
const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
const id = `marker${i}`;
const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
const viewX = rn(x - zoomSize / 2, 1);
const viewY = rn(y - zoomSize, 1);
const pinHTML = getPin(pin, fill, stroke);
return `<svg id="${id}" viewbox="0 0 30 30" width="${zoomSize}" height="${zoomSize}" x="${viewX}" y="${viewY}"><g>${pinHTML}</g><text x="${dx}%" y="${dy}%" font-size="${px}px" >${icon}</text></svg>`;
}
function toggleLabels(event) {
if (!layerIsOn('toggleLabels')) {
turnButtonOn('toggleLabels');
@ -1620,21 +1652,21 @@ function drawEmblems() {
const validBurgs = burgs.filter((b) => b.i && !b.removed && b.coa && b.coaSize != 0);
const getStateEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 40, 10), 100);
const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
const sizeMod = +document.getElementById('emblemsStateSizeInput').value || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 100, 5), 70);
const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('emblemsProvinceSizeInput').value || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
const startSize = Math.min(Math.max((graphHeight + graphWidth) / 185, 2), 50);
const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
const sizeMod = +document.getElementById('emblemsBurgSizeInput').value || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
@ -1685,11 +1717,9 @@ function drawEmblems() {
const burgNodes = nodes.filter((node) => node.type === 'burg');
const burgString = burgNodes.map((d) => `<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${d.size}em"/>`).join('');
emblems.select('#burgEmblems').attr('font-size', sizeBurgs).html(burgString);
const provinceNodes = nodes.filter((node) => node.type === 'province');
const provinceString = provinceNodes.map((d) => `<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${d.size}em"/>`).join('');
emblems.select('#provinceEmblems').attr('font-size', sizeProvinces).html(provinceString);
const stateNodes = nodes.filter((node) => node.type === 'state');
const stateString = stateNodes.map((d) => `<use data-i="${d.i}" x="${rn(d.x - d.shift)}" y="${rn(d.y - d.shift)}" width="${d.size}em" height="${d.size}em"/>`).join('');

1970
modules/ui/layers.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,291 +1,261 @@
'use strict';
function editMarker() {
function editMarker(markerI) {
if (customization) return;
closeDialogs('#markerEditor, .stable');
$('#markerEditor').dialog();
closeDialogs(".stable");
const [element, marker] = getElement(markerI, d3.event);
if (!marker || !element) return;
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
elSelected = d3.select(d3.event.target).call(d3.drag().on('start', dragMarker)).classed('draggable', true);
updateInputs();
if (modules.editMarker) return;
modules.editMarker = true;
$('#markerEditor').dialog({
title: 'Edit Marker',
title: "Edit Marker",
resizable: false,
position: {my: 'center top+30', at: 'bottom', of: d3.event, collision: 'fit'},
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeMarkerEditor
});
// add listeners
document.getElementById('markerGroup').addEventListener('click', toggleGroupSection);
document.getElementById('markerAddGroup').addEventListener('click', toggleGroupInput);
document.getElementById('markerSelectGroup').addEventListener('change', changeGroup);
document.getElementById('markerInputGroup').addEventListener('change', createGroup);
document.getElementById('markerRemoveGroup').addEventListener('click', removeGroup);
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
listen(markerSize, "input", changeMarkerSize),
listen(markerPin, "change", changeMarkerPin),
listen(markerFill, "input", changePinFill),
listen(markerStroke, "input", changePinStroke),
listen(markerNotes, "click", editMarkerLegend),
listen(markerLock, "click", toggleMarkerLock),
listen(markerAdd, "click", toggleAddMarker),
listen(markerRemove, "click", confirmMarkerDeletion)
];
document.getElementById('markerIcon').addEventListener('click', toggleIconSection);
document.getElementById('markerIconSize').addEventListener('input', changeIconSize);
document.getElementById('markerIconShiftX').addEventListener('input', changeIconShiftX);
document.getElementById('markerIconShiftY').addEventListener('input', changeIconShiftY);
document.getElementById('markerIconSelect').addEventListener('click', selectMarkerIcon);
function getElement(markerI, event) {
if (event) {
const element = event.target?.closest("svg");
const marker = pack.markers.find(({i}) => Number(element.id.slice(6)) === i);
return [element, marker];
}
document.getElementById('markerStyle').addEventListener('click', toggleStyleSection);
document.getElementById('markerSize').addEventListener('input', changeMarkerSize);
document.getElementById('markerBaseStroke').addEventListener('input', changePinStroke);
document.getElementById('markerBaseFill').addEventListener('input', changePinFill);
document.getElementById('markerIconStrokeWidth').addEventListener('input', changeIconStrokeWidth);
document.getElementById('markerIconStroke').addEventListener('input', changeIconStroke);
document.getElementById('markerIconFill').addEventListener('input', changeIconFill);
const element = document.getElementById(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
document.getElementById('markerToggleBubble').addEventListener('click', togglePinVisibility);
document.getElementById('markerLegendButton').addEventListener('click', editMarkerLegend);
document.getElementById('markerAdd').addEventListener('click', toggleAddMarker);
document.getElementById('markerRemove').addEventListener('click', removeMarker);
updateGroupOptions();
function getSameTypeMarkers() {
const currentType = marker.type;
if (!currentType) return [marker];
return pack.markers.filter(({type}) => type === currentType);
}
function dragMarker() {
const tr = parseTransform(this.getAttribute('transform'));
const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
const dx = +this.getAttribute("x") - d3.event.x;
const dy = +this.getAttribute("y") - d3.event.y;
d3.event.on('drag', function () {
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
this.setAttribute('transform', transform);
const {x, y} = d3.event;
this.setAttribute("x", dx + x);
this.setAttribute("y", dy + y);
});
d3.event.on("end", function () {
const {x, y} = d3.event;
this.setAttribute("x", rn(dx + x, 2));
this.setAttribute("y", rn(dy + y, 2));
const size = marker.size || 30;
const zoomSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
marker.x = rn(x + dx + zoomSize / 2, 1);
marker.y = rn(y + dy + zoomSize, 1);
marker.cell = findCell(marker.x, marker.y);
.selectAll('symbol')
.each(function () {
});
}
function updateInputs() {
const id = elSelected.attr('data-id');
const symbol = d3.select('#defs-markers').select(id);
const icon = symbol.select('text');
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
markerSelectGroup.value = id.slice(1);
markerIconSize.value = parseFloat(icon.attr('font-size'));
markerIconShiftX.value = parseFloat(icon.attr('x'));
markerIconShiftY.value = parseFloat(icon.attr('y'));
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerSize.value = elSelected.attr('data-size');
markerBaseStroke.value = symbol.select('path').attr('fill');
markerBaseFill.value = symbol.select('circle').attr('fill');
markerIconStrokeWidth.value = icon.attr('stroke-width');
markerIconStroke.value = icon.attr('stroke');
markerIconFill.value = icon.attr('fill');
markerToggleBubble.className = symbol.select('circle').attr('opacity') === '0' ? 'icon-info' : 'icon-info-circled';
markerIconSelect.innerHTML = icon.text();
}
function toggleGroupSection() {
if (markerGroupSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerGroup)').forEach((b) => (b.style.display = 'inline-block'));
markerGroupSection.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerGroup)').forEach((b) => (b.style.display = 'none'));
markerGroupSection.style.display = 'inline-block';
}
}
function updateGroupOptions() {
markerSelectGroup.innerHTML = '';
d3.select('#defs-markers')
.selectAll('symbol')
.each(function () {
markerSelectGroup.options.add(new Option(this.id, this.id));
});
markerSelectGroup.value = elSelected.attr('data-id').slice(1);
}
function toggleGroupInput() {
if (markerInputGroup.style.display === 'inline-block') {
markerSelectGroup.style.display = 'inline-block';
markerInputGroup.style.display = 'none';
} else {
markerSelectGroup.style.display = 'none';
markerInputGroup.style.display = 'inline-block';
markerInputGroup.focus();
}
}
function changeGroup() {
elSelected.attr('xlink:href', '#' + this.value);
elSelected.attr('data-id', '#' + this.value);
}
function createGroup() {
let newGroup = this.value
.toLowerCase()
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (Number.isFinite(+newGroup.charAt(0))) newGroup = 'm' + newGroup;
if (document.getElementById(newGroup)) {
tip('Element with this id already exists. Please provide a unique name', false, 'error');
return;
}
markerInputGroup.value = '';
// clone old group assigning new id
const id = elSelected.attr('data-id');
const clone = d3.select('#defs-markers').select(id).node().cloneNode(true);
clone.id = newGroup;
document.getElementById('defs-markers').insertBefore(clone, null);
elSelected.attr('xlink:href', '#' + newGroup).attr('data-id', '#' + newGroup);
// select new group
markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
toggleGroupInput();
}
function removeGroup() {
const id = elSelected.attr('data-id');
const used = document.querySelectorAll("use[data-id='" + id + "']");
const count = used.length === 1 ? '1 element' : used.length + ' elements';
const message = `Are you sure you want to remove all markers of that type (${count})? <br>This action cannot be reverted`;
const onConfirm = () => {
if (id !== '#marker0') d3.select('#defs-markers').select(id).remove();
used.forEach((e) => {
const index = notes.findIndex((n) => n.id === e.id);
if (index != -1) notes.splice(index, 1);
e.remove();
});
updateGroupOptions();
updateGroupOptions();
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker type', message, confirm: 'Remove', onConfirm});
function changeMarkerType() {
marker.type = this.value;
}
function toggleIconSection() {
if (markerIconSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'inline-block'));
markerIconSection.style.display = 'none';
markerIconSelect.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerIcon)').forEach((b) => (b.style.display = 'none'));
markerIconSection.style.display = 'inline-block';
markerIconSelect.style.display = 'inline-block';
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
}
function selectMarkerIcon() {
selectIcon(this.innerHTML, (v) => {
this.innerHTML = v;
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').text(v);
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
});
}
function changeIconSize() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('font-size', this.value + 'px');
const px = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.px = px;
redrawIcon(marker);
});
}
function changeIconShiftX() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('x', this.value + '%');
const dx = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dx = dx;
redrawIcon(marker);
});
}
function changeIconShiftY() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers')
.select(id)
.select('text')
.attr('y', this.value + '%');
}
function toggleStyleSection() {
if (markerStyleSection.style.display === 'inline-block') {
markerEditor.querySelectorAll('button:not(#markerStyle)').forEach((b) => (b.style.display = 'inline-block'));
markerStyleSection.style.display = 'none';
} else {
markerEditor.querySelectorAll('button:not(#markerStyle)').forEach((b) => (b.style.display = 'none'));
markerStyleSection.style.display = 'inline-block';
}
const dy = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dy = dy;
redrawIcon(marker);
});
}
function changeMarkerSize() {
const id = elSelected.attr('data-id');
document.querySelectorAll("use[data-id='" + id + "']").forEach((e) => {
const x = +e.dataset.x,
y = +e.dataset.y;
const size = +this.value;
const rescale = +markers.attr("rescale");
const desired = (e.dataset.size = +markerSize.value);
const size = Math.max(desired * 5 + 25 / scale, 1);
e.setAttribute('x', x - size / 2);
e.setAttribute('y', y - size / 2);
e.setAttribute('width', size);
e.setAttribute('height', size);
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
invokeActiveZooming();
}
function changePinStroke() {
const id = elSelected.attr('data-id');
d3.select(id).select('path').attr('fill', this.value);
d3.select(id).select('circle').attr('stroke', this.value);
function changeMarkerPin() {
const pin = this.value;
getSameTypeMarkers().forEach(marker => {
marker.pin = pin;
redrawPin(marker);
});
}
function changePinFill() {
const id = elSelected.attr('data-id');
d3.select(id).select('circle').attr('fill', this.value);
const fill = this.value;
getSameTypeMarkers().forEach(marker => {
marker.fill = fill;
redrawPin(marker);
});
}
function changeIconStrokeWidth() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('stroke-width', this.value);
function changePinStroke() {
const stroke = this.value;
getSameTypeMarkers().forEach(marker => {
marker.stroke = stroke;
redrawPin(marker);
});
}
function changeIconStroke() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('stroke', this.value);
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
}
}
function changeIconFill() {
const id = elSelected.attr('data-id');
d3.select('#defs-markers').select(id).select('text').attr('fill', this.value);
}
function togglePinVisibility() {
const id = elSelected.attr('data-id');
let show = 1;
if (this.className === 'icon-info-circled') {
this.className = 'icon-info';
show = 0;
} else this.className = 'icon-info-circled';
function redrawPin({i, hidden, pin = "bubble", fill = "#fff", stroke = "#000"}) {
const pinGroup = !hidden && document.querySelector(`#marker${i} > g`);
if (pinGroup) pinGroup.innerHTML = getPin(pin, fill, stroke);
d3.select(id).select('circle').attr('opacity', show);
d3.select(id).select('path').attr('opacity', show);
}
function editMarkerLegend() {
const id = elSelected.attr('id');
const id = element.id;
editNotes(id, id);
}
function toggleAddMarker() {
document.getElementById('addMarker').click();
function toggleMarkerLock() {
marker.lock = !marker.lock;
markerLock.classList.toggle("icon-lock-open");
markerLock.classList.toggle("icon-lock");
}
function removeMarker() {
const message = 'Are you sure you want to remove the marker? <br>This action cannot be reverted';
const onConfirm = () => {
const index = notes.findIndex((n) => n.id === elSelected.attr('id'));
if (index != -1) notes.splice(index, 1);
elSelected.remove();
$('#markerEditor').dialog('close');
};
confirmationDialog({title: 'Remove marker', message, confirm: 'Remove', onConfirm});
function toggleAddMarker() {
markerAdd.classList.toggle("pressed");
addMarker.click();
}
function confirmMarkerDeletion() {
confirmationDialog({
title: "Remove marker",
message: "Are you sure you want to remove this marker? The action cannot be reverted",
confirm: "Remove",
onConfirm: deleteMarker
function deleteMarker() {
notes = notes.filter(note => note.id !== element.id);
pack.markers = pack.markers.filter(m => m.i !== marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {
listeners.forEach(removeListener => removeListener());
unselect();
if (addMarker.classList.contains('pressed')) addMarker.classList.remove('pressed');
if (markerAdd.classList.contains('pressed')) markerAdd.classList.remove('pressed');

View file

@ -0,0 +1,265 @@
'use strict';
function editMarker(markerI) {
if (customization) return;
closeDialogs(".stable");
const [element, marker] = getElement(markerI, d3.event);
if (!marker || !element) return;
elSelected = d3.select(element).raise().call(d3.drag().on("start", dragMarker)).classed("draggable", true);
if (document.getElementById("notesEditor").offsetParent) editNotes(element.id, element.id);
// dom elements
const markerType = document.getElementById("markerType");
const markerIcon = document.getElementById("markerIcon");
const markerIconSelect = document.getElementById("markerIconSelect");
const markerIconSize = document.getElementById("markerIconSize");
const markerIconShiftX = document.getElementById("markerIconShiftX");
const markerIconShiftY = document.getElementById("markerIconShiftY");
const markerSize = document.getElementById("markerSize");
const markerPin = document.getElementById("markerPin");
const markerFill = document.getElementById("markerFill");
const markerStroke = document.getElementById("markerStroke");
const markerNotes = document.getElementById("markerNotes");
const markerLock = document.getElementById("markerLock");
const addMarker = document.getElementById("addMarker");
const markerAdd = document.getElementById("markerAdd");
const markerRemove = document.getElementById("markerRemove");
updateInputs();
$('#markerEditor').dialog({
title: "Edit Marker",
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "svg", collision: "fit"},
close: closeMarkerEditor
});
const listeners = [
listen(markerType, "change", changeMarkerType),
listen(markerIcon, "input", changeMarkerIcon),
listen(markerIconSelect, "click", selectMarkerIcon),
listen(markerIconSize, "input", changeIconSize),
listen(markerIconShiftX, "input", changeIconShiftX),
listen(markerIconShiftY, "input", changeIconShiftY),
listen(markerSize, "input", changeMarkerSize),
listen(markerPin, "change", changeMarkerPin),
listen(markerFill, "input", changePinFill),
listen(markerStroke, "input", changePinStroke),
listen(markerNotes, "click", editMarkerLegend),
listen(markerLock, "click", toggleMarkerLock),
listen(markerAdd, "click", toggleAddMarker),
listen(markerRemove, "click", confirmMarkerDeletion)
];
function getElement(markerI, event) {
if (event) {
const element = event.target?.closest("svg");
const marker = pack.markers.find(({i}) => Number(element.id.slice(6)) === i);
return [element, marker];
}
const element = document.getElementById(`marker${markerI}`);
const marker = pack.markers.find(({i}) => i === markerI);
return [element, marker];
}
function getSameTypeMarkers() {
const currentType = marker.type;
if (!currentType) return [marker];
return pack.markers.filter(({type}) => type === currentType);
}
function dragMarker() {
const dx = +this.getAttribute("x") - d3.event.x;
const dy = +this.getAttribute("y") - d3.event.y;
d3.event.on('drag', function () {
const {x, y} = d3.event;
this.setAttribute("x", dx + x);
this.setAttribute("y", dy + y);
});
d3.event.on("end", function () {
const {x, y} = d3.event;
this.setAttribute("x", rn(dx + x, 2));
this.setAttribute("y", rn(dy + y, 2));
const size = marker.size || 30;
const zoomSize = Math.max(rn(size / 5 + 24 / scale, 2), 1);
marker.x = rn(x + dx + zoomSize / 2, 1);
marker.y = rn(y + dy + zoomSize, 1);
marker.cell = findCell(marker.x, marker.y);
.selectAll('symbol')
.each(function () {
});
}
function updateInputs() {
const {icon, type = "", size = 30, dx = 50, dy = 50, px = 12, stroke = "#000000", fill = "#ffffff", pin = "bubble", lock} = marker;
markerType.value = type;
markerIcon.value = icon;
markerIconSize.value = px;
markerIconShiftX.value = dx;
markerIconShiftY.value = dy;
markerSize.value = size;
markerPin.value = pin;
markerFill.value = fill;
markerStroke.value = stroke;
markerLock.className = lock ? "icon-lock" : "icon-lock-open";
.replace(/ /g, '_')
.replace(/[^\w\s]/gi, '');
if (Number.isFinite(+newGroup.charAt(0))) newGroup = 'm' + newGroup;
}
function changeMarkerType() {
marker.type = this.value;
}
function changeMarkerIcon() {
const icon = this.value;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
}
function selectMarkerIcon() {
selectIcon(marker.icon, icon => {
markerIcon.value = icon;
getSameTypeMarkers().forEach(marker => {
marker.icon = icon;
redrawIcon(marker);
});
});
}
function changeIconSize() {
const px = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.px = px;
redrawIcon(marker);
});
}
function changeIconShiftX() {
const dx = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dx = dx;
redrawIcon(marker);
});
}
function changeIconShiftY() {
const dy = +this.value;
getSameTypeMarkers().forEach(marker => {
marker.dy = dy;
redrawIcon(marker);
});
}
function changeMarkerSize() {
const size = +this.value;
const rescale = +markers.attr("rescale");
const desired = (e.dataset.size = +markerSize.value);
getSameTypeMarkers().forEach(marker => {
marker.size = size;
const {i, x, y, hidden} = marker;
const el = !hidden && document.getElementById(`marker${i}`);
if (!el) return;
const zoomedSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
el.setAttribute("width", zoomedSize);
el.setAttribute("height", zoomedSize);
el.setAttribute("x", rn(x - zoomedSize / 2, 1));
el.setAttribute("y", rn(y - zoomedSize, 1));
});
}
function changeMarkerPin() {
const pin = this.value;
getSameTypeMarkers().forEach(marker => {
marker.pin = pin;
redrawPin(marker);
});
}
function changePinFill() {
const fill = this.value;
getSameTypeMarkers().forEach(marker => {
marker.fill = fill;
redrawPin(marker);
});
}
function changePinStroke() {
const stroke = this.value;
getSameTypeMarkers().forEach(marker => {
marker.stroke = stroke;
redrawPin(marker);
});
}
function redrawIcon({i, hidden, icon, dx = 50, dy = 50, px = 12}) {
const iconElement = !hidden && document.querySelector(`#marker${i} > text`);
if (iconElement) {
iconElement.innerHTML = icon;
iconElement.setAttribute("x", dx + "%");
iconElement.setAttribute("y", dy + "%");
iconElement.setAttribute("font-size", px + "px");
}
}
function redrawPin({i, hidden, pin = "bubble", fill = "#fff", stroke = "#000"}) {
const pinGroup = !hidden && document.querySelector(`#marker${i} > g`);
if (pinGroup) pinGroup.innerHTML = getPin(pin, fill, stroke);
d3.select(id).select('circle').attr('opacity', show);
d3.select(id).select('path').attr('opacity', show);
}
function editMarkerLegend() {
const id = element.id;
editNotes(id, id);
}
function toggleMarkerLock() {
marker.lock = !marker.lock;
markerLock.classList.toggle("icon-lock-open");
markerLock.classList.toggle("icon-lock");
}
function toggleAddMarker() {
markerAdd.classList.toggle("pressed");
addMarker.click();
}
function confirmMarkerDeletion() {
confirmationDialog({
title: "Remove marker",
message: "Are you sure you want to remove this marker? The action cannot be reverted",
confirm: "Remove",
onConfirm: deleteMarker
function deleteMarker() {
notes = notes.filter(note => note.id !== element.id);
pack.markers = pack.markers.filter(m => m.i !== marker.i);
element.remove();
$("#markerEditor").dialog("close");
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function closeMarkerEditor() {
listeners.forEach(removeListener => removeListener());
unselect();
addMarker.classList.remove("pressed");
markerAdd.classList.remove("pressed");
restoreDefaultEvents();
clearMainTip();
}
}

View file

@ -0,0 +1,196 @@
'use strict';
function overviewMarkers() {
if (customization) return;
closeDialogs('#markersOverview, .stable');
if (!layerIsOn('toggleMarkers')) toggleMarkers();
const markerGroup = document.getElementById('markers');
const body = document.getElementById('markersBody');
const markersInverPin = document.getElementById('markersInverPin');
const markersInverLock = document.getElementById('markersInverLock');
const markersFooterNumber = document.getElementById('markersFooterNumber');
const markersOverviewRefresh = document.getElementById('markersOverviewRefresh');
const markersAddFromOverview = document.getElementById('markersAddFromOverview');
const markersGenerationConfig = document.getElementById('markersGenerationConfig');
const markersRemoveAll = document.getElementById('markersRemoveAll');
const markersExport = document.getElementById('markersExport');
addLines();
$('#markersOverview').dialog({
title: 'Markers Overview',
resizable: false,
width: fitContent(),
close: close,
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
const listeners = [
listen(body, 'click', handleLineClick),
listen(markersInverPin, 'click', invertPin),
listen(markersInverLock, 'click', invertLock),
listen(markersOverviewRefresh, 'click', addLines),
listen(markersAddFromOverview, 'click', toggleAddMarker),
listen(markersGenerationConfig, 'click', configMarkersGeneration),
listen(markersRemoveAll, 'click', triggerRemoveAll),
listen(markersExport, 'click', exportMarkers)
];
function handleLineClick(ev) {
const el = ev.target;
const i = +el.parentNode.dataset.i;
if (el.classList.contains('icon-pencil')) return openEditor(i);
if (el.classList.contains('icon-dot-circled')) return focusOnMarker(i);
if (el.classList.contains('icon-pin')) return pinMarker(el, i);
if (el.classList.contains('locks')) return toggleLockStatus(el, i);
if (el.classList.contains('icon-trash-empty')) return triggerRemove(i);
}
function addLines() {
const lines = pack.markers
.map(({i, type, icon, pinned, lock}) => {
return `<div class="states" data-i=${i} data-type="${type}">
<div data-tip="Marker icon and type" style="width:12em">${icon} ${type}</div>
<span style="padding-right:.1em" data-tip="Edit marker" class="icon-pencil"></span>
<span style="padding-right:.1em" data-tip="Focus on marker position" class="icon-dot-circled pointer"></span>
<span style="padding-right:.1em" data-tip="Pin marker (display only pinned markers)" class="icon-pin ${pinned ? '' : 'inactive'}" pointer"></span>
<span style="padding-right:.1em" class="locks pointer ${lock ? 'icon-lock' : 'icon-lock-open inactive'}" onmouseover="showElementLockTip(event)"></span>
<span data-tip="Remove marker" class="icon-trash-empty"></span>
</div>`;
})
.join('');
body.innerHTML = lines;
markersFooterNumber.innerText = pack.markers.length;
applySorting(markersHeader);
}
function invertPin() {
let anyPinned = false;
pack.markers.forEach((marker) => {
const pinned = !marker.pinned;
if (pinned) {
marker.pinned = true;
anyPinned = true;
} else delete marker.pinned;
});
markerGroup.setAttribute('pinned', anyPinned ? 1 : null);
drawMarkers();
addLines();
}
function invertLock() {
pack.markers = pack.markers.map((marker) => ({...marker, lock: !marker.lock}));
addLines();
}
function openEditor(i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (!marker) return;
const {x, y} = marker;
zoomTo(x, y, 8, 2000);
editMarker(i);
}
function focusOnMarker(i) {
highlightElement(document.getElementById(`marker${i}`), 2);
}
function pinMarker(el, i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (marker.pinned) {
delete marker.pinned;
const anyPinned = pack.markers.some((marker) => marker.pinned);
if (!anyPinned) markerGroup.removeAttribute('pinned');
} else {
marker.pinned = true;
markerGroup.setAttribute('pinned', 1);
}
el.classList.toggle('inactive');
drawMarkers();
}
function toggleLockStatus(el, i) {
const marker = pack.markers.find((marker) => marker.i === i);
if (marker.lock) {
delete marker.lock;
el.className = 'locks pointer icon-lock-open inactive';
} else {
marker.lock = true;
el.className = 'locks pointer icon-lock';
}
}
function triggerRemove(i) {
confirmationDialog({
title: 'Remove marker',
message: 'Are you sure you want to remove this marker? The action cannot be reverted',
confirm: 'Remove',
onConfirm: () => removeMarker(i)
});
}
function toggleAddMarker() {
markersAddFromOverview.classList.toggle('pressed');
addMarker.click();
}
function removeMarker(i) {
notes = notes.filter((note) => note.id !== `marker${i}`);
pack.markers = pack.markers.filter((marker) => marker.i !== i);
document.getElementById(`marker${i}`)?.remove();
addLines();
}
function triggerRemoveAll() {
confirmationDialog({
title: 'Remove all markers',
message: 'Are you sure you want to remove all non-locked markers? The action cannot be reverted',
confirm: 'Remove all',
onConfirm: removeAllMarkers
});
}
function removeAllMarkers() {
pack.markers = pack.markers.filter(({i, lock}) => {
if (lock) return true;
const id = `marker${i}`;
document.getElementById(id)?.remove();
notes = notes.filter((note) => note.id !== id);
return false;
});
addLines();
}
function exportMarkers() {
const headers = 'Id,Type,Icon,Name,Note,X,Y\n';
const body = pack.markers.map((marker) => {
const {i, type, icon, x, y} = marker;
const id = `marker${i}`;
const note = notes.find((note) => note.id === id);
const legend = escape(note.legend);
return [id, type, icon, note.name, legend, x, y].join(',');
});
const data = headers + body.join('\n');
const fileName = getFileName('Markers') + '.csv';
downloadFile(data, fileName);
}
function close() {
listeners.forEach((removeListener) => removeListener());
addMarker.classList.remove('pressed');
markerAdd.classList.remove('pressed');
restoreDefaultEvents();
clearMainTip();
}
}

View file

@ -10,33 +10,33 @@ class Rulers {
}
toString() {
return this.data.map(ruler => ruler.toString()).join("; ");
return this.data.map((ruler) => ruler.toString()).join('; ');
}
fromString(string) {
this.data = [];
const rulers = string.split("; ");
const rulers = string.split('; ');
for (const rulerString of rulers) {
const [type, pointsString] = rulerString.split(": ");
const points = pointsString.split(" ").map(el => el.split(",").map(n => +n));
const Type = type === "Ruler" ? Ruler : type === "Opisometer" ? Opisometer : type === "RouteOpisometer" ? RouteOpisometer : type === "Planimeter" ? Planimeter : null;
const [type, pointsString] = rulerString.split(': ');
const points = pointsString.split(' ').map((el) => el.split(',').map((n) => +n));
const Type = type === 'Ruler' ? Ruler : type === 'Opisometer' ? Opisometer : type === 'RouteOpisometer' ? RouteOpisometer : type === 'Planimeter' ? Planimeter : null;
this.create(Type, points);
}
}
draw() {
this.data.forEach(ruler => ruler.draw());
this.data.forEach((ruler) => ruler.draw());
}
undraw() {
this.data.forEach(ruler => ruler.undraw());
this.data.forEach((ruler) => ruler.undraw());
}
remove(id) {
if (id === undefined) return;
const ruler = this.data.find(ruler => ruler.id === id);
const ruler = this.data.find((ruler) => ruler.id === id);
ruler.undraw();
const rulerIndex = this.data.indexOf(ruler);
rulers.data.splice(rulerIndex, 1);
@ -50,7 +50,7 @@ class Measurer {
}
toString() {
return this.constructor.name + ": " + this.points.join(" ");
return this.constructor.name + ': ' + this.points.join(' ');
}
getSize() {
@ -62,13 +62,13 @@ class Measurer {
}
drag() {
const tr = parseTransform(this.getAttribute("transform"));
const tr = parseTransform(this.getAttribute('transform'));
const x = +tr[0] - d3.event.x,
y = +tr[1] - d3.event.y;
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const transform = `translate(${x + d3.event.x},${y + d3.event.y})`;
this.setAttribute("transform", transform);
this.setAttribute('transform', transform);
});
}
@ -111,7 +111,7 @@ class Ruler extends Measurer {
}
getPointsString() {
return this.points.join(" ");
return this.points.join(' ');
}
updatePoint(index, x, y) {
@ -119,7 +119,7 @@ class Ruler extends Measurer {
}
getPointId(x, y) {
return this.points.findIndex(el => el[0] == x && el[1] == y);
return this.points.findIndex((el) => el[0] == x && el[1] == y);
}
pushPoint(i) {
@ -128,42 +128,42 @@ class Ruler extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const points = this.getPointsString();
const size = this.getSize();
const dash = this.getDash();
const el = (this.el = ruler
.append("g")
.attr("class", "ruler")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("polyline")
.attr("points", points)
.attr("class", "white")
.attr("stroke-width", size)
.call(d3.drag().on("start", () => this.addControl(this)));
el.append("polyline")
.attr("points", points)
.attr("class", "gray")
.attr("stroke-width", rn(size * 1.2, 2))
.attr("stroke-dasharray", dash);
el.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
.append('g')
.attr('class', 'ruler')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('polyline')
.attr('points', points)
.attr('class', 'white')
.attr('stroke-width', size)
.call(d3.drag().on('start', () => this.addControl(this)));
el.append('polyline')
.attr('points', points)
.attr('class', 'gray')
.attr('stroke-width', rn(size * 1.2, 2))
.attr('stroke-dasharray', dash);
el.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.drawPoints(el);
this.updateLabel();
return this;
}
drawPoints(el) {
const g = el.select(".rulerPoints");
g.selectAll("circle").remove();
const g = el.select('.rulerPoints');
g.selectAll('circle').remove();
for (let i = 0; i < this.points.length; i++) {
const [x, y] = this.points[i];
@ -173,19 +173,19 @@ class Ruler extends Measurer {
drawPoint(el, x, y, i) {
const context = this;
el.append("circle")
.attr("r", "1em")
.attr("cx", x)
.attr("cy", y)
.attr("class", this.isEdge(i) ? "edge" : "control")
.on("click", function () {
el.append('circle')
.attr('r', '1em')
.attr('cx', x)
.attr('cy', y)
.attr('class', this.isEdge(i) ? 'edge' : 'control')
.on('click', function () {
context.removePoint(context, i);
})
.call(
d3
.drag()
.clickDistance(3)
.on("start", function () {
.on('start', function () {
context.dragControl(context, i);
})
);
@ -197,9 +197,9 @@ class Ruler extends Measurer {
updateLabel() {
const length = this.getLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
getLength() {
@ -215,13 +215,13 @@ class Ruler extends Measurer {
dragControl(context, pointId) {
let addPoint = context.isEdge(pointId) && d3.event.sourceEvent.ctrlKey;
let circle = context.el.select(`circle:nth-child(${pointId + 1})`);
const line = context.el.selectAll("polyline");
const line = context.el.selectAll('polyline');
let x0 = rn(d3.event.x, 1);
let y0 = rn(d3.event.y, 1);
let axis;
d3.event.on("drag", function () {
d3.event.on('drag', function () {
if (addPoint) {
if (d3.event.dx < 0.1 && d3.event.dy < 0.1) return;
context.pushPoint(pointId);
@ -232,10 +232,10 @@ class Ruler extends Measurer {
}
const shiftPressed = d3.event.sourceEvent.shiftKey;
if (shiftPressed && !axis) axis = Math.abs(d3.event.dx) > Math.abs(d3.event.dy) ? "x" : "y";
if (shiftPressed && !axis) axis = Math.abs(d3.event.dx) > Math.abs(d3.event.dy) ? 'x' : 'y';
const x = axis === "y" ? x0 : rn(d3.event.x, 1);
const y = axis === "x" ? y0 : rn(d3.event.y, 1);
const x = axis === 'y' ? x0 : rn(d3.event.x, 1);
const y = axis === 'x' ? y0 : rn(d3.event.y, 1);
if (!shiftPressed) {
axis = null;
@ -244,8 +244,8 @@ class Ruler extends Measurer {
}
context.updatePoint(pointId, x, y);
line.attr("points", context.getPointsString());
circle.attr("cx", x).attr("cy", y);
line.attr('points', context.getPointsString());
circle.attr('cx', x).attr('cy', y);
context.updateLabel();
});
}
@ -273,43 +273,43 @@ class Opisometer extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const dash = this.getDash();
const context = this;
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
.append('g')
.attr('class', 'opisometer')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('path').attr('class', 'white').attr('stroke-width', size);
el.append('path').attr('class', 'gray').attr('stroke-width', size).attr('stroke-dasharray', dash);
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -319,26 +319,26 @@ class Opisometer extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
const left = this.points[0];
const right = last(this.points);
this.el.select(".rulerPoints > circle:first-child").attr("cx", left[0]).attr("cy", left[1]);
this.el.select(".rulerPoints > circle:last-child").attr("cx", right[0]).attr("cy", right[1]);
this.el.select('.rulerPoints > circle:first-child').attr('cx', left[0]).attr('cy', left[1]);
this.el.select('.rulerPoints > circle:last-child').attr('cx', right[0]).attr('cy', right[1]);
}
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const length = this.el.select('path').node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
dragControl(context, rigth) {
const MIN_DIST = d3.event.sourceEvent.shiftKey ? 9 : 100;
let prev = rigth ? last(context.points) : context.points[0];
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const point = [d3.event.x | 0, d3.event.y | 0];
const dist2 = (prev[0] - point[0]) ** 2 + (prev[1] - point[1]) ** 2;
@ -351,7 +351,7 @@ class Opisometer extends Measurer {
context.updateLabel();
});
d3.event.on("end", function () {
d3.event.on('end', function () {
if (!d3.event.sourceEvent.shiftKey) context.optimize();
});
}
@ -361,7 +361,7 @@ class RouteOpisometer extends Measurer {
constructor(points) {
super(points);
if (pack.cells) {
this.cellStops = points.map(p => findCell(p[0], p[1]));
this.cellStops = points.map((p) => findCell(p[0], p[1]));
} else {
this.cellStops = null;
}
@ -369,7 +369,7 @@ class RouteOpisometer extends Measurer {
checkCellStops() {
if (!this.cellStops) {
this.cellStops = this.points.map(p => findCell(p[0], p[1]));
this.cellStops = this.points.map((p) => findCell(p[0], p[1]));
}
}
@ -412,42 +412,42 @@ class RouteOpisometer extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const dash = this.getDash();
const context = this;
const el = (this.el = ruler
.append("g")
.attr("class", "opisometer")
.attr("font-size", 10 * size));
el.append("path").attr("class", "white").attr("stroke-width", size);
el.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
.append('g')
.attr('class', 'opisometer')
.attr('font-size', 10 * size));
el.append('path').attr('class', 'white').attr('stroke-width', size);
el.append('path').attr('class', 'gray').attr('stroke-width', size).attr('stroke-dasharray', dash);
const rulerPoints = el
.append("g")
.attr("class", "rulerPoints")
.attr("stroke-width", 0.5 * size)
.attr("font-size", 2 * size);
.append('g')
.attr('class', 'rulerPoints')
.attr('stroke-width', 0.5 * size)
.attr('font-size', 2 * size);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 0);
})
);
rulerPoints
.append("circle")
.attr("r", "1em")
.append('circle')
.attr('r', '1em')
.call(
d3.drag().on("start", function () {
d3.drag().on('start', function () {
context.dragControl(context, 1);
})
);
el.append("text")
.attr("dx", ".35em")
.attr("dy", "-.45em")
.on("click", () => rulers.remove(this.id));
el.append('text')
.attr('dx', '.35em')
.attr('dy', '-.45em')
.on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -457,23 +457,23 @@ class RouteOpisometer extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRom.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
const left = this.points[0];
const right = last(this.points);
this.el.select(".rulerPoints > circle:first-child").attr("cx", left[0]).attr("cy", left[1]);
this.el.select(".rulerPoints > circle:last-child").attr("cx", right[0]).attr("cy", right[1]);
this.el.select('.rulerPoints > circle:first-child').attr('cx', left[0]).attr('cy', left[1]);
this.el.select('.rulerPoints > circle:last-child').attr('cx', right[0]).attr('cy', right[1]);
}
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const length = this.el.select('path').node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + ' ' + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
this.el.select('text').attr('x', x).attr('y', y).text(text);
}
dragControl(context, rigth) {
d3.event.on("drag", function () {
d3.event.on('drag', function () {
const mousePoint = [d3.event.x | 0, d3.event.y | 0];
const cells = pack.cells;
@ -493,16 +493,16 @@ class Planimeter extends Measurer {
}
draw() {
if (this.el) this.el.selectAll("*").remove();
if (this.el) this.el.selectAll('*').remove();
const size = this.getSize();
const el = (this.el = ruler
.append("g")
.attr("class", "planimeter")
.call(d3.drag().on("start", this.drag))
.attr("font-size", 10 * size));
el.append("path").attr("class", "planimeter").attr("stroke-width", size);
el.append("text").on("click", () => rulers.remove(this.id));
.append('g')
.attr('class', 'planimeter')
.call(d3.drag().on('start', this.drag))
.attr('font-size', 10 * size));
el.append('path').attr('class', 'planimeter').attr('stroke-width', size);
el.append('text').on('click', () => rulers.remove(this.id));
this.updateCurve();
this.updateLabel();
@ -512,32 +512,33 @@ class Planimeter extends Measurer {
updateCurve() {
lineGen.curve(d3.curveCatmullRomClosed.alpha(0.5));
const path = round(lineGen(this.points));
this.el.selectAll("path").attr("d", path);
this.el.selectAll('path').attr('d', path);
}
updateLabel() {
if (this.points.length < 3) return;
const polygonArea = rn(Math.abs(d3.polygonArea(this.points)));
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + " " + unit;
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const area = si(polygonArea * distanceScaleInput.value ** 2) + ' ' + unit;
const c = polylabel([this.points], 1.0);
this.el.select("text").attr("x", c[0]).attr("y", c[1]).text(area);
this.el.select('text').attr('x', c[0]).attr('y', c[1]).text(area);
}
}
// Scale bar
function drawScaleBar() {
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
scaleBar.selectAll("*").remove(); // fully redraw every time
function drawScaleBar(requestedScale) {
if (scaleBar.style('display') === 'none') return; // no need to re-draw hidden element
scaleBar.selectAll('*').remove(); // fully redraw every time
const scaleLevel = requestedScale || scale;
const dScale = distanceScaleInput.value;
const distanceScale = distanceScaleInput.value;
const unit = distanceUnitInput.value;
const size = +barSizeInput.value;
// calculate size
const init = 100; // actual length in pixels if scale, dScale and size = 1;
const size = +barSizeInput.value;
let val = (init * size * dScale) / scale; // bar length in distance unit
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);
@ -545,81 +546,81 @@ function drawScaleBar() {
else if (val > 9) val = rn(val, -1);
// round to 10
else val = rn(val); // round to 1
const l = (val * scale) / dScale; // actual length in pixels on this scale
const length = (val * scaleLevel) / distanceScale; // actual length in pixels on this scale
scaleBar
.append("line")
.attr("x1", 0.5)
.attr("y1", 0)
.attr("x2", l + size - 0.5)
.attr("y2", 0)
.attr("stroke-width", size)
.attr("stroke", "white");
.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", l + size)
.attr("y2", size)
.attr("stroke-width", size)
.attr("stroke", "#3d3d3d");
const dash = size + " " + rn(l / 5 - size, 2);
.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", l + size)
.attr("y2", 0)
.attr("stroke-width", rn(size * 3, 2))
.attr("stroke-dasharray", dash)
.attr("stroke", "#3d3d3d");
.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")
.selectAll('text')
.data(d3.range(0, 6))
.enter()
.append("text")
.attr("x", d => rn((d * l) / 5, 2))
.attr("y", 0)
.attr("dy", "-.5em")
.attr("font-size", fontSize)
.text(d => rn((((d * l) / 5) * dScale) / scale) + (d < 5 ? "" : " " + unit));
.append('text')
.attr('x', (d) => rn((d * length) / 5, 2))
.attr('y', 0)
.attr('dy', '-.5em')
.attr('font-size', fontSize)
.text((d) => rn((((d * length) / 5) * distanceScale) / scaleLevel) + (d < 5 ? '' : ' ' + unit));
if (barLabel.value !== "") {
if (barLabel.value !== '') {
scaleBar
.append("text")
.attr("x", (l + 1) / 2)
.attr("y", 2 * size)
.attr("dominant-baseline", "text-before-edge")
.attr("font-size", fontSize)
.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);
.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);
fitScaleBar();
}
// fit ScaleBar to canvas size
function fitScaleBar() {
if (!scaleBar.select("rect").size() || scaleBar.style("display") === "none") return;
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 bbox = scaleBar.select('rect').node().getBBox();
const x = rn(svgWidth * px - bbox.width + 10),
y = rn(svgHeight * py - bbox.height + 20);
scaleBar.attr("transform", `translate(${x},${y})`);
scaleBar.attr('transform', `translate(${x},${y})`);
}

View file

@ -199,9 +199,9 @@ function overviewMilitary() {
function militaryCustomize() {
const types = ['melee', 'ranged', 'mounted', 'machinery', 'naval', 'armored', 'aviation', 'magical'];
const table = document.getElementById('militaryOptions').querySelector('tbody');
const tableBody = document.getElementById('militaryOptions').querySelector('tbody');
removeUnitLines();
options.military.map((u) => addUnitLine(u));
options.military.map((unit) => addUnitLine(unit));
$('#militaryOptions').dialog({
title: 'Edit Military Units',
@ -219,44 +219,127 @@ function overviewMilitary() {
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener('mousemove', () => tip('Add new military unit to the table'));
buttons[2].addEventListener('mousemove', () => tip('Restore default military units and settings'));
buttons[3].addEventListener('mousemove', () => tip('Close the window without saving the changes'));
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
tableBody.addEventListener('click', (event) => {
const el = event.target;
if (el.tagName !== 'BUTTON') return;
const type = el.dataset.type;
if (type === 'icon') return selectIcon(el.innerHTML, (v) => (el.innerHTML = v));
if (type === 'biomes') {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
return selectLimitation(el, biomes);
}
if (type === 'states') return selectLimitation(el, pack.states);
if (type === 'cultures') return selectLimitation(el, pack.cultures);
if (type === 'religions') return selectLimitation(el, pack.religions);
});
function removeUnitLines() {
table.querySelectorAll('tr').forEach((el) => el.remove());
tableBody.querySelectorAll('tr').forEach((el) => el.remove());
}
function addUnitLine(u) {
const row = document.createElement('tr');
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types
.map((t) => `<option ${u.type === t ? 'selected' : ''} value="${t}">${t}</option>`)
.join(' ')}</select></td>
function getLimitValue(attr) {
return attr?.join(',') || '';
}
function getLimitText(attr) {
return attr?.length ? 'some' : 'all';
}
function getLimitTip(attr, data) {
if (!attr || !attr.length) return '';
return attr.map((i) => data?.[i]?.name || '').join(', ');
}
function addUnitLine(unit) {
const typeOptions = types.map((t) => `<option ${unit.type === t ? 'selected' : ''} value="${t}">${t}</option>`).join(' ');
const getLimitButton = (attr) =>
`<button
data-tip="Select allowed ${attr}"
data-type="${attr}"
title="${getLimitTip(unit[attr], pack[attr])}"
data-value="${getLimitValue(unit[attr])}">
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
<td>${getLimitButton('biomes')}</td>
<td>${getLimitButton('states')}</td>
<td>${getLimitButton('cultures')}</td>
<td>${getLimitButton('religions')}</td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? 'checked' : ''}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? 'checked' : ''}>
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector('button').addEventListener('click', function (e) {
selectIcon(this.innerHTML, (v) => (this.innerHTML = v));
});
table.appendChild(row);
tableBody.appendChild(row);
}
function restoreDefaultUnits() {
removeUnitLines();
Military.getDefaultOptions().map((u) => addUnitLine(u));
Military.getDefaultOptions().map((unit) => addUnitLine(unit));
}
function selectLimitation(el, data) {
const type = el.dataset.type;
const value = el.dataset.value;
const initial = value ? value.split(',').map((v) => +v) : [];
const filtered = data.filter((datum) => datum.i && !datum.removed);
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' : ''} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><table style="margin-top:.3em"><tbody>${lines.join('')}</tbody></table>`;
$('#alert').dialog({
width: fitContent(),
title: `Limit unit`,
buttons: {
Invert: function () {
alertMessage.querySelectorAll('input').forEach((el) => (el.checked = !el.checked));
},
Apply: function () {
const inputs = Array.from(alertMessage.querySelectorAll('input'));
const selected = inputs.reduce((acc, input) => {
if (input.checked) acc.push(input.dataset.i);
return acc;
}, []);
if (!selected.length) return tip('Select at least one element', false, 'error');
const allAreSelected = selected.length === inputs.length;
el.dataset.value = allAreSelected ? '' : selected.join(',');
el.innerHTML = allAreSelected ? 'all' : 'some';
el.setAttribute('title', getLimitTip(selected, data));
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll('tr'));
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');
@ -265,14 +348,22 @@ function overviewMilitary() {
$('#militaryOptions').dialog('close');
options.military = unitLines.map((r, i) => {
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll('input, select, button')).map((d) => {
let value = d.value;
if (d.type === 'number') value = +d.value || 0;
if (d.type === 'checkbox') value = +d.checked || 0;
if (d.type === 'button') value = d.innerHTML || '';
return value;
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;
});
return {icon, name: names[i], rural, urban, crew, power, type, separate};
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
if (states) unit.states = states;
if (cultures) unit.cultures = cultures;
if (religions) unit.religions = religions;
return unit;
});
localStorage.setItem('military', JSON.stringify(options.military));
Military.generate();

View file

@ -0,0 +1,492 @@
'use strict';
function overviewMilitary() {
if (customization) return;
closeDialogs('#militaryOverview, .stable');
if (!layerIsOn('toggleStates')) toggleStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
if (!layerIsOn('toggleMilitary')) toggleMilitary();
const body = document.getElementById('militaryBody');
addLines();
$('#militaryOverview').dialog();
if (modules.overviewMilitary) return;
modules.overviewMilitary = true;
updateHeaders();
$('#militaryOverview').dialog({
title: 'Military Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('militaryOverviewRefresh').addEventListener('click', addLines);
document.getElementById('militaryPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('militaryOptionsButton').addEventListener('click', militaryCustomize);
document.getElementById('militaryRegimentsList').addEventListener('click', () => overviewRegiments(-1));
document.getElementById('militaryOverviewRecalculate').addEventListener('click', militaryRecalculate);
document.getElementById('militaryExport').addEventListener('click', downloadMilitaryData);
document.getElementById('militaryWiki').addEventListener('click', () => wiki('Military-Forces'));
body.addEventListener('change', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
changeAlert(state, line, +el.value);
});
body.addEventListener('click', function (ev) {
const el = ev.target,
line = el.parentNode,
state = +line.dataset.id;
if (el.tagName === 'SPAN') overviewRegiments(state);
});
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById('militaryHeader');
header.querySelectorAll('.removable').forEach((el) => el.remove());
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>`);
}
header.querySelectorAll('.removable').forEach(function (e) {
e.addEventListener('click', function () {
sortLines(this);
});
});
}
// add line for each state
function addLines() {
body.innerHTML = '';
let lines = '';
const states = pack.states.filter((s) => s.i && !s.removed);
for (const s of states) {
const population = rn((s.rural + s.urban * urbanization) * populationRate);
const getForces = (u) => s.military.reduce((s, r) => s + (r.u[u.name] || 0), 0);
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(' ');
lines += `<div class="states" data-id=${s.i} data-state="${
s.name
}" ${sortData} data-total="${total}" data-population="${population}" data-rate="${rate}" data-alert="${s.alert}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${
s.color
}" class="fillRect"></svg>
<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="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>
<input data-tip="War Alert. Editable modifier to military forces number, depends of political situation" style="width:4.1em" type="number" min=0 step=.01 value="${rn(
s.alert,
2
)}">
<span data-tip="Show regiments list" class="icon-list-bullet pointer"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
updateFooter();
// add listeners
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => stateHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => stateHighlightOff(ev)));
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(militaryHeader);
}
function changeAlert(state, line, alert) {
const s = pack.states[state];
const dif = s.alert || alert ? alert / s.alert : 0; // modifier
s.alert = line.dataset.alert = alert;
s.military.forEach((r) => {
Object.keys(r.u).forEach((u) => (r.u[u] = rn(r.u[u] * dif))); // change units value
r.a = d3.sum(Object.values(r.u)); // change total
armies.select(`g>g#regiment${s.i}-${r.i}>text`).text(Military.getTotal(r)); // change icon text
});
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)));
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));
const rate = (line.dataset.rate = (total / population) * 100);
line.querySelector("div[data-type='total']").innerHTML = si(total);
line.querySelector("div[data-type='rate']").innerHTML = rn(rate, 2) + '%';
updateFooter();
}
function updateFooter() {
const lines = Array.from(body.querySelectorAll(':scope > div'));
const statesNumber = (militaryFooterStates.innerHTML = pack.states.filter((s) => s.i && !s.removed).length);
const total = d3.sum(lines.map((el) => el.dataset.total));
militaryFooterForcesTotal.innerHTML = si(total);
militaryFooterForces.innerHTML = si(total / statesNumber);
militaryFooterRate.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.rate)) / statesNumber, 2) + '%';
militaryFooterAlert.innerHTML = rn(d3.sum(lines.map((el) => el.dataset.alert)) / statesNumber, 2);
}
function stateHighlightOn(event) {
const state = +event.target.dataset.id;
if (customization || !state) return;
armies
.select('#army' + state)
.transition()
.duration(2000)
.style('fill', '#ff0000');
if (!layerIsOn('toggleStates')) return;
const d = regions.select('#state' + state).attr('d');
<<<<<<< HEAD
const path = debug.append('path').attr('class', 'highlight').attr('d', d).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 1).attr('opacity', 1).attr('filter', 'url(#blur1)');
=======
const path = debug
.append("path")
.attr("class", "highlight")
.attr("d", d)
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("opacity", 1)
.attr("filter", "url(#blur1)");
>>>>>>> master
const l = path.node().getTotalLength(),
dur = (l + 5000) / 2;
const i = d3.interpolateString('0,' + l, l + ',' + l);
path
.transition()
.duration(dur)
.attrTween('stroke-dasharray', function () {
return (t) => i(t);
});
}
function stateHighlightOff(event) {
debug.selectAll('.highlight').each(function () {
d3.select(this).transition().duration(1000).attr('opacity', 0).remove();
});
const state = +event.target.dataset.id;
armies
.select('#army' + state)
.transition()
.duration(1000)
.style('fill', null);
}
function togglePercentageMode() {
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const lines = body.querySelectorAll(':scope > div');
const array = Array.from(lines),
cache = [];
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map((el) => +el.dataset[type]));
return cache[type];
};
lines.forEach(function (el) {
el.querySelectorAll('div').forEach(function (div) {
const type = div.dataset.type;
if (type === 'rate') return;
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + '%' : '0%';
});
});
} else {
body.dataset.type = 'absolute';
addLines();
}
}
function militaryCustomize() {
<<<<<<< HEAD
const types = ['melee', 'ranged', 'mounted', 'machinery', 'naval', 'armored', 'aviation', 'magical'];
const table = document.getElementById('militaryOptions').querySelector('tbody');
removeUnitLines();
options.military.map((u) => addUnitLine(u));
=======
const types = ["melee", "ranged", "mounted", "machinery", "naval", "armored", "aviation", "magical"];
const tableBody = document.getElementById("militaryOptions").querySelector("tbody");
removeUnitLines();
options.military.map(unit => addUnitLine(unit));
>>>>>>> master
$('#militaryOptions').dialog({
title: 'Edit Military Units',
resizable: false,
width: fitContent(),
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'}),
Restore: restoreDefaultUnits,
Cancel: function () {
$(this).dialog('close');
}
},
open: function () {
<<<<<<< HEAD
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>"));
buttons[1].addEventListener('mousemove', () => tip('Add new military unit to the table'));
buttons[2].addEventListener('mousemove', () => tip('Restore default military units and settings'));
buttons[3].addEventListener('mousemove', () => tip('Close the window without saving the changes'));
=======
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () =>
tip("Apply military units settings. <span style='color:#cb5858'>All forces will be recalculated!</span>")
);
buttons[1].addEventListener("mousemove", () => tip("Add new military unit to the table"));
buttons[2].addEventListener("mousemove", () => tip("Restore default military units and settings"));
buttons[3].addEventListener("mousemove", () => tip("Close the window without saving the changes"));
>>>>>>> master
}
});
if (modules.overviewMilitaryCustomize) return;
modules.overviewMilitaryCustomize = true;
tableBody.addEventListener("click", event => {
const el = event.target;
if (el.tagName !== "BUTTON") return;
const type = el.dataset.type;
if (type === "icon") return selectIcon(el.innerHTML, v => (el.innerHTML = v));
if (type === "biomes") {
const {i, name, color} = biomesData;
const biomesArray = Array(i.length).fill(null);
const biomes = biomesArray.map((_, i) => ({i, name: name[i], color: color[i]}));
return selectLimitation(el, biomes);
}
if (type === "states") return selectLimitation(el, pack.states);
if (type === "cultures") return selectLimitation(el, pack.cultures);
if (type === "religions") return selectLimitation(el, pack.religions);
});
function removeUnitLines() {
<<<<<<< HEAD
table.querySelectorAll('tr').forEach((el) => el.remove());
}
function addUnitLine(u) {
const row = document.createElement('tr');
row.innerHTML = `<td><button type="button" data-tip="Click to select unit icon">${u.icon || ' '}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${u.name}"></td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${u.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${u.urban}"></td>
<td><input data-tip="Enter average number of people in crew (used for total personnel calculation)" type="number" min=1 step=1 value="${u.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${u.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${types
.map((t) => `<option ${u.type === t ? 'selected' : ''} value="${t}">${t}</option>`)
.join(' ')}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${u.name}Separate" type="checkbox" class="checkbox" ${u.separate ? 'checked' : ''}>
<label for="${u.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
row.querySelector('button').addEventListener('click', function (e) {
selectIcon(this.innerHTML, (v) => (this.innerHTML = v));
});
table.appendChild(row);
=======
tableBody.querySelectorAll("tr").forEach(el => el.remove());
}
function getLimitValue(attr) {
return attr?.join(",") || "";
}
function getLimitText(attr) {
return attr?.length ? "some" : "all";
}
function getLimitTip(attr, data) {
if (!attr || !attr.length) return "";
return attr.map(i => data?.[i]?.name || "").join(", ");
}
function addUnitLine(unit) {
const row = document.createElement("tr");
const typeOptions = types.map(t => `<option ${unit.type === t ? "selected" : ""} value="${t}">${t}</option>`).join(" ");
const getLimitButton = attr =>
`<button
data-tip="Select allowed ${attr}"
data-type="${attr}"
title="${getLimitTip(unit[attr], pack[attr])}"
data-value="${getLimitValue(unit[attr])}">
${getLimitText(unit[attr])}
</button>`;
row.innerHTML = `<td><button data-type="icon" data-tip="Click to select unit icon">${unit.icon || " "}</button></td>
<td><input data-tip="Type unit name. If name is changed for existing unit, old unit will be replaced" value="${unit.name}"></td>
<td>${getLimitButton("biomes")}</td>
<td>${getLimitButton("states")}</td>
<td>${getLimitButton("cultures")}</td>
<td>${getLimitButton("religions")}</td>
<td><input data-tip="Enter conscription percentage for rural population" type="number" min=0 max=100 step=.01 value="${unit.rural}"></td>
<td><input data-tip="Enter conscription percentage for urban population" type="number" min=0 max=100 step=.01 value="${unit.urban}"></td>
<td><input data-tip="Enter average number of people in crew (for total personnel calculation)" type="number" min=1 step=1 value="${unit.crew}"></td>
<td><input data-tip="Enter military power (used for battle simulation)" type="number" min=0 step=.1 value="${unit.power}"></td>
<td><select data-tip="Select unit type to apply special rules on forces recalculation">${typeOptions}</select></td>
<td data-tip="Check if unit is separate and can be stacked only with units of the same type">
<input id="${unit.name}Separate" type="checkbox" class="checkbox" ${unit.separate ? "checked" : ""}>
<label for="${unit.name}Separate" class="checkbox-label"></label></td>
<td data-tip="Remove the unit"><span data-tip="Remove unit type" class="icon-trash-empty pointer" onclick="this.parentElement.parentElement.remove();"></span></td>`;
tableBody.appendChild(row);
>>>>>>> master
}
function restoreDefaultUnits() {
removeUnitLines();
<<<<<<< HEAD
Military.getDefaultOptions().map((u) => addUnitLine(u));
}
function applyMilitaryOptions() {
const unitLines = Array.from(table.querySelectorAll('tr'));
const names = unitLines.map((r) => r.querySelector('input').value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, '_'));
=======
Military.getDefaultOptions().map(unit => addUnitLine(unit));
}
function selectLimitation(el, data) {
const type = el.dataset.type;
const value = el.dataset.value;
const initial = value ? value.split(",").map(v => +v) : [];
const filtered = data.filter(datum => datum.i && !datum.removed);
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" : ""} >
<label for="el${i}" class="checkbox-label">${fullName || name}</label>
</td></tr>`
);
alertMessage.innerHTML = `<b>Limit unit by ${type}:</b><table style="margin-top:.3em"><tbody>${lines.join("")}</tbody></table>`;
$("#alert").dialog({
width: fitContent(),
title: `Limit unit`,
buttons: {
Invert: function () {
alertMessage.querySelectorAll("input").forEach(el => (el.checked = !el.checked));
},
Apply: function () {
const inputs = Array.from(alertMessage.querySelectorAll("input"));
const selected = inputs.reduce((acc, input) => {
if (input.checked) acc.push(input.dataset.i);
return acc;
}, []);
if (!selected.length) return tip("Select at least one element", false, "error");
const allAreSelected = selected.length === inputs.length;
el.dataset.value = allAreSelected ? "" : selected.join(",");
el.innerHTML = allAreSelected ? "all" : "some";
el.setAttribute("title", getLimitTip(selected, data));
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
function applyMilitaryOptions() {
const unitLines = Array.from(tableBody.querySelectorAll("tr"));
const names = unitLines.map(r => r.querySelector("input").value.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, "_"));
>>>>>>> master
if (new Set(names).size !== names.length) {
tip('All units should have unique names', false, 'error');
return;
}
$('#militaryOptions').dialog('close');
options.military = unitLines.map((r, i) => {
<<<<<<< HEAD
const [icon, name, rural, urban, crew, power, type, separate] = Array.from(r.querySelectorAll('input, select, button')).map((d) => {
let value = d.value;
if (d.type === 'number') value = +d.value || 0;
if (d.type === 'checkbox') value = +d.checked || 0;
if (d.type === 'button') value = d.innerHTML || '';
return value;
=======
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;
>>>>>>> master
});
const unit = {icon, name: names[i], rural, urban, crew, power, type, separate};
if (biomes) unit.biomes = biomes;
if (states) unit.states = states;
if (cultures) unit.cultures = cultures;
if (religions) unit.religions = religions;
return unit;
});
localStorage.setItem('military', JSON.stringify(options.military));
Military.generate();
updateHeaders();
addLines();
}
}
function militaryRecalculate() {
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',
buttons: {
Recalculate: function () {
$(this).dialog('close');
Military.generate();
addLines();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function downloadMilitaryData() {
const units = options.military.map((u) => u.name);
let data = 'Id,State,' + units.map((u) => capitalize(u)).join(',') + ',Total,Population,Rate,War Alert\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.state + ',';
data += units.map((u) => el.dataset[u]).join(',') + ',';
data += el.dataset.total + ',';
data += el.dataset.population + ',';
data += rn(el.dataset.rate, 2) + '%,';
data += el.dataset.alert + '\n';
});
const name = getFileName('Military') + '.csv';
downloadFile(data, name);
}
}

View file

@ -1,4 +1,5 @@
'use strict';
function editNotes(id, name) {
// update list of objects
const select = document.getElementById('notesSelect');
@ -8,11 +9,12 @@ function editNotes(id, name) {
}
// initiate pell (html editor)
const notesText = document.getElementById('notesText');
notesText.innerHTML = '';
const editor = Pell.init({
element: document.getElementById('notesText'),
element: notesText,
onChange: (html) => {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
const note = notes.find((note) => note.id === select.value);
if (!note) return;
note.legend = html;
showNote(note);
@ -43,8 +45,7 @@ function editNotes(id, name) {
title: 'Notes Editor',
minWidth: '40em',
width: '50vw',
position: {my: 'center', at: 'center', of: 'svg'},
close: () => (notesText.innerHTML = '')
position: {my: 'center', at: 'center', of: 'svg'}
});
if (modules.editNotes) return;
@ -107,7 +108,7 @@ function editNotes(id, name) {
return;
}
highlightElement(element); // if element is found
highlightElement(element, 3); // if element is found
}
function downloadLegends() {

View file

@ -0,0 +1,183 @@
<<<<<<< HEAD
'use strict';
=======
"use strict";
>>>>>>> master
function editNotes(id, name) {
// update list of objects
const select = document.getElementById('notesSelect');
select.options.length = 0;
for (const note of notes) {
select.options.add(new Option(note.id, note.id));
}
// initiate pell (html editor)
const notesText = document.getElementById("notesText");
notesText.innerHTML = "";
const editor = Pell.init({
<<<<<<< HEAD
element: document.getElementById('notesText'),
onChange: (html) => {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
=======
element: notesText,
onChange: html => {
const note = notes.find(note => note.id === select.value);
>>>>>>> master
if (!note) return;
note.legend = html;
showNote(note);
}
});
// select an object
if (notes.length || id) {
if (!id) id = notes[0].id;
let note = notes.find((note) => note.id === id);
if (note === undefined) {
if (!name) name = id;
note = {id, name, legend: ''};
notes.push(note);
select.options.add(new Option(id, id));
}
select.value = id;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
showNote(note);
} else {
editor.content.innerHTML = 'There are no added notes. Click on element (e.g. label) and add a free text note';
document.getElementById('notesName').value = '';
}
// open a dialog
<<<<<<< HEAD
$('#notesEditor').dialog({
title: 'Notes Editor',
minWidth: '40em',
width: '50vw',
position: {my: 'center', at: 'center', of: 'svg'},
close: () => (notesText.innerHTML = '')
=======
$("#notesEditor").dialog({
title: "Notes Editor",
minWidth: "40em",
width: "50vw",
position: {my: "center", at: "center", of: "svg"}
>>>>>>> master
});
if (modules.editNotes) return;
modules.editNotes = true;
// add listeners
document.getElementById('notesSelect').addEventListener('change', changeObject);
document.getElementById('notesName').addEventListener('input', changeName);
document.getElementById('notesPin').addEventListener('click', () => (options.pinNotes = !options.pinNotes));
document.getElementById('notesSpeak').addEventListener('click', () => speak(editor.content.innerHTML));
document.getElementById('notesFocus').addEventListener('click', validateHighlightElement);
document.getElementById('notesDownload').addEventListener('click', downloadLegends);
document.getElementById('notesUpload').addEventListener('click', () => legendsToLoad.click());
document.getElementById('legendsToLoad').addEventListener('change', function () {
uploadFile(this, uploadLegends);
});
document.getElementById('notesClearStyle').addEventListener('click', clearStyle);
document.getElementById('notesRemove').addEventListener('click', triggerNotesRemove);
function showNote(note) {
document.getElementById('notes').style.display = 'block';
document.getElementById('notesHeader').innerHTML = note.name;
document.getElementById('notesBody').innerHTML = note.legend;
}
function changeObject() {
const note = notes.find((note) => note.id === this.value);
if (!note) return;
notesName.value = note.name;
editor.content.innerHTML = note.legend;
}
function changeName() {
const id = document.getElementById('notesSelect').value;
const note = notes.find((note) => note.id === id);
if (!note) return;
note.name = this.value;
showNote(note);
}
function validateHighlightElement() {
const select = document.getElementById('notesSelect');
const element = document.getElementById(select.value);
if (element === null) {
alertMessage.innerHTML = 'Related element is not found. Would you like to remove the note?';
$('#alert').dialog({
resizable: false,
title: 'Element not found',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
return;
}
highlightElement(element, 3); // if element is found
}
function downloadLegends() {
const data = JSON.stringify(notes);
const name = getFileName('Notes') + '.txt';
downloadFile(data, name);
}
function uploadLegends(dataLoaded) {
if (!dataLoaded) {
tip('Cannot load the file. Please check the data format', false, 'error');
return;
}
notes = JSON.parse(dataLoaded);
document.getElementById('notesSelect').options.length = 0;
editNotes(notes[0].id, notes[0].name);
}
function clearStyle() {
editor.content.innerHTML = editor.content.textContent;
}
function triggerNotesRemove() {
alertMessage.innerHTML = 'Are you sure you want to remove the selected note?';
$('#alert').dialog({
resizable: false,
title: 'Remove note',
buttons: {
Remove: function () {
$(this).dialog('close');
removeLegend();
},
Keep: function () {
$(this).dialog('close');
}
}
});
}
function removeLegend() {
const select = document.getElementById('notesSelect');
const index = notes.findIndex((n) => n.id === select.value);
notes.splice(index, 1);
select.options.length = 0;
if (!notes.length) {
$('#notesEditor').dialog('close');
return;
}
notesText.innerHTML = '';
editNotes(notes[0].id, notes[0].name);
}
}

View file

@ -89,17 +89,19 @@ function showSupporters() {
Maxwell Hill,Drunken_Legends,rob bee,Jesse Holmes,YYako,Detocroix,Anoplexian,Hannah,Paul,Sandra Krohn,Lucid,Richard Keating,Allen Varney,Rick Falkvinge,
Seth Fusion,Adam Butler,Gus,StroboWolf,Sadie Blackthorne,Zewen Senpai,Dell McKnight,Oneiris,Darinius Dragonclaw Studios,Christopher Whitney,Rhodes HvZ,
Jeppe Skov Jensen,María Martín López,Martin Seeger,Annie Rishor,Aram Sabatés,MadNomadMedia,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,
Thirty-OneR ,ThatGuyGW ,Dee Chiu,MontyBoosh ,Achillain ,Jaden ,SashaTK,Steve Johnson,Eric Foley,Vito Martono,James H. Anthony,Kevin Cossutta,Thirty-OneR,
ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,Andrew Rostaing,Daniel Gill,
Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,Alex Debus,Joshua Vaught,
Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,Radovan Zapletal,Jmmat6,
Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,Guilherme Aguiar,Jarno Hallikainen,
Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,Cooper Counts,Patrick Jones,Clonetone,
PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,Page One Project,Spencer Morris,Paul Ingram,
Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,
Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,
Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,
"Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas"`;
Thirty-OneR,ThatGuyGW,Dee Chiu,MontyBoosh,Achillain,Jaden,SashaTK,Steve Johnson,Pierrick Bertrand,Jared Kennedy,Dylan Devenny,Kyle Robertson,
Andrew Rostaing,Daniel Gill,Char,Jack,Barna Csíkos,Ian Rousseau,Nicholas Grabstas,Tom Van Orden jr,Bryan Brake,Akylos,Riley Seaman,MaxOliver,Evan-DiLeo,
Alex Debus,Joshua Vaught,Kyle S,Eric Moore,Dean Dunakin,Uniquenameosaurus,WarWizardGames,Chance Mena,Jan Ka,Miguel Alejandro,Dalton Clark,Simon Drapeau,
Radovan Zapletal,Jmmat6,Justa Badge,Blargh Blarghmoomoo,Vanessa Anjos,Grant A. Murray,Akirsop,Rikard Wolff,Jake Fish,teco 47,Antiroo,Jakob Siegel,
Guilherme Aguiar,Jarno Hallikainen,Justin Mcclain,Kristin Chernoff,Rowland Kingman,Esther Busch,Grayson McClead,Austin,Hakon the Viking,Chad Riley,
Cooper Counts,Patrick Jones,Clonetone,PlayByMail.Net,Brad Wardell,Lance Saba,Egoensis,Brea Richards,Tiber,Chris Bloom,Maxim Lowe,Aquelion,
Page One Project,Spencer Morris,Paul Ingram,Dust Bunny,Adrian Wright,Eric Alexander Cartaya,GameNight,Thomas Mortensen Hansen,Zklaus,Drinarius,
Ed Wright,Lon Varnadore,Crys Cain,Heaven N Lee,Jeffrey Henning,Lazer Elf,Jordan Bellah,Alex Beard,Kass Frisson,Petro Lombaard,Emanuel Pietri,Rox,
PinkEvil,Gavin Madrigal,Martin Lorber,Prince of Morgoth,Jaryd Armstrong,Andrew Pirkola,ThyHolyDevil,Gary Smith,Tyshaun Wise,Ethan Cook,Jon Stroman,
Nobody679,良义 ,Chris Gray,Phoenix Boatwright,Mackenzie,Milo Cohen,Jason Matthew Wuerfel,Rasmus Legêne,Andrew Hines,Wexxler,Espen Sæverud,Binks,
Dominick Ormsby,Linn Browning,Václav Švec,Alan Buehne,George J.Lekkas,Alexandre Boivin,Tommy Mayfield,Skylar Mangum-Turner,Karen Blythe,Stefan Gugerel,
Mike Conley,Xavier privé,Hope You're Well,Mark Sprietsma,Robert Landry,Nick Mowry,steve hall,Markell,Josh Wren,Neutrix,BLRageQuit,Rocky,
Dario Spadavecchia,Bas Kroot,John Patrick Callahan Jr,Alexandra Vesey,D`;
const array = supporters
.replace(/(?:\r\n|\r|\n)/g, '')
@ -148,7 +150,9 @@ optionsContent.addEventListener('input', function (event) {
else if (id === 'regionsInput' || id === 'regionsOutput') changeStatesNumber(value);
else if (id === 'emblemShape') changeEmblemShape(value);
else if (id === 'tooltipSizeInput' || id === 'tooltipSizeOutput') changeTooltipSize(value);
else if (id === 'transparencyInput') changeDialogsTransparency(value);
else if (id === "themeHueInput") changeThemeHue(value);
else if (id === "themeColorInput") changeDialogsTheme(themeColorInput.value, transparencyInput.value);
else if (id === "transparencyInput") changeDialogsTheme(themeColorInput.value, value);
});
optionsContent.addEventListener('change', function (event) {
@ -156,23 +160,24 @@ optionsContent.addEventListener('change', function (event) {
const value = event.target.value;
if (id === 'zoomExtentMin' || id === 'zoomExtentMax') changeZoomExtent(value);
else if (id === 'optionsSeed') generateMapWithSeed();
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === 'uiSizeInput' || id === 'uiSizeOutput') changeUIsize(value);
if (id === 'shapeRendering') viewbox.attr('shape-rendering', value);
else if (id === 'yearInput') changeYear();
else if (id === 'eraInput') changeEra();
else if (id === "stateLabelsModeInput") options.stateLabelsMode = value;
});
optionsContent.addEventListener('click', function (event) {
const id = event.target.id;
if (id === 'toggleFullscreen') toggleFullscreen();
else if (id === 'optionsSeedGenerate') generateMapWithSeed();
else if (id === 'optionsMapHistory') showSeedHistoryDialog();
else if (id === 'optionsCopySeed') copyMapURL();
else if (id === 'optionsEraRegenerate') regenerateEra();
else if (id === 'zoomExtentDefault') restoreDefaultZoomExtent();
else if (id === 'translateExtent') toggleTranslateExtent(event.target);
else if (id === 'speakerTest') testSpeaker();
else if (id === "themeColorRestore") restoreDefaultThemeColor();
});
function mapSizeInputChange() {
@ -206,8 +211,8 @@ function changeMapSize() {
// just apply canvas size that was already set
function applyMapSize() {
const zoomMin = +zoomExtentMin.value,
zoomMax = +zoomExtentMax.value;
const zoomMin = +zoomExtentMin.value;
const zoomMax = +zoomExtentMax.value;
graphWidth = +mapWidthInput.value;
graphHeight = +mapHeightInput.value;
svgWidth = Math.min(graphWidth, window.innerWidth);
@ -275,12 +280,9 @@ function testSpeaker() {
speechSynthesis.speak(speaker);
}
function generateMapWithSeed() {
if (optionsSeed.value == seed) {
tip('The current map already has this seed', false, 'error');
return;
}
regeneratePrompt();
function generateMapWithSeed(source) {
if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error");
regeneratePrompt(source);
}
function showSeedHistoryDialog() {
@ -311,7 +313,7 @@ function restoreSeed(id) {
mapHeightInput.value = mapHistory[id].height;
templateInput.value = mapHistory[id].template;
if (locked('template')) unlock('template');
regeneratePrompt();
regeneratePrompt("seed history");
}
function restoreDefaultZoomExtent() {
@ -415,7 +417,7 @@ function changeUIsize(value) {
if (+value > max) value = max;
uiSizeInput.value = uiSizeOutput.value = value;
document.getElementsByTagName('body')[0].style.fontSize = value * 11 + 'px';
document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px";
document.getElementById('options').style.width = value * 300 + 'px';
}
@ -427,26 +429,56 @@ function changeTooltipSize(value) {
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
}
// change transparency for modal windows
function changeDialogsTransparency(value) {
transparencyInput.value = transparencyOutput.value = value;
const alpha = (100 - +value) / 100;
const optionsColor = 'rgba(164, 139, 149, ' + alpha + ')';
const dialogsColor = 'rgba(255, 255, 255, ' + alpha + ')';
const optionButtonsColor = 'rgba(145, 110, 127, ' + Math.min(alpha + 0.3, 1) + ')';
const optionLiColor = 'rgba(153, 123, 137, ' + Math.min(alpha + 0.3, 1) + ')';
document.getElementById('options').style.backgroundColor = optionsColor;
document.getElementById('dialogs').style.backgroundColor = dialogsColor;
document.querySelectorAll('.tabcontent button').forEach((el) => (el.style.backgroundColor = optionButtonsColor));
document.querySelectorAll('.tabcontent li').forEach((el) => (el.style.backgroundColor = optionLiColor));
document.querySelectorAll('button.options').forEach((el) => (el.style.backgroundColor = optionLiColor));
const THEME_COLOR = "#997787";
function restoreDefaultThemeColor() {
localStorage.removeItem("themeColor");
changeDialogsTheme(THEME_COLOR, transparencyInput.value);
}
function changeThemeHue(hue) {
const {s, l} = d3.hsl(themeColorInput.value);
const newColor = d3.hsl(+hue, s, l).hex();
changeDialogsTheme(newColor, transparencyInput.value);
}
// change color and transparency for modal windows
function changeDialogsTheme(themeColor, transparency) {
transparencyInput.value = transparencyOutput.value = transparency;
const alpha = (100 - +transparency) / 100;
const alphaReduced = Math.min(alpha + 0.3, 1);
const {h, s, l} = d3.hsl(themeColor || THEME_COLOR);
themeColorInput.value = themeColor || THEME_COLOR;
themeHueInput.value = h;
const getRGBA = (hue, saturation, lightness, alpha) => {
const color = d3.hsl(hue, saturation, lightness, alpha);
return color.toString();
};
const theme = [
{name: "--bg-main", h, s, l, alpha},
{name: "--bg-lighter", h, s, l: l + 0.02, alpha},
{name: "--bg-light", h, s: s - 0.02, l: l + 0.06, alpha},
{name: "--light-solid", h, s: s + 0.01, l: l + 0.05, alpha: 1},
{name: "--dark-solid", h, s, l: l - 0.2, alpha: 1},
{name: "--header", h, s: s, l: l - 0.03, alpha: alphaReduced},
{name: "--header-active", h, s: s, l: l - 0.09, alpha: alphaReduced},
{name: "--bg-disabled", h, s: s - 0.04, l: l + 0.09, alphaReduced},
{name: "--bg-dialogs", h: 0, s: 0, l: 0.98, alpha}
];
const sx = document.documentElement.style;
theme.forEach(({name, h, s, l, alpha}) => {
sx.setProperty(name, getRGBA(h, s, l, alpha));
});
}
function changeZoomExtent(value) {
const min = Math.max(+zoomExtentMin.value, 0.01),
max = Math.min(+zoomExtentMax.value, 200);
const min = Math.max(+zoomExtentMin.value, 0.01);
const max = Math.min(+zoomExtentMax.value, 200);
zoom.scaleExtent([min, max]);
const scale = Math.max(Math.min(+value, 200), 0.01);
const scale = minmax(+value, 0.01, 200);
zoom.scaleTo(svg, scale);
}
@ -482,13 +514,12 @@ function applyStoredOptions() {
.map((w) => +w);
if (localStorage.getItem('military')) options.military = JSON.parse(localStorage.getItem('military'));
changeDialogsTransparency(localStorage.getItem('transparency') || 5);
if (localStorage.getItem('tooltipSize')) changeTooltipSize(localStorage.getItem('tooltipSize'));
if (localStorage.getItem('regions')) changeStatesNumber(localStorage.getItem('regions'));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (localStorage.getItem('uiSize')) changeUIsize(localStorage.getItem('uiSize'));
else changeUIsize(Math.max(Math.min(rn(mapWidthInput.value / 1280, 1), 2.5), 1));
else changeUIsize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
const params = new URL(window.location.href).searchParams;
@ -497,8 +528,14 @@ function applyStoredOptions() {
if (width) mapWidthInput.value = width;
if (height) mapHeightInput.value = height;
const transparency = localStorage.getItem("transparency") || 5;
const themeColor = localStorage.getItem("themeColor");
changeDialogsTheme(themeColor, transparency);
// set shape rendering
viewbox.attr('shape-rendering', shapeRendering.value);
options.stateLabelsMode = stateLabelsModeInput.value;
}
// randomize options if randomization is allowed (not locked or options='default')
@ -529,10 +566,9 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === 'en-US';
const UK = navigator.language === 'en-GB';
if (randomize || !locked('distanceScale')) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored('distanceUnit')) distanceUnitInput.value = US || UK ? 'mi' : 'km';
if (!stored('heightUnit')) heightUnit.value = US || UK ? 'ft' : 'm';
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored('temperatureScale')) temperatureScale.value = US ? '°F' : '°C';
// World settings
@ -619,22 +655,16 @@ function restoreDefaultOptions() {
// Sticked menu Options listeners
document.getElementById('sticked').addEventListener('click', function (event) {
const id = event.target.id;
if (id === 'newMapButton') regeneratePrompt();
if (id === "newMapButton") regeneratePrompt("sticky button");
else if (id === 'saveButton') showSavePane();
else if (id === 'loadButton') showLoadPane();
else if (id === "exportButton") showExportPane();
else if (id === 'zoomReset') resetZoom(1000);
});
function regeneratePrompt() {
if (customization) {
tip('New map cannot be generated when edit mode is active, please exit the mode and retry', false, 'error');
return;
}
function regeneratePrompt(source) {
if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error");
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {
regenerateMap();
return;
}
if (workingTime < 5) return regenerateMap(source);
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
All unsaved changes made to the current map will be lost`;
@ -647,19 +677,20 @@ function regeneratePrompt() {
},
Generate: function () {
closeDialogs();
regenerateMap();
regenerateMap(source);
}
}
});
}
function showSavePane() {
document.getElementById('showLabels').checked = !hideLabels.checked;
const sharableLinkContainer = document.getElementById("sharableLinkContainer");
sharableLinkContainer.style.display = "none";
$('#saveMapData').dialog({
title: 'Save map',
resizable: false,
width: '30em',
width: "25em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Close: function () {
@ -669,21 +700,21 @@ function showSavePane() {
});
}
// download map data as GeoJSON
function saveGeoJSON() {
alertMessage.innerHTML = `You can export map data in GeoJSON format used in GIS tools such as QGIS.
Check out ${link('https://github.com/Azgaar/Fantasy-Map-Generator/wiki/GIS-data-export', 'wiki-page')} for guidance`;
function copyLinkToClickboard() {
const shrableLink = document.getElementById("sharableLink");
const link = shrableLink.getAttribute("href");
navigator.clipboard.writeText(link).then(() => tip("Link is copied to the clipboard", true, "success", 8000));
}
$('#alert').dialog({
title: 'GIS data export',
function showExportPane() {
document.getElementById("showLabels").checked = !hideLabels.checked;
$("#exportMapData").dialog({
title: "Export map data",
resizable: false,
width: '35em',
width: "26em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Cells: saveGeoJSON_Cells,
Routes: saveGeoJSON_Routes,
Rivers: saveGeoJSON_Rivers,
Markers: saveGeoJSON_Markers,
Close: function () {
$(this).dialog('close');
}
@ -691,11 +722,11 @@ function saveGeoJSON() {
});
}
function showLoadPane() {
async function showLoadPane() {
$('#loadMapData').dialog({
title: 'Load map',
resizable: false,
width: '17em',
width: "24em",
position: {my: 'center', at: 'center', of: 'svg'},
buttons: {
Close: function () {
@ -703,6 +734,25 @@ function showLoadPane() {
}
}
});
const loadFromDropboxButtons = document.getElementById("loadFromDropboxButtons");
const fileSelect = document.getElementById("loadFromDropboxSelect");
const files = await Cloud.providers.dropbox.list();
if (!files) {
loadFromDropboxButtons.style.display = "none";
fileSelect.innerHTML = `<option value="" disabled selected>Save files to Dropbox first</option>`;
return;
}
loadFromDropboxButtons.style.display = "block";
fileSelect.innerHTML = "";
files.forEach(file => {
const opt = document.createElement("option");
opt.innerText = file.name;
opt.value = file.path;
fileSelect.appendChild(opt);
});
}
function loadURL() {
@ -747,7 +797,9 @@ function openSaveTiles() {
status.innerHTML = '';
let loading = null;
$('#saveTilesScreen').dialog({
const inputs = document.getElementById("saveTilesScreen").querySelectorAll("input");
inputs.forEach(input => input.addEventListener("input", updateTilesOptions));
resizable: false,
title: 'Download tiles',
width: '23em',
@ -767,17 +819,12 @@ function openSaveTiles() {
}
},
close: () => {
debug.selectAll('*').remove();
inputs.forEach(input => input.removeEventListener("input", updateTilesOptions));
clearInterval(loading);
}
});
}
document
.getElementById('saveTilesScreen')
.querySelectorAll('input')
.forEach((el) => el.addEventListener('input', updateTilesOptions));
function updateTilesOptions() {
if (this?.tagName === 'INPUT') {
const {nextElementSibling: next, previousElementSibling: prev} = this;

1187
modules/ui/options.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,7 @@ function editProvinces() {
document.getElementById('provincesManually').addEventListener('click', enterProvincesManualAssignent);
document.getElementById('provincesManuallyApply').addEventListener('click', applyProvincesManualAssignent);
document.getElementById('provincesManuallyCancel').addEventListener('click', () => exitProvincesManualAssignment());
document.getElementById('provincesAdd').addEventListener('click', enterAddProvinceMode);
document.getElementById('provincesRelease').addEventListener('click', triggerProvincesRelease);
document.getElementById('provincesRecolor').addEventListener('click', recolorProvinces);
body.addEventListener('click', function (ev) {
@ -148,7 +148,6 @@ function editProvinces() {
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<span data-tip="Declare province independence (turn non-capital province with burgs into a new state)" class="icon-flag-empty ${separable ? '' : 'placeholder'} hide"></span>
<span data-tip="Toggle province focus" class="icon-pin ${focused ? '' : ' inactive'} hide"></span>
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
</div>`;
}
@ -228,74 +227,63 @@ function editProvinces() {
function capitalZoomIn(p) {
const capital = pack.provinces[p].burg;
const l = burgLabels.select("[data-id='" + capital + "']");
const x = +l.attr('x'),
y = +l.attr('y');
const x = +l.attr('x');
const y = +l.attr('y');
zoomTo(x, y, 8, 2000);
}
function triggerIndependencePromps(p) {
alertMessage.innerHTML = 'Are you sure you want to declare province independence? <br>It will turn province into a new state';
$('#alert').dialog({
resizable: false,
confirmationDialog({
title: 'Declare independence',
buttons: {
Declare: function () {
declareProvinceIndependence(p);
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
message: 'Are you sure you want to declare province independence? <br>It will turn province into a new state',
confirm: 'Declare',
onConfirm: () => {
const [oldStateId, newStateId] = declareProvinceIndependence(p);
updateStatesPostRelease([oldStateId], [newStateId]);
}
});
}
function declareProvinceIndependence(p) {
const states = pack.states,
provinces = pack.provinces,
cells = pack.cells;
if (provinces[p].burgs.some((b) => pack.burgs[b].capital)) {
tip('Cannot declare independence of a province having capital burg. Please change capital first', false, 'error');
return;
}
function declareProvinceIndependence(provinceId) {
const {states, provinces, cells, burgs} = pack;
const province = provinces[provinceId];
const {name, burg: burgId, burgs: provinceBurgs} = province;
const oldState = pack.provinces[p].state;
const newState = pack.states.length;
if (provinceBurgs.some((b) => burgs[b].capital)) return tip('Cannot declare independence of a province having capital burg. Please change capital first', false, 'error');
if (!burgId) return tip('Cannot declare independence of a province without burg', false, 'error');
const oldStateId = province.state;
const newStateId = states.length;
// turn province burg into a capital
const burg = provinces[p].burg;
if (!burg) return;
pack.burgs[burg].capital = 1;
moveBurgToGroup(burg, 'cities');
burgs[burgId].capital = 1;
moveBurgToGroup(burgId, 'cities');
// move all burgs to a new state
provinces[p].burgs.forEach((b) => (pack.burgs[b].state = newState));
province.burgs.forEach((b) => (burgs[b].state = newStateId));
// difine new state attributes
const center = pack.burgs[burg].cell;
const culture = pack.burgs[burg].culture;
const name = provinces[p].name;
const {cell: center, culture} = burgs[burgId];
const color = getRandomColor();
const coa = provinces[p].coa;
const coaEl = document.getElementById('provinceCOA' + p);
if (coaEl) coaEl.id = 'stateCOA' + newState;
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
const coa = province.coa;
const coaEl = document.getElementById('provinceCOA' + provinceId);
if (coaEl) coaEl.id = 'stateCOA' + newStateId;
emblems.select(`#provinceEmblems > use[data-i='${provinceId}']`).remove();
// update cells
cells.i
.filter((i) => cells.province[i] === p)
.filter((i) => cells.province[i] === provinceId)
.forEach((i) => {
cells.province[i] = 0;
cells.state[i] = newState;
cells.state[i] = newStateId;
});
// update diplomacy and reverse relations
const diplomacy = states.map((s) => {
if (!s.i || s.removed) return 'x';
let relations = states[oldState].diplomacy[s.i]; // relations between Nth state and old overlord
if (s.i === oldState) relations = 'Enemy';
// new state is Enemy to its old overlord
let relations = states[oldStateId].diplomacy[s.i]; // relations between Nth state and old overlord
// new state is Enemy to its old owner
if (s.i === oldStateId) relations = 'Enemy';
else if (relations === 'Ally') relations = 'Suspicion';
else if (relations === 'Friendly') relations = 'Suspicion';
else if (relations === 'Suspicion') relations = 'Neutral';
@ -307,28 +295,51 @@ function editProvinces() {
return relations;
});
diplomacy.push('x');
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldState].name}`]);
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}`]);
// create new state
states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: 'Generic', center, culture, military: [], alert: 1, coa});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
if (layerIsOn('toggleProvinces')) toggleProvinces();
if (!layerIsOn('toggleStates')) toggleStates();
else drawStates();
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
BurgsAndStates.drawStateLabels([newState, oldState]);
states.push({
i: newStateId,
name,
diplomacy,
provinces: [],
color,
expansionism: 0.5,
capital: burgId,
type: 'Generic',
center,
culture,
military: [],
alert: 1,
coa
});
// remove old province
unfog('focusProvince' + p);
if (states[oldState].provinces.includes(p)) states[oldState].provinces.splice(states[oldState].provinces.indexOf(p), 1);
provinces[p] = {i: p, removed: true};
states[oldStateId].provinces = states[oldStateId].provinces.filter((p) => p !== provinceId);
provinces[provinceId] = {i: provinceId, removed: true};
// draw emblem
COArenderer.add('state', newState, coa, pack.states[newState].pole[0], pack.states[newState].pole[1]);
return [oldStateId, newStateId];
}
function updateStatesPostRelease(oldStates, newStates) {
const allStates = unique([...oldStates, ...newStates]);
layerIsOn('toggleProvinces') && toggleProvinces();
layerIsOn('toggleStates') ? drawStates() : toggleStates();
layerIsOn('toggleBorders') ? drawBorders() : toggleBorders();
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms(newStates);
BurgsAndStates.drawStateLabels(allStates);
// redraw emblems
allStates.forEach((stateId) => {
emblems.select(`#stateEmblems > use[data-i='${stateId}']`)?.remove();
const {coa, pole} = pack.states[stateId];
COArenderer.add('state', stateId, coa, ...pole);
});
unfog();
closeDialogs();
editStates();
}
@ -547,7 +558,17 @@ function editProvinces() {
const provinces = pack.provinces
.filter((p) => p.i && !p.removed)
.map((p) => {
return {id: p.i + states.length - 1, i: p.i, state: p.state, color: p.color, name: p.name, fullName: p.fullName, area: p.area, urban: p.urban, rural: p.rural};
return {
id: p.i + states.length - 1,
i: p.i,
state: p.state,
color: p.color,
name: p.name,
fullName: p.fullName,
area: p.area,
urban: p.urban,
rural: p.rural
};
});
const data = states.concat(provinces);
const root = d3
@ -571,8 +592,6 @@ function editProvinces() {
</select>`;
alertMessage.innerHTML += `<div id='provinceInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3.select('#alertMessage').insert('svg', '#provinceInfo').attr('id', 'provincesTree').attr('width', width).attr('height', height).attr('font-size', '10px');
const graph = svg.append('g').attr('transform', `translate(10, 0)`);
document.getElementById('provincesTreeType').addEventListener('change', updateChart);
treeLayout(root);
@ -694,6 +713,34 @@ function editProvinces() {
provs.selectAll('text').call(d3.drag().on('drag', dragLabel)).classed('draggable', true);
}
function triggerProvincesRelease() {
confirmationDialog({
title: 'Release provinces',
message: `Are you sure you want to release all provinces?
</br>It will turn all separable provinces into independent states.
</br>Capital province and provinces without any burgs will state as they are`,
confirm: 'Release',
onConfirm: () => {
const oldStateIds = [];
const newStateIds = [];
body.querySelectorAll(':scope > div').forEach((el) => {
const provinceId = +el.dataset.id;
const province = pack.provinces[provinceId];
if (!province.burg) return;
if (province.burg === pack.states[province.state].capital) return;
if (province.burgs.some((burgId) => pack.burgs[burgId].capital)) return;
const [oldStateId, newStateId] = declareProvinceIndependence(provinceId);
oldStateIds.push(oldStateId);
newStateIds.push(newStateId);
});
updateStatesPostRelease(unique(oldStateIds), newStateIds);
}
});
}
function enterProvincesManualAssignent() {
if (!layerIsOn('toggleProvinces')) toggleProvinces();
if (!layerIsOn('toggleBorders')) toggleBorders();
@ -852,10 +899,8 @@ function editProvinces() {
}
function enterAddProvinceMode() {
if (this.classList.contains('pressed')) {
exitAddProvinceMode();
return;
}
if (this.classList.contains('pressed')) return exitAddProvinceMode();
customization = 12;
this.classList.add('pressed');
tip('Click on the map to place a new province center', true);
@ -864,24 +909,16 @@ function editProvinces() {
}
function addProvince() {
const cells = pack.cells,
provinces = pack.provinces;
const {cells, provinces} = pack;
const point = d3.mouse(this);
const center = findCell(point[0], point[1]);
if (cells.h[center] < 20) {
tip('You cannot place province into the water. Please click on a land cell', false, 'error');
return;
}
if (cells.h[center] < 20) return tip('You cannot place province into the water. Please click on a land cell', false, 'error');
const oldProvince = cells.province[center];
if (oldProvince && provinces[oldProvince].center === center) {
tip('The cell is already a center of a different province. Select other cell', false, 'error');
return;
}
if (oldProvince && provinces[oldProvince].center === center) return tip('The cell is already a center of a different province. Select other cell', false, 'error');
const state = cells.state[center];
if (!state) {
tip('You cannot create a province in neutral lands. Please assign this land to a state first', false, 'error');
return;
}
if (!state) return tip('You cannot create a province in neutral lands. Please assign this land to a state first', false, 'error');
if (d3.event.shiftKey === false) exitAddProvinceMode();
@ -892,8 +929,8 @@ function editProvinces() {
const name = burg ? pack.burgs[burg].name : Names.getState(Names.getCultureShort(c), c);
const formName = oldProvince ? provinces[oldProvince].formName : 'Province';
const fullName = name + ' ' + formName;
const stateColor = pack.states[state].color,
rndColor = getRandomColor();
const stateColor = pack.states[state].color;
const rndColor = getRandomColor();
const color = stateColor[0] === '#' ? d3.color(d3.interpolate(stateColor, rndColor)(0.2)).hex() : rndColor;
// generate emblem
@ -947,20 +984,20 @@ function editProvinces() {
function downloadProvincesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Province,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\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
let key = parseInt(el.dataset.id);
data += el.dataset.id + ',';
const key = parseInt(el.dataset.id);
const provincePack = pack.provinces[key];
data += el.dataset.name + ',';
data += el.dataset.form + ',';
data += el.dataset.state + ',';
data += provincePack.fullName + ',';
data += el.dataset.color + ',';
data += el.dataset.capital + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.provinces[key].rural * populationRate)},`;
data += `${Math.round(pack.provinces[key].urban * populationRate * urbanization)}\n`;
data += `${Math.round(provincePack.rural * populationRate)},`;
data += `${Math.round(provincePack.urban * populationRate * urbanization)}\n`;
});
const name = getFileName('Provinces') + '.csv';

File diff suppressed because it is too large Load diff

View file

@ -1,49 +1,53 @@
"use strict";
'use strict';
function overviewRegiments(state) {
if (customization) return;
closeDialogs(".stable");
if (!layerIsOn("toggleMilitary")) toggleMilitary();
closeDialogs('.stable');
if (!layerIsOn('toggleMilitary')) toggleMilitary();
const body = document.getElementById("regimentsBody");
const body = document.getElementById('regimentsBody');
updateFilter(state);
addLines();
$("#regimentsOverview").dialog();
$('#regimentsOverview').dialog();
if (modules.overviewRegiments) return;
modules.overviewRegiments = true;
updateHeaders();
$("#regimentsOverview").dialog({
title: "Regiments Overview", resizable: false, width: fitContent(),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
$('#regimentsOverview').dialog({
title: 'Regiments Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("regimentsOverviewRefresh").addEventListener("click", addLines);
document.getElementById("regimentsPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("regimentsAddNew").addEventListener("click", toggleAdd);
document.getElementById("regimentsExport").addEventListener("click", downloadRegimentsData);
document.getElementById("regimentsFilter").addEventListener("change", addLines);
document.getElementById('regimentsOverviewRefresh').addEventListener('click', addLines);
document.getElementById('regimentsPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('regimentsAddNew').addEventListener('click', toggleAdd);
document.getElementById('regimentsExport').addEventListener('click', downloadRegimentsData);
document.getElementById('regimentsFilter').addEventListener('change', addLines);
// update military types in header and tooltips
function updateHeaders() {
const header = document.getElementById("regimentsHeader");
header.querySelectorAll(".removable").forEach(el => el.remove());
const insert = html => document.getElementById("regimentsTotal").insertAdjacentHTML("beforebegin", html);
const header = document.getElementById('regimentsHeader');
header.querySelectorAll('.removable').forEach((el) => el.remove());
const insert = (html) => document.getElementById('regimentsTotal').insertAdjacentHTML('beforebegin', html);
for (const u of options.military) {
const label = capitalize(u.name.replace(/_/g, ' '));
insert(`<div data-tip="Regiment ${u.name} units number. Click to sort" class="sortable removable" data-sortby="${u.name}">${label}&nbsp;</div>`);
}
header.querySelectorAll(".removable").forEach(function(e) {
e.addEventListener("click", function() {sortLines(this);});
header.querySelectorAll('.removable').forEach(function (e) {
e.addEventListener('click', function () {
sortLines(this);
});
});
}
// add line for each state
function addLines() {
const state = +regimentsFilter.value;
body.innerHTML = "";
let lines = "";
body.innerHTML = '';
let lines = '';
const regiments = [];
for (const s of pack.states) {
@ -51,8 +55,8 @@ function overviewRegiments(state) {
if (state !== -1 && s.i !== state) continue; // specific state is selected
for (const r of s.military) {
const sortData = options.military.map(u => `data-${u.name}=${r.u[u.name]||0}`).join(" ");
const lineData = options.military.map(u => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name]||0}</div>`).join(" ");
const sortData = options.military.map((u) => `data-${u.name}=${r.u[u.name] || 0}`).join(' ');
const lineData = options.military.map((u) => `<div data-type="${u.name}" data-tip="${capitalize(u.name)} units number">${r.u[u.name] || 0}</div>`).join(' ');
lines += `<div class="states" data-id=${r.i} data-s="${s.i}" data-state="${s.name}" data-name="${r.name}" ${sortData} data-total="${r.a}">
<svg data-tip="${s.fullName}" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${s.color}" class="fillRect"></svg>
@ -70,90 +74,98 @@ function overviewRegiments(state) {
lines += `<div id="regimentsTotalLine" class="totalLine" data-tip="Total of all displayed regiments">
<div style="width: 21em; margin-left: 1em">Regiments: ${regiments.length}</div>
${options.military.map(u => `<div style="width:5em">${si(d3.sum(regiments.map(r => r.u[u.name]||0)))}</div>`).join(" ")}
<div style="width:5em">${si(d3.sum(regiments.map(r => r.a)))}</div>
${options.military.map((u) => `<div style="width:5em">${si(d3.sum(regiments.map((r) => r.u[u.name] || 0)))}</div>`).join(' ')}
<div style="width:5em">${si(d3.sum(regiments.map((r) => r.a)))}</div>
</div>`;
body.insertAdjacentHTML("beforeend", lines);
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
body.insertAdjacentHTML('beforeend', lines);
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
applySorting(regimentsHeader);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => regimentHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => regimentHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => regimentHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => regimentHighlightOff(ev)));
}
function updateFilter(state) {
const filter = document.getElementById("regimentsFilter");
const filter = document.getElementById('regimentsFilter');
filter.options.length = 0; // remove all options
filter.options.add(new Option(`all`, -1, false, state === -1));
const statesSorted = pack.states.filter(s => s.i && !s.removed).sort((a, b) => (a.name > b.name) ? 1 : -1);
statesSorted.forEach(s => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
const statesSorted = pack.states.filter((s) => s.i && !s.removed).sort((a, b) => (a.name > b.name ? 1 : -1));
statesSorted.forEach((s) => filter.options.add(new Option(s.name, s.i, false, s.i == state)));
}
function regimentHighlightOn(event) {
const state = +event.target.dataset.s;
const id = +event.target.dataset.id;
if (customization || !state) return;
armies.select(`g > g#regiment${state}-${id}`).transition().duration(2000).style("fill", "#ff0000");
armies.select(`g > g#regiment${state}-${id}`).transition().duration(2000).style('fill', '#ff0000');
}
function regimentHighlightOff(event) {
const state = +event.target.dataset.s;
const id = +event.target.dataset.id;
armies.select(`g > g#regiment${state}-${id}`).transition().duration(1000).style("fill", null);
armies.select(`g > g#regiment${state}-${id}`).transition().duration(1000).style('fill', null);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
const lines = body.querySelectorAll(":scope > div:not(.totalLine)");
const array = Array.from(lines), cache = [];
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const lines = body.querySelectorAll(':scope > div:not(.totalLine)');
const array = Array.from(lines),
cache = [];
const total = function(type) {
const total = function (type) {
if (cache[type]) cache[type];
cache[type] = d3.sum(array.map(el => +el.dataset[type]));
cache[type] = d3.sum(array.map((el) => +el.dataset[type]));
return cache[type];
}
};
lines.forEach(function(el) {
el.querySelectorAll("div").forEach(function(div) {
lines.forEach(function (el) {
el.querySelectorAll('div').forEach(function (div) {
const type = div.dataset.type;
if (type === "rate") return;
div.textContent = total(type) ? rn(+el.dataset[type] / total(type) * 100) + "%" : "0%";
if (type === 'rate') return;
div.textContent = total(type) ? rn((+el.dataset[type] / total(type)) * 100) + '%' : '0%';
});
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
addLines();
}
}
function toggleAdd() {
document.getElementById("regimentsAddNew").classList.toggle("pressed");
if (document.getElementById("regimentsAddNew").classList.contains("pressed")) {
viewbox.style("cursor", "crosshair").on("click", addRegimentOnClick);
tip("Click on map to create new regiment or fleet", true);
if (regimentAdd.offsetParent) regimentAdd.classList.add("pressed");
document.getElementById('regimentsAddNew').classList.toggle('pressed');
if (document.getElementById('regimentsAddNew').classList.contains('pressed')) {
viewbox.style('cursor', 'crosshair').on('click', addRegimentOnClick);
tip('Click on map to create new regiment or fleet', true);
if (regimentAdd.offsetParent) regimentAdd.classList.add('pressed');
} else {
clearMainTip();
viewbox.on("click", clicked).style("cursor", "default");
viewbox.on('click', clicked).style('cursor', 'default');
addLines();
if (regimentAdd.offsetParent) regimentAdd.classList.remove("pressed");
if (regimentAdd.offsetParent) regimentAdd.classList.remove('pressed');
}
}
function addRegimentOnClick() {
const state = +regimentsFilter.value;
if (state === -1) {tip("Please select state from the list", false, "error"); return;}
if (state === -1) {
tip('Please select state from the list', false, 'error');
return;
}
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 = pack.cells.p[cell][0],
y = pack.cells.p[cell][1];
const military = pack.states[state].military;
const i = military.length ? last(military).i + 1 : 0;
const n = +(pack.cells.h[cell] < 20); // naval or land
const reg = {a:0, cell, i, n, u:{}, x, y, bx:x, by:y, state, icon:"🛡️"};
const reg = {a: 0, cell, i, n, u: {}, x, y, bx: x, by: y, state, icon: '🛡️'};
reg.name = Military.getName(reg, military);
military.push(reg);
Military.generateNote(reg, pack.states[state]); // add legend
@ -162,19 +174,18 @@ function overviewRegiments(state) {
}
function downloadRegimentsData() {
const units = options.military.map(u => u.name);
let data = "State,Id,Name,"+units.map(u => capitalize(u)).join(",")+",Total\n"; // headers
const units = options.military.map((u) => u.name);
let data = 'State,Id,Name,' + units.map((u) => capitalize(u)).join(',') + ',Total\n'; // headers
body.querySelectorAll(":scope > div:not(.totalLine)").forEach(function(el) {
data += el.dataset.state + ",";
data += el.dataset.id + ",";
data += el.dataset.name + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += el.dataset.total + "\n";
body.querySelectorAll(':scope > div:not(.totalLine)').forEach(function (el) {
data += el.dataset.state + ',';
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += units.map((u) => el.dataset[u]).join(',') + ',';
data += el.dataset.total + '\n';
});
const name = getFileName("Regiments") + ".csv";
const name = getFileName('Regiments') + '.csv';
downloadFile(data, name);
}
}
}

View file

@ -1,23 +1,23 @@
"use strict";
'use strict';
function createRiver() {
if (customization) return;
closeDialogs();
if (!layerIsOn("toggleRivers")) toggleRivers();
if (!layerIsOn('toggleRivers')) toggleRivers();
document.getElementById("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
if (!layerIsOn("toggleCells")) toggleCells();
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
tip("Click to add river point, click again to remove", true);
debug.append("g").attr("id", "controlCells");
viewbox.style("cursor", "crosshair").on("click", onCellClick);
tip('Click to add river point, click again to remove', true);
debug.append('g').attr('id', 'controlCells');
viewbox.style('cursor', 'crosshair').on('click', onCellClick);
createRiver.cells = [];
const body = document.getElementById("riverCreatorBody");
const body = document.getElementById('riverCreatorBody');
$("#riverCreator").dialog({
title: "Create River",
$('#riverCreator').dialog({
title: 'Create River',
resizable: false,
position: {my: "left top", at: "left+10 top+10", of: "#map"},
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverCreator
});
@ -25,14 +25,14 @@ function createRiver() {
modules.createRiver = true;
// add listeners
document.getElementById("riverCreatorComplete").addEventListener("click", addRiver);
document.getElementById("riverCreatorCancel").addEventListener("click", () => $("#riverCreator").dialog("close"));
body.addEventListener("click", function (ev) {
document.getElementById('riverCreatorComplete').addEventListener('click', addRiver);
document.getElementById('riverCreatorCancel').addEventListener('click', () => $('#riverCreator').dialog('close'));
body.addEventListener('click', function (ev) {
const el = ev.target;
const cl = el.classList;
const cell = +el.parentNode.dataset.cell;
if (cl.contains("editFlux")) pack.cells.fl[cell] = +el.value;
else if (cl.contains("icon-trash-empty")) removeCell(cell);
if (cl.contains('editFlux')) pack.cells.fl[cell] = +el.value;
else if (cl.contains('icon-trash-empty')) removeCell(cell);
});
function onCellClick() {
@ -57,19 +57,19 @@ function createRiver() {
}
function removeCell(cell) {
createRiver.cells = createRiver.cells.filter(c => c !== cell);
createRiver.cells = createRiver.cells.filter((c) => c !== cell);
drawCells(createRiver.cells);
body.querySelector(`div[data-cell='${cell}']`)?.remove();
}
function drawCells(cells) {
debug
.select("#controlCells")
.select('#controlCells')
.selectAll(`polygon`)
.data(cells)
.join("polygon")
.attr("points", d => getPackPolygon(d))
.attr("class", "current");
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', 'current');
}
function addRiver() {
@ -77,12 +77,12 @@ function createRiver() {
const {addMeandering, getApproximateLength, getWidth, getOffset, getName, getRiverPath, getBasin} = Rivers;
const riverCells = createRiver.cells;
if (riverCells.length < 2) return tip("Add at least 2 cells", false, "error");
if (riverCells.length < 2) return tip('Add at least 2 cells', false, 'error');
const riverId = rivers.length ? last(rivers).i + 1 : 1;
const parent = cells.r[last(riverCells)] || riverId;
riverCells.forEach(cell => {
riverCells.forEach((cell) => {
if (!cells.r[cell]) cells.r[cell] = riverId;
});
@ -99,27 +99,24 @@ function createRiver() {
const name = getName(mouth);
const basin = getBasin(parent);
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: "River"});
rivers.push({i: riverId, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, cells: riverCells, basin, name, type: 'River'});
const id = 'river' + riverId;
// render river
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
viewbox
.select("#rivers")
.append("path")
.attr("id", "river" + riverId)
.attr("d", getRiverPath(meanderedPoints, widthFactor, sourceWidth));
viewbox.select('#rivers').append('path').attr('id', id).attr('d', getRiverPath(meanderedPoints, widthFactor, sourceWidth));
editRiver(riverId);
editRiver(id);
}
function closeRiverCreator() {
body.innerHTML = "";
debug.select("#controlCells").remove();
body.innerHTML = '';
debug.select('#controlCells').remove();
restoreDefaultEvents();
clearMainTip();
const forced = +document.getElementById("toggleCells").dataset.forced;
document.getElementById("toggleCells").dataset.forced = 0;
if (forced && layerIsOn("toggleCells")) toggleCells();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -8,9 +8,9 @@ function editRiver(id) {
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
elSelected = d3.select('#' + id);
elSelected = d3.select('#' + id).on('click', addControlPoint);
tip('Drag control points to change the river course. 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');
@ -19,8 +19,8 @@ function editRiver(id) {
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
drawControlPoints(riverPoints);
drawCells(cells);
$('#riverEditor').dialog({
title: 'Edit River',
@ -92,37 +92,35 @@ function editRiver(id) {
document.getElementById('riverWidth').value = width;
}
function drawControlPoints(points, cells) {
function drawControlPoints(points) {
debug
.select('#controlPoints')
.selectAll('circle')
.data(points)
.enter()
.append('circle')
.join('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
.call(d3.drag().on('start', dragControlPoint))
.on('click', removeControlPoint);
}
function drawCells(cells, type) {
function drawCells(cells) {
const validCells = [...new Set(cells)].filter((i) => pack.cells.i[i]);
debug
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.selectAll(`polygon`)
.data(validCells)
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
.attr('points', (d) => getPackPolygon(d));
}
function dragControlPoint() {
const {i, r, fl} = pack.cells;
const {r, fl} = pack.cells;
const river = getRiver();
const initCell = +this.dataset.cell;
const index = +this.dataset.i;
const {x: x0, y: y0} = d3.event;
const initCell = findCell(x0, y0);
let movedToCell = null;
@ -136,22 +134,18 @@ function editRiver(id) {
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
drawCells(river.cells);
});
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
if (movedToCell && !r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
redrawRiver();
}
});
}
@ -159,8 +153,10 @@ function editRiver(id) {
function redrawRiver() {
const river = getRiver();
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
@ -170,6 +166,27 @@ function editRiver(id) {
if (modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const point = [rn(x, 1), rn(y, 1)];
const river = getRiver();
if (!river.points) river.points = debug.selectAll('#controlPoints > *').data();
const index = getSegmentId(river.points, point, 2);
river.points.splice(index, 0, point);
drawControlPoints(river.points);
redrawRiver();
}
function removeControlPoint() {
this.remove();
redrawRiver();
const {cells} = getRiver();
drawCells(cells);
}
function changeName() {
getRiver().name = this.value;
}
@ -244,6 +261,8 @@ function editRiver(id) {
function closeRiverEditor() {
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
elSelected.on('click', null);
unselect();
clearMainTip();

View file

@ -0,0 +1,334 @@
'use strict';
function editRiver(id) {
if (customization) return;
if (elSelected && id === elSelected.attr('id')) return;
closeDialogs('.stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
document.getElementById('toggleCells').dataset.forced = +!layerIsOn('toggleCells');
if (!layerIsOn('toggleCells')) toggleCells();
<<<<<<< HEAD
elSelected = d3.select('#' + id);
tip('Drag control points to change the river course. For major changes please create a new river instead', true);
debug.append('g').attr('id', 'controlCells');
debug.append('g').attr('id', 'controlPoints');
=======
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);
debug.append("g").attr("id", "controlCells");
debug.append("g").attr("id", "controlPoints");
>>>>>>> master
updateRiverData();
const river = getRiver();
const {cells, points} = river;
const riverPoints = Rivers.getRiverPoints(cells, points);
<<<<<<< HEAD
drawControlPoints(riverPoints, cells);
drawCells(cells, 'current');
=======
drawControlPoints(riverPoints);
drawCells(cells);
>>>>>>> master
$('#riverEditor').dialog({
title: 'Edit River',
resizable: false,
position: {my: 'left top', at: 'left+10 top+10', of: '#map'},
close: closeRiverEditor
});
if (modules.editRiver) return;
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);
function getRiver() {
const riverId = +elSelected.attr('id').slice(5);
const river = pack.rivers.find((r) => r.i === riverId);
return river;
}
function updateRiverData() {
const r = getRiver();
document.getElementById('riverName').value = r.name;
document.getElementById('riverType').value = r.type;
const parentSelect = document.getElementById('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));
sortedRivers.forEach((river) => {
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;
document.getElementById('riverDischarge').value = r.discharge + ' m³/s';
document.getElementById('riverSourceWidth').value = r.sourceWidth;
document.getElementById('riverWidthFactor').value = r.widthFactor;
updateRiverLength(r);
updateRiverWidth(r);
}
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;
}
function updateRiverWidth(river) {
const {addMeandering, getWidth, getOffset} = Rivers;
const {cells, discharge, widthFactor, sourceWidth} = river;
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;
}
function drawControlPoints(points) {
debug
.select('#controlPoints')
.selectAll('circle')
.data(points)
<<<<<<< HEAD
.enter()
.append('circle')
.attr('cx', (d) => d[0])
.attr('cy', (d) => d[1])
.attr('r', 0.6)
.attr('data-cell', (d, i) => cells[i])
.attr('data-i', (d, i) => i)
.call(d3.drag().on('start', dragControlPoint));
=======
.join("circle")
.attr("cx", d => d[0])
.attr("cy", d => d[1])
.attr("r", 0.6)
.call(d3.drag().on("start", dragControlPoint))
.on("click", removeControlPoint);
>>>>>>> master
}
function drawCells(cells) {
const validCells = [...new Set(cells)].filter(i => pack.cells.i[i]);
debug
<<<<<<< HEAD
.select('#controlCells')
.selectAll(`polygon.${type}`)
.data(cells.filter((i) => pack.cells.i[i]))
.join('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('class', type);
=======
.select("#controlCells")
.selectAll(`polygon`)
.data(validCells)
.join("polygon")
.attr("points", d => getPackPolygon(d));
>>>>>>> master
}
function dragControlPoint() {
const {r, fl} = pack.cells;
const river = getRiver();
const {x: x0, y: y0} = d3.event;
const initCell = findCell(x0, y0);
let movedToCell = null;
d3.event.on('drag', function () {
const {x, y} = d3.event;
const currentCell = findCell(x, y);
movedToCell = initCell !== currentCell ? currentCell : null;
this.setAttribute('cx', x);
this.setAttribute('cy', y);
this.__data__ = [rn(x, 1), rn(y, 1)];
redrawRiver();
drawCells(river.cells);
});
<<<<<<< HEAD
d3.event.on('end', () => {
if (movedToCell) {
this.dataset.cell = movedToCell;
river.cells[index] = movedToCell;
drawCells(river.cells, 'current');
if (!r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
}
=======
d3.event.on("end", () => {
if (movedToCell && !r[movedToCell]) {
// swap river data
r[initCell] = 0;
r[movedToCell] = river.i;
const sourceFlux = fl[initCell];
fl[initCell] = fl[movedToCell];
fl[movedToCell] = sourceFlux;
redrawRiver();
>>>>>>> master
}
});
}
function redrawRiver() {
const river = getRiver();
<<<<<<< HEAD
river.points = debug.selectAll('#controlPoints > *').data();
const {cells, widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(cells, river.points);
=======
river.points = debug.selectAll("#controlPoints > *").data();
river.cells = river.points.map(([x, y]) => findCell(x, y));
const {widthFactor, sourceWidth} = river;
const meanderedPoints = Rivers.addMeandering(river.cells, river.points);
>>>>>>> master
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth);
elSelected.attr('d', path);
updateRiverLength(river);
if (modules.elevation) showEPForRiver(elSelected.node());
}
function addControlPoint() {
const [x, y] = d3.mouse(this);
const point = [rn(x, 1), rn(y, 1)];
const river = getRiver();
if (!river.points) river.points = debug.selectAll("#controlPoints > *").data();
const index = getSegmentId(river.points, point, 2);
river.points.splice(index, 0, point);
drawControlPoints(river.points);
redrawRiver();
}
function removeControlPoint() {
this.remove();
redrawRiver();
const {cells} = getRiver();
drawCells(cells);
}
function changeName() {
getRiver().name = this.value;
}
function changeType() {
getRiver().type = this.value;
}
function generateNameCulture() {
const r = getRiver();
r.name = riverName.value = Rivers.getName(r.mouth);
}
function generateNameRandom() {
const r = getRiver();
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length - 1));
}
function changeParent() {
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;
}
function changeSourceWidth() {
const river = getRiver();
river.sourceWidth = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function changeWidthFactor() {
const river = getRiver();
river.widthFactor = +this.value;
updateRiverWidth(river);
redrawRiver();
}
function showElevationProfile() {
modules.elevation = true;
showEPForRiver(elSelected.node());
}
function editRiverLegend() {
const id = elSelected.attr('id');
const river = getRiver();
editNotes(id, river.name + ' ' + river.type);
}
function removeRiver() {
alertMessage.innerHTML = 'Are you sure you want to remove the river and all its tributaries';
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river and tributaries',
buttons: {
Remove: function () {
$(this).dialog('close');
const river = +elSelected.attr('id').slice(5);
Rivers.remove(river);
elSelected.remove();
$('#riverEditor').dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function closeRiverEditor() {
<<<<<<< HEAD
debug.select('#controlPoints').remove();
debug.select('#controlCells').remove();
=======
debug.select("#controlPoints").remove();
debug.select("#controlCells").remove();
elSelected.on("click", null);
>>>>>>> master
unselect();
clearMainTip();
const forced = +document.getElementById('toggleCells').dataset.forced;
document.getElementById('toggleCells').dataset.forced = 0;
if (forced && layerIsOn('toggleCells')) toggleCells();
}
}

View file

@ -91,7 +91,7 @@ function overviewRivers() {
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
const river = rivers.select('#river' + r).node();
highlightElement(river);
highlightElement(river, 3);
}
function toggleBasinsHightlight() {

View file

@ -0,0 +1,187 @@
'use strict';
function overviewRivers() {
if (customization) return;
closeDialogs('#riversOverview, .stable');
if (!layerIsOn('toggleRivers')) toggleRivers();
const body = document.getElementById('riversBody');
riversOverviewAddLines();
$('#riversOverview').dialog();
if (modules.overviewRivers) return;
modules.overviewRivers = true;
$('#riversOverview').dialog({
title: 'Rivers Overview',
resizable: false,
width: fitContent(),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById('riversOverviewRefresh').addEventListener('click', riversOverviewAddLines);
document.getElementById('addNewRiver').addEventListener('click', toggleAddRiver);
document.getElementById('riverCreateNew').addEventListener('click', createRiver);
document.getElementById('riversBasinHighlight').addEventListener('click', toggleBasinsHightlight);
document.getElementById('riversExport').addEventListener('click', downloadRiversData);
document.getElementById('riversRemoveAll').addEventListener('click', triggerAllRiversRemove);
// add line for each river
function riversOverviewAddLines() {
body.innerHTML = '';
let lines = '';
const unit = distanceUnitInput.value;
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 basin = pack.rivers.find((river) => river.i === r.basin)?.name;
lines += `<div class="states" data-id=${r.i} data-name="${r.name}" data-type="${r.type}" data-discharge="${r.discharge}" data-length="${r.length}" data-width="${r.width}" 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 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>
<div data-tip="River mouth width" class="biomeArea">${width}</div>
<input data-tip="River basin (name of the main stem)" class="stateName" value="${basin}" disabled>
<span data-tip="Edit river" class="icon-pencil"></span>
<span data-tip="Remove river" class="icon-trash-empty"></span>
</div>`;
}
body.insertAdjacentHTML('beforeend', lines);
// update footer
riversFooterNumber.innerHTML = pack.rivers.length;
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;
const averageWidth = rn(d3.mean(pack.rivers.map((r) => r.width)), 3);
riversFooterWidth.innerHTML = rn(averageWidth * distanceScaleInput.value, 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));
applySorting(riversHeader);
}
function riverHighlightOn(event) {
if (!layerIsOn('toggleRivers')) toggleRivers();
const r = +event.target.dataset.id;
rivers
.select('#river' + r)
.attr('stroke', 'red')
.attr('stroke-width', 1);
}
function riverHighlightOff(e) {
const r = +e.target.dataset.id;
rivers
.select('#river' + r)
.attr('stroke', null)
.attr('stroke-width', null);
}
function zoomToRiver() {
const r = +this.parentNode.dataset.id;
<<<<<<< HEAD
const river = rivers.select('#river' + r).node();
highlightElement(river);
=======
const river = rivers.select("#river" + r).node();
highlightElement(river, 3);
>>>>>>> master
}
function toggleBasinsHightlight() {
if (rivers.attr('data-basin') === 'hightlighted') {
rivers.selectAll('*').attr('fill', null);
rivers.attr('data-basin', null);
} 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'];
basins.forEach((b, i) => {
const color = colors[i % colors.length];
pack.rivers
.filter((r) => r.basin === b)
.forEach((r) => {
rivers.select('#river' + r.i).attr('fill', color);
});
});
}
}
function downloadRiversData() {
let data = 'Id,River,Type,Discharge,Length,Width,Basin\n'; // headers
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;
data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(',') + '\n';
});
const name = getFileName('Rivers') + '.csv';
downloadFile(data, name);
}
function openRiverEditor() {
const id = 'river' + this.parentNode.dataset.id;
editRiver(id);
}
function triggerRiverRemove() {
const river = +this.parentNode.dataset.id;
alertMessage.innerHTML = `Are you sure you want to remove the river?
All tributaries will be auto-removed`;
$('#alert').dialog({
resizable: false,
width: '22em',
title: 'Remove river',
buttons: {
Remove: function () {
Rivers.remove(river);
riversOverviewAddLines();
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function triggerAllRiversRemove() {
alertMessage.innerHTML = `Are you sure you want to remove all rivers?`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rivers',
buttons: {
Remove: function () {
$(this).dialog('close');
removeAllRivers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
function removeAllRivers() {
pack.rivers = [];
pack.cells.r = new Uint16Array(pack.cells.i.length);
rivers.selectAll('*').remove();
riversOverviewAddLines();
}
}

View file

@ -502,6 +502,7 @@ function editStates() {
pack.cells.province.forEach((pr, i) => {
if (pr === p) pack.cells.province[i] = 0;
});
const coaId = 'provinceCOA' + p;
if (document.getElementById(coaId)) document.getElementById(coaId).remove();
emblems.select(`#provinceEmblems > use[data-i='${p}']`).remove();
@ -568,19 +569,20 @@ function editStates() {
function showStatesChart() {
// build hierarchy tree
const data = pack.states.filter((s) => !s.removed);
const statesData = pack.states.filter((s) => !s.removed);
if (statesData.length < 2) return tip('There are no states to show', false, 'error');
const root = d3
.stratify()
.id((d) => d.i)
.parentId((d) => (d.i ? 0 : null))(data)
.parentId((d) => (d.i ? 0 : null))(statesData)
.sum((d) => d.area)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value,
height = 150 + 200 * uiSizeOutput.value;
const size = 150 + 200 * uiSizeOutput.value;
const margin = {top: 0, right: -50, bottom: 0, left: -50};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
const w = size - margin.left - margin.right;
const h = size - margin.top - margin.bottom;
const treeLayout = d3.pack().size([w, h]).padding(3);
// prepare svg
@ -592,12 +594,13 @@ function editStates() {
<option value="burgs">Burgs number</option>
</select>`;
alertMessage.innerHTML += `<div id='statesInfo' class='chartInfo'>&#8205;</div>`;
const svg = d3
.select('#alertMessage')
.insert('svg', '#statesInfo')
.attr('id', 'statesTree')
.attr('width', width)
.attr('height', height)
.attr('width', size)
.attr('height', size)
.style('font-family', 'Almendra SC')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central');
@ -819,9 +822,9 @@ function editStates() {
}
function applyStatesManualAssignent() {
const cells = pack.cells,
affectedStates = [],
affectedProvinces = [];
const {cells} = pack;
const affectedStates = [];
const affectedProvinces = [];
statesBody
.select('#temp')
@ -837,77 +840,143 @@ function editStates() {
if (affectedStates.length) {
refreshStatesEditor();
if (!layerIsOn('toggleStates')) toggleStates();
else drawStates();
layerIsOn('toggleStates') ? drawStates() : toggleStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
if (!layerIsOn('toggleBorders')) toggleBorders();
else drawBorders();
layerIsOn('toggleBorders') ? drawBorders() : toggleBorders();
if (layerIsOn('toggleProvinces')) drawProvinces();
}
exitStatesManualAssignment();
}
function adjustProvinces(affectedProvinces) {
const {cells, provinces, states} = pack;
const form = {Zone: 1, Area: 1, Territory: 2, Province: 1};
affectedProvinces.forEach((p) => {
if (!p) return; // do nothing if neutral lands are captured
const old = provinces[p].state;
// remove province from state provinces list
if (states[old]?.provinces?.includes(p)) states[old].provinces.splice(states[old].provinces.indexOf(p), 1);
const {cells, provinces, states, burgs} = pack;
affectedProvinces.forEach((provinceId) => {
// find states owning at least 1 province cell
const provCells = cells.i.filter((i) => cells.province[i] === p);
const provCells = cells.i.filter((i) => cells.state[i] && cells.province[i] === provinceId);
const provStates = [...new Set(provCells.map((i) => cells.state[i]))];
// assign province to its center owner; if center is neutral, remove province
const owner = cells.state[provinces[p].center];
if (owner) {
const name = provinces[p].name;
// province is captured completely => change owner or remove
if (provinceId && provStates.length === 1) return changeProvinceOwner(provinceId, provStates[0], provCells);
// if province is a historical part of another state's province, unite with old province
const part = states[owner].provinces.find((n) => name.includes(provinces[n].name));
if (part) {
provinces[p].removed = true;
provCells.filter((i) => cells.state[i] === owner).forEach((i) => (cells.province[i] = part));
} else {
provinces[p].state = owner;
states[owner].provinces.push(p);
provinces[p].color = getMixedColor(states[owner].color);
}
} else {
provinces[p].removed = true;
provCells.filter((i) => !cells.state[i]).forEach((i) => (cells.province[i] = 0));
}
// create new provinces for non-main part
provStates
.filter((s) => s && s !== owner)
.forEach((s) =>
createProvince(
p,
s,
provCells.filter((i) => cells.state[i] === s)
)
);
// province is captured partially => split province
splitProvince(provinceId, provStates, provCells);
});
function createProvince(initProv, state, provCells) {
const province = provinces.length;
provCells.forEach((i) => (cells.province[i] = province));
function changeProvinceOwner(provinceId, newOwnerId, provinceCells) {
const province = provinces[provinceId];
const prevOwner = states[province.state];
const burgCell = provCells.find((i) => cells.burg[i]);
const center = burgCell ? burgCell : provCells[0];
const burg = burgCell ? cells.burg[burgCell] : 0;
// remove province from old owner list
prevOwner.provinces = prevOwner.provinces.filter((province) => province !== provinceId);
const name = burgCell && P(0.7) ? getAdjective(pack.burgs[burg].name) : getAdjective(states[state].name) + ' ' + provinces[initProv].name.split(' ').slice(-1)[0];
const formName = name.split(' ').length > 1 ? provinces[initProv].formName : rw(form);
const fullName = name + ' ' + formName;
const color = getMixedColor(states[state].color);
provinces.push({i: province, state, center, burg, name, formName, fullName, color});
if (newOwnerId) {
// new owner is a state => change owner
province.state = newOwnerId;
states[newOwnerId].provinces.push(provinceId);
} else {
// new owner is neutral => remove province
provinces[provinceId] = {i: provinceId, removed: true};
provinceCells.forEach((i) => {
cells.province[i] = 0;
});
}
}
function splitProvince(provinceId, provinceStates, provinceCells) {
const province = provinces[provinceId];
const prevOwner = states[province.state];
const provinceCenterOwner = cells.state[province.center];
provinceStates.forEach((stateId) => {
const stateProvinceCells = provinceCells.filter((i) => cells.state[i] === stateId);
if (stateId === provinceCenterOwner) {
// province center is owned by the same state => do nothing for this state
if (stateId === prevOwner.i) return;
// province center is captured by neutrals => remove state
if (!stateId) {
provinces[provinceId] = {i: provinceId, removed: true};
stateProvinceCells.forEach((i) => {
cells.province[i] = 0;
});
return;
}
// reassign province ownership to province center owner
prevOwner.provinces = prevOwner.provinces.filter((province) => province !== provinceId);
province.state = stateId;
province.color = getMixedColor(states[stateId].color);
states[stateId].provinces.push(provinceId);
return;
}
// province cells captured by neutrals => clear province
if (!stateId) {
stateProvinceCells.forEach((i) => {
cells.province[i] = 0;
});
return;
}
// a few province cells owned by state => add to closes province
if (stateProvinceCells.length < 20) {
const closestProvince = findClosestProvince(provinceId, stateId, stateProvinceCells);
if (closestProvince) {
stateProvinceCells.forEach((i) => {
cells.province[i] = closestProvince;
});
return;
}
}
// some province cells owned by state => create new province
createProvince(province, stateId, stateProvinceCells);
});
}
function createProvince(oldProvince, stateId, provinceCells) {
const newProvinceId = provinces.length;
const burgCell = provinceCells.find((i) => cells.burg[i]);
const center = burgCell ? burgCell : provinceCells[0];
const burgId = burgCell ? cells.burg[burgCell] : 0;
const burg = burgId ? burgs[burgId] : null;
const culture = cells.culture[center];
const nameByBurg = burgCell && P(0.5);
const name = nameByBurg ? burg.name : oldProvince.name || Names.getState(Names.getCultureShort(culture), culture);
const formOptions = ['Zone', 'Area', 'Territory', 'Province'];
const formName = burgCell && oldProvince.formName ? oldProvince.formName : ra(formOptions);
const color = getMixedColor(states[stateId].color);
const kinship = nameByBurg ? 0.8 : 0.4;
const type = BurgsAndStates.getType(center, burg?.port);
const coa = COA.generate(burg?.coa || states[stateId].coa, kinship, burg ? null : 0.9, type);
coa.shield = COA.getShield(culture, stateId);
provinces.push({i: newProvinceId, state: stateId, center, burg: burgId, name, formName, fullName: `${name} ${formName}`, color, coa});
provinceCells.forEach((i) => {
cells.province[i] = newProvinceId;
});
states[stateId].provinces.push(newProvinceId);
}
function findClosestProvince(provinceId, stateId, sourceCells) {
const borderCell = sourceCells.find((i) =>
cells.c[i].some((c) => {
return cells.state[c] === stateId && cells.province[c] && cells.province[c] !== provinceId;
})
);
const closesProvince = borderCell && cells.c[borderCell].map((c) => cells.province[c]).find((province) => province && province !== provinceId);
return closesProvince;
}
}
@ -1007,7 +1076,22 @@ function editStates() {
cells.state[center] = newState;
cells.province[center] = 0;
states.push({i: newState, name, diplomacy, provinces: [], color, expansionism: 0.5, capital: burg, type: 'Generic', center, culture, military: [], alert: 1, coa, pole});
states.push({
i: newState,
name,
diplomacy,
provinces: [],
color,
expansionism: 0.5,
capital: burg,
type: 'Generic',
center,
culture,
military: [],
alert: 1,
coa,
pole
});
BurgsAndStates.collectStatistics();
BurgsAndStates.defineStateForms([newState]);
adjustProvinces([cells.province[center]]);
@ -1050,12 +1134,13 @@ function editStates() {
function downloadStatesData() {
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,State,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
let data = 'Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area ' + unit + ',Total Population,Rural Population,Urban Population\n'; // headers
body.querySelectorAll(':scope > div').forEach(function (el) {
const key = parseInt(el.dataset.id);
const statePack = pack.states[key];
data += el.dataset.id + ',';
data += el.dataset.name + ',';
data += (statePack.fullName ? statePack.fullName : '') + ',';
data += el.dataset.form + ',';
data += el.dataset.color + ',';
data += el.dataset.capital + ',';
@ -1066,8 +1151,8 @@ function editStates() {
data += el.dataset.burgs + ',';
data += el.dataset.area + ',';
data += el.dataset.population + ',';
data += `${Math.round(pack.states[key].rural * populationRate)},`;
data += `${Math.round(pack.states[key].urban * populationRate * urbanization)}\n`;
data += `${Math.round(statePack.rural * populationRate)},`;
data += `${Math.round(statePack.urban * populationRate * urbanization)}\n`;
});
const name = getFileName('States') + '.csv';

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

1701
modules/ui/style.js.orig Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,15 +1,15 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
'use strict';
// module to control the Tools options (click to edit, to re-geenerate, tp add)
toolsContent.addEventListener('click', function (event) {
if (customization) {
tip('Please exit the customization mode first', false, 'warning');
return;
}
if (event.target.tagName !== 'BUTTON') return;
if (!['BUTTON', 'I'].includes(event.target.tagName)) return;
const button = event.target.id;
// Click to open Editor buttons
// click on open Editor buttons
if (button === 'editHeightmapButton') editHeightmap();
else if (button === 'editBiomesButton') editBiomes();
else if (button === 'editStatesButton') editStates();
@ -17,7 +17,6 @@ toolsContent.addEventListener('click', function (event) {
else if (button === 'editDiplomacyButton') editDiplomacy();
else if (button === 'editCulturesButton') editCultures();
else if (button === 'editReligions') editReligions();
else if (button === 'editResources') editResources();
else if (button === 'editEmblemButton') openEmblemEditor();
else if (button === 'editNamesBaseButton') editNamesbase();
else if (button === 'editUnitsButton') editUnits();
@ -26,9 +25,10 @@ toolsContent.addEventListener('click', function (event) {
else if (button === 'overviewBurgsButton') overviewBurgs();
else if (button === 'overviewRiversButton') overviewRivers();
else if (button === 'overviewMilitaryButton') overviewMilitary();
else if (button === 'overviewMarkersButton') overviewMarkers();
else if (button === 'overviewCellsButton') viewCellDetails();
// Click to Regenerate buttons
// click on Regenerate buttons
if (event.target.parentNode.id === 'regenerateFeature') {
if (sessionStorage.getItem('regenerateFeatureDontAsk')) {
processFeatureRegeneration(event, button);
@ -61,7 +61,10 @@ toolsContent.addEventListener('click', function (event) {
});
}
// Click to Add buttons
// click on Configure regenerate buttons
if (button === 'configRegenerateMarkers') configMarkersGeneration();
// click on Add buttons
if (button === 'addLabel') toggleAddLabel();
else if (button === 'addBurgTool') toggleAddBurg();
else if (button === 'addRiver') toggleAddRiver();
@ -84,13 +87,12 @@ function processFeatureRegeneration(event, button) {
else if (button === 'regenerateStates') regenerateStates();
else if (button === 'regenerateProvinces') regenerateProvinces();
else if (button === 'regenerateBurgs') regenerateBurgs();
else if (button === 'regenerateResources') regenerateResources();
else if (button === 'regenerateEmblems') regenerateEmblems();
else if (button === 'regenerateReligions') regenerateReligions();
else if (button === 'regenerateCultures') regenerateCultures();
else if (button === 'regenerateMilitary') regenerateMilitary();
else if (button === 'regenerateIce') regenerateIce();
else if (button === 'regenerateMarkers') regenerateMarkers(event);
else if (button === 'regenerateMarkers') regenerateMarkers();
else if (button === 'regenerateZones') regenerateZones(event);
}
@ -119,6 +121,7 @@ function regenerateRivers() {
Lakes.defineGroup();
Rivers.specify();
if (!layerIsOn('toggleRivers')) toggleRivers();
else drawRivers();
}
function recalculatePopulation() {
@ -137,21 +140,11 @@ function recalculatePopulation() {
function regenerateStates() {
const localSeed = Math.floor(Math.random() * 1e9); // new random seed
Math.random = aleaPRNG(localSeed);
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (!burgs.length) {
tip('No burgs to generate states. Please create burgs first', false, 'error');
return;
}
if (burgs.length < +regionsInput.value) {
tip(`Not enough burgs to generate ${regionsInput.value} states. Will generate only ${burgs.length} states`, false, 'warn');
}
// burg local ids sorted by a bit randomized population:
const sorted = burgs
.map((b, i) => [i, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map((b) => b[0]);
const capitalsTree = d3.quadtree();
const statesCount = +regionsInput.value;
const burgs = pack.burgs.filter((b) => b.i && !b.removed);
if (!burgs.length) return tip('There are no any burgs to generate states. Please create burgs first', false, 'error');
if (burgs.length < statesCount) tip(`Not enough burgs to generate ${statesCount} states. Will generate only ${burgs.length} states`, false, 'warn');
// turn all old capitals into towns
burgs
@ -168,8 +161,7 @@ function regenerateStates() {
unfog();
// if desired states number is 0
if (regionsInput.value == 0) {
if (!statesCount) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, 'warn');
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
@ -185,26 +177,34 @@ function regenerateStates() {
return;
}
const neutral = pack.states[0].name;
const count = Math.min(+regionsInput.value, burgs.length);
// burg local ids sorted by a bit randomized population:
const sortedBurgs = burgs
.map((b, i) => [b, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map((b) => b[0]);
const capitalsTree = d3.quadtree();
const neutral = pack.states[0].name; // neutrals name
const count = Math.min(statesCount, burgs.length) + 1; // +1 for neutral
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
pack.states = d3.range(count).map((i) => {
if (!i) return {i, name: neutral};
let capital = null,
x = 0,
y = 0;
for (const i of sorted) {
capital = burgs[i];
(x = capital.x), (y = capital.y);
if (capitalsTree.find(x, y, spacing) === undefined) break;
let capital = null;
for (const burg of sortedBurgs) {
const {x, y} = burg;
if (capitalsTree.find(x, y, spacing) === undefined) {
burg.capital = 1;
capital = burg;
capitalsTree.add([x, y]);
moveBurgToGroup(burg.i, 'cities');
break;
}
spacing = Math.max(spacing - 1, 1);
}
capitalsTree.add([x, y]);
capital.capital = 1;
moveBurgToGroup(capital.i, 'cities');
const culture = capital.culture;
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, '', 0);
const name = Names.getState(basename, culture);
@ -337,13 +337,6 @@ function regenerateBurgs() {
if (document.getElementById('statesEditorRefresh').offsetParent) statesEditorRefresh.click();
}
function regenerateResources() {
Resources.generate();
goods.selectAll('*').remove();
if (layerIsOn('toggleResources')) drawResources();
refreshAllEditors();
}
function regenerateEmblems() {
// remove old emblems
document.querySelectorAll('[id^=stateCOA]').forEach((el) => el.remove());
@ -424,23 +417,11 @@ function regenerateIce() {
drawIce();
}
function regenerateMarkers(event) {
if (isCtrlClick(event)) prompt('Please provide markers number multiplier', {default: 1, step: 0.01, min: 0, max: 100}, (v) => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2));
function addNumberOfMarkers(number) {
// remove existing markers and assigned notes
markers
.selectAll('use')
.each(function () {
const index = notes.findIndex((n) => n.id === this.id);
if (index != -1) notes.splice(index, 1);
})
.remove();
addMarkers(number);
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
function regenerateMarkers() {
Markers.regenerate();
turnButtonOn('toggleMarkers');
drawMarkers();
if (document.getElementById('markersOverviewRefresh').offsetParent) markersOverviewRefresh.click();
}
function regenerateZones(event) {
@ -485,7 +466,10 @@ function addLabelOnClick() {
const name = Names.getCulture(culture);
const id = getNextId('label');
let group = labels.select('#addedLabels');
// use most recently selected label group
let selected = labelGroupSelect.value;
const symbol = selected ? '#' + selected : '#addedLabels';
let group = labels.select(symbol);
if (!group.size())
group = labels
.append('g')
@ -495,7 +479,6 @@ function addLabelOnClick() {
.attr('stroke', '#3a3a3a')
.attr('stroke-width', 0)
.attr('font-family', 'Almendra SC')
.attr('data-font', 'Almendra+SC')
.attr('font-size', 18)
.attr('data-size', 18)
.attr('filter', null);
@ -697,7 +680,7 @@ function addRouteOnClick() {
}
function toggleAddMarker() {
const pressed = document.getElementById('addMarker').classList.contains('pressed');
const pressed = document.getElementById('addMarker')?.classList.contains('pressed');
if (pressed) {
unpressClickToAddButton();
return;
@ -705,45 +688,115 @@ function toggleAddMarker() {
addFeature.querySelectorAll('button.pressed').forEach((b) => b.classList.remove('pressed'));
addMarker.classList.add('pressed');
closeDialogs('.stable');
markersAddFromOverview.classList.add('pressed');
viewbox.style('cursor', 'crosshair').on('click', addMarkerOnClick);
tip('Click on map to add a marker. Hold Shift to add multiple', true);
if (!layerIsOn('toggleMarkers')) toggleMarkers();
}
function addMarkerOnClick() {
const {markers} = pack;
const point = d3.mouse(this);
const x = rn(point[0], 2),
y = rn(point[1], 2);
const id = getNextId('markerElement');
const x = rn(point[0], 2);
const y = rn(point[1], 2);
const i = last(markers).i + 1;
const selected = markerSelectGroup.value;
const valid =
selected &&
d3
.select('#defs-markers')
.select('#' + selected)
.size();
const symbol = valid ? '#' + selected : '#marker0';
const added = markers.select("[data-id='" + symbol + "']").size();
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr('data-size') : 1;
if (isNaN(desired)) desired = 1;
const size = desired * 5 + 25 / scale;
const isMarkerSelected = elSelected?.node()?.parentElement?.id === 'markers';
const selectedMarker = isMarkerSelected ? markers.find((marker) => marker.i === +elSelected.attr('id').slice(6)) : null;
const baseMarker = selectedMarker || {icon: '❓'};
const marker = {...baseMarker, i, x, y};
markers
.append('use')
.attr('id', id)
.attr('xlink:href', symbol)
.attr('data-id', symbol)
.attr('data-x', x)
.attr('data-y', y)
.attr('x', x - size / 2)
.attr('y', y - size)
.attr('data-size', desired)
.attr('width', size)
.attr('height', size);
markers.push(marker);
const markersElement = document.getElementById('markers');
const rescale = +markersElement.getAttribute('rescale');
markersElement.insertAdjacentHTML('beforeend', drawMarker(marker, rescale));
if (d3.event.shiftKey === false) unpressClickToAddButton();
if (d3.event.shiftKey === false) {
document.getElementById('markerAdd').classList.remove('pressed');
document.getElementById('markersAddFromOverview').classList.remove('pressed');
unpressClickToAddButton();
}
}
function configMarkersGeneration() {
drawConfigTable();
function drawConfigTable() {
const {markers} = pack;
const config = Markers.getConfig();
const headers = `<thead style='font-weight:bold'><tr>
<td data-tip="Marker type name">Type</td>
<td data-tip="Marker icon">Icon</td>
<td data-tip="Marker number multiplier">Multiplier</td>
<td data-tip="Number of markers of that type on the current map">Number</td>
</tr></thead>`;
const lines = config.map(({type, icon, multiplier}, index) => {
const inputId = `markerIconInput${index}`;
return `<tr>
<td><input value="${type}" /></td>
<td>
<input id="${inputId}" style="width: 5em" value="${icon}" />
<i class="icon-edit pointer" style="position: absolute; margin:.4em 0 0 -1.4em; font-size:.85em"></i>
</td>
<td><input type="number" min="0" max="100" step="0.1" value="${multiplier}" /></td>
<td style="text-align:center">${markers.filter((marker) => marker.type === type).length}</td>
</tr>`;
});
const table = `<table class="table">${headers}<tbody>${lines.join('')}</tbody></table>`;
alertMessage.innerHTML = table;
alertMessage.querySelectorAll('i').forEach((selectIconButton) => {
selectIconButton.addEventListener('click', function () {
const input = this.previousElementSibling;
selectIcon(input.value, (icon) => (input.value = icon));
});
});
}
const applyChanges = () => {
const rows = alertMessage.querySelectorAll('tbody > tr');
const rowsData = Array.from(rows).map((row) => {
const inputs = row.querySelectorAll('input');
return {
type: inputs[0].value,
icon: inputs[1].value,
multiplier: parseFloat(inputs[2].value)
};
});
const config = Markers.getConfig();
const newConfig = config.map((markerType, index) => {
const {type, icon, multiplier} = rowsData[index];
return {...markerType, type, icon, multiplier};
});
Markers.setConfig(newConfig);
};
$('#alert').dialog({
resizable: false,
title: 'Markers generation settings',
position: {my: 'left top', at: 'left+10 top+10', of: 'svg', collision: 'fit'},
buttons: {
Regenerate: () => {
applyChanges();
regenerateMarkers();
drawConfigTable();
},
Close: function () {
$(this).dialog('close');
}
},
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip('Apply changes and regenerate markers'));
buttons[1].addEventListener('mousemove', () => tip('Close the window'));
},
close: function () {
$(this).dialog('destroy');
}
});
}
function viewCellDetails() {

1014
modules/ui/tools.js.orig Normal file

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,8 @@ function editUnits() {
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
document.getElementById('urbanDensityOutput').addEventListener('input', changeUrbanDensity);
document.getElementById('urbanDensityInput').addEventListener('change', changeUrbanDensity);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
@ -93,6 +95,10 @@ function editUnits() {
urbanization = +this.value;
}
function changeUrbanDensity() {
urbanDensity = +this.value;
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById('distanceScaleOutput').value = 3;
@ -135,8 +141,9 @@ function editUnits() {
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
localStorage.removeItem('populationRate');
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
localStorage.removeItem('urbanization');
localStorage.removeItem('urbanDensity');
}
function addRuler() {

View file

@ -0,0 +1,329 @@
'use strict';
function editUnits() {
closeDialogs('#unitsEditor, .stable');
$('#unitsEditor').dialog();
if (modules.editUnits) return;
modules.editUnits = true;
$('#unitsEditor').dialog({
title: 'Units Editor',
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
<<<<<<< HEAD
document.getElementById('distanceUnitInput').addEventListener('change', changeDistanceUnit);
document.getElementById('distanceScaleOutput').addEventListener('input', changeDistanceScale);
document.getElementById('distanceScaleInput').addEventListener('change', changeDistanceScale);
document.getElementById('heightUnit').addEventListener('change', changeHeightUnit);
document.getElementById('heightExponentInput').addEventListener('input', changeHeightExponent);
document.getElementById('heightExponentOutput').addEventListener('input', changeHeightExponent);
document.getElementById('temperatureScale').addEventListener('change', changeTemperatureScale);
document.getElementById('barSizeOutput').addEventListener('input', drawScaleBar);
document.getElementById('barSizeInput').addEventListener('input', drawScaleBar);
document.getElementById('barLabel').addEventListener('input', drawScaleBar);
document.getElementById('barPosX').addEventListener('input', fitScaleBar);
document.getElementById('barPosY').addEventListener('input', fitScaleBar);
document.getElementById('barBackOpacity').addEventListener('input', changeScaleBarOpacity);
document.getElementById('barBackColor').addEventListener('input', changeScaleBarColor);
document.getElementById('populationRateOutput').addEventListener('input', changePopulationRate);
document.getElementById('populationRateInput').addEventListener('change', changePopulationRate);
document.getElementById('urbanizationOutput').addEventListener('input', changeUrbanizationRate);
document.getElementById('urbanizationInput').addEventListener('change', changeUrbanizationRate);
document.getElementById('addLinearRuler').addEventListener('click', addRuler);
document.getElementById('addOpisometer').addEventListener('click', toggleOpisometerMode);
document.getElementById('addRouteOpisometer').addEventListener('click', toggleRouteOpisometerMode);
document.getElementById('addPlanimeter').addEventListener('click', togglePlanimeterMode);
document.getElementById('removeRulers').addEventListener('click', removeAllRulers);
document.getElementById('unitsRestore').addEventListener('click', restoreDefaultUnits);
=======
document.getElementById("distanceUnitInput").addEventListener("change", changeDistanceUnit);
document.getElementById("distanceScaleOutput").addEventListener("input", changeDistanceScale);
document.getElementById("distanceScaleInput").addEventListener("change", changeDistanceScale);
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
document.getElementById("heightExponentInput").addEventListener("input", changeHeightExponent);
document.getElementById("heightExponentOutput").addEventListener("input", changeHeightExponent);
document.getElementById("temperatureScale").addEventListener("change", changeTemperatureScale);
document.getElementById("barSizeOutput").addEventListener("input", drawScaleBar);
document.getElementById("barSizeInput").addEventListener("input", drawScaleBar);
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
document.getElementById("barBackOpacity").addEventListener("input", changeScaleBarOpacity);
document.getElementById("barBackColor").addEventListener("input", changeScaleBarColor);
document.getElementById("populationRateOutput").addEventListener("input", changePopulationRate);
document.getElementById("populationRateInput").addEventListener("change", changePopulationRate);
document.getElementById("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
document.getElementById("urbanizationInput").addEventListener("change", changeUrbanizationRate);
document.getElementById("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
document.getElementById("urbanDensityInput").addEventListener("change", changeUrbanDensity);
document.getElementById("addLinearRuler").addEventListener("click", addRuler);
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
document.getElementById("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
document.getElementById("unitsRestore").addEventListener("click", restoreDefaultUnits);
>>>>>>> master
function changeDistanceUnit() {
if (this.value === 'custom_name') {
prompt('Provide a custom name for a distance unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('distanceUnit');
drawScaleBar();
calculateFriendlyGridSize();
});
return;
}
drawScaleBar();
calculateFriendlyGridSize();
}
function changeDistanceScale() {
drawScaleBar();
calculateFriendlyGridSize();
}
function changeHeightUnit() {
if (this.value !== 'custom_name') return;
prompt('Provide a custom name for a height unit', {default: ''}, (custom) => {
this.options.add(new Option(custom, custom, false, true));
lock('heightUnit');
});
}
function changeHeightExponent() {
calculateTemperatures();
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeTemperatureScale() {
if (layerIsOn('toggleTemp')) drawTemp();
}
function changeScaleBarOpacity() {
scaleBar.select('rect').attr('opacity', this.value);
}
function changeScaleBarColor() {
scaleBar.select('rect').attr('fill', this.value);
}
function changePopulationRate() {
populationRate = +this.value;
}
function changeUrbanizationRate() {
urbanization = +this.value;
}
function changeUrbanDensity() {
urbanDensity = +this.value;
}
function restoreDefaultUnits() {
// distanceScale
document.getElementById('distanceScaleOutput').value = 3;
document.getElementById('distanceScaleInput').value = 3;
unlock('distanceScale');
// units
const US = navigator.language === 'en-US';
const UK = navigator.language === 'en-GB';
distanceUnitInput.value = US || UK ? 'mi' : 'km';
heightUnit.value = US || UK ? 'ft' : 'm';
temperatureScale.value = US ? '°F' : '°C';
areaUnit.value = 'square';
localStorage.removeItem('distanceUnit');
localStorage.removeItem('heightUnit');
localStorage.removeItem('temperatureScale');
localStorage.removeItem('areaUnit');
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
localStorage.removeItem('heightExponent');
calculateTemperatures();
// scale bar
barSizeOutput.value = barSizeInput.value = 2;
barLabel.value = '';
barBackOpacity.value = 0.2;
barBackColor.value = '#ffffff';
barPosX.value = barPosY.value = 99;
localStorage.removeItem('barSize');
localStorage.removeItem('barLabel');
localStorage.removeItem('barBackOpacity');
localStorage.removeItem('barBackColor');
localStorage.removeItem('barPosX');
localStorage.removeItem('barPosY');
drawScaleBar();
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
<<<<<<< HEAD
localStorage.removeItem('populationRate');
localStorage.removeItem('urbanization');
=======
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
localStorage.removeItem("urbanDensity");
>>>>>>> master
}
function addRuler() {
if (!layerIsOn('toggleRulers')) toggleRulers();
const pt = document.getElementById('map').createSVGPoint();
(pt.x = graphWidth / 2), (pt.y = graphHeight / 4);
const p = pt.matrixTransform(viewbox.node().getScreenCTM().inverse());
const dx = graphWidth / 4 / scale;
const dy = (rulers.data.length * 40) % (graphHeight / 2);
const from = [(p.x - dx) | 0, (p.y + dy) | 0];
const to = [(p.x + dx) | 0, (p.y + dy) | 0];
rulers.create(Ruler, [from, to]).draw();
}
function toggleOpisometerMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure length. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const opisometer = rulers.create(Opisometer, [point]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
opisometer.addPoint(point);
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addOpisometer.classList.remove('pressed');
if (opisometer.points.length < 2) rulers.remove(opisometer.id);
if (!d3.event.sourceEvent.shiftKey) opisometer.optimize();
});
})
);
}
}
function toggleRouteOpisometerMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve along routes to measure length. Hold Shift to measure away from roads.', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
const routeOpisometer = rulers.create(RouteOpisometer, [[x, y]]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove('pressed');
if (routeOpisometer.points.length < 2) {
rulers.remove(routeOpisometer.id);
}
});
} else {
restoreDefaultEvents();
clearMainTip();
addRouteOpisometer.classList.remove('pressed');
tip('Must start in a cell with a route in it', false, 'error');
}
})
);
}
}
function togglePlanimeterMode() {
if (this.classList.contains('pressed')) {
restoreDefaultEvents();
clearMainTip();
this.classList.remove('pressed');
} else {
if (!layerIsOn('toggleRulers')) toggleRulers();
tip('Draw a curve to measure its area. Hold Shift to disallow path optimization', true);
unitsBottom.querySelectorAll('.pressed').forEach((button) => button.classList.remove('pressed'));
this.classList.add('pressed');
viewbox.style('cursor', 'crosshair').call(
d3.drag().on('start', function () {
const point = d3.mouse(this);
const planimeter = rulers.create(Planimeter, [point]).draw();
d3.event.on('drag', function () {
const point = d3.mouse(this);
planimeter.addPoint(point);
});
d3.event.on('end', function () {
restoreDefaultEvents();
clearMainTip();
addPlanimeter.classList.remove('pressed');
if (planimeter.points.length < 3) rulers.remove(planimeter.id);
else if (!d3.event.sourceEvent.shiftKey) planimeter.optimize();
});
})
);
}
}
function removeAllRulers() {
if (!rulers.data.length) return;
alertMessage.innerHTML = `
Are you sure you want to remove all placed rulers?
<br>If you just want to hide rulers, toggle the Rulers layer off in Menu`;
$('#alert').dialog({
resizable: false,
title: 'Remove all rulers',
buttons: {
Remove: function () {
$(this).dialog('close');
rulers.undraw();
rulers = new Rulers();
},
Cancel: function () {
$(this).dialog('close');
}
}
});
}
}

View file

@ -1,25 +1,33 @@
function editWorld() {
if (customization) return;
$("#worldConfigurator").dialog({title: "Configure World", resizable: false, width: "42em",
$('#worldConfigurator').dialog({
title: 'Configure World',
resizable: false,
width: '42em',
buttons: {
"Whole World": () => applyWorldPreset(100, 50),
"Northern": () => applyWorldPreset(33, 25),
"Tropical": () => applyWorldPreset(33, 50),
"Southern": () => applyWorldPreset(33, 75),
"Restore Winds": restoreDefaultWinds
}, open: function() {
const buttons = $(this).dialog("widget").find(".ui-dialog-buttonset > button");
buttons[0].addEventListener("mousemove", () => tip("Click to set map size to cover the whole World"));
buttons[1].addEventListener("mousemove", () => tip("Click to set map size to cover the Northern latitudes"));
buttons[2].addEventListener("mousemove", () => tip("Click to set map size to cover the Tropical latitudes"));
buttons[3].addEventListener("mousemove", () => tip("Click to set map size to cover the Southern latitudes"));
buttons[4].addEventListener("mousemove", () => tip("Click to restore default wind directions"));
'Whole World': () => applyWorldPreset(100, 50),
Northern: () => applyWorldPreset(33, 25),
Tropical: () => applyWorldPreset(33, 50),
Southern: () => applyWorldPreset(33, 75),
'Restore Winds': restoreDefaultWinds
},
open: function () {
const buttons = $(this).dialog('widget').find('.ui-dialog-buttonset > button');
buttons[0].addEventListener('mousemove', () => tip('Click to set map size to cover the whole World'));
buttons[1].addEventListener('mousemove', () => tip('Click to set map size to cover the Northern latitudes'));
buttons[2].addEventListener('mousemove', () => tip('Click to set map size to cover the Tropical latitudes'));
buttons[3].addEventListener('mousemove', () => tip('Click to set map size to cover the Southern latitudes'));
buttons[4].addEventListener('mousemove', () => tip('Click to restore default wind directions'));
},
close: function () {
$(this).dialog('destroy');
}
});
const globe = d3.select("#globe");
const globe = d3.select('#globe');
const clr = d3.scaleSequential(d3.interpolateSpectral);
const tMax = 30, tMin = -25; // temperature extremes
const tMax = 30,
tMin = -25; // temperature extremes
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
const path = d3.geoPath(projection);
@ -29,15 +37,15 @@ function editWorld() {
if (modules.editWorld) return;
modules.editWorld = true;
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
globe.select("#globeWindArrows").on("click", changeWind);
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
document.getElementById('worldControls').addEventListener('input', (e) => updateWorld(e.target));
globe.select('#globeWindArrows').on('click', changeWind);
globe.select('#globeGraticule').attr('d', round(path(d3.geoGraticule()()))); // globe graticule
updateWindDirections();
function updateWorld(el) {
if (el) {
document.getElementById(el.dataset.stored+"Input").value = el.value;
document.getElementById(el.dataset.stored+"Output").value = el.value;
document.getElementById(el.dataset.stored + 'Input').value = el.value;
document.getElementById(el.dataset.stored + 'Output').value = el.value;
if (el.dataset.stored) lock(el.dataset.stored);
}
@ -52,84 +60,94 @@ function editWorld() {
pack.cells.h = new Float32Array(heights);
defineBiomes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 500);
if (layerIsOn('toggleTemp')) drawTemp();
if (layerIsOn('togglePrec')) drawPrec();
if (layerIsOn('toggleBiomes')) drawBiomes();
if (layerIsOn('toggleCoordinates')) drawCoordinates();
if (layerIsOn('toggleRivers')) drawRivers();
if (document.getElementById('canvas3d')) setTimeout(ThreeD.update(), 500);
}
function updateGlobePosition() {
const size = +document.getElementById("mapSizeOutput").value;
const eqD = graphHeight / 2 * 100 / size;
const size = +document.getElementById('mapSizeOutput').value;
const eqD = ((graphHeight / 2) * 100) / size;
calculateMapCoordinates();
const mc = mapCoordinates; // shortcut
const scale = +distanceScaleInput.value, unit = distanceUnitInput.value;
const scale = +distanceScaleInput.value,
unit = distanceUnitInput.value;
const meridian = toKilometer(eqD * 2 * scale);
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById("meridianLengthEarth").innerHTML = meridian ? " = " + rn(meridian / 200) + "%🌏" : "";
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
document.getElementById('mapSize').innerHTML = `${graphWidth}x${graphHeight}`;
document.getElementById('mapSizeFriendly').innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
document.getElementById('meridianLength').innerHTML = rn(eqD * 2);
document.getElementById('meridianLengthFriendly').innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
document.getElementById('meridianLengthEarth').innerHTML = meridian ? ' = ' + rn(meridian / 200) + '%🌏' : '';
document.getElementById('mapCoordinates').innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
function toKilometer(v) {
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 === 'km') return v;
else if (unit === 'mi') return v * 1.60934;
else if (unit === 'lg') return v * 5.556;
else if (unit === 'vr') return v * 1.0668;
return 0; // 0 if distanceUnitInput is a custom unit
}
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
function lat(lat) {
return lat > 0 ? Math.abs(rn(lat)) + '°N' : Math.abs(rn(lat)) + '°S';
} // parse latitude value
const area = d3.geoGraticule().extent([
[mc.lonW, mc.latN],
[mc.lonE, mc.latS]
]);
globe.select('#globeArea').attr('d', round(path(area.outline()))); // map area
}
function updateGlobeTemperature() {
const tEq = +document.getElementById("temperatureEquatorOutput").value;
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
const tPole = +document.getElementById("temperaturePoleOutput").value;
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
const tEq = +document.getElementById('temperatureEquatorOutput').value;
document.getElementById('temperatureEquatorF').innerHTML = rn((tEq * 9) / 5 + 32);
const tPole = +document.getElementById('temperaturePoleOutput').value;
document.getElementById('temperaturePoleF').innerHTML = rn((tPole * 9) / 5 + 32);
globe.selectAll('.tempGradient90').attr('stop-color', clr(1 - (tPole - tMin) / (tMax - tMin)));
globe.selectAll('.tempGradient60').attr('stop-color', clr(1 - (tEq - ((tEq - tPole) * 2) / 3 - tMin) / (tMax - tMin)));
globe.selectAll('.tempGradient30').attr('stop-color', clr(1 - (tEq - ((tEq - tPole) * 1) / 3 - tMin) / (tMax - tMin)));
globe.select('.tempGradient0').attr('stop-color', clr(1 - (tEq - tMin) / (tMax - tMin)));
}
function updateWindDirections() {
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
const tr = parseTransform(this.getAttribute("transform"));
this.setAttribute("transform", `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
globe
.select('#globeWindArrows')
.selectAll('path')
.each(function (d, i) {
const tr = parseTransform(this.getAttribute('transform'));
this.setAttribute('transform', `rotate(${options.winds[i]} ${tr[1]} ${tr[2]})`);
});
}
function changeWind() {
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);
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();
}
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 mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map((c) => ((90 - c) / 30) | 0);
const update = mapTiers.some((t) => options.winds[t] != defaultWinds[t]);
options.winds = defaultWinds;
updateWindDirections();
if (update) updateWorld();
}
function applyWorldPreset(size, lat) {
document.getElementById("mapSizeInput").value = document.getElementById("mapSizeOutput").value = size;
document.getElementById("latitudeInput").value = document.getElementById("latitudeOutput").value = lat;
lock("mapSize");
lock("latitude");
document.getElementById('mapSizeInput').value = document.getElementById('mapSizeOutput').value = size;
document.getElementById('latitudeInput').value = document.getElementById('latitudeOutput').value = lat;
lock('mapSize');
lock('latitude');
updateWorld();
}
}
}

View file

@ -1,83 +1,83 @@
"use strict";
'use strict';
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
if (!layerIsOn('toggleZones')) toggleZones();
const body = document.getElementById('zonesBodySection');
zonesEditorAddLines();
if (modules.editZones) return;
modules.editZones = true;
$("#zonesEditor").dialog({
title: "Zones Editor",
$('#zonesEditor').dialog({
title: 'Zones Editor',
resizable: false,
width: fitContent(),
close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
close: () => exitZonesManualAssignment('close'),
position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}
});
// add listeners
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
document.getElementById('zonesEditorRefresh').addEventListener('click', zonesEditorAddLines);
document.getElementById('zonesEditStyle').addEventListener('click', () => editStyle('zones'));
document.getElementById('zonesLegend').addEventListener('click', toggleLegend);
document.getElementById('zonesPercentage').addEventListener('click', togglePercentageMode);
document.getElementById('zonesManually').addEventListener('click', enterZonesManualAssignent);
document.getElementById('zonesManuallyApply').addEventListener('click', applyZonesManualAssignent);
document.getElementById('zonesManuallyCancel').addEventListener('click', cancelZonesManualAssignent);
document.getElementById('zonesAdd').addEventListener('click', addZonesLayer);
document.getElementById('zonesExport').addEventListener('click', downloadZonesData);
document.getElementById('zonesRemove').addEventListener('click', toggleEraseMode);
body.addEventListener("click", function (ev) {
body.addEventListener('click', function (ev) {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
if (cl.contains("culturePopulation")) {
if (cl.contains('culturePopulation')) {
changePopulation(zone);
return;
}
if (cl.contains("icon-trash-empty")) {
if (cl.contains('icon-trash-empty')) {
zoneRemove(zone);
return;
}
if (cl.contains("icon-eye")) {
if (cl.contains('icon-eye')) {
toggleVisibility(el);
return;
}
if (cl.contains("icon-pin")) {
if (cl.contains('icon-pin')) {
toggleFog(zone, cl);
return;
}
if (cl.contains("fillRect")) {
if (cl.contains('fillRect')) {
changeFill(el);
return;
}
if (customization) selectZone(el);
});
body.addEventListener("input", function (ev) {
body.addEventListener('input', function (ev) {
const el = ev.target,
zone = el.parentNode.dataset.id;
if (el.classList.contains("religionName")) zones.select("#" + zone).attr("data-description", el.value);
if (el.classList.contains('religionName')) zones.select('#' + zone).attr('data-description', el.value);
});
// add line for each zone
function zonesEditorAddLines() {
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
let lines = "";
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
let lines = '';
zones.selectAll("g").each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(",").map(c => +c) : [];
zones.selectAll('g').each(function () {
const c = this.dataset.cells ? this.dataset.cells.split(',').map((c) => +c) : [];
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const area = d3.sum(c.map(i => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const fill = this.getAttribute('fill');
const area = d3.sum(c.map((i) => pack.cells.area[i])) * distanceScaleInput.value ** 2;
const rural = d3.sum(c.map((i) => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map((i) => pack.cells.burg[i]).map((b) => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const inactive = this.style.display === "none";
const focused = defs.select("#fog #focus" + this.id).size();
const inactive = this.style.display === 'none';
const focused = defs.select('#fog #focus' + this.id).size();
lines += `<div class="states" data-id="${this.id}" data-fill="${fill}" data-description="${description}" data-cells=${c.length} data-area=${area} data-population=${population}>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
@ -89,8 +89,8 @@ 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>`;
});
@ -99,73 +99,73 @@ function editZones() {
// update footer
const totalArea = (zonesFooterArea.dataset.area = graphWidth * graphHeight * distanceScaleInput.value ** 2);
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter((b) => !b.removed).map((b) => b.population)) * urbanization) * populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
zonesFooterNumber.innerHTML = zones.selectAll('g').size();
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseenter', (ev) => zoneHighlightOn(ev)));
body.querySelectorAll('div.states').forEach((el) => el.addEventListener('mouseleave', (ev) => zoneHighlightOff(ev)));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
if (body.dataset.type === 'percentage') {
body.dataset.type = 'absolute';
togglePercentageMode();
}
$("#zonesEditor").dialog({width: fitContent()});
$('#zonesEditor').dialog({width: fitContent()});
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", "1px solid red");
zones.select('#' + zone).style('outline', '1px solid red');
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", null);
zones.select('#' + zone).style('outline', null);
}
$(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"));
const zone = $('#' + ui.item.attr('data-id'));
const prev = $('#' + ui.item.prev().attr('data-id'));
if (prev) {
zone.insertAfter(prev);
return;
}
const next = $("#" + ui.item.next().attr("data-id"));
const next = $('#' + ui.item.next().attr('data-id'));
if (next) zone.insertBefore(next);
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
if (!layerIsOn('toggleZones')) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
document.querySelectorAll('#zonesBottom > button').forEach((el) => (el.style.display = 'none'));
document.getElementById('zonesManuallyButtons').style.display = 'inline-block';
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
zonesEditor.querySelectorAll('.hide').forEach((el) => el.classList.add('hidden'));
zonesFooter.style.display = 'none';
body.querySelectorAll('div > input, select, svg').forEach((e) => (e.style.pointerEvents = 'none'));
$('#zonesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush);
tip('Click to select a zone, drag to paint a zone', true);
viewbox.style('cursor', 'crosshair').on('click', selectZoneOnMapClick).call(d3.drag().on('start', dragZoneBrush)).on('touchmove mousemove', moveZoneBrush);
body.querySelector("div").classList.add("selected");
zones.selectAll("g").each(function () {
this.setAttribute("data-init", this.getAttribute("data-cells"));
body.querySelector('div').classList.add('selected');
zones.selectAll('g').each(function () {
this.setAttribute('data-init', this.getAttribute('data-cells'));
});
}
function selectZone(el) {
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
body.querySelector('div.selected').classList.remove('selected');
el.classList.add('selected');
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
if (d3.event.target.parentElement.parentElement.id !== 'zones') return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
@ -174,7 +174,7 @@ function editZones() {
function dragZoneBrush() {
const r = +zonesBrush.value;
d3.event.on("drag", () => {
d3.event.on('drag', () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
@ -182,34 +182,34 @@ function editZones() {
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
const selected = body.querySelector("div.selected");
const zone = zones.select("#" + selected.dataset.id);
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr("data-cells");
let cells = dataCells ? dataCells.split(",").map(i => +i) : [];
const 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");
const erase = document.getElementById('zonesRemove').classList.contains('pressed');
if (erase) {
// remove
selection.forEach(i => {
selection.forEach((i) => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
zone.select('polygon#' + base + i).remove();
cells.splice(index, 1);
});
} else {
// add
selection.forEach(i => {
selection.forEach((i) => {
if (cells.includes(i)) return;
cells.push(i);
zone
.append("polygon")
.attr("points", getPackPolygon(i))
.attr("id", base + i);
.append('polygon')
.attr('points', getPackPolygon(i))
.attr('id', base + i);
});
}
zone.attr("data-cells", cells);
zone.attr('data-cells', cells);
});
}
@ -221,11 +221,11 @@ function editZones() {
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
if (this.dataset.cells) return;
// all zone cells are removed
unfog("focusZone" + this.id);
this.style.display = "block";
unfog('focusZone' + this.id);
this.style.display = 'block';
});
zonesEditorAddLines();
@ -234,20 +234,20 @@ function editZones() {
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
const dataCells = zone.attr('data-init');
const cells = dataCells ? dataCells.split(',').map((i) => +i) : [];
zone.attr('data-cells', cells);
zone.selectAll('*').remove();
const base = zone.attr('id') + '_'; // id generic part
zone
.selectAll("*")
.selectAll('*')
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
.append('polygon')
.attr('points', (d) => getPackPolygon(d))
.attr('id', (d) => base + d);
});
exitZonesManualAssignment();
@ -256,97 +256,97 @@ function editZones() {
function exitZonesManualAssignment(close) {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
document.querySelectorAll('#zonesBottom > button').forEach((el) => (el.style.display = 'inline-block'));
document.getElementById('zonesManuallyButtons').style.display = 'none';
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
zonesEditor.querySelectorAll('.hide:not(.show)').forEach((el) => el.classList.remove('hidden'));
zonesFooter.style.display = 'block';
body.querySelectorAll('div > input, select, svg').forEach((e) => (e.style.pointerEvents = 'all'));
if (!close) $('#zonesEditor').dialog({position: {my: 'right top', at: 'right-10 top+10', of: 'svg', collision: 'fit'}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function () {
this.removeAttribute("data-init");
zones.selectAll('g').each(function () {
this.removeAttribute('data-init');
});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
const selected = body.querySelector('div.selected');
if (selected) selected.classList.remove('selected');
}
function changeFill(el) {
const fill = el.getAttribute("fill");
const fill = el.getAttribute('fill');
const callback = function (fill) {
el.setAttribute("fill", fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute("fill", fill);
el.setAttribute('fill', fill);
document.getElementById(el.parentNode.parentNode.dataset.id).setAttribute('fill', fill);
};
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#" + el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
const zone = zones.select('#' + el.parentNode.dataset.id);
const inactive = zone.style('display') === 'none';
inactive ? zone.style('display', 'block') : zone.style('display', 'none');
el.classList.toggle('inactive');
}
function toggleFog(z, cl) {
const dataCells = zones.select("#" + z).attr("data-cells");
const dataCells = zones.select('#' + z).attr('data-cells');
if (!dataCells) return;
const path =
"M" +
'M' +
dataCells
.split(",")
.map(c => getPackPolygon(+c))
.join("M") +
"Z",
id = "focusZone" + z;
cl.contains("inactive") ? fog(id, path) : unfog(id);
cl.toggle("inactive");
.split(',')
.map((c) => getPackPolygon(+c))
.join('M') +
'Z',
id = 'focusZone' + z;
cl.contains('inactive') ? fog(id, path) : unfog(id);
cl.toggle('inactive');
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
if (legend.selectAll('*').size()) {
clearLegend();
return;
} // hide legend
const data = [];
zones.selectAll("g").each(function () {
zones.selectAll('g').each(function () {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
const fill = this.getAttribute('fill');
data.push([id, fill, description]);
});
drawLegend("Zones", data);
drawLegend('Zones', data);
}
function togglePercentageMode() {
if (body.dataset.type === "absolute") {
body.dataset.type = "percentage";
if (body.dataset.type === 'absolute') {
body.dataset.type = 'percentage';
const totalCells = +zonesFooterCells.innerHTML;
const totalArea = +zonesFooterArea.dataset.area;
const totalPopulation = +zonesFooterPopulation.dataset.population;
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
body.querySelectorAll(':scope > div').forEach(function (el) {
el.querySelector('.stateCells').innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + '%';
el.querySelector('.biomeArea').innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + '%';
el.querySelector('.culturePopulation').innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + '%';
});
} else {
body.dataset.type = "absolute";
body.dataset.type = 'absolute';
zonesEditorAddLines();
}
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const fill = "url(#hatch" + (id.slice(4) % 14) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-cells", "").attr("fill", fill);
const unit = areaUnit.value === "square" ? " " + distanceUnitInput.value + "²" : " " + areaUnit.value;
const id = getNextId('zone');
const description = 'Unknown zone';
const fill = 'url(#hatch' + (id.slice(4) % 14) + ')';
zones.append('g').attr('id', id).attr('data-description', description).attr('data-cells', '').attr('fill', fill);
const unit = areaUnit.value === 'square' ? ' ' + distanceUnitInput.value + '²' : ' ' + areaUnit.value;
const line = `<div class="states" data-id="${id}" data-fill="${fill}" data-description="${description}" data-cells=0 data-area=0 data-population=0>
<svg data-tip="Zone fill style. Click to change" width=".9em" height=".9em" style="margin-bottom:-1px"><rect x="0" y="0" width="100%" height="100%" fill="${fill}" class="fillRect pointer"></svg>
@ -363,53 +363,53 @@ function editZones() {
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
</div>`;
body.insertAdjacentHTML("beforeend", line);
zonesFooterNumber.innerHTML = zones.selectAll("g").size();
body.insertAdjacentHTML('beforeend', line);
zonesFooterNumber.innerHTML = zones.selectAll('g').size();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Cells,Area " + unit + ",Population\n"; // headers
const unit = areaUnit.value === 'square' ? distanceUnitInput.value + '2' : areaUnit.value;
let data = 'Id,Fill,Description,Cells,Area ' + unit + ',Population\n'; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.description + ",";
data += el.dataset.cells + ",";
data += el.dataset.area + ",";
data += el.dataset.population + "\n";
body.querySelectorAll(':scope > div').forEach(function (el) {
data += el.dataset.id + ',';
data += el.dataset.fill + ',';
data += el.dataset.description + ',';
data += el.dataset.cells + ',';
data += el.dataset.area + ',';
data += el.dataset.population + '\n';
});
const name = getFileName("Zones") + ".csv";
const name = getFileName('Zones') + '.csv';
downloadFile(data, name);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
this.classList.toggle('pressed');
}
function changePopulation(zone) {
const dataCells = zones.select("#" + zone).attr("data-cells");
const dataCells = zones.select('#' + zone).attr('data-cells');
const cells = dataCells
? dataCells
.split(",")
.map(i => +i)
.filter(i => pack.cells.h[i] >= 20)
.split(',')
.map((i) => +i)
.filter((i) => pack.cells.h[i] >= 20)
: [];
if (!cells.length) {
tip("Zone does not have any land cells, cannot change population", false, "error");
tip('Zone does not have any land cells, cannot change population', false, 'error');
return;
}
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const 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 rural = rn(d3.sum(cells.map((i) => pack.cells.pop[i])) * populationRate);
const urban = rn(d3.sum(cells.map((i) => pack.cells.burg[i]).map((b) => pack.burgs[b].population)) * populationRate * urbanization);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
const l = (n) => Number(n).toLocaleString();
alertMessage.innerHTML = `
Rural: <input type="number" min=0 step=1 id="ruralPop" value=${rural} style="width:6em">
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"}>
Urban: <input type="number" min=0 step=1 id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? '' : 'disabled'}>
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
@ -422,41 +422,41 @@ function editZones() {
ruralPop.oninput = () => update();
urbanPop.oninput = () => update();
$("#alert").dialog({
$('#alert').dialog({
resizable: false,
title: "Change zone population",
width: "24em",
title: 'Change zone population',
width: '24em',
buttons: {
Apply: function () {
applyPopulationChange();
$(this).dialog("close");
$(this).dialog('close');
},
Cancel: function () {
$(this).dialog("close");
$(this).dialog('close');
}
},
position: {my: "center", at: "center", of: "svg"}
position: {my: 'center', at: 'center', of: 'svg'}
});
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
cells.forEach((i) => (pack.cells.pop[i] *= ruralChange));
}
if (!isFinite(ruralChange) && +ruralPop.value > 0) {
const points = ruralPop.value / populationRate;
const pop = rn(points / cells.length);
cells.forEach(i => (pack.cells.pop[i] = pop));
cells.forEach((i) => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
if (isFinite(urbanChange) && urbanChange !== 1) {
burgs.forEach(b => (b.population = rn(b.population * urbanChange, 4)));
burgs.forEach((b) => (b.population = rn(b.population * urbanChange, 4)));
}
if (!isFinite(urbanChange) && +urbanPop.value > 0) {
const points = urbanPop.value / populationRate / urbanization;
const population = rn(points / burgs.length, 4);
burgs.forEach(b => (b.population = population));
burgs.forEach((b) => (b.population = population));
}
zonesEditorAddLines();
@ -464,8 +464,8 @@ function editZones() {
}
function zoneRemove(zone) {
zones.select("#" + zone).remove();
unfog("focusZone" + zone);
zones.select('#' + zone).remove();
unfog('focusZone' + zone);
zonesEditorAddLines();
}
}