Merge branch 'Azgaar:master' into Province-legend-box

This commit is contained in:
Ángel Montero Lamas 2024-09-11 18:22:29 +02:00 committed by GitHub
commit 30261ce962
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
209 changed files with 35205 additions and 3393 deletions

116
modules/ui/ai-generator.js Normal file
View file

@ -0,0 +1,116 @@
"use strict";
const GPT_MODELS = ["gpt-4o-mini", "chatgpt-4o-latest", "gpt-4o", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"];
const SYSTEM_MESSAGE = "I'm working on my fantasy map.";
function geneateWithAi(defaultPrompt, onApply) {
updateValues();
$("#aiGenerator").dialog({
title: "AI Text Generator",
position: {my: "center", at: "center", of: "svg"},
resizable: false,
buttons: {
Generate: function (e) {
generate(e.target);
},
Apply: function () {
const result = byId("aiGeneratorResult").value;
if (!result) return tip("No result to apply", true, "error", 4000);
onApply(result);
$(this).dialog("close");
},
Close: function () {
$(this).dialog("close");
}
}
});
if (modules.geneateWithAi) return;
modules.geneateWithAi = true;
function updateValues() {
byId("aiGeneratorResult").value = "";
byId("aiGeneratorPrompt").value = defaultPrompt;
byId("aiGeneratorKey").value = localStorage.getItem("fmg-ai-kl") || "";
const select = byId("aiGeneratorModel");
select.options.length = 0;
GPT_MODELS.forEach(model => select.options.add(new Option(model, model)));
select.value = localStorage.getItem("fmg-ai-model") || GPT_MODELS[0];
}
async function generate(button) {
const key = byId("aiGeneratorKey").value;
if (!key) return tip("Please enter an OpenAI API key", true, "error", 4000);
localStorage.setItem("fmg-ai-kl", key);
const model = byId("aiGeneratorModel").value;
if (!model) return tip("Please select a model", true, "error", 4000);
localStorage.setItem("fmg-ai-model", model);
const prompt = byId("aiGeneratorPrompt").value;
if (!prompt) return tip("Please enter a prompt", true, "error", 4000);
try {
button.disabled = true;
const resultArea = byId("aiGeneratorResult");
resultArea.value = "";
resultArea.disabled = true;
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${key}`
},
body: JSON.stringify({
model,
messages: [
{role: "system", content: SYSTEM_MESSAGE},
{role: "user", content: prompt}
],
temperature: 1.2,
stream: true // Enable streaming
})
});
if (!response.ok) {
const json = await response.json();
throw new Error(json?.error?.message || "Failed to generate");
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
const {done, value} = await reader.read();
if (done) break;
buffer += decoder.decode(value, {stream: true});
const lines = buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data: ") && line !== "data: [DONE]") {
try {
const jsonData = JSON.parse(line.slice(6));
const content = jsonData.choices[0].delta.content;
if (content) resultArea.value += content;
} catch (jsonError) {
console.warn("Failed to parse JSON:", jsonError, "Line:", line);
}
}
}
buffer = lines[lines.length - 1];
}
} catch (error) {
return tip(error.message, true, "error", 4000);
} finally {
button.disabled = false;
byId("aiGeneratorResult").disabled = false;
}
}
}

View file

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

View file

@ -317,7 +317,7 @@ function editBiomes() {
}
function regenerateIcons() {
ReliefIcons();
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
}
@ -383,14 +383,14 @@ function editBiomes() {
}
function dragBiomeBrush() {
const r = +biomesManuallyBrush.value;
const r = +biomesBrush.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 found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand);
if (selection) changeBiomeForSelection(selection);
});
@ -425,7 +425,7 @@ function editBiomes() {
function moveBiomeBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +biomesManuallyBrush.value;
const radius = +biomesBrush.value;
moveCircle(point[0], point[1], radius);
}

View file

@ -47,6 +47,7 @@ function editBurg(id) {
byId("burgEmblem").addEventListener("click", openEmblemEdit);
byId("burgTogglePreview").addEventListener("click", toggleBurgPreview);
byId("burgEditEmblem").addEventListener("click", openEmblemEdit);
byId("burgLocate").addEventListener("click", zoomIntoBurg);
byId("burgRelocate").addEventListener("click", toggleRelocateBurg);
byId("burglLegend").addEventListener("click", editBurgLegend);
byId("burgLock").addEventListener("click", toggleBurgLockButton);
@ -228,34 +229,26 @@ function editBurg(id) {
const burgsToRemove = burgsInGroup.filter(b => !(pack.burgs[b].capital || pack.burgs[b].lock));
const capital = burgsToRemove.length < burgsInGroup.length;
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}?
<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}`;
$("#alert").dialog({
resizable: false,
confirmationDialog({
title: "Remove burg group",
buttons: {
Remove: function () {
$(this).dialog("close");
$("#burgEditor").dialog("close");
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
message: `Are you sure you want to remove ${
basic || capital ? "all unlocked elements in the burg group" : "the entire burg group"
}?<br />Please note that capital or locked burgs will not be deleted. <br /><br />Burgs to be removed: ${
burgsToRemove.length
}. This action cannot be reverted`,
confirm: "Remove",
onConfirm: () => {
$("#burgEditor").dialog("close");
hideGroupSection();
burgsToRemove.forEach(b => removeBurg(b));
if (!basic && !capital) {
// entirely remove group
const labelG = document.querySelector("#burgLabels > #" + group.id);
const iconG = document.querySelector("#burgIcons > #" + group.id);
const anchorG = document.querySelector("#anchors > #" + group.id);
if (labelG) labelG.remove();
if (iconG) iconG.remove();
if (anchorG) anchorG.remove();
}
},
Cancel: function () {
$(this).dialog("close");
if (!basic && !capital) {
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();
}
}
});
@ -405,6 +398,14 @@ function editBurg(id) {
byId("burgTogglePreview").className = options.showBurgPreview ? "icon-map" : "icon-map-o";
}
function zoomIntoBurg() {
const id = +elSelected.attr("data-id");
const burg = pack.burgs[id];
const x = burg.x;
const y = burg.y;
zoomTo(x, y, 8, 2000);
}
function toggleRelocateBurg() {
const toggler = byId("toggleCells");
byId("burgRelocate").classList.toggle("pressed");
@ -509,19 +510,13 @@ function editBurg(id) {
}
});
} else {
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
$("#alert").dialog({
resizable: false,
confirmationDialog({
title: "Remove burg",
buttons: {
Remove: function () {
$(this).dialog("close");
removeBurg(id); // see Editors module
$("#burgEditor").dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
removeBurg(id); // see Editors module
$("#burgEditor").dialog("close");
}
});
}

View file

@ -36,7 +36,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
});
byId("burgsLockAll").addEventListener("click", toggleLockAll);
byId("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
byId("burgsInvertLock").addEventListener("click", invertLock);
function refreshBurgsEditor() {
updateFilter();
@ -246,7 +245,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
confirmationDialog({
title: "Remove burg",
message: "Are you sure you want to remove the burg? This actiove cannot be reverted",
message: "Are you sure you want to remove the burg? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
removeBurg(burg);
@ -279,7 +278,8 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
function addBurgOnClick() {
const point = d3.mouse(this);
const cell = findCell(point[0], point[1]);
const cell = findCell(...point);
if (pack.cells.h[cell] < 20)
return tip("You cannot place state into the water. Please click on a land cell", false, "error");
if (pack.cells.burg[cell])
@ -340,8 +340,8 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
.sum(d => d.population)
.sort((a, b) => b.value - a.value);
const width = 150 + 200 * uiSizeOutput.value;
const height = 150 + 200 * uiSizeOutput.value;
const width = 150 + 200 * uiSize.value;
const height = 150 + 200 * uiSize.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;
@ -603,11 +603,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
burgsOverviewAddLines();
}
function invertLock() {
pack.burgs = pack.burgs.map(burg => ({...burg, lock: !burg.lock}));
burgsOverviewAddLines();
}
function toggleLockAll() {
const activeBurgs = pack.burgs.filter(b => b.i && !b.removed);
const allLocked = activeBurgs.every(burg => burg.lock);

View file

@ -22,7 +22,7 @@ function clicked() {
if (grand.id === "emblems") editEmblem();
else if (parent.id === "rivers") editRiver(el.id);
else if (grand.id === "routes") editRoute();
else if (grand.id === "routes") editRoute(el.id);
else if (el.tagName === "tspan" && grand.parentNode.parentNode.id === "labels") editLabel();
else if (grand.id === "burgLabels") editBurg();
else if (grand.id === "burgIcons") editBurg();
@ -132,27 +132,43 @@ function applySorting(headers) {
}
function addBurg(point) {
const cells = pack.cells;
const x = rn(point[0], 2),
y = rn(point[1], 2);
const cell = findCell(x, point[1]);
const i = pack.burgs.length;
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
const state = cells.state[cell];
const feature = cells.f[cell];
const {cells, states} = pack;
const x = rn(point[0], 2);
const y = rn(point[1], 2);
const temple = pack.states[state].form === "Theocracy";
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + (cell % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cell, false);
const cellId = findCell(x, y);
const i = pack.burgs.length;
const culture = cells.culture[cellId];
const name = Names.getCulture(culture);
const state = cells.state[cellId];
const feature = cells.f[cellId];
const population = Math.max(cells.s[cellId] / 3 + i / 1000 + (cellId % 100) / 1000, 0.1);
const type = BurgsAndStates.getType(cellId, false);
// generate emblem
const coa = COA.generate(pack.states[state].coa, 0.25, null, type);
const coa = COA.generate(states[state].coa, 0.25, null, type);
coa.shield = COA.getShield(culture, state);
COArenderer.add("burg", i, coa, x, y);
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: 0, port: 0, temple, population, coa, type});
cells.burg[cell] = i;
const burg = {
name,
cell: cellId,
x,
y,
state,
i,
culture,
feature,
capital: 0,
port: 0,
temple: 0,
population,
coa,
type
};
pack.burgs.push(burg);
cells.burg[cellId] = i;
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
burgIcons
@ -173,7 +189,17 @@ function addBurg(point) {
.attr("dy", `${townSize * -1.5}px`)
.text(name);
BurgsAndStates.defineBurgFeatures(pack.burgs[i]);
BurgsAndStates.defineBurgFeatures(burg);
const newRoute = Routes.connect(cellId);
if (newRoute && layerIsOn("toggleRoutes")) {
routes
.select("#" + newRoute.group)
.append("path")
.attr("d", Routes.getPath(newRoute))
.attr("id", "route" + newRoute.i);
}
return i;
}
@ -223,18 +249,19 @@ function addBurgsGroup(group) {
}
function removeBurg(id) {
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
if (label) label.remove();
if (icon) icon.remove();
if (anchor) anchor.remove();
document.querySelector("#burgLabels [data-id='" + id + "']")?.remove();
document.querySelector("#burgIcons [data-id='" + id + "']")?.remove();
document.querySelector("#anchors [data-id='" + id + "']")?.remove();
const cells = pack.cells;
const burg = pack.burgs[id];
const cells = pack.cells,
burg = pack.burgs[id];
burg.removed = true;
cells.burg[burg.cell] = 0;
const noteId = notes.findIndex(note => note.id === `burg${id}`);
if (noteId !== -1) notes.splice(noteId, 1);
if (burg.coa) {
const coaId = "burgCOA" + id;
if (byId(coaId)) byId(coaId).remove();
@ -326,8 +353,7 @@ function createMfcgLink(burg) {
const citadel = +burg.citadel;
const urban_castle = +(citadel && each(2)(i));
const hub = +cells.road[cell] > 50;
const hub = Routes.isCrossroad(cell);
const walls = +burg.walls;
const plaza = +burg.plaza;
const temple = +burg.temple;
@ -371,10 +397,12 @@ function createVillageGeneratorLink(burg) {
else if (cells.r[cell]) tags.push("river");
else if (pop < 200 && each(4)(cell)) tags.push("pond");
const roadsAround = cells.c[cell].filter(c => cells.h[c] >= 20 && cells.road[c]).length;
if (roadsAround > 1) tags.push("highway");
else if (roadsAround === 1) tags.push("dead end");
else tags.push("isolated");
const connections = pack.cells.routes[cell] || {};
const roads = Object.values(connections).filter(routeId => {
const route = pack.routes[routeId];
return route.group === "roads" || route.group === "trails";
}).length;
tags.push(roads > 1 ? "highway" : roads === 1 ? "dead end" : "isolated");
const biome = cells.biome[cell];
const arableBiomes = cells.r[cell] ? [1, 2, 3, 4, 5, 6, 7, 8] : [5, 6, 7, 8];
@ -488,13 +516,14 @@ function fitLegendBox() {
// draw legend with the same data, but using different settings
function redrawLegend() {
if (!legend.select("rect").size()) return;
const name = legend.select("#legendLabel").text();
const data = legend
.attr("data")
.split("|")
.map(l => l.split(","));
drawLegend(name, data);
if (legend.select("rect").size()) {
const name = legend.select("#legendLabel").text();
const data = legend
.attr("data")
.split("|")
.map(l => l.split(","));
drawLegend(name, data);
}
}
function dragLegendBox() {
@ -1173,7 +1202,6 @@ function getAreaUnit(squareMark = "²") {
}
function getArea(rawArea) {
const distanceScale = byId("distanceScaleInput")?.value;
return rawArea * distanceScale ** 2;
}
@ -1224,18 +1252,18 @@ function refreshAllEditors() {
// dynamically loaded editors
async function editStates() {
if (customization) return;
const Editor = await import("../dynamic/editors/states-editor.js?v=1.96.06");
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.05");
Editor.open();
}
async function editCultures() {
if (customization) return;
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.96.01");
const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.99.05");
Editor.open();
}
async function editReligions() {
if (customization) return;
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.96.00");
const Editor = await import("../dynamic/editors/religions-editor.js?v=1.99.05");
Editor.open();
}

View file

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

View file

@ -67,9 +67,9 @@ function showDataTip(event) {
function showElementLockTip(event) {
const locked = event?.target?.classList?.contains("icon-lock");
if (locked) {
tip("Click to unlock the element and allow it to be changed by regeneration tools");
tip("Locked. Click to unlock the element and allow it to be changed by regeneration tools");
} else {
tip("Click to lock the element and prevent changes to it by regeneration tools");
tip("Unlocked. Click to lock the element and prevent changes to it by regeneration tools");
}
}
@ -151,7 +151,12 @@ function showMapTooltip(point, e, i, g) {
return;
}
if (group === "routes") return tip("Click to edit the Route");
if (group === "routes") {
const routeId = +e.target.id.slice(5);
const name = pack.routes[routeId]?.name;
if (name) return tip(`${name}. Click to edit the Route`);
return tip("Click to edit the Route");
}
if (group === "terrain") return tip("Click to edit the Relief Icon");
@ -163,6 +168,7 @@ function showMapTooltip(point, e, i, g) {
if (burgsOverview?.offsetParent) highlightEditorLine(burgsOverview, burg, 5000);
return;
}
if (group === "labels") return tip("Click to edit the Label");
if (group === "markers") return tip("Click to edit the Marker. Hold Shift to not close the assosiated note");
@ -194,9 +200,11 @@ function showMapTooltip(point, e, i, g) {
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);
const element = path[path.length - 8];
const zoneId = +element.dataset.id;
const zone = pack.zones.find(zone => zone.i === zoneId);
tip(zone.name);
if (zonesEditor?.offsetParent) highlightEditorLine(zonesEditor, zoneId, 5000);
return;
}
@ -251,10 +259,11 @@ function updateCellInfo(point, i, g) {
const f = cells.f[i];
infoLat.innerHTML = toDMS(getLatitude(y, 4), "lat");
infoLon.innerHTML = toDMS(getLongitude(x, 4), "lon");
infoGeozone.innerHTML = getGeozone(getLatitude(y, 4));
infoCell.innerHTML = i;
infoArea.innerHTML = cells.area[i] ? si(getArea(cells.area[i])) + " " + getAreaUnit() : "n/a";
infoEvelation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoElevation.innerHTML = getElevation(pack.features[f], pack.cells.h[i]);
infoDepth.innerHTML = getDepth(pack.features[f], point);
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
@ -278,6 +287,18 @@ function updateCellInfo(point, i, g) {
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
}
function getGeozone(latitude) {
if (latitude > 66.5) return "Arctic";
if (latitude > 35) return "Temperate North";
if (latitude > 23.5) return "Subtropical North";
if (latitude > 1) return "Tropical North";
if (latitude > -1) return "Equatorial";
if (latitude > -23.5) return "Tropical South";
if (latitude > -35) return "Subtropical South";
if (latitude > -66.5) return "Temperate South";
return "Antarctic";
}
// convert coordinate to DMS format
function toDMS(coord, c) {
const degrees = Math.floor(Math.abs(coord));
@ -285,7 +306,7 @@ function toDMS(coord, c) {
const minutes = Math.floor(minutesNotTruncated);
const seconds = Math.floor((minutesNotTruncated - minutes) * 60);
const cardinal = c === "lat" ? (coord >= 0 ? "N" : "S") : coord >= 0 ? "E" : "W";
return degrees + " + minutes + " " + seconds + "″ " + cardinal;
return degrees + " + minutes + "" + seconds + "″" + cardinal;
}
// get surface elevation
@ -421,17 +442,17 @@ function highlightEmblemElement(type, el) {
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function (e) {
e.addEventListener("mouseover", function (event) {
e.addEventListener("mouseover", function (e) {
e.stopPropagation();
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);
const ids = this.dataset.ids ? this.dataset.ids.split(",") : [this.id.slice(5)];
const fn = this.className === "icon-lock" ? unlock : lock;
ids.forEach(fn);
});
});

View file

@ -90,7 +90,7 @@ function editHeightmap(options) {
if (!sessionStorage.getItem("noExitButtonAnimation")) {
sessionStorage.setItem("noExitButtonAnimation", true);
exitCustomization.style.opacity = 0;
const width = 12 * uiSizeOutput.value * 11;
const width = 12 * uiSize.value * 11;
exitCustomization.style.right = (svgWidth - width) / 2 + "px";
exitCustomization.style.bottom = svgHeight / 2 + "px";
exitCustomization.style.transform = "scale(2)";
@ -136,7 +136,7 @@ function editHeightmap(options) {
return;
}
moveCircle(x, y, brushRadius.valueAsNumber, "#333");
moveCircle(x, y, heightmapBrushRadius.valueAsNumber, "#333");
}
// get user-friendly (real-world) height value from map data
@ -246,6 +246,7 @@ function editHeightmap(options) {
Cultures.expand();
BurgsAndStates.generate();
Routes.generate();
Religions.generate();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces();
@ -260,7 +261,7 @@ function editHeightmap(options) {
Military.generate();
Markers.generate();
addZones();
Zones.generate();
TIME && console.timeEnd("regenerateErasedData");
INFO && console.groupEnd("Edit Heightmap");
}
@ -281,8 +282,7 @@ function editHeightmap(options) {
const l = grid.cells.i.length;
const biome = new Uint8Array(l);
const pop = new Uint16Array(l);
const road = new Uint16Array(l);
const crossroad = new Uint16Array(l);
const routes = {};
const s = new Uint16Array(l);
const burg = new Uint16Array(l);
const state = new Uint16Array(l);
@ -300,8 +300,7 @@ function editHeightmap(options) {
biome[g] = pack.cells.biome[i];
culture[g] = pack.cells.culture[i];
pop[g] = pack.cells.pop[i];
road[g] = pack.cells.road[i];
crossroad[g] = pack.cells.crossroad[i];
routes[g] = pack.cells.routes[i];
s[g] = pack.cells.s[i];
state[g] = pack.cells.state[i];
province[g] = pack.cells.province[i];
@ -353,8 +352,7 @@ function editHeightmap(options) {
// assign saved pack data from grid back to pack
const n = pack.cells.i.length;
pack.cells.pop = new Float32Array(n);
pack.cells.road = new Uint16Array(n);
pack.cells.crossroad = new Uint16Array(n);
pack.cells.routes = {};
pack.cells.s = new Uint16Array(n);
pack.cells.burg = new Uint16Array(n);
pack.cells.state = new Uint16Array(n);
@ -389,8 +387,7 @@ function editHeightmap(options) {
if (!isLand) continue;
pack.cells.culture[i] = culture[g];
pack.cells.pop[i] = pop[g];
pack.cells.road[i] = road[g];
pack.cells.crossroad[i] = crossroad[g];
pack.cells.routes[i] = routes[g];
pack.cells.s[i] = s[g];
pack.cells.state[i] = state[g];
pack.cells.province[i] = province[g];
@ -667,7 +664,7 @@ function editHeightmap(options) {
const fromCell = +lineCircle.attr("data-cell");
debug.selectAll("*").remove();
const power = byId("linePower").valueAsNumber;
const power = byId("heightmapLinePower").valueAsNumber;
if (power === 0) return tip("Power should not be zero", false, "error");
const heights = grid.cells.h;
@ -689,7 +686,7 @@ function editHeightmap(options) {
}
function dragBrush() {
const r = brushRadius.valueAsNumber;
const r = heightmapBrushRadius.valueAsNumber;
const [x, y] = d3.mouse(this);
const start = findGridCell(x, y, grid);
@ -707,7 +704,7 @@ function editHeightmap(options) {
}
function changeHeightForSelection(selection, start) {
const power = brushPower.valueAsNumber;
const power = heightmapBrushPower.valueAsNumber;
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;

View file

@ -18,10 +18,9 @@ function handleKeyup(event) {
event.stopPropagation();
const {code, key, ctrlKey, metaKey, shiftKey, altKey} = event;
const {code, key, ctrlKey, metaKey, shiftKey} = 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();
@ -30,7 +29,7 @@ function handleKeyup(event) {
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 (code === "KeyO" && byId("canvas3d")) toggle3dOptions();
else if (ctrl && code === "KeyQ") toggleSaveReminder();
else if (ctrl && code === "KeyS") saveMap("machine");
else if (ctrl && code === "KeyC") saveMap("dropbox");
@ -50,6 +49,7 @@ function handleKeyup(event) {
else if (shift && code === "KeyO") editNotes();
else if (shift && code === "KeyA") overviewCharts();
else if (shift && code === "KeyT") overviewBurgs();
else if (shift && code === "KeyU") overviewRoutes();
else if (shift && code === "KeyV") overviewRivers();
else if (shift && code === "KeyM") overviewMilitary();
else if (shift && code === "KeyK") overviewMarkers();
@ -57,13 +57,8 @@ function handleKeyup(event) {
else if (key === "!") toggleAddBurg();
else if (key === "@") toggleAddLabel();
else if (key === "#") toggleAddRiver();
else if (key === "$") toggleAddRoute();
else if (key === "$") createRoute();
else if (key === "%") toggleAddMarker();
else if (alt && code === "KeyB") console.table(pack.burgs);
else if (alt && code === "KeyS") console.table(pack.states);
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();
@ -122,24 +117,21 @@ function allowHotkeys() {
function handleSizeChange(key) {
let brush = null;
if (document.getElementById("brushRadius")?.offsetParent) brush = document.getElementById("brushRadius");
else if (document.getElementById("linePower")?.offsetParent) brush = document.getElementById("linePower");
else if (document.getElementById("biomesManuallyBrush")?.offsetParent)
brush = document.getElementById("biomesManuallyBrush");
else if (document.getElementById("statesManuallyBrush")?.offsetParent)
brush = document.getElementById("statesManuallyBrush");
else if (document.getElementById("provincesManuallyBrush")?.offsetParent)
brush = document.getElementById("provincesManuallyBrush");
else if (document.getElementById("culturesManuallyBrush")?.offsetParent)
brush = document.getElementById("culturesManuallyBrush");
else if (document.getElementById("zonesBrush")?.offsetParent) brush = document.getElementById("zonesBrush");
else if (document.getElementById("religionsManuallyBrush")?.offsetParent)
brush = document.getElementById("religionsManuallyBrush");
if (byId("heightmapBrushRadius")?.offsetParent) brush = byId("heightmapBrushRadius");
else if (byId("heightmapLinePower")?.offsetParent) brush = byId("heightmapLinePower");
else if (byId("biomesBrush")?.offsetParent) brush = byId("biomesBrush");
else if (byId("culturesBrush")?.offsetParent) brush = byId("culturesBrush");
else if (byId("statesBrush")?.offsetParent) brush = byId("statesBrush");
else if (byId("provincesBrush")?.offsetParent) brush = byId("provincesBrush");
else if (byId("religionsBrush")?.offsetParent) brush = byId("religionsBrush");
else if (byId("zonesBrush")?.offsetParent) brush = byId("zonesBrush");
if (brush) {
const change = key === "-" ? -5 : 5;
const value = minmax(+brush.value + change, +brush.min, +brush.max);
brush.value = document.getElementById(brush.id + "Number").value = value;
const min = +brush.getAttribute("min") || 5;
const max = +brush.getAttribute("max") || 100;
const value = +brush.value + change;
brush.value = minmax(value, min, max);
return;
}

View file

@ -26,28 +26,32 @@ function editLabel() {
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);
byId("labelGroupShow").on("click", showGroupSection);
byId("labelGroupHide").on("click", hideGroupSection);
byId("labelGroupSelect").on("click", changeGroup);
byId("labelGroupInput").on("change", createNewGroup);
byId("labelGroupNew").on("click", toggleNewGroupInput);
byId("labelGroupRemove").on("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);
byId("labelTextShow").on("click", showTextSection);
byId("labelTextHide").on("click", hideTextSection);
byId("labelText").on("input", changeText);
byId("labelTextRandom").on("click", generateRandomName);
document.getElementById("labelEditStyle").addEventListener("click", editGroupStyle);
byId("labelEditStyle").on("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);
byId("labelSizeShow").on("click", showSizeSection);
byId("labelSizeHide").on("click", hideSizeSection);
byId("labelStartOffset").on("input", changeStartOffset);
byId("labelRelativeSize").on("input", changeRelativeSize);
document.getElementById("labelAlign").addEventListener("click", editLabelAlign);
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
byId("labelLetterSpacingShow").on("click", showLetterSpacingSection);
byId("labelLetterSpacingHide").on("click", hideLetterSpacingSection);
byId("labelLetterSpacingSize").on("input", changeLetterSpacingSize);
byId("labelAlign").on("click", editLabelAlign);
byId("labelLegend").on("click", editLabelLegend);
byId("labelRemoveSingle").on("click", removeLabel);
function showEditorTips() {
showMainTip();
@ -62,12 +66,12 @@ function editLabel() {
const group = text.parentNode.id;
if (group === "states" || group === "burgLabels") {
document.getElementById("labelGroupShow").style.display = "none";
byId("labelGroupShow").style.display = "none";
return;
}
hideGroupSection();
const select = document.getElementById("labelGroupSelect");
const select = byId("labelGroupSelect");
select.options.length = 0; // remove all options
labels.selectAll(":scope > g").each(function () {
@ -78,17 +82,17 @@ function editLabel() {
}
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"));
byId("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|");
byId("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset"));
byId("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size"));
let letterSpacingSize = textPath.getAttribute("letter-spacing") ? textPath.getAttribute("letter-spacing") : 0;
byId("labelLetterSpacingSize").value = parseFloat(letterSpacingSize);
}
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"));
const path = byId("textPath_" + elSelected.attr("id"));
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
const l = path.getTotalLength();
if (!l) return;
@ -117,7 +121,7 @@ function editLabel() {
}
function redrawLabelPath() {
const path = document.getElementById("textPath_" + elSelected.attr("id"));
const path = byId("textPath_" + elSelected.attr("id"));
lineGen.curve(d3.curveBundle.beta(1));
const points = [];
debug
@ -188,19 +192,19 @@ function editLabel() {
function showGroupSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelGroupSection").style.display = "inline-block";
byId("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";
byId("labelGroupSection").style.display = "none";
byId("labelGroupInput").style.display = "none";
byId("labelGroupInput").value = "";
byId("labelGroupSelect").style.display = "inline-block";
}
function changeGroup() {
document.getElementById(this.value).appendChild(elSelected.node());
byId(this.value).appendChild(elSelected.node());
}
function toggleNewGroupInput() {
@ -224,7 +228,7 @@ function editLabel() {
.replace(/ /g, "_")
.replace(/[^\w\s]/gi, "");
if (document.getElementById(group)) {
if (byId(group)) {
tip("Element with this id already exists. Please provide a unique name", false, "error");
return;
}
@ -237,22 +241,22 @@ function editLabel() {
// 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));
byId("labelGroupSelect").selectedOptions[0].remove();
byId("labelGroupSelect").options.add(new Option(group, group, false, true));
oldGroup.id = group;
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
byId("labelGroupInput").value = "";
return;
}
const newGroup = elSelected.node().parentNode.cloneNode(false);
document.getElementById("labels").appendChild(newGroup);
byId("labels").appendChild(newGroup);
newGroup.id = group;
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
document.getElementById(group).appendChild(elSelected.node());
byId("labelGroupSelect").options.add(new Option(group, group, false, true));
byId(group).appendChild(elSelected.node());
toggleNewGroupInput();
document.getElementById("labelGroupInput").value = "";
byId("labelGroupInput").value = "";
}
function removeLabelsGroup() {
@ -275,7 +279,7 @@ function editLabel() {
.select("#" + group)
.selectAll("text")
.each(function () {
document.getElementById("textPath_" + this.id).remove();
byId("textPath_" + this.id).remove();
this.remove();
});
if (!basic) labels.select("#" + group).remove();
@ -289,16 +293,16 @@ function editLabel() {
function showTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelTextSection").style.display = "inline-block";
byId("labelTextSection").style.display = "inline-block";
}
function hideTextSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelTextSection").style.display = "none";
byId("labelTextSection").style.display = "none";
}
function changeText() {
const input = document.getElementById("labelText").value;
const input = byId("labelText").value;
const el = elSelected.select("textPath").node();
const lines = input.split("|");
@ -323,7 +327,7 @@ function editLabel() {
const culture = pack.cells.culture[cell];
name = Names.getCulture(culture);
}
document.getElementById("labelText").value = name;
byId("labelText").value = name;
changeText();
}
@ -334,12 +338,22 @@ function editLabel() {
function showSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
document.getElementById("labelSizeSection").style.display = "inline-block";
byId("labelSizeSection").style.display = "inline-block";
}
function hideSizeSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
document.getElementById("labelSizeSection").style.display = "none";
byId("labelSizeSection").style.display = "none";
}
function showLetterSpacingSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "none"));
byId("labelLetterSpacingSection").style.display = "inline-block";
}
function hideLetterSpacingSection() {
document.querySelectorAll("#labelEditor > button").forEach(el => (el.style.display = "inline-block"));
byId("labelLetterSpacingSection").style.display = "none";
}
function changeStartOffset() {
@ -353,6 +367,12 @@ function editLabel() {
changeText();
}
function changeLetterSpacingSize() {
elSelected.select("textPath").attr("letter-spacing", this.value + "px");
tip("Label letter-spacing size: " + this.value + "px");
changeText();
}
function editLabelAlign() {
const bbox = elSelected.node().getBBox();
const c = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];

View file

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

View file

@ -93,7 +93,7 @@ function restoreCustomPresets() {
// run on map generation
function applyPreset() {
const preset = localStorage.getItem("preset") || document.getElementById("layersPreset").value;
const preset = localStorage.getItem("preset") || byId("layersPreset").value;
changePreset(preset);
}
@ -113,12 +113,12 @@ function changePreset(preset) {
const isDefault = getDefaultPresets()[preset];
removePresetButton.style.display = isDefault ? "none" : "inline-block";
savePresetButton.style.display = "none";
if (document.getElementById("canvas3d")) setTimeout(ThreeD.update(), 400);
if (byId("canvas3d")) setTimeout(ThreeD.update(), 400);
}
function savePreset() {
prompt("Please provide a preset name", {default: ""}, preset => {
presets[preset] = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)"))
presets[preset] = Array.from(byId("mapLayers").querySelectorAll("li:not(.buttonoff)"))
.map(node => node.id)
.sort();
layersPreset.add(new Option(preset, preset, false, true));
@ -143,7 +143,7 @@ function removePreset() {
}
function getCurrentPreset() {
const layers = Array.from(document.getElementById("mapLayers").querySelectorAll("li:not(.buttonoff)"))
const layers = Array.from(byId("mapLayers").querySelectorAll("li:not(.buttonoff)"))
.map(node => node.id)
.sort();
const defaultPresets = getDefaultPresets();
@ -169,17 +169,19 @@ function restoreLayers() {
if (layerIsOn("toggleGrid")) drawGrid();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.style("display", "block");
if (layerIsOn("toggleRoutes")) drawRoutes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("togglePopulation")) drawPopulation();
if (layerIsOn("toggleBiomes")) drawBiomes();
if (layerIsOn("toggleRelief")) ReliefIcons();
if (layerIsOn("toggleRelief")) ReliefIcons.draw();
if (layerIsOn("toggleCultures")) drawCultures();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (layerIsOn("toggleReligions")) drawReligions();
if (layerIsOn("toggleIce")) drawIce();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (layerIsOn("toggleMarkers")) drawMarkers();
if (layerIsOn("toggleZones")) drawZones();
// some layers are rendered each time, remove them if they are not on
if (!layerIsOn("toggleBorders")) borders.selectAll("path").remove();
@ -392,7 +394,6 @@ function drawTemp() {
const start = findStart(i, t);
if (!start) continue;
used[i] = 1;
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
const chain = connectVertices(start, t); // vertices chain to form a path
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
@ -1070,27 +1071,29 @@ function drawStates() {
const bodyData = body.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const gapData = gap.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const bodyString = bodyData.map(d => `<path id="state${d[1]}" d="${d[0]}" fill="${d[2]}" stroke="none"/>`).join("");
const gapString = gapData.map(d => `<path id="state-gap${d[1]}" d="${d[0]}" fill="none" stroke="${d[2]}"/>`).join("");
const clipString = bodyData
.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`)
.join("");
const haloString = haloData
.map(
d =>
`<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${
d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666"
}"/>`
)
.join("");
statesBody.html(bodyString + gapString);
defs.select("#statePaths").html(clipString);
statesHalo.html(haloString);
// connect vertices to chain
const isOptimized = shapeRendering.value === "optimizeSpeed";
if (!isOptimized) {
const haloData = halo.map((p, s) => [p.length > 10 ? p : null, s, states[s].color]).filter(d => d[0]);
const haloString = haloData
.map(d => {
const stroke = d3.color(d[2]) ? d3.color(d[2]).darker().hex() : "#666666";
return `<path id="state-border${d[1]}" d="${d[0]}" clip-path="url(#state-clip${d[1]})" stroke="${stroke}"/>`;
})
.join("");
statesHalo.html(haloString);
const clipString = bodyData
.map(d => `<clipPath id="state-clip${d[1]}"><use href="#state${d[1]}"/></clipPath>`)
.join("");
defs.select("#statePaths").html(clipString);
}
function connectVertices(start, state) {
const chain = []; // vertices chain to form a path
const getType = c => {
@ -1514,8 +1517,8 @@ function drawCoordinates() {
// conver svg point into viewBox point
function getViewPoint(x, y) {
const view = document.getElementById("viewbox");
const svg = document.getElementById("map");
const view = byId("viewbox");
const svg = byId("map");
const pt = svg.createSVGPoint();
(pt.x = x), (pt.y = y);
return pt.matrixTransform(view.getScreenCTM().inverse());
@ -1525,10 +1528,6 @@ function toggleCompass(event) {
if (!layerIsOn("toggleCompass")) {
turnButtonOn("toggleCompass");
$("#compass").fadeIn();
if (!compass.selectAll("*").size()) {
compass.append("use").attr("xlink:href", "#rose");
shiftCompass();
}
if (event && isCtrlClick(event)) editStyle("compass");
} else {
if (event && isCtrlClick(event)) {
@ -1543,7 +1542,7 @@ function toggleCompass(event) {
function toggleRelief(event) {
if (!layerIsOn("toggleRelief")) {
turnButtonOn("toggleRelief");
if (!terrain.selectAll("*").size()) ReliefIcons();
if (!terrain.selectAll("*").size()) ReliefIcons.draw();
$("#terrain").fadeIn();
if (event && isCtrlClick(event)) editStyle("terrain");
} else {
@ -1624,18 +1623,34 @@ function drawRivers() {
function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes");
$("#routes").fadeIn();
drawRoutes();
if (event && isCtrlClick(event)) editStyle("routes");
} else {
if (event && isCtrlClick(event)) {
editStyle("routes");
return;
}
$("#routes").fadeOut();
if (event && isCtrlClick(event)) return editStyle("routes");
routes.selectAll("path").remove();
turnButtonOff("toggleRoutes");
}
}
function drawRoutes() {
TIME && console.time("drawRoutes");
const routePaths = {};
for (const route of pack.routes) {
const {i, group, points} = route;
if (!points || points.length < 2) continue;
if (!routePaths[group]) routePaths[group] = [];
routePaths[group].push(`<path id="route${i}" d="${Routes.getPath(route)}"/>`);
}
routes.selectAll("path").remove();
for (const group in routePaths) {
routes.select("#" + group).html(routePaths[group].join(""));
}
TIME && console.timeEnd("drawRoutes");
}
function toggleMilitary() {
if (!layerIsOn("toggleMilitary")) {
turnButtonOn("toggleMilitary");
@ -1760,7 +1775,6 @@ function toggleScaleBar(event) {
function drawScaleBar(scaleBar, scaleLevel) {
if (!scaleBar.size() || scaleBar.style("display") === "none") return;
const distanceScale = +distanceScaleInput.value;
const unit = distanceUnitInput.value;
const size = +scaleBar.attr("data-bar-size");
@ -1859,18 +1873,29 @@ function fitScaleBar(scaleBar, fullWidth, fullHeight) {
function toggleZones(event) {
if (!layerIsOn("toggleZones")) {
turnButtonOn("toggleZones");
$("#zones").fadeIn();
drawZones();
if (event && isCtrlClick(event)) editStyle("zones");
} else {
if (event && isCtrlClick(event)) {
editStyle("zones");
return;
}
if (event && isCtrlClick(event)) return editStyle("zones");
turnButtonOff("toggleZones");
$("#zones").fadeOut();
zones.selectAll("*").remove();
}
}
function drawZones() {
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(
({hidden, cells, type}) => !hidden && cells.length && (!isFiltered || type === filterBy)
);
zones.html(visibleZones.map(drawZone).join(""));
}
function drawZone({i, cells, type, color}) {
const path = getVertexPath(cells);
return `<path id="zone${i}" data-id="${i}" data-type="${type}" d="${path}" fill="${color}" />`;
}
function toggleEmblems(event) {
if (!layerIsOn("toggleEmblems")) {
turnButtonOn("toggleEmblems");
@ -1895,21 +1920,21 @@ function drawEmblems() {
const getStateEmblemsSize = () => {
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;
const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
};
const getProvinceEmblemsSize = () => {
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;
const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
};
const getBurgEmblemSize = () => {
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;
const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
};
@ -2008,17 +2033,17 @@ function toggleVignette(event) {
}
function layerIsOn(el) {
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
const buttonoff = byId(el).classList.contains("buttonoff");
return !buttonoff;
}
function turnButtonOff(el) {
document.getElementById(el).classList.add("buttonoff");
byId(el).classList.add("buttonoff");
getCurrentPreset();
}
function turnButtonOn(el) {
document.getElementById(el).classList.remove("buttonoff");
byId(el).classList.remove("buttonoff");
getCurrentPreset();
}

View file

@ -66,7 +66,7 @@ class Measurer {
}
getDash() {
return rn(30 / distanceScaleInput.value, 2);
return rn(30 / distanceScale, 2);
}
drag() {
@ -205,7 +205,7 @@ class Ruler extends Measurer {
updateLabel() {
const length = this.getLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -337,7 +337,7 @@ class Opisometer extends Measurer {
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -475,7 +475,7 @@ class RouteOpisometer extends Measurer {
updateLabel() {
const length = this.el.select("path").node().getTotalLength();
const text = rn(length * distanceScaleInput.value) + " " + distanceUnitInput.value;
const text = rn(length * distanceScale) + " " + distanceUnitInput.value;
const [x, y] = last(this.points);
this.el.select("text").attr("x", x).attr("y", y).text(text);
}
@ -486,9 +486,7 @@ class RouteOpisometer extends Measurer {
const cells = pack.cells;
const c = findCell(mousePoint[0], mousePoint[1]);
if (!cells.road[c] && !d3.event.sourceEvent.shiftKey) {
return;
}
if (!Routes.isConnected(c) && !d3.event.sourceEvent.shiftKey) return;
context.trackCell(c, rigth);
});

View file

@ -55,7 +55,9 @@ function overviewMilitary() {
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>`
`<div data-tip="State ${
u.name
} units number. Click to sort" class="sortable removable" data-sortby="${u.name.toLowerCase()}">${label}&nbsp;</div>`
);
}
header.querySelectorAll(".removable").forEach(function (e) {
@ -77,7 +79,7 @@ function overviewMilitary() {
const total = options.military.reduce((s, u) => s + getForces(u) * u.crew, 0);
const rate = (total / population) * 100;
const sortData = options.military.map(u => `data-${u.name}="${getForces(u)}"`).join(" ");
const sortData = options.military.map(u => `data-${u.name.toLowerCase()}="${getForces(u)}"`).join(" ");
const lineData = options.military
.map(u => `<div data-type="${u.name}" data-tip="State ${u.name} units number">${getForces(u)}</div>`)
.join(" ");
@ -469,7 +471,7 @@ function overviewMilitary() {
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.state + ",";
data += units.map(u => el.dataset[u]).join(",") + ",";
data += units.map(u => el.dataset[u.toLowerCase()]).join(",") + ",";
data += el.dataset.total + ",";
data += el.dataset.population + ",";
data += rn(el.dataset.rate, 2) + "%,";

View file

@ -55,6 +55,7 @@ function editNotes(id, name) {
byId("notesLegend").addEventListener("blur", updateLegend);
byId("notesPin").addEventListener("click", toggleNotesPin);
byId("notesFocus").addEventListener("click", validateHighlightElement);
byId("notesGenerateWithAi").addEventListener("click", openAiGenerator);
byId("notesDownload").addEventListener("click", downloadLegends);
byId("notesUpload").addEventListener("click", () => legendsToLoad.click());
byId("legendsToLoad").addEventListener("change", function () {
@ -64,7 +65,7 @@ function editNotes(id, name) {
async function initEditor() {
if (!window.tinymce) {
const url = "https://cdn.tiny.cloud/1/4i6a79ymt2y0cagke174jp3meoi28vyecrch12e5puyw3p9a/tinymce/5/tinymce.min.js";
const url = "https://azgaar.github.io/Fantasy-Map-Generator/libs/tinymce/tinymce.min.js";
try {
await import(url);
} catch (error) {
@ -79,11 +80,13 @@ function editNotes(id, name) {
}
if (window.tinymce) {
window.tinymce._setBaseUrl("https://azgaar.github.io/Fantasy-Map-Generator/libs/tinymce");
tinymce.init({
license_key: "gpl",
selector: "#notesLegend",
height: "90%",
menubar: false,
plugins: `autolink lists link charmap print code fullscreen image link media table paste hr wordcount`,
plugins: `autolink lists link charmap code fullscreen image link media table wordcount`,
toolbar: `code | undo redo | removeformat | bold italic strikethrough | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image media table | fontselect fontsizeselect | blockquote hr charmap | print fullscreen`,
media_alt_source: false,
media_poster: false,
@ -141,6 +144,25 @@ function editNotes(id, name) {
});
}
function openAiGenerator() {
const note = notes.find(note => note.id === notesSelect.value);
let prompt = `Respond with description. Use simple dry language. Invent facts, names and details. Split to paragraphs and format to HTML. Remove h tags, remove markdown.`;
if (note?.name) prompt += ` Name: ${note.name}.`;
if (note?.legend) prompt += ` Data: ${note.legend}`;
const onApply = result => {
notesLegend.innerHTML = result;
if (note) {
note.legend = result;
updateNotesBox(note);
if (window.tinymce) tinymce.activeEditor.setContent(note.legend);
}
};
geneateWithAi(prompt, onApply);
}
function downloadLegends() {
const notesData = JSON.stringify(notes);
const name = getFileName("Notes") + ".txt";

View file

@ -66,17 +66,23 @@ document
.querySelectorAll(".tabcontent")
.forEach(e => (e.style.display = "none"));
if (id === "layersTab") layersContent.style.display = "block";
else if (id === "styleTab") styleContent.style.display = "block";
else if (id === "optionsTab") optionsContent.style.display = "block";
else if (id === "toolsTab")
if (id === "layersTab") {
layersContent.style.display = "block";
} else if (id === "styleTab") {
styleContent.style.display = "block";
selectStyleElement();
} else if (id === "optionsTab") {
optionsContent.style.display = "block";
} else if (id === "toolsTab") {
customization === 1 ? (customizationMenu.style.display = "block") : (toolsContent.style.display = "block");
else if (id === "aboutTab") aboutContent.style.display = "block";
} else if (id === "aboutTab") {
aboutContent.style.display = "block";
}
});
// show popup with a list of Patreon supportes (updated manually)
async function showSupporters() {
const {supporters} = await import("../dynamic/supporters.js?v=1.93.08");
const {supporters} = await import("../dynamic/supporters.js?v=1.97.14");
const list = supporters.split("\n").sort();
const columns = window.innerWidth < 800 ? 2 : 5;
@ -119,35 +125,33 @@ function updateOutputToFollowInput(ev) {
// Option listeners
const optionsContent = byId("optionsContent");
optionsContent.addEventListener("input", function (event) {
const id = event.target.id;
const value = event.target.value;
optionsContent.addEventListener("input", event => {
const {id, value} = event.target;
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
else if (id === "pointsInput") changeCellsDensity(+value);
else if (id === "culturesSet") changeCultureSet();
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
else if (id === "statesNumber") changeStatesNumber(value);
else if (id === "emblemShape") changeEmblemShape(value);
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
else if (id === "tooltipSize") changeTooltipSize(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) {
const id = event.target.id;
const value = event.target.value;
optionsContent.addEventListener("change", event => {
const {id, value} = event.target;
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
else if (id === "optionsSeed") generateMapWithSeed("seed change");
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUiSize(value);
else if (id === "uiSize") changeUiSize(+value);
else if (id === "shapeRendering") setRendering(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;
optionsContent.addEventListener("click", event => {
const {id} = event.target;
if (id === "restoreDefaultCanvasSize") restoreDefaultCanvasSize();
else if (id === "optionsMapHistory") showSeedHistoryDialog();
else if (id === "optionsCopySeed") copyMapURL();
@ -327,6 +331,7 @@ const cellsDensityMap = {
};
function changeCellsDensity(value) {
pointsInput.value = value;
const cells = cellsDensityMap[value] || 1000;
pointsInput.dataset.cells = cells;
pointsOutputFormatted.value = getCellsDensityValue(cells);
@ -390,18 +395,18 @@ function changeEmblemShape(emblemShape) {
}
function changeStatesNumber(value) {
regionsOutput.style.color = +value ? null : "#b12117";
byId("statesNumber").style.color = +value ? null : "#b12117";
burgLabels.select("#capitals").attr("data-size", Math.max(rn(6 - value / 20), 3));
labels.select("#countries").attr("data-size", Math.max(rn(18 - value / 6), 4));
}
function changeUiSize(value) {
if (isNaN(+value) || +value < 0.5) return;
if (isNaN(value) || value < 0.5) return;
const max = getUImaxSize();
if (+value > max) value = max;
if (value > max) value = max;
uiSizeInput.value = uiSizeOutput.value = value;
uiSize.value = value;
document.getElementsByTagName("body")[0].style.fontSize = rn(value * 10, 2) + "px";
byId("options").style.width = value * 300 + "px";
}
@ -428,7 +433,7 @@ function changeThemeHue(hue) {
// change color and transparency for modal windows
function changeDialogsTheme(themeColor, transparency) {
transparencyInput.value = transparencyOutput.value = transparency;
transparencyInput.value = transparency;
const alpha = (100 - +transparency) / 100;
const alphaReduced = Math.min(alpha + 0.3, 1);
@ -490,11 +495,11 @@ function resetLanguage() {
if (!languageSelect.value) return;
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
languageSelect.handleChange(new Event("change"));
// do once again to actually reset the language
languageSelect.value = "en";
languageSelect.dispatchEvent(new Event("change"));
languageSelect.handleChange(new Event("change"));
}
function changeZoomExtent(value) {
@ -534,8 +539,8 @@ function applyStoredOptions() {
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key === "speakerVoice") continue;
const input = byId(key + "Input") || byId(key);
const output = byId(key + "Output");
@ -544,6 +549,9 @@ function applyStoredOptions() {
if (output) output.value = value;
lock(key);
if (key === "points") changeCellsDensity(+value);
if (key === "distanceScale") distanceScale = +value;
// add saved style presets to options
if (key.slice(0, 5) === "style") applyOption(stylePreset, key, key.slice(5));
}
@ -557,8 +565,8 @@ function applyStoredOptions() {
if (stored("tooltipSize")) changeTooltipSize(stored("tooltipSize"));
if (stored("regions")) changeStatesNumber(stored("regions"));
uiSizeInput.max = uiSizeOutput.max = getUImaxSize();
if (stored("uiSize")) changeUiSize(stored("uiSize"));
uiSize.max = uiSize.max = getUImaxSize();
if (stored("uiSize")) changeUiSize(+stored("uiSize"));
else changeUiSize(minmax(rn(mapWidthInput.value / 1280, 1), 1, 2.5));
// search params overwrite stored and default options
@ -581,16 +589,17 @@ function randomizeOptions() {
const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options
// 'Options' settings
if (randomize || !locked("points")) changeCellsDensity(4); // reset to default, no need to randomize
if (randomize || !locked("template")) randomizeHeightmapTemplate();
if (randomize || !locked("regions")) regionsInput.value = regionsOutput.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provinces")) provincesInput.value = provincesOutput.value = gauss(20, 10, 20, 100);
if (randomize || !locked("statesNumber")) statesNumber.value = gauss(18, 5, 2, 30);
if (randomize || !locked("provincesRatio")) provincesRatio.value = gauss(20, 10, 20, 100);
if (randomize || !locked("manors")) {
manorsInput.value = 1000;
manorsOutput.value = "auto";
}
if (randomize || !locked("religions")) religionsInput.value = religionsOutput.value = gauss(6, 3, 2, 10);
if (randomize || !locked("power")) powerInput.value = powerOutput.value = gauss(4, 2, 0, 10, 2);
if (randomize || !locked("neutral")) neutralInput.value = neutralOutput.value = rn(1 + Math.random(), 1);
if (randomize || !locked("religionsNumber")) religionsNumber.value = gauss(6, 3, 2, 10);
if (randomize || !locked("sizeVariety")) sizeVariety.value = gauss(4, 2, 0, 10, 1);
if (randomize || !locked("growthRate")) growthRate.value = rn(1 + Math.random(), 1);
if (randomize || !locked("cultures")) culturesInput.value = culturesOutput.value = gauss(12, 3, 5, 30);
if (randomize || !locked("culturesSet")) randomizeCultureSet();
@ -602,7 +611,7 @@ function randomizeOptions() {
// 'Units Editor' settings
const US = navigator.language === "en-US";
if (randomize || !locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (randomize || !locked("distanceScale")) distanceScale = distanceScaleInput.value = gauss(3, 1, 1, 5);
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
@ -641,17 +650,16 @@ function randomizeCultureSet() {
function setRendering(value) {
viewbox.attr("shape-rendering", value);
// if (value === "optimizeSpeed") {
// // block some styles
// coastline.select("#sea_island").style("filter", "none");
// statesHalo.style("display", "none");
// emblems.style("opacity", 1);
// } else {
// // remove style block
// coastline.select("#sea_island").style("filter", null);
// statesHalo.style("display", null);
// emblems.style("opacity", null);
// }
if (value === "optimizeSpeed") {
// block some styles
coastline.select("#sea_island").style("filter", "none");
statesHalo.style("display", "none");
} else {
// remove style block
coastline.select("#sea_island").style("filter", null);
statesHalo.style("display", null);
if (pack.cells && statesHalo.selectAll("*").size() === 0) drawStates();
}
}
// generate current year and era name
@ -774,7 +782,7 @@ function showExportPane() {
}
async function exportToJson(type) {
const {exportToJson} = await import("../dynamic/export-json.js?v=1.96.00");
const {exportToJson} = await import("../dynamic/export-json.js?v=1.100.00");
exportToJson(type);
}
@ -899,9 +907,9 @@ function updateTilesOptions() {
}
const tileSize = byId("tileSize");
const tilesX = +byId("tileColsOutput").value;
const tilesY = +byId("tileRowsOutput").value;
const scale = +byId("tileScaleOutput").value;
const tilesX = +byId("tileColsOutput").value || 2;
const tilesY = +byId("tileRowsOutput").value || 2;
const scale = +byId("tileScaleOutput").value || 1;
// calculate size
const sizeX = graphWidth * scale * tilesX;
@ -918,11 +926,16 @@ function updateTilesOptions() {
const tileH = (graphHeight / tilesY) | 0;
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
function getRowLabel(row) {
const first = row >= alphabet.length ? alphabet[Math.floor(row / alphabet.length) - 1] : "";
const last = alphabet[row % alphabet.length];
return first + last;
}
for (let y = 0, row = 0; y + tileH <= graphHeight; y += tileH, row++) {
for (let x = 0, column = 1; x + tileW <= graphWidth; x += tileW, column++) {
rects.push(`<rect x=${x} y=${y} width=${tileW} height=${tileH} />`);
const label = alphabet[row % alphabet.length] + column;
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${label}</text>`);
labels.push(`<text x=${x + tileW / 2} y=${y + tileH / 2}>${getRowLabel(row)}${column}</text>`);
}
}

View file

@ -640,8 +640,8 @@ function editProvinces() {
.parentId(d => d.state)(data)
.sum(d => d.area);
const width = 300 + 300 * uiSizeOutput.value,
height = 90 + 90 * uiSizeOutput.value;
const width = 300 + 300 * uiSize.value,
height = 90 + 90 * uiSize.value;
const margin = {top: 10, right: 10, bottom: 0, left: 10};
const w = width - margin.left - margin.right;
const h = height - margin.top - margin.bottom;
@ -891,14 +891,14 @@ function editProvinces() {
}
function dragBrush() {
const r = +provincesManuallyBrush.value;
const r = +provincesBrush.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 found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
const selection = found.filter(isLand);
if (selection) changeForSelection(selection);
});
@ -949,7 +949,7 @@ function editProvinces() {
function moveBrush() {
showMainTip();
const point = d3.mouse(this);
const radius = +provincesManuallyBrush.value;
const radius = +provincesBrush.value;
moveCircle(point[0], point[1], radius);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ const systemPresets = [
"watercolor",
"clean",
"atlas",
"darkSeas",
"cyberpunk",
"night",
"monochrome"
@ -63,7 +64,7 @@ async function getStylePreset(desiredPreset) {
async function fetchSystemPreset(preset) {
try {
const res = await fetch(`./styles/${preset}.json`);
const res = await fetch(`./styles/${preset}.json?v=${VERSION}`);
return await res.json();
} catch (err) {
throw new Error("Cannot fetch style preset", preset);
@ -198,7 +199,7 @@ function addStylePreset() {
"mask"
],
"#compass": ["opacity", "transform", "filter", "mask", "shape-rendering"],
"#rose": ["transform"],
"#compass > use": ["transform"],
"#relig": ["opacity", "stroke", "stroke-width", "filter"],
"#cults": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#landmass": ["opacity", "fill", "filter"],
@ -237,6 +238,9 @@ function addStylePreset() {
],
"#ice": ["opacity", "fill", "stroke", "stroke-width", "filter"],
"#emblems": ["opacity", "stroke-width", "filter"],
"#emblems > #stateEmblems": ["data-size"],
"#emblems > #provinceEmblems": ["data-size"],
"#emblems > #burgEmblems": ["data-size"],
"#texture": ["opacity", "filter", "mask", "data-x", "data-y", "data-href"],
"#zones": ["opacity", "stroke", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter", "mask"],
"#oceanLayers": ["filter", "layers"],
@ -267,7 +271,15 @@ function addStylePreset() {
"data-columns"
],
"#legendBox": ["fill", "fill-opacity"],
"#burgLabels > #cities": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgLabels > #cities": [
"opacity",
"fill",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family"
],
"#burgIcons > #cities": [
"opacity",
"fill",
@ -279,7 +291,15 @@ function addStylePreset() {
"stroke-linecap"
],
"#anchors > #cities": ["opacity", "fill", "size", "stroke", "stroke-width"],
"#burgLabels > #towns": ["opacity", "fill", "text-shadow", "data-size", "font-size", "font-family"],
"#burgLabels > #towns": [
"opacity",
"fill",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family"
],
"#burgIcons > #towns": [
"opacity",
"fill",
@ -297,6 +317,7 @@ function addStylePreset() {
"stroke",
"stroke-width",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family",
@ -308,6 +329,7 @@ function addStylePreset() {
"stroke",
"stroke-width",
"text-shadow",
"letter-spacing",
"data-size",
"font-size",
"font-family",

View file

@ -18,7 +18,7 @@
}
// store some style inputs as options
styleElements.addEventListener("change", function (ev) {
styleElements.on("change", function (ev) {
if (ev.target.dataset.stored) lock(ev.target.dataset.stored);
});
@ -71,7 +71,8 @@ function getColorScheme(scheme = "bright") {
}
// Toggle style sections on element select
styleElementSelect.addEventListener("change", selectStyleElement);
styleElementSelect.on("change", selectStyleElement);
function selectStyleElement() {
const styleElement = styleElementSelect.value;
let el = d3.select("#" + styleElement);
@ -92,7 +93,7 @@ function selectStyleElement() {
// opacity
if (!["landmass", "ocean", "regions", "legend"].includes(styleElement)) {
styleOpacity.style.display = "block";
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
styleOpacityInput.value = el.attr("opacity") || 1;
}
// filter
@ -129,7 +130,7 @@ function selectStyleElement() {
styleStroke.style.display = "block";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
}
// stroke dash
@ -146,15 +147,17 @@ function selectStyleElement() {
// clipping
if (
[
"cells",
"gridOverlay",
"coordinates",
"compass",
"terrain",
"temperature",
"routes",
"texture",
"biomes",
"cells",
"compass",
"coordinates",
"gridOverlay",
"population",
"prec",
"routes",
"temperature",
"terrain",
"texture",
"zones"
].includes(styleElement)
) {
@ -176,9 +179,9 @@ function selectStyleElement() {
styleHeightmapRenderOcean.checked = +el.attr("data-render");
styleHeightmapScheme.value = el.attr("scheme");
styleHeightmapTerracingInput.value = styleHeightmapTerracingOutput.value = el.attr("terracing");
styleHeightmapSkipInput.value = styleHeightmapSkipOutput.value = el.attr("skip");
styleHeightmapSimplificationInput.value = styleHeightmapSimplificationOutput.value = el.attr("relax");
styleHeightmapTerracing.value = el.attr("terracing");
styleHeightmapSkip.value = el.attr("skip");
styleHeightmapSimplification.value = el.attr("relax");
styleHeightmapCurve.value = el.attr("curve");
}
@ -201,13 +204,13 @@ function selectStyleElement() {
const tr = parseTransform(compass.select("use").attr("transform"));
styleCompassShiftX.value = tr[0];
styleCompassShiftY.value = tr[1];
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2];
styleCompassSizeInput.value = tr[2];
}
if (styleElement === "terrain") {
styleRelief.style.display = "block";
styleReliefSizeOutput.innerHTML = styleReliefSizeInput.value = terrain.attr("size");
styleReliefDensityOutput.innerHTML = styleReliefDensityInput.value = terrain.attr("density");
styleReliefSize.value = terrain.attr("size") || 1;
styleReliefDensity.value = terrain.attr("density") || 0.4;
styleReliefSet.value = terrain.attr("set");
}
@ -220,30 +223,31 @@ function selectStyleElement() {
.select("#urban")
.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
}
if (styleElement === "regions") {
styleStates.style.display = "block";
styleStatesBodyOpacity.value = styleStatesBodyOpacityOutput.value = statesBody.attr("opacity") || 1;
styleStatesBodyOpacity.value = statesBody.attr("opacity") || 1;
styleStatesBodyFilter.value = statesBody.attr("filter") || "";
styleStatesHaloWidth.value = styleStatesHaloWidthOutput.value = statesHalo.attr("data-width") || 10;
styleStatesHaloOpacity.value = styleStatesHaloOpacityOutput.value = statesHalo.attr("opacity") || 1;
const blur = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0;
styleStatesHaloBlur.value = styleStatesHaloBlurOutput.value = blur;
styleStatesHaloWidth.value = statesHalo.attr("data-width") || 10;
styleStatesHaloOpacity.value = statesHalo.attr("opacity") || 1;
styleStatesHaloBlur.value = parseFloat(statesHalo.attr("filter")?.match(/blur\(([^)]+)\)/)?.[1]) || 0;
}
if (styleElement === "labels") {
styleFill.style.display = "block";
styleStroke.style.display = "block";
styleStrokeWidth.style.display = "block";
styleLetterSpacing.style.display = "block";
styleShadow.style.display = "block";
styleSize.style.display = "block";
styleVisibility.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
styleLetterSpacingInput.value = el.attr("letter-spacing") || 0;
styleShadowInput.value = el.style("text-shadow") || "white 0 0 4px";
styleFont.style.display = "block";
@ -258,7 +262,7 @@ function selectStyleElement() {
styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family");
styleFontSize.value = el.attr("data-size");
styleFontSize.value = el.attr("font-size");
}
if (styleElement == "burgIcons") {
@ -269,7 +273,7 @@ function selectStyleElement() {
styleRadius.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.24;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.24;
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
styleRadiusInput.value = el.attr("size") || 1;
@ -282,7 +286,7 @@ function selectStyleElement() {
styleIconSize.style.display = "block";
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.24;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.24;
styleIconSizeInput.value = el.attr("size") || 2;
}
@ -292,12 +296,13 @@ function selectStyleElement() {
styleSize.style.display = "block";
styleLegend.style.display = "block";
styleLegendColItemsOutput.value = styleLegendColItems.value = el.attr("data-columns");
styleLegendBackOutput.value = styleLegendBack.value = el.select("#legendBox").attr("fill");
styleLegendOpacityOutput.value = styleLegendOpacity.value = el.select("#legendBox").attr("fill-opacity");
styleLegendColItems.value = el.attr("data-columns");
const legendBox = el.select("#legendBox");
styleLegendBack.value = styleLegendBackOutput.value = legendBox.size() ? legendBox.attr("fill") : "#ffffff";
styleLegendOpacity.value = legendBox.size() ? legendBox.attr("fill-opacity") : 1;
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#111111";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0.5;
styleStrokeWidthInput.value = el.attr("stroke-width") || 0.5;
styleFont.style.display = "block";
styleSelectFont.value = el.attr("font-family");
@ -308,18 +313,17 @@ function selectStyleElement() {
styleOcean.style.display = "block";
styleOceanFill.value = styleOceanFillOutput.value = oceanLayers.select("#oceanBase").attr("fill");
styleOceanPattern.value = byId("oceanicPattern")?.getAttribute("href");
styleOceanPatternOpacity.value = styleOceanPatternOpacityOutput.value =
byId("oceanicPattern").getAttribute("opacity") || 1;
styleOceanPatternOpacity.value = byId("oceanicPattern").getAttribute("opacity") || 1;
outlineLayers.value = oceanLayers.attr("layers");
}
if (styleElement === "temperature") {
styleStrokeWidth.style.display = "block";
styleTemperature.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || 0.1;
styleStrokeWidthInput.value = el.attr("stroke-width") || "";
styleTemperatureFillOpacityInput.value = el.attr("fill-opacity") || 0.1;
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000";
styleTemperatureFontSizeInput.value = styleTemperatureFontSizeOutput.value = el.attr("font-size") || "8px";
styleTemperatureFontSizeInput.value = el.attr("font-size") || "8px";
}
if (styleElement === "coordinates") {
@ -329,14 +333,17 @@ function selectStyleElement() {
if (styleElement === "armies") {
styleArmies.style.display = "block";
styleArmiesFillOpacity.value = styleArmiesFillOpacityOutput.value = el.attr("fill-opacity");
styleArmiesSize.value = styleArmiesSizeOutput.value = el.attr("box-size");
styleArmiesFillOpacity.value = el.attr("fill-opacity");
styleArmiesSize.value = el.attr("box-size");
}
if (styleElement === "emblems") {
styleEmblems.style.display = "block";
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 1;
styleStrokeWidthInput.value = el.attr("stroke-width") || 1;
emblemsStateSizeInput.value = emblems.select("#stateEmblems").attr("data-size") || 1;
emblemsProvinceSizeInput.value = emblems.select("#provinceEmblems").attr("data-size") || 1;
emblemsBurgSizeInput.value = emblems.select("#burgEmblems").attr("data-size") || 1;
}
// update group options
@ -372,11 +379,9 @@ function selectStyleElement() {
const scaleBarBack = el.select("#scaleBarBack");
if (scaleBarBack.size()) {
styleScaleBarBackgroundOpacityInput.value = styleScaleBarBackgroundOpacityOutput.value =
scaleBarBack.attr("opacity");
styleScaleBarBackgroundFillInput.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundStrokeInput.value = styleScaleBarBackgroundStrokeOutput.value =
scaleBarBack.attr("stroke");
styleScaleBarBackgroundOpacity.value = scaleBarBack.attr("opacity");
styleScaleBarBackgroundFill.value = styleScaleBarBackgroundFillOutput.value = scaleBarBack.attr("fill");
styleScaleBarBackgroundStroke.value = styleScaleBarBackgroundStrokeOutput.value = scaleBarBack.attr("stroke");
styleScaleBarBackgroundStrokeWidth.value = scaleBarBack.attr("stroke-width");
styleScaleBarBackgroundFilter.value = scaleBarBack.attr("filter");
styleScaleBarBackgroundPaddingTop.value = scaleBarBack.attr("data-top");
@ -398,13 +403,13 @@ function selectStyleElement() {
styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter"));
styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
}
}
}
// Handle style inputs change
styleGroupSelect.addEventListener("change", selectStyleElement);
styleGroupSelect.on("change", selectStyleElement);
function getEl() {
const el = styleElementSelect.value;
@ -413,44 +418,46 @@ function getEl() {
else return svg.select("#" + el).select("#" + g);
}
styleFillInput.addEventListener("input", function () {
styleFillInput.on("input", function () {
styleFillOutput.value = this.value;
getEl().attr("fill", this.value);
});
styleStrokeInput.addEventListener("input", function () {
styleStrokeInput.on("input", function () {
styleStrokeOutput.value = this.value;
getEl().attr("stroke", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeWidthInput.addEventListener("input", function () {
styleStrokeWidthOutput.value = this.value;
getEl().attr("stroke-width", +this.value);
styleStrokeWidthInput.on("input", e => {
getEl().attr("stroke-width", e.target.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeDasharrayInput.addEventListener("input", function () {
styleLetterSpacingInput.on("input", e => {
getEl().attr("letter-spacing", e.target.value);
});
styleStrokeDasharrayInput.on("input", function () {
getEl().attr("stroke-dasharray", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleStrokeLinecapInput.addEventListener("change", function () {
styleStrokeLinecapInput.on("change", function () {
getEl().attr("stroke-linecap", this.value);
if (styleElementSelect.value === "gridOverlay" && layerIsOn("toggleGrid")) drawGrid();
});
styleOpacityInput.addEventListener("input", function () {
styleOpacityOutput.value = this.value;
getEl().attr("opacity", this.value);
styleOpacityInput.on("input", e => {
getEl().attr("opacity", e.target.value);
});
styleFilterInput.addEventListener("change", function () {
styleFilterInput.on("change", function () {
if (styleGroupSelect.value === "ocean") return oceanLayers.attr("filter", this.value);
getEl().attr("filter", this.value);
});
styleTextureInput.addEventListener("change", function () {
styleTextureInput.on("change", function () {
changeTexture(this.value);
});
@ -469,7 +476,7 @@ function updateTextureSelectValue(href) {
}
}
styleTextureShiftX.addEventListener("input", function () {
styleTextureShiftX.on("input", function () {
texture.attr("data-x", this.value);
texture
.select("image")
@ -477,7 +484,7 @@ styleTextureShiftX.addEventListener("input", function () {
.attr("width", graphWidth - this.valueAsNumber);
});
styleTextureShiftY.addEventListener("input", function () {
styleTextureShiftY.on("input", function () {
texture.attr("data-y", this.value);
texture
.select("image")
@ -485,17 +492,17 @@ styleTextureShiftY.addEventListener("input", function () {
.attr("height", graphHeight - this.valueAsNumber);
});
styleClippingInput.addEventListener("change", function () {
styleClippingInput.on("change", function () {
getEl().attr("mask", this.value);
});
styleGridType.addEventListener("change", function () {
styleGridType.on("change", function () {
getEl().attr("type", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize();
});
styleGridScale.addEventListener("input", function () {
styleGridScale.on("input", function () {
getEl().attr("scale", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
calculateFriendlyGridSize();
@ -503,57 +510,56 @@ styleGridScale.addEventListener("input", function () {
function calculateFriendlyGridSize() {
const size = styleGridScale.value * 25;
const friendly = `${rn(size * distanceScaleInput.value, 2)} ${distanceUnitInput.value}`;
const friendly = `${rn(size * distanceScale, 2)} ${distanceUnitInput.value}`;
styleGridSizeFriendly.value = friendly;
}
styleGridShiftX.addEventListener("input", function () {
styleGridShiftX.on("input", function () {
getEl().attr("dx", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
});
styleGridShiftY.addEventListener("input", function () {
styleGridShiftY.on("input", function () {
getEl().attr("dy", this.value);
if (layerIsOn("toggleGrid")) drawGrid();
});
styleRescaleMarkers.addEventListener("change", function () {
styleRescaleMarkers.on("change", function () {
markers.attr("rescale", +this.checked);
invokeActiveZooming();
});
styleCoastlineAuto.addEventListener("change", function () {
styleCoastlineAuto.on("change", function () {
coastline.select("#sea_island").attr("auto-filter", +this.checked);
styleFilter.style.display = this.checked ? "none" : "block";
invokeActiveZooming();
});
styleOceanFill.addEventListener("input", function () {
styleOceanFill.on("input", function () {
oceanLayers.select("rect").attr("fill", this.value);
styleOceanFillOutput.value = this.value;
});
styleOceanPattern.addEventListener("change", function () {
styleOceanPattern.on("change", function () {
byId("oceanicPattern")?.setAttribute("href", this.value);
});
styleOceanPatternOpacity.addEventListener("input", function () {
byId("oceanicPattern").setAttribute("opacity", this.value);
styleOceanPatternOpacityOutput.value = this.value;
styleOceanPatternOpacity.on("input", e => {
byId("oceanicPattern").setAttribute("opacity", e.target.value);
});
outlineLayers.addEventListener("change", function () {
outlineLayers.on("change", function () {
oceanLayers.selectAll("path").remove();
oceanLayers.attr("layers", this.value);
OceanLayers();
});
styleHeightmapScheme.addEventListener("change", function () {
styleHeightmapScheme.on("change", function () {
getEl().attr("scheme", this.value);
drawHeightmap();
});
openCreateHeightmapSchemeButton.addEventListener("click", function () {
openCreateHeightmapSchemeButton.on("click", function () {
// start with current scheme
const scheme = getEl().attr("scheme");
this.dataset.stops = scheme.startsWith("#")
@ -672,106 +678,97 @@ openCreateHeightmapSchemeButton.addEventListener("click", function () {
});
});
styleHeightmapRenderOcean.addEventListener("change", function () {
getEl().attr("data-render", +this.checked);
styleHeightmapRenderOcean.on("change", e => {
const checked = +e.target.checked;
getEl().attr("data-render", checked);
drawHeightmap();
});
styleHeightmapTerracingInput.addEventListener("input", function () {
getEl().attr("terracing", this.value);
styleHeightmapTerracing.on("input", e => {
getEl().attr("terracing", e.target.value);
drawHeightmap();
});
styleHeightmapSkipInput.addEventListener("input", function () {
getEl().attr("skip", this.value);
styleHeightmapSkip.on("input", e => {
getEl().attr("skip", e.target.value);
drawHeightmap();
});
styleHeightmapSimplificationInput.addEventListener("input", function () {
getEl().attr("relax", this.value);
styleHeightmapSimplification.on("input", e => {
getEl().attr("relax", e.target.value);
drawHeightmap();
});
styleHeightmapCurve.addEventListener("change", function () {
getEl().attr("curve", this.value);
styleHeightmapCurve.on("change", e => {
getEl().attr("curve", e.target.value);
drawHeightmap();
});
styleReliefSet.addEventListener("change", function () {
terrain.attr("set", this.value);
ReliefIcons();
styleReliefSet.on("change", e => {
terrain.attr("set", e.target.value);
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefSizeInput.addEventListener("change", function () {
terrain.attr("size", this.value);
styleReliefSizeOutput.value = this.value;
ReliefIcons();
styleReliefSize.on("change", e => {
terrain.attr("size", e.target.value);
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleReliefDensityInput.addEventListener("change", function () {
terrain.attr("density", this.value);
styleReliefDensityOutput.value = this.value;
ReliefIcons();
styleReliefDensity.on("change", e => {
terrain.attr("density", e.target.value);
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
});
styleTemperatureFillOpacityInput.addEventListener("input", function () {
temperature.attr("fill-opacity", this.value);
styleTemperatureFillOpacityOutput.value = this.value;
styleTemperatureFillOpacityInput.on("input", e => {
temperature.attr("fill-opacity", e.target.value);
});
styleTemperatureFontSizeInput.addEventListener("input", function () {
temperature.attr("font-size", this.value + "px");
styleTemperatureFontSizeOutput.value = this.value + "px";
styleTemperatureFontSizeInput.on("input", e => {
temperature.attr("font-size", e.target.value + "px");
});
styleTemperatureFillInput.addEventListener("input", function () {
temperature.attr("fill", this.value);
styleTemperatureFillOutput.value = this.value;
styleTemperatureFillInput.on("input", e => {
temperature.attr("fill", e.target.value);
styleTemperatureFillOutput.value = e.target.value;
});
stylePopulationRuralStrokeInput.addEventListener("input", function () {
population.select("#rural").attr("stroke", this.value);
stylePopulationRuralStrokeOutput.value = this.value;
stylePopulationRuralStrokeInput.on("input", e => {
population.select("#rural").attr("stroke", e.target.value);
stylePopulationRuralStrokeOutput.value = e.target.value;
});
stylePopulationUrbanStrokeInput.addEventListener("input", function () {
population.select("#urban").attr("stroke", this.value);
stylePopulationUrbanStrokeOutput.value = this.value;
stylePopulationUrbanStrokeInput.on("input", e => {
population.select("#urban").attr("stroke", e.target.value);
stylePopulationUrbanStrokeOutput.value = e.target.value;
});
styleCompassSizeInput.addEventListener("input", function () {
styleCompassSizeOutput.value = this.value;
shiftCompass();
});
styleCompassShiftX.addEventListener("input", shiftCompass);
styleCompassShiftY.addEventListener("input", shiftCompass);
styleCompassSizeInput.on("input", shiftCompass);
styleCompassShiftX.on("input", shiftCompass);
styleCompassShiftY.on("input", shiftCompass);
function shiftCompass() {
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
compass.select("use").attr("transform", tr);
}
styleLegendColItems.addEventListener("input", function () {
styleLegendColItemsOutput.value = this.value;
legend.select("#legendBox").attr("data-columns", this.value);
styleLegendColItems.on("input", e => {
legend.select("#legendBox").attr("data-columns", e.target.value);
redrawLegend();
});
styleLegendBack.addEventListener("input", function () {
styleLegendBackOutput.value = this.value;
legend.select("#legendBox").attr("fill", this.value);
styleLegendBack.on("input", e => {
styleLegendBackOutput.value = e.target.value;
legend.select("#legendBox").attr("fill", e.target.value);
});
styleLegendOpacity.addEventListener("input", function () {
styleLegendOpacityOutput.value = this.value;
legend.select("#legendBox").attr("fill-opacity", this.value);
styleLegendOpacity.on("input", e => {
legend.select("#legendBox").attr("fill-opacity", e.target.value);
});
styleSelectFont.addEventListener("change", changeFont);
styleSelectFont.on("change", changeFont);
function changeFont() {
const family = styleSelectFont.value;
getEl().attr("font-family", family);
@ -779,11 +776,11 @@ function changeFont() {
if (styleElementSelect.value === "legend") redrawLegend();
}
styleShadowInput.addEventListener("input", function () {
styleShadowInput.on("input", function () {
getEl().style("text-shadow", this.value);
});
styleFontAdd.addEventListener("click", function () {
styleFontAdd.on("click", function () {
addFontNameInput.value = "";
addFontURLInput.value = "";
@ -820,22 +817,22 @@ styleFontAdd.addEventListener("click", function () {
});
});
addFontMethod.addEventListener("change", function () {
addFontMethod.on("change", function () {
addFontURLInput.style.display = this.value === "fontURL" ? "inline" : "none";
});
styleFontSize.addEventListener("change", function () {
styleFontSize.on("change", function () {
changeFontSize(getEl(), +this.value);
});
styleFontPlus.addEventListener("click", function () {
const size = +getEl().attr("data-size") + 1;
changeFontSize(getEl(), Math.min(size, 999));
styleFontPlus.on("click", function () {
const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.min(current + 1, 999));
});
styleFontMinus.addEventListener("click", function () {
const size = +getEl().attr("data-size") - 1;
changeFontSize(getEl(), Math.max(size, 1));
styleFontMinus.on("click", function () {
const current = +styleFontSize.value || 12;
changeFontSize(getEl(), Math.max(current - 1, 1));
});
function changeFontSize(el, size) {
@ -856,16 +853,16 @@ function changeFontSize(el, size) {
if (styleElementSelect.value === "legend") redrawLegend();
}
styleRadiusInput.addEventListener("change", function () {
styleRadiusInput.on("change", function () {
changeRadius(+this.value);
});
styleRadiusPlus.addEventListener("click", function () {
styleRadiusPlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeRadius(size);
});
styleRadiusMinus.addEventListener("click", function () {
styleRadiusMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeRadius(size);
});
@ -887,16 +884,16 @@ function changeRadius(size, group) {
changeIconSize(size * 2, g); // change also anchor icons
}
styleIconSizeInput.addEventListener("change", function () {
styleIconSizeInput.on("change", function () {
changeIconSize(+this.value);
});
styleIconSizePlus.addEventListener("click", function () {
styleIconSizePlus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), 0.2);
changeIconSize(size);
});
styleIconSizeMinus.addEventListener("click", function () {
styleIconSizeMinus.on("click", function () {
const size = Math.max(rn(getEl().attr("size") * 0.9, 2), 0.2);
changeIconSize(size);
});
@ -921,39 +918,37 @@ function changeIconSize(size, group) {
styleIconSizeInput.value = size;
}
styleStatesBodyOpacity.addEventListener("input", function () {
styleStatesBodyOpacityOutput.value = this.value;
statesBody.attr("opacity", this.value);
styleStatesBodyOpacity.on("input", e => {
statesBody.attr("opacity", e.target.value);
});
styleStatesBodyFilter.addEventListener("change", function () {
styleStatesBodyFilter.on("change", function () {
statesBody.attr("filter", this.value);
});
styleStatesHaloWidth.addEventListener("input", function () {
styleStatesHaloWidthOutput.value = this.value;
statesHalo.attr("data-width", this.value).attr("stroke-width", this.value);
styleStatesHaloWidth.on("input", e => {
const value = e.target.value;
statesHalo.attr("data-width", value).attr("stroke-width", value);
});
styleStatesHaloOpacity.addEventListener("input", function () {
styleStatesHaloOpacityOutput.value = this.value;
statesHalo.attr("opacity", this.value);
styleStatesHaloOpacity.on("input", e => {
statesHalo.attr("opacity", e.target.value);
});
styleStatesHaloBlur.addEventListener("input", function () {
styleStatesHaloBlurOutput.value = this.value;
const blur = +this.value > 0 ? `blur(${this.value}px)` : null;
styleStatesHaloBlur.on("input", e => {
const value = Number(e.target.value);
const blur = value > 0 ? `blur(${value}px)` : null;
statesHalo.attr("filter", blur);
});
styleArmiesFillOpacity.addEventListener("input", function () {
armies.attr("fill-opacity", this.value);
styleArmiesFillOpacityOutput.value = this.value;
styleArmiesFillOpacity.on("input", e => {
armies.attr("fill-opacity", e.target.value);
});
styleArmiesSize.addEventListener("input", function () {
armies.attr("box-size", this.value).attr("font-size", this.value * 2);
styleArmiesSizeOutput.value = this.value;
styleArmiesSize.on("input", e => {
const value = Number(e.target.value);
armies.attr("box-size", value).attr("font-size", value * 2);
armies.selectAll("g").remove(); // clear armies layer
pack.states.forEach(s => {
if (!s.i || s.removed || !s.military.length) return;
@ -961,9 +956,20 @@ styleArmiesSize.addEventListener("input", function () {
});
});
emblemsStateSizeInput.addEventListener("change", drawEmblems);
emblemsProvinceSizeInput.addEventListener("change", drawEmblems);
emblemsBurgSizeInput.addEventListener("change", drawEmblems);
emblemsStateSizeInput.on("change", e => {
emblems.select("#stateEmblems").attr("data-size", e.target.value);
drawEmblems();
});
emblemsProvinceSizeInput.on("change", e => {
emblems.select("#provinceEmblems").attr("data-size", e.target.value);
drawEmblems();
});
emblemsBurgSizeInput.on("change", e => {
emblems.select("#burgEmblems").attr("data-size", e.target.value);
drawEmblems();
});
// request a URL to image to be used as a texture
function textureProvideURL() {
@ -1015,7 +1021,7 @@ Object.keys(vignettePresets).forEach(preset => {
styleVignettePreset.options.add(new Option(preset, preset, false, false));
});
styleVignettePreset.addEventListener("change", function () {
styleVignettePreset.on("change", function () {
const attributes = JSON.parse(vignettePresets[this.value]);
for (const selector in attributes) {
@ -1029,7 +1035,7 @@ styleVignettePreset.addEventListener("change", function () {
const vignette = byId("vignette");
if (vignette) {
styleOpacityInput.value = styleOpacityOutput.value = vignette.getAttribute("opacity");
styleOpacityInput.value = vignette.getAttribute("opacity");
styleFillInput.value = styleFillOutput.value = vignette.getAttribute("fill");
styleFilterInput.value = vignette.getAttribute("filter");
}
@ -1043,40 +1049,39 @@ styleVignettePreset.addEventListener("change", function () {
styleVignetteHeight.value = digit(maskRect.getAttribute("height"));
styleVignetteRx.value = digit(maskRect.getAttribute("rx"));
styleVignetteRy.value = digit(maskRect.getAttribute("ry"));
styleVignetteBlur.value = styleVignetteBlurOutput.value = digit(maskRect.getAttribute("filter"));
styleVignetteBlur.value = digit(maskRect.getAttribute("filter"));
}
});
styleVignetteX.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("x", `${this.value}%`);
styleVignetteX.on("input", e => {
byId("vignette-rect")?.setAttribute("x", `${e.target.value}%`);
});
styleVignetteWidth.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("width", `${this.value}%`);
styleVignetteWidth.on("input", e => {
byId("vignette-rect")?.setAttribute("width", `${e.target.value}%`);
});
styleVignetteY.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("y", `${this.value}%`);
styleVignetteY.on("input", e => {
byId("vignette-rect")?.setAttribute("y", `${e.target.value}%`);
});
styleVignetteHeight.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("height", `${this.value}%`);
styleVignetteHeight.on("input", e => {
byId("vignette-rect")?.setAttribute("height", `${e.target.value}%`);
});
styleVignetteRx.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("rx", `${this.value}%`);
styleVignetteRx.on("input", e => {
byId("vignette-rect")?.setAttribute("rx", `${e.target.value}%`);
});
styleVignetteRy.addEventListener("input", function () {
byId("vignette-rect")?.setAttribute("ry", `${this.value}%`);
styleVignetteRy.on("input", e => {
byId("vignette-rect")?.setAttribute("ry", `${e.target.value}%`);
});
styleVignetteBlur.addEventListener("input", function () {
styleVignetteBlurOutput.value = this.value;
byId("vignette-rect")?.setAttribute("filter", `blur(${this.value}px)`);
styleVignetteBlur.on("input", e => {
byId("vignette-rect")?.setAttribute("filter", `blur(${e.target.value}px)`);
});
styleScaleBar.addEventListener("input", function (event) {
styleScaleBar.on("input", function (event) {
const scaleBarBack = scaleBar.select("#scaleBarBack");
if (!scaleBarBack.size()) return;
@ -1087,9 +1092,9 @@ styleScaleBar.addEventListener("input", function (event) {
else if (id === "styleScaleBarPositionX") scaleBar.attr("data-x", value);
else if (id === "styleScaleBarPositionY") scaleBar.attr("data-y", value);
else if (id === "styleScaleBarLabel") scaleBar.attr("data-label", value);
else if (id === "styleScaleBarBackgroundOpacityInput") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFillInput") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStrokeInput") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundOpacity") scaleBarBack.attr("opacity", value);
else if (id === "styleScaleBarBackgroundFill") scaleBarBack.attr("fill", value);
else if (id === "styleScaleBarBackgroundStroke") scaleBarBack.attr("stroke", value);
else if (id === "styleScaleBarBackgroundStrokeWidth") scaleBarBack.attr("stroke-width", value);
else if (id === "styleScaleBarBackgroundFilter") scaleBarBack.attr("filter", value);
else if (id === "styleScaleBarBackgroundPaddingTop") scaleBarBack.attr("data-top", value);
@ -1156,7 +1161,7 @@ function updateElements() {
}
// GLOBAL FILTERS
mapFilters.addEventListener("click", applyMapFilter);
mapFilters.on("click", applyMapFilter);
function applyMapFilter(event) {
if (event.target.tagName !== "BUTTON") return;
const button = event.target;

View file

@ -159,18 +159,6 @@ window.UISubmap = (function () {
return canvas;
}
// currently unused alternative to PNG version
async function loadPreviewSVG($container, w, h) {
$container.innerHTML = /*html*/ `
<svg id="submapPreviewSVG" viewBox="0 0 ${graphWidth} ${graphHeight}">
<rect width="100%" height="100%" fill="${byId("styleOceanFill").value}" />
<rect fill="url(#oceanic)" width="100%" height="100%" />
<use href="#map"></use>
</svg>
`;
return byId("submapPreviewSVG");
}
// Resample the whole map to different cell resolution or shape
const resampleCurrentMap = debounce(function () {
WARN && console.warn("Resampling current map");
@ -258,11 +246,9 @@ window.UISubmap = (function () {
byId("latitudeInput").value = latitudeOutput.value;
// fix scale
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2);
populationRateInput.value = populationRateOutput.value = rn(
(populationRate = populationRateOutput.value / scale),
2
);
distanceScale = distanceScaleInput.value = rn(distanceScaleInput.value / scale, 2);
populationRate = populationRateInput.value = rn(populationRateInput.value / scale, 2);
customization = 0;
startResample(options);
}, 1000);
@ -318,11 +304,6 @@ window.UISubmap = (function () {
bl.dataset["size"] = Math.max(rn((size + size / scale) / 2, 2), 1) * scale;
}
// emblems
const emblemMod = minmax((scale - 1) * 0.3 + 1, 0.5, 5);
emblemsStateSizeInput.value = minmax(+emblemsStateSizeInput.value * emblemMod, 0.5, 5);
emblemsProvinceSizeInput.value = minmax(+emblemsProvinceSizeInput.value * emblemMod, 0.5, 5);
emblemsBurgSizeInput.value = minmax(+emblemsBurgSizeInput.value * emblemMod, 0.5, 5);
drawEmblems();
}

View file

@ -22,6 +22,7 @@ toolsContent.addEventListener("click", function (event) {
else if (button === "editZonesButton") editZones();
else if (button === "overviewChartsButton") overviewCharts();
else if (button === "overviewBurgsButton") overviewBurgs();
else if (button === "overviewRoutesButton") overviewRoutes();
else if (button === "overviewRiversButton") overviewRivers();
else if (button === "overviewMilitaryButton") overviewMilitary();
else if (button === "overviewMarkersButton") overviewMarkers();
@ -66,7 +67,7 @@ toolsContent.addEventListener("click", function (event) {
if (button === "addLabel") toggleAddLabel();
else if (button === "addBurgTool") toggleAddBurg();
else if (button === "addRiver") toggleAddRiver();
else if (button === "addRoute") toggleAddRoute();
else if (button === "addRoute") createRoute();
else if (button === "addMarker") toggleAddMarker();
// click to create a new map buttons
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
@ -76,10 +77,10 @@ toolsContent.addEventListener("click", function (event) {
function processFeatureRegeneration(event, button) {
if (button === "regenerateStateLabels") drawStateLabels();
else if (button === "regenerateReliefIcons") {
ReliefIcons();
ReliefIcons.draw();
if (!layerIsOn("toggleRelief")) toggleRelief();
} else if (button === "regenerateRoutes") {
Routes.regenerate();
regenerateRoutes();
if (!layerIsOn("toggleRoutes")) toggleRoutes();
} else if (button === "regenerateRivers") regenerateRivers();
else if (button === "regeneratePopulation") recalculatePopulation();
@ -115,6 +116,14 @@ async function openEmblemEditor() {
editEmblem(type, id, el);
}
function regenerateRoutes() {
const locked = pack.routes.filter(route => route.lock).map((route, index) => ({...route, i: index}));
Routes.generate(locked);
routes.selectAll("path").remove();
if (layerIsOn("toggleRoutes")) drawRoutes();
}
function regenerateRivers() {
Rivers.generate();
Lakes.defineGroup();
@ -129,7 +138,7 @@ function recalculatePopulation() {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
@ -158,16 +167,16 @@ function regenerateStates() {
Military.generate();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (document.getElementById("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (byId("militaryOverviewRefresh")?.offsetParent) militaryOverviewRefresh.click();
}
function recreateStates() {
const localSeed = generateSeed();
Math.random = aleaPRNG(localSeed);
const statesCount = +regionsOutput.value;
const statesCount = +byId("statesNumber").value;
if (!statesCount) {
tip(`<i>States Number</i> option value is zero. No counties are generated`, false, "error");
return null;
@ -189,7 +198,7 @@ function recreateStates() {
const lockedStatesIds = lockedStates.map(s => s.i);
const lockedStatesCapitals = lockedStates.map(s => s.capital);
if (lockedStates.length === validStates.length) {
if (validStates.length && lockedStates.length === validStates.length) {
tip("Unable to regenerate as all states are locked", false, "error");
return null;
}
@ -308,7 +317,7 @@ function recreateStates() {
: pack.cultures[culture].type === "Nomadic"
? "Generic"
: pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const expansionism = rn(Math.random() * byId("sizeVariety").value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
@ -335,29 +344,44 @@ function regenerateProvinces() {
}
function regenerateBurgs() {
const {cells, states} = pack;
const lockedburgs = pack.burgs.filter(b => b.i && !b.removed && b.lock);
const {cells, features, burgs, states, provinces} = pack;
rankCells();
cells.burg = new Uint16Array(cells.i.length);
const burgs = (pack.burgs = [0]); // clear burgs array
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
pack.provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
// remove notes for unlocked burgs
notes = notes.filter(note => {
if (note.id.startsWith("burg")) {
const burgId = +note.id.slice(4);
return burgs[burgId]?.lock;
}
return true;
});
const newBurgs = [0]; // new burgs array
const burgsTree = d3.quadtree();
// add locked burgs
cells.burg = new Uint16Array(cells.i.length); // clear cells burg data
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
// readd locked burgs
const lockedburgs = burgs.filter(burg => burg.i && !burg.removed && burg.lock);
for (let j = 0; j < lockedburgs.length; j++) {
const id = burgs.length;
const lockedBurg = lockedburgs[j];
lockedBurg.i = id;
burgs.push(lockedBurg);
const newId = newBurgs.length;
const noteIndex = notes.findIndex(note => note.id === `burg${lockedBurg.i}`);
if (noteIndex !== -1) notes[noteIndex].id = `burg${newId}`;
lockedBurg.i = newId;
newBurgs.push(lockedBurg);
burgsTree.add([lockedBurg.x, lockedBurg.y]);
cells.burg[lockedBurg.cell] = id;
cells.burg[lockedBurg.cell] = newId;
if (lockedBurg.capital) {
const stateId = lockedBurg.state;
states[stateId].capital = id;
states[stateId].capital = newId;
states[stateId].center = lockedBurg.cell;
}
}
@ -370,8 +394,8 @@ function regenerateBurgs() {
existingStatesCount;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
for (let i = 0; i < sorted.length && newBurgs.length < burgsCount; i++) {
const id = newBurgs.length;
const cell = sorted[i];
const [x, y] = cells.p[cell];
@ -387,39 +411,42 @@ function regenerateBurgs() {
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
burgs.push({cell, x, y, state: stateId, i: id, culture, name, capital, feature: cells.f[cell]});
newBurgs.push({cell, x, y, state: stateId, i: id, culture, name, capital, feature: cells.f[cell]});
burgsTree.add([x, y]);
cells.burg[cell] = id;
}
pack.burgs = newBurgs; // assign new burgs array
// add a capital at former place for states without added capitals
states
.filter(s => s.i && !s.removed && !s.capital)
.forEach(s => {
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
s.capital = burg;
s.center = pack.burgs[burg].cell;
pack.burgs[burg].capital = 1;
pack.burgs[burg].state = s.i;
moveBurgToGroup(burg, "cities");
const [x, y] = cells.p[s.center];
const burgId = addBurg([x, y]);
s.capital = burgId;
s.center = pack.burgs[burgId].cell;
pack.burgs[burgId].capital = 1;
pack.burgs[burgId].state = s.i;
moveBurgToGroup(burgId, "cities");
});
pack.features.forEach(f => {
features.forEach(f => {
if (f.port) f.port = 0; // reset features ports counter
});
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
Routes.regenerate();
regenerateRoutes();
// remove emblems
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
emblems.selectAll("use").remove();
if (layerIsOn("toggleEmblems")) drawEmblems();
if (document.getElementById("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (document.getElementById("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
if (byId("burgsOverviewRefresh")?.offsetParent) burgsOverviewRefresh.click();
if (byId("statesEditorRefresh")?.offsetParent) statesEditorRefresh.click();
}
function regenerateEmblems() {
@ -494,7 +521,7 @@ function regenerateCultures() {
function regenerateMilitary() {
Military.generate();
if (!layerIsOn("toggleMilitary")) toggleMilitary();
if (document.getElementById("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
if (byId("militaryOverviewRefresh").offsetParent) militaryOverviewRefresh.click();
}
function regenerateIce() {
@ -507,7 +534,7 @@ function regenerateMarkers() {
Markers.regenerate();
turnButtonOn("toggleMarkers");
drawMarkers();
if (document.getElementById("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
if (byId("markersOverviewRefresh").offsetParent) markersOverviewRefresh.click();
}
function regenerateZones(event) {
@ -518,10 +545,9 @@ function regenerateZones(event) {
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
function addNumberOfZones(number) {
zones.selectAll("g").remove(); // remove existing zones
addZones(number);
if (document.getElementById("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (!layerIsOn("toggleZones")) toggleZones();
Zones.generate(number);
if (byId("zonesEditorRefresh").offsetParent) zonesEditorRefresh.click();
if (layerIsOn("toggleZones")) drawZones();
}
}
@ -532,7 +558,7 @@ function unpressClickToAddButton() {
}
function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed");
const pressed = byId("addLabel").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
@ -600,22 +626,22 @@ function addLabelOnClick() {
function toggleAddBurg() {
unpressClickToAddButton();
document.getElementById("addBurgTool").classList.add("pressed");
byId("addBurgTool").classList.add("pressed");
overviewBurgs();
document.getElementById("addNewBurg").click();
byId("addNewBurg").click();
}
function toggleAddRiver() {
const pressed = document.getElementById("addRiver").classList.contains("pressed");
const pressed = byId("addRiver").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
byId("addNewRiver").classList.remove("pressed");
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add("pressed");
document.getElementById("addNewRiver").classList.add("pressed");
byId("addNewRiver").classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true, "warn");
@ -701,7 +727,7 @@ function addRiverOnClick() {
}
// continue old river
document.getElementById("river" + oldRiverId)?.remove();
byId("river" + oldRiverId)?.remove();
riverCells.forEach(i => (cells.r[i] = oldRiverId));
oldRiverCells.forEach(cell => {
if (h[cell] > h[min]) {
@ -769,41 +795,13 @@ function addRiverOnClick() {
if (d3.event.shiftKey === false) {
Lakes.cleanupLakeData();
unpressClickToAddButton();
document.getElementById("addNewRiver").classList.remove("pressed");
byId("addNewRiver").classList.remove("pressed");
if (addNewRiver.offsetParent) riversOverviewRefresh.click();
}
}
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRoute.classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
tip("Click on map to add a first control point", true);
if (!layerIsOn("toggleRoutes")) toggleRoutes();
}
function addRouteOnClick() {
unpressClickToAddButton();
const point = d3.mouse(this);
const id = getNextId("route");
elSelected = routes
.select("g")
.append("path")
.attr("id", id)
.attr("data-new", 1)
.attr("d", `M${point[0]},${point[1]}`);
editRoute(true);
}
function toggleAddMarker() {
const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
const pressed = byId("addMarker")?.classList.contains("pressed");
if (pressed) {
unpressClickToAddButton();
return;
@ -831,7 +829,7 @@ function addMarkerOnClick() {
const isMarkerSelected = markers.length && elSelected?.node()?.parentElement?.id === "markers";
const selectedMarker = isMarkerSelected ? markers.find(marker => marker.i === +elSelected.attr("id").slice(6)) : null;
const selectedType = document.getElementById("addedMarkerType").value;
const selectedType = byId("addedMarkerType").value;
const selectedConfig = Markers.getConfig().find(({type}) => type === selectedType);
const baseMarker = selectedMarker || selectedConfig || {icon: "❓"};
@ -841,13 +839,13 @@ function addMarkerOnClick() {
selectedConfig.add("marker" + marker.i, cell);
}
const markersElement = document.getElementById("markers");
const markersElement = byId("markers");
const rescale = +markersElement.getAttribute("rescale");
markersElement.insertAdjacentHTML("beforeend", drawMarker(marker, rescale));
if (d3.event.shiftKey === false) {
document.getElementById("markerAdd").classList.remove("pressed");
document.getElementById("markersAddFromOverview").classList.remove("pressed");
byId("markerAdd").classList.remove("pressed");
byId("markersAddFromOverview").classList.remove("pressed");
unpressClickToAddButton();
}
}
@ -942,6 +940,6 @@ function viewCellDetails() {
}
async function overviewCharts() {
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.89.24");
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.99.00");
Overview.open();
}

View file

@ -17,27 +17,22 @@ function editUnits() {
};
// add listeners
byId("distanceUnitInput").addEventListener("change", changeDistanceUnit);
byId("distanceScaleOutput").addEventListener("input", changeDistanceScale);
byId("distanceScaleInput").addEventListener("change", changeDistanceScale);
byId("heightUnit").addEventListener("change", changeHeightUnit);
byId("heightExponentInput").addEventListener("input", changeHeightExponent);
byId("heightExponentOutput").addEventListener("input", changeHeightExponent);
byId("temperatureScale").addEventListener("change", changeTemperatureScale);
byId("distanceUnitInput").on("change", changeDistanceUnit);
byId("distanceScaleInput").on("change", changeDistanceScale);
byId("heightUnit").on("change", changeHeightUnit);
byId("heightExponentInput").on("input", changeHeightExponent);
byId("temperatureScale").on("change", changeTemperatureScale);
byId("populationRateOutput").addEventListener("input", changePopulationRate);
byId("populationRateInput").addEventListener("change", changePopulationRate);
byId("urbanizationOutput").addEventListener("input", changeUrbanizationRate);
byId("urbanizationInput").addEventListener("change", changeUrbanizationRate);
byId("urbanDensityOutput").addEventListener("input", changeUrbanDensity);
byId("urbanDensityInput").addEventListener("change", changeUrbanDensity);
byId("populationRateInput").on("change", changePopulationRate);
byId("urbanizationInput").on("change", changeUrbanizationRate);
byId("urbanDensityInput").on("change", changeUrbanDensity);
byId("addLinearRuler").addEventListener("click", addRuler);
byId("addOpisometer").addEventListener("click", toggleOpisometerMode);
byId("addRouteOpisometer").addEventListener("click", toggleRouteOpisometerMode);
byId("addPlanimeter").addEventListener("click", togglePlanimeterMode);
byId("removeRulers").addEventListener("click", removeAllRulers);
byId("unitsRestore").addEventListener("click", restoreDefaultUnits);
byId("addLinearRuler").on("click", addRuler);
byId("addOpisometer").on("click", toggleOpisometerMode);
byId("addRouteOpisometer").on("click", toggleRouteOpisometerMode);
byId("addPlanimeter").on("click", togglePlanimeterMode);
byId("removeRulers").on("click", removeAllRulers);
byId("unitsRestore").on("click", restoreDefaultUnits);
function changeDistanceUnit() {
if (this.value === "custom_name") {
@ -55,6 +50,7 @@ function editUnits() {
}
function changeDistanceScale() {
distanceScale = +this.value;
renderScaleBar();
calculateFriendlyGridSize();
}
@ -90,10 +86,8 @@ function editUnits() {
}
function restoreDefaultUnits() {
// distanceScale
distanceScale = 3;
byId("distanceScaleOutput").value = 3;
byId("distanceScaleInput").value = 3;
byId("distanceScaleInput").value = distanceScale;
unlock("distanceScale");
// units
@ -110,16 +104,16 @@ function editUnits() {
calculateFriendlyGridSize();
// height exponent
heightExponentInput.value = heightExponentOutput.value = 1.8;
heightExponentInput.value = 1.8;
localStorage.removeItem("heightExponent");
calculateTemperatures();
renderScaleBar();
// population
populationRate = populationRateOutput.value = populationRateInput.value = 1000;
urbanization = urbanizationOutput.value = urbanizationInput.value = 1;
urbanDensity = urbanDensityOutput.value = urbanDensityInput.value = 10;
populationRate = populationRateInput.value = 1000;
urbanization = urbanizationInput.value = 1;
urbanDensity = urbanDensityInput.value = 10;
localStorage.removeItem("populationRate");
localStorage.removeItem("urbanization");
localStorage.removeItem("urbanDensity");
@ -179,13 +173,15 @@ function editUnits() {
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
this.classList.add("pressed");
viewbox.style("cursor", "crosshair").call(
d3.drag().on("start", function () {
const cells = pack.cells;
const burgs = pack.burgs;
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
const b = cells.burg[c];
const x = b ? burgs[b].x : cells.p[c][0];
const y = b ? burgs[b].y : cells.p[c][1];
@ -194,7 +190,7 @@ function editUnits() {
d3.event.on("drag", function () {
const point = d3.mouse(this);
const c = findCell(point[0], point[1]);
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
routeOpisometer.trackCell(c, true);
}
});

View file

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

View file

@ -3,7 +3,7 @@
function editZones() {
closeDialogs();
if (!layerIsOn("toggleZones")) toggleZones();
const body = document.getElementById("zonesBodySection");
const body = byId("zonesBodySection");
updateFilters();
zonesEditorAddLines();
@ -14,93 +14,101 @@ function editZones() {
$("#zonesEditor").dialog({
title: "Zones Editor",
resizable: false,
width: fitContent(),
close: () => exitZonesManualAssignment("close"),
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
// add listeners
document.getElementById("zonesFilterType").addEventListener("click", updateFilters);
document.getElementById("zonesFilterType").addEventListener("change", filterZonesByType);
document.getElementById("zonesEditorRefresh").addEventListener("click", zonesEditorAddLines);
document.getElementById("zonesEditStyle").addEventListener("click", () => editStyle("zones"));
document.getElementById("zonesLegend").addEventListener("click", toggleLegend);
document.getElementById("zonesPercentage").addEventListener("click", togglePercentageMode);
document.getElementById("zonesManually").addEventListener("click", enterZonesManualAssignent);
document.getElementById("zonesManuallyApply").addEventListener("click", applyZonesManualAssignent);
document.getElementById("zonesManuallyCancel").addEventListener("click", cancelZonesManualAssignent);
document.getElementById("zonesAdd").addEventListener("click", addZonesLayer);
document.getElementById("zonesExport").addEventListener("click", downloadZonesData);
document.getElementById("zonesRemove").addEventListener("click", toggleEraseMode);
byId("zonesFilterType").on("click", updateFilters);
byId("zonesFilterType").on("change", filterZonesByType);
byId("zonesEditorRefresh").on("click", zonesEditorAddLines);
byId("zonesEditStyle").on("click", () => editStyle("zones"));
byId("zonesLegend").on("click", toggleLegend);
byId("zonesPercentage").on("click", togglePercentageMode);
byId("zonesManually").on("click", enterZonesManualAssignent);
byId("zonesManuallyApply").on("click", applyZonesManualAssignent);
byId("zonesManuallyCancel").on("click", cancelZonesManualAssignent);
byId("zonesAdd").on("click", addZonesLayer);
byId("zonesExport").on("click", downloadZonesData);
byId("zonesRemove").on("click", e => e.target.classList.toggle("pressed"));
body.addEventListener("click", function (ev) {
const el = ev.target,
cl = el.classList,
zone = el.parentNode.dataset.id;
if (el.tagName === "FILL-BOX") changeFill(el);
else if (cl.contains("culturePopulation")) changePopulation(zone);
else if (cl.contains("icon-trash-empty")) zoneRemove(zone);
else if (cl.contains("icon-eye")) toggleVisibility(el);
else if (cl.contains("icon-pin")) toggleFog(zone, cl);
if (customization) selectZone(el);
body.on("click", function (ev) {
const line = ev.target.closest("div.states");
const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
if (customization) {
if (zone.hidden) return;
body.querySelector("div.selected").classList.remove("selected");
line.classList.add("selected");
return;
}
if (ev.target.closest("fill-box")) changeFill(ev.target.closest("fill-box").getAttribute("fill"), zone);
else if (ev.target.classList.contains("zonePopulation")) changePopulation(zone);
else if (ev.target.classList.contains("zoneRemove")) zoneRemove(zone);
else if (ev.target.classList.contains("zoneHide")) toggleVisibility(zone);
else if (ev.target.classList.contains("zoneFog")) toggleFog(zone, ev.target.classList);
});
body.addEventListener("input", function (ev) {
const el = ev.target;
const zone = zones.select("#" + el.parentNode.dataset.id);
body.on("input", function (ev) {
const line = ev.target.closest("div.states");
const zone = pack.zones.find(z => z.i === +line.dataset.id);
if (!zone) return;
if (el.classList.contains("zoneName")) zone.attr("data-description", el.value);
else if (el.classList.contains("zoneType")) zone.attr("data-type", el.value);
if (ev.target.classList.contains("zoneName")) changeDescription(zone, ev.target.value);
else if (ev.target.classList.contains("zoneType")) changeType(zone, ev.target.value);
});
// update type filter with a list of used types
function updateFilters() {
const zones = Array.from(document.querySelectorAll("#zones > g"));
const types = unique(zones.map(zone => zone.dataset.type));
const filterSelect = document.getElementById("zonesFilterType");
const filterSelect = byId("zonesFilterType");
const types = unique(pack.zones.map(zone => zone.type));
const typeToFilterBy = types.includes(zonesFilterType.value) ? zonesFilterType.value : "all";
filterSelect.innerHTML = "<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.innerHTML =
"<option value='all'>all</option>" + types.map(type => `<option value="${type}">${type}</option>`).join("");
filterSelect.value = typeToFilterBy;
}
// add line for each zone
function zonesEditorAddLines() {
const unit = " " + getAreaUnit();
const typeToFilterBy = byId("zonesFilterType").value;
const filteredZones =
typeToFilterBy === "all" ? pack.zones : pack.zones.filter(zone => zone.type === typeToFilterBy);
const typeToFilterBy = document.getElementById("zonesFilterType").value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
const filteredZones = typeToFilterBy === "all" ? zones : zones.filter(zone => zone.dataset.type === typeToFilterBy);
const lines = filteredZones.map(({i, name, type, cells, color, hidden}) => {
const area = getArea(d3.sum(cells.map(i => pack.cells.area[i])));
const rural = d3.sum(cells.map(i => pack.cells.pop[i])) * populationRate;
const urban =
d3.sum(cells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rn(rural + urban);
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
rural
)}; Urban population: ${si(urban)}. Click to change`;
const focused = defs.select("#fog #focusZone" + i).size();
const lines = filteredZones.map(zoneEl => {
const c = zoneEl.dataset.cells ? zoneEl.dataset.cells.split(",").map(c => +c) : [];
const description = zoneEl.dataset.description;
const type = zoneEl.dataset.type;
const fill = zoneEl.getAttribute("fill");
const area = getArea(d3.sum(c.map(i => pack.cells.area[i])));
const rural = d3.sum(c.map(i => pack.cells.pop[i])) * populationRate;
const urban = d3.sum(c.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization;
const population = rural + urban;
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}. Click to change`;
const inactive = zoneEl.style.display === "none";
const focused = defs.select("#fog #focus" + zoneEl.id).size();
return `<div class="states" data-id="${zoneEl.id}" data-fill="${fill}" data-description="${description}"
data-type="${type}" data-cells=${c.length} data-area=${area} data-population=${population}>
<fill-box fill="${fill}"></fill-box>
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${description}" autocorrect="off" spellcheck="false">
return /* html */ `<div class="states" data-id="${i}" data-color="${color}" data-description="${name}"
data-type="${type}" data-cells=${cells.length} data-area=${area} data-population=${population} style="${
hidden && "opacity: 0.5"
}">
<fill-box fill="${color}"></fill-box>
<input data-tip="Zone description. Click and type to change" style="width: 11em" class="zoneName" value="${name}" autocorrect="off" spellcheck="false">
<input data-tip="Zone type. Click and type to change" class="zoneType" value="${type}">
<span data-tip="Cells count" class="icon-check-empty hide"></span>
<div data-tip="Cells count" class="stateCells hide">${c.length}</div>
<div data-tip="Cells count" class="stateCells hide">${cells.length}</div>
<span data-tip="Zone area" style="padding-right:4px" class="icon-map-o hide"></span>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + unit}</div>
<div data-tip="Zone area" class="biomeArea hide">${si(area) + " " + getAreaUnit()}</div>
<span data-tip="${populationTip}" class="icon-male hide"></span>
<div data-tip="${populationTip}" class="culturePopulation hide">${si(population)}</div>
<div data-tip="${populationTip}" class="zonePopulation hide pointer">${si(population)}</div>
<span data-tip="Drag to raise or lower the zone" class="icon-resize-vertical hide"></span>
<span data-tip="Toggle zone focus" class="icon-pin ${focused ? "" : " inactive"} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Toggle zone visibility" class="icon-eye ${inactive ? " inactive" : ""} hide ${c.length ? "" : " placeholder"}"></span>
<span data-tip="Remove zone" class="icon-trash-empty hide"></span>
<span data-tip="Toggle zone focus" class="zoneFog icon-pin ${focused ? "" : "inactive"} hide ${
cells.length ? "" : "placeholder"
}"></span>
<span data-tip="Toggle zone visibility" class="zoneHide icon-eye hide ${
cells.length ? "" : " placeholder"
}"></span>
<span data-tip="Remove zone" class="zoneRemove icon-trash-empty hide"></span>
</div>`;
});
@ -109,16 +117,17 @@ function editZones() {
// update footer
const totalArea = getArea(graphWidth * graphHeight);
zonesFooterArea.dataset.area = totalArea;
const totalPop = (d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) * populationRate;
const totalPop =
(d3.sum(pack.cells.pop) + d3.sum(pack.burgs.filter(b => !b.removed).map(b => b.population)) * urbanization) *
populationRate;
zonesFooterPopulation.dataset.population = totalPop;
zonesFooterNumber.innerHTML = /* html */ `${filteredZones.length} of ${zones.length}`;
zonesFooterNumber.innerHTML = `${filteredZones.length} of ${pack.zones.length}`;
zonesFooterCells.innerHTML = pack.cells.i.length;
zonesFooterArea.innerHTML = si(totalArea) + unit;
zonesFooterArea.innerHTML = si(totalArea) + " " + getAreaUnit();
zonesFooterPopulation.innerHTML = si(totalPop);
// add listeners
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => zoneHighlightOn(ev)));
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => zoneHighlightOff(ev)));
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", zoneHighlightOn));
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", zoneHighlightOff));
if (body.dataset.type === "percentage") {
body.dataset.type = "absolute";
@ -128,111 +137,126 @@ function editZones() {
}
function zoneHighlightOn(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", "1px solid red");
const zoneId = event.target.dataset.id;
zones.select("#zone" + zoneId).style("outline", "1px solid red");
}
function zoneHighlightOff(event) {
const zone = event.target.dataset.id;
zones.select("#" + zone).style("outline", null);
const zoneId = event.target.dataset.id;
zones.select("#zone" + zoneId).style("outline", null);
}
function filterZonesByType() {
const typeToFilterBy = this.value;
const zones = Array.from(document.querySelectorAll("#zones > g"));
for (const zone of zones) {
const type = zone.dataset.type;
const visible = typeToFilterBy === "all" || type === typeToFilterBy;
zone.style.display = visible ? "block" : "none";
}
drawZones();
zonesEditorAddLines();
}
$(body).sortable({items: "div.states", handle: ".icon-resize-vertical", containment: "parent", axis: "y", update: movezone});
function movezone(ev, ui) {
const zone = $("#" + ui.item.attr("data-id"));
const prev = $("#" + ui.item.prev().attr("data-id"));
if (prev) {
zone.insertAfter(prev);
return;
}
const next = $("#" + ui.item.next().attr("data-id"));
if (next) zone.insertBefore(next);
$(body).sortable({
items: "div.states",
handle: ".icon-resize-vertical",
containment: "parent",
axis: "y",
update: movezone
});
function movezone(_ev, ui) {
const zone = pack.zones.find(z => z.i === +ui.item[0].dataset.id);
const oldIndex = pack.zones.indexOf(zone);
const newIndex = ui.item.index();
if (oldIndex === newIndex) return;
pack.zones.splice(oldIndex, 1);
pack.zones.splice(newIndex, 0, zone);
drawZones();
}
function enterZonesManualAssignent() {
if (!layerIsOn("toggleZones")) toggleZones();
customization = 10;
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
document.getElementById("zonesManuallyButtons").style.display = "inline-block";
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "none"));
byId("zonesManuallyButtons").style.display = "inline-block";
zonesEditor.querySelectorAll(".hide").forEach(el => el.classList.add("hidden"));
zonesFooter.style.display = "none";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "none"));
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
tip("Click to select a zone, drag to paint a zone", true);
viewbox.style("cursor", "crosshair").on("click", selectZoneOnMapClick).call(d3.drag().on("start", dragZoneBrush)).on("touchmove mousemove", moveZoneBrush);
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"));
});
// draw zones as individual cells
zones.selectAll("*").remove();
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
const data = visibleZones.map(({i, cells, color}) => cells.map(cell => ({cell, zoneId: i, fill: color}))).flat();
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d.cell))
.attr("fill", d => d.fill)
.attr("data-zone", d => d.zoneId)
.attr("data-cell", d => d.cell);
}
function selectZone(el) {
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.id !== "zones") return;
const zoneId = d3.event.target.dataset.zone;
const el = body.querySelector("div[data-id='" + zoneId + "']");
body.querySelector("div.selected").classList.remove("selected");
el.classList.add("selected");
}
function selectZoneOnMapClick() {
if (d3.event.target.parentElement.parentElement.id !== "zones") return;
const zone = d3.event.target.parentElement.id;
const el = body.querySelector("div[data-id='" + zone + "']");
selectZone(el);
}
function dragZoneBrush() {
const r = +zonesBrush.value;
const radius = +byId("zonesBrush").value;
const eraseMode = byId("zonesRemove").classList.contains("pressed");
const landOnly = byId("zonesBrushLandOnly").checked;
d3.event.on("drag", () => {
if (!d3.event.dx && !d3.event.dy) return;
const p = d3.mouse(this);
moveCircle(p[0], p[1], r);
const [x, y] = d3.mouse(this);
moveCircle(x, y, radius);
const selection = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
if (!selection) return;
let selection = radius > 5 ? findAll(x, y, radius) : [findCell(x, y)];
if (landOnly) selection = selection.filter(i => pack.cells.h[i] >= 20);
if (!selection.length) 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 zoneId = +body.querySelector("div.selected")?.dataset.id;
const zone = pack.zones.find(z => z.i === zoneId);
if (!zone) return;
const erase = document.getElementById("zonesRemove").classList.contains("pressed");
if (erase) {
// remove
selection.forEach(i => {
const index = cells.indexOf(i);
if (index === -1) return;
zone.select("polygon#" + base + i).remove();
cells.splice(index, 1);
});
if (eraseMode) {
const data = zones
.selectAll("polygon")
.data()
.filter(d => !(d.zoneId === zoneId && selection.includes(d.cell)));
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.exit()
.remove();
} else {
// add
selection.forEach(i => {
if (cells.includes(i)) return;
cells.push(i);
zone
.append("polygon")
.attr("points", getPackPolygon(i))
.attr("id", base + i);
});
const data = selection.map(cell => ({cell, zoneId, fill: zone.color}));
zones
.selectAll("polygon")
.data(data, d => `${d.zoneId}-${d.cell}`)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d.cell))
.attr("fill", d => d.fill)
.attr("data-zone", d => d.zoneId)
.attr("data-cell", d => d.cell);
}
zone.attr("data-cells", cells);
});
}
@ -240,39 +264,29 @@ function editZones() {
showMainTip();
const point = d3.mouse(this);
const radius = +zonesBrush.value;
moveCircle(point[0], point[1], radius);
moveCircle(...point, radius);
}
function applyZonesManualAssignent() {
zones.selectAll("g").each(function () {
if (this.dataset.cells) return;
// all zone cells are removed
unfog("focusZone" + this.id);
this.style.display = "block";
});
const data = zones.selectAll("polygon").data();
const zoneCells = data.reduce((acc, d) => {
if (!acc[d.zoneId]) acc[d.zoneId] = [];
acc[d.zoneId].push(d.cell);
return acc;
}, {});
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
visibleZones.forEach(zone => (zone.cells = zoneCells[zone.i] || []));
drawZones();
zonesEditorAddLines();
exitZonesManualAssignment();
}
// restore initial zone cells
function cancelZonesManualAssignent() {
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-init");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
drawZones();
exitZonesManualAssignment();
}
@ -280,69 +294,57 @@ function editZones() {
customization = 0;
removeCircle();
document.querySelectorAll("#zonesBottom > *").forEach(el => (el.style.display = "inline-block"));
document.getElementById("zonesManuallyButtons").style.display = "none";
byId("zonesManuallyButtons").style.display = "none";
zonesEditor.querySelectorAll(".hide:not(.show)").forEach(el => el.classList.remove("hidden"));
zonesFooter.style.display = "block";
body.querySelectorAll("div > input, select, svg").forEach(e => (e.style.pointerEvents = "all"));
if (!close) $("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
if (!close)
$("#zonesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
restoreDefaultEvents();
clearMainTip();
zones.selectAll("g").each(function () {
this.removeAttribute("data-init");
});
const selected = body.querySelector("div.selected");
if (selected) selected.classList.remove("selected");
}
function changeFill(el) {
const fill = el.getAttribute("fill");
function changeFill(fill, zone) {
const callback = newFill => {
el.fill = newFill;
document.getElementById(el.parentNode.dataset.id).setAttribute("fill", newFill);
zone.color = newFill;
drawZones();
zonesEditorAddLines();
};
openPicker(fill, callback);
}
function toggleVisibility(el) {
const zone = zones.select("#" + el.parentNode.dataset.id);
const inactive = zone.style("display") === "none";
inactive ? zone.style("display", "block") : zone.style("display", "none");
el.classList.toggle("inactive");
function toggleVisibility(zone) {
const isHidden = Boolean(zone.hidden);
if (isHidden) delete zone.hidden;
else zone.hidden = true;
drawZones();
zonesEditorAddLines();
}
function toggleFog(z, cl) {
const dataCells = zones.select("#" + z).attr("data-cells");
if (!dataCells) return;
const path =
"M" +
dataCells
.split(",")
.map(c => getPackPolygon(+c))
.join("M") +
"Z",
id = "focusZone" + z;
cl.contains("inactive") ? fog(id, path) : unfog(id);
function toggleFog(zone, cl) {
const inactive = cl.contains("inactive");
cl.toggle("inactive");
if (inactive) {
const path = zones.select("#zone" + zone.i).attr("d");
fog("focusZone" + zone.i, path);
} else {
unfog("focusZone" + zone.i);
}
}
function toggleLegend() {
if (legend.selectAll("*").size()) {
clearLegend();
return;
} // hide legend
const data = [];
zones.selectAll("g").each(function () {
const id = this.dataset.id;
const description = this.dataset.description;
const fill = this.getAttribute("fill");
data.push([id, fill, description]);
});
const filterBy = byId("zonesFilterType").value;
const isFiltered = filterBy && filterBy !== "all";
const visibleZones = pack.zones.filter(zone => !zone.hidden && (!isFiltered || zone.type === filterBy));
const data = visibleZones.map(({i, name, color}) => ["zone" + i, color, name]);
drawLegend("Zones", data);
}
@ -356,7 +358,7 @@ function editZones() {
body.querySelectorAll(":scope > div").forEach(function (el) {
el.querySelector(".stateCells").innerHTML = rn((+el.dataset.cells / totalCells) * 100, 2) + "%";
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100, 2) + "%";
el.querySelector(".culturePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
el.querySelector(".zonePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100, 2) + "%";
});
} else {
body.dataset.type = "absolute";
@ -365,22 +367,23 @@ function editZones() {
}
function addZonesLayer() {
const id = getNextId("zone");
const description = "Unknown zone";
const zoneId = pack.zones.length ? Math.max(...pack.zones.map(z => z.i)) + 1 : 0;
const name = "Unknown zone";
const type = "Unknown";
const fill = "url(#hatch" + (id.slice(4) % 42) + ")";
zones.append("g").attr("id", id).attr("data-description", description).attr("data-type", type).attr("data-cells", "").attr("fill", fill);
const color = "url(#hatch" + (zoneId % 42) + ")";
pack.zones.push({i: zoneId, name, type, color, cells: []});
zonesEditorAddLines();
drawZones();
}
function downloadZonesData() {
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
let data = "Id,Fill,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
let data = "Id,Color,Description,Type,Cells,Area " + unit + ",Population\n"; // headers
body.querySelectorAll(":scope > div").forEach(function (el) {
data += el.dataset.id + ",";
data += el.dataset.fill + ",";
data += el.dataset.color + ",";
data += el.dataset.description + ",";
data += el.dataset.type + ",";
data += el.dataset.cells + ",";
@ -392,32 +395,35 @@ function editZones() {
downloadFile(data, name);
}
function toggleEraseMode() {
this.classList.toggle("pressed");
function changeDescription(zone, value) {
zone.name = value;
zones.select("#zone" + zone.i).attr("data-description", value);
}
function changeType(zone, value) {
zone.type = value;
zones.select("#zone" + zone.i).attr("data-type", value);
}
function changePopulation(zone) {
const dataCells = zones.select("#" + zone).attr("data-cells");
const cells = dataCells
? dataCells
.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");
return;
}
const burgs = pack.burgs.filter(b => !b.removed && cells.includes(b.cell));
const landCells = zone.cells.filter(i => pack.cells.h[i] >= 20);
if (!landCells.length) return tip("Zone does not have any land cells, cannot change population", false, "error");
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 burgs = pack.burgs.filter(b => !b.removed && landCells.includes(b.cell));
const rural = rn(d3.sum(landCells.map(i => pack.cells.pop[i])) * populationRate);
const urban = rn(
d3.sum(landCells.map(i => pack.cells.burg[i]).map(b => pack.burgs[b].population)) * populationRate * urbanization
);
const total = rural + urban;
const l = n => Number(n).toLocaleString();
alertMessage.innerHTML = /* html */ `Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${burgs.length ? "" : "disabled"} />
<p>Total population: ${l(total)} <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
burgs.length ? "" : "disabled"
} />
<p>Total population: ${l(total)} <span id="totalPop">${l(
total
)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
const update = function () {
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
@ -448,12 +454,12 @@ function editZones() {
function applyPopulationChange() {
const ruralChange = ruralPop.value / rural;
if (isFinite(ruralChange) && ruralChange !== 1) {
cells.forEach(i => (pack.cells.pop[i] *= ruralChange));
landCells.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));
const pop = rn(points / landCells.length);
landCells.forEach(i => (pack.cells.pop[i] = pop));
}
const urbanChange = urbanPop.value / urban;
@ -471,8 +477,16 @@ function editZones() {
}
function zoneRemove(zone) {
zones.select("#" + zone).remove();
unfog("focusZone" + zone);
zonesEditorAddLines();
confirmationDialog({
title: "Remove zone",
message: "Are you sure you want to remove the zone? <br>This action cannot be reverted",
confirm: "Remove",
onConfirm: () => {
pack.zones = pack.zones.filter(z => z.i !== zone.i);
zones.select("#zone" + zone.i).remove();
unfog("focusZone" + zone.i);
zonesEditorAddLines();
}
});
}
}