mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 01:41:22 +01:00
Urquhart routes (#1072)
* feat: routes generation * feat: routes rendering * feat: searoutes fix, changing reGraph * feat: searoute - change pathfinding algo * feat: routes - cleanup code * feat: routes - change data format * feat: routes - add routes to json export * feat: edit routes - start * feat: edit routes - main * feat: edit routes - EP * feat: edit routes - remove route * feat: route - generate names * feat: route - continue * Refactor route merging logic for improved performance * feat: routes - show name in tooltip * feat: routes - create route dialog * feat: update data on control point remove * feat: routes editor - split route * feat: add join route functionality to routes editor * feat: Add join route functionality to routes editor * feat: Update join route tooltip in routes editor * feat: routes overview - sort by length * feat: routes overview - fix distanceScale value * feat: routes overview - create route * Refactor getMiddlePoint function to getCloseToEdgePoint * feat: routes - change data format, fix issues * feat: routes - regenerateRoutes * feat: routes - add route on burg creation * chore - remove merge conflict markers * chore - remove merge conflict markers * feat: routes name - no unnamed burg names * feat: routes - lock routes * fix: routes - split routes * feat: routes - tip correction * feat: routes - auto-update part 1 * feat: routes - return old rePacj logic to not break auto-update * feat: routes - auto-update - add connections --------- Co-authored-by: Azgaar <azgaar.fmg@yandex.com>
This commit is contained in:
parent
c6dd331eb6
commit
f19b891421
47 changed files with 2462 additions and 1032 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -390,7 +390,7 @@ function editBiomes() {
|
|||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeBiomeForSelection(selection);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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])
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -327,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;
|
||||
|
|
@ -372,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];
|
||||
|
|
@ -1174,7 +1201,6 @@ function getAreaUnit(squareMark = "²") {
|
|||
}
|
||||
|
||||
function getArea(rawArea) {
|
||||
const distanceScale = byId("distanceScaleInput")?.value;
|
||||
return rawArea * distanceScale ** 2;
|
||||
}
|
||||
|
||||
|
|
@ -1225,7 +1251,7 @@ function refreshAllEditors() {
|
|||
// dynamically loaded editors
|
||||
async function editStates() {
|
||||
if (customization) return;
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.97.06");
|
||||
const Editor = await import("../dynamic/editors/states-editor.js?v=1.99.00");
|
||||
Editor.open();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -174,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")
|
||||
|
|
@ -313,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);
|
||||
}
|
||||
|
||||
|
|
@ -388,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")
|
||||
|
|
@ -413,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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -246,6 +246,7 @@ function editHeightmap(options) {
|
|||
Cultures.expand();
|
||||
|
||||
BurgsAndStates.generate();
|
||||
Routes.generate();
|
||||
Religions.generate();
|
||||
BurgsAndStates.defineStateForms();
|
||||
BurgsAndStates.generateProvinces();
|
||||
|
|
@ -281,8 +282,7 @@ function editHeightmap(options) {
|
|||
const l = grid.cells.i.length;
|
||||
const biome = new Uint8Array(l);
|
||||
const pop = new Uint16Array(l);
|
||||
const road = new Uint16Array(l);
|
||||
const crossroad = new Uint16Array(l);
|
||||
const routes = {};
|
||||
const s = new Uint16Array(l);
|
||||
const burg = new Uint16Array(l);
|
||||
const state = new Uint16Array(l);
|
||||
|
|
@ -300,8 +300,7 @@ function editHeightmap(options) {
|
|||
biome[g] = pack.cells.biome[i];
|
||||
culture[g] = pack.cells.culture[i];
|
||||
pop[g] = pack.cells.pop[i];
|
||||
road[g] = pack.cells.road[i];
|
||||
crossroad[g] = pack.cells.crossroad[i];
|
||||
routes[g] = pack.cells.routes[i];
|
||||
s[g] = pack.cells.s[i];
|
||||
state[g] = pack.cells.state[i];
|
||||
province[g] = pack.cells.province[i];
|
||||
|
|
@ -353,8 +352,7 @@ function editHeightmap(options) {
|
|||
// assign saved pack data from grid back to pack
|
||||
const n = pack.cells.i.length;
|
||||
pack.cells.pop = new Float32Array(n);
|
||||
pack.cells.road = new Uint16Array(n);
|
||||
pack.cells.crossroad = new Uint16Array(n);
|
||||
pack.cells.routes = {};
|
||||
pack.cells.s = new Uint16Array(n);
|
||||
pack.cells.burg = new Uint16Array(n);
|
||||
pack.cells.state = new Uint16Array(n);
|
||||
|
|
@ -389,8 +387,7 @@ function editHeightmap(options) {
|
|||
if (!isLand) continue;
|
||||
pack.cells.culture[i] = culture[g];
|
||||
pack.cells.pop[i] = pop[g];
|
||||
pack.cells.road[i] = road[g];
|
||||
pack.cells.crossroad[i] = crossroad[g];
|
||||
pack.cells.routes[i] = routes[g];
|
||||
pack.cells.s[i] = s[g];
|
||||
pack.cells.state[i] = state[g];
|
||||
pack.cells.province[i] = province[g];
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ function handleKeyup(event) {
|
|||
else if (shift && code === "KeyO") editNotes();
|
||||
else if (shift && code === "KeyA") overviewCharts();
|
||||
else if (shift && code === "KeyT") overviewBurgs();
|
||||
else if (shift && code === "KeyU") overviewRoutes();
|
||||
else if (shift && code === "KeyV") overviewRivers();
|
||||
else if (shift && code === "KeyM") overviewMilitary();
|
||||
else if (shift && code === "KeyK") overviewMarkers();
|
||||
|
|
@ -57,7 +58,7 @@ function handleKeyup(event) {
|
|||
else if (key === "!") toggleAddBurg();
|
||||
else if (key === "@") toggleAddLabel();
|
||||
else if (key === "#") toggleAddRiver();
|
||||
else if (key === "$") toggleAddRoute();
|
||||
else if (key === "$") createRoute();
|
||||
else if (key === "%") toggleAddMarker();
|
||||
else if (alt && code === "KeyB") console.table(pack.burgs);
|
||||
else if (alt && code === "KeyS") console.table(pack.states);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ function restoreLayers() {
|
|||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleCompass")) compass.style("display", "block");
|
||||
if (layerIsOn("toggleRoutes")) drawRoutes();
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
|
|
@ -392,7 +393,6 @@ function drawTemp() {
|
|||
const start = findStart(i, t);
|
||||
if (!start) continue;
|
||||
used[i] = 1;
|
||||
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
|
||||
|
||||
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||
const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
|
||||
|
|
@ -1622,18 +1622,33 @@ function drawRivers() {
|
|||
function toggleRoutes(event) {
|
||||
if (!layerIsOn("toggleRoutes")) {
|
||||
turnButtonOn("toggleRoutes");
|
||||
$("#routes").fadeIn();
|
||||
drawRoutes();
|
||||
if (event && isCtrlClick(event)) editStyle("routes");
|
||||
} else {
|
||||
if (event && isCtrlClick(event)) {
|
||||
editStyle("routes");
|
||||
return;
|
||||
}
|
||||
$("#routes").fadeOut();
|
||||
if (event && isCtrlClick(event)) return editStyle("routes");
|
||||
routes.selectAll("path").remove();
|
||||
turnButtonOff("toggleRoutes");
|
||||
}
|
||||
}
|
||||
|
||||
function drawRoutes() {
|
||||
TIME && console.time("drawRoutes");
|
||||
const routePaths = {};
|
||||
|
||||
for (const route of pack.routes) {
|
||||
const {i, group} = route;
|
||||
if (!routePaths[group]) routePaths[group] = [];
|
||||
routePaths[group].push(`<path id="route${i}" d="${Routes.getPath(route)}"/>`);
|
||||
}
|
||||
|
||||
routes.selectAll("path").remove();
|
||||
for (const group in routePaths) {
|
||||
routes.select("#" + group).html(routePaths[group].join(""));
|
||||
}
|
||||
|
||||
TIME && console.timeEnd("drawRoutes");
|
||||
}
|
||||
|
||||
function toggleMilitary() {
|
||||
if (!layerIsOn("toggleMilitary")) {
|
||||
turnButtonOn("toggleMilitary");
|
||||
|
|
@ -1758,7 +1773,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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -545,6 +545,7 @@ function applyStoredOptions() {
|
|||
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));
|
||||
|
|
@ -605,7 +606,8 @@ function randomizeOptions() {
|
|||
|
||||
// 'Units Editor' settings
|
||||
const US = navigator.language === "en-US";
|
||||
if (randomize || !locked("distanceScale")) distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
|
||||
if (randomize || !locked("distanceScale"))
|
||||
distanceScale = distanceScaleOutput.value = distanceScaleInput.value = gauss(3, 1, 1, 5);
|
||||
if (!stored("distanceUnit")) distanceUnitInput.value = US ? "mi" : "km";
|
||||
if (!stored("heightUnit")) heightUnit.value = US ? "ft" : "m";
|
||||
if (!stored("temperatureScale")) temperatureScale.value = US ? "°F" : "°C";
|
||||
|
|
|
|||
|
|
@ -886,7 +886,7 @@ function editProvinces() {
|
|||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1])];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeForSelection(selection);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
});
|
||||
|
||||
|
|
|
|||
85
modules/ui/route-group-editor.js
Normal file
85
modules/ui/route-group-editor.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
140
modules/ui/routes-creator.js
Normal file
140
modules/ui/routes-creator.js
Normal 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 = Math.max(...pack.routes.map(route => route.i)) + 1;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,308 +1,403 @@
|
|||
"use strict";
|
||||
|
||||
const CONTROL_POINST_DISTANCE = 10;
|
||||
|
||||
function editRoute(onClick) {
|
||||
function editRoute(id) {
|
||||
if (customization) return;
|
||||
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
|
||||
if (elSelected && id === elSelected.attr("id")) return;
|
||||
closeDialogs(".stable");
|
||||
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
byId("toggleCells").dataset.forced = +!layerIsOn("toggleCells");
|
||||
if (!layerIsOn("toggleCells")) toggleCells();
|
||||
|
||||
elSelected = d3.select("#" + id).on("click", addControlPoint);
|
||||
|
||||
tip(
|
||||
"Drag control points to change the route. Click on point to remove it. Click on the route to add additional control point. For major changes please create a new route instead",
|
||||
true
|
||||
);
|
||||
debug.append("g").attr("id", "controlCells");
|
||||
debug.append("g").attr("id", "controlPoints");
|
||||
|
||||
{
|
||||
const route = getRoute();
|
||||
updateRouteData(route);
|
||||
drawControlPoints(route.points);
|
||||
drawCells(route.points);
|
||||
updateLockIcon();
|
||||
}
|
||||
|
||||
$("#routeEditor").dialog({
|
||||
title: "Edit Route",
|
||||
resizable: false,
|
||||
position: {my: "center top+60", at: "top", of: d3.event, collision: "fit"},
|
||||
close: closeRoutesEditor
|
||||
position: {my: "left top", at: "left+10 top+10", of: "#map"},
|
||||
close: closeRouteEditor
|
||||
});
|
||||
|
||||
debug.append("g").attr("id", "controlPoints");
|
||||
const node = onClick ? elSelected.node() : d3.event.target;
|
||||
elSelected = d3.select(node).on("click", addInterimControlPoint);
|
||||
drawControlPoints(node);
|
||||
selectRouteGroup(node);
|
||||
|
||||
viewbox.on("touchmove mousemove", showEditorTips);
|
||||
if (onClick) toggleRouteCreationMode();
|
||||
|
||||
if (modules.editRoute) return;
|
||||
modules.editRoute = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup);
|
||||
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
|
||||
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
|
||||
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile);
|
||||
byId("routeCreateSelectingCells").on("click", showCreationDialog);
|
||||
byId("routeSplit").on("click", togglePressed);
|
||||
byId("routeJoin").on("click", openJoinRoutesDialog);
|
||||
byId("routeElevationProfile").on("click", showRouteElevationProfile);
|
||||
byId("routeLegend").on("click", editRouteLegend);
|
||||
byId("routeLock").on("click", toggleLockButton);
|
||||
byId("routeRemove").on("click", removeRoute);
|
||||
byId("routeName").on("input", changeName);
|
||||
byId("routeGroup").on("input", changeGroup);
|
||||
byId("routeGroupEdit").on("click", editRouteGroups);
|
||||
byId("routeEditStyle").on("click", editRouteGroupStyle);
|
||||
byId("routeGenerateName").on("click", generateName);
|
||||
|
||||
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle);
|
||||
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
|
||||
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
|
||||
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
|
||||
document.getElementById("routeRemove").addEventListener("click", removeRoute);
|
||||
|
||||
function showEditorTips() {
|
||||
showMainTip();
|
||||
if (routeNew.classList.contains("pressed")) return;
|
||||
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point");
|
||||
else if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
|
||||
function getRoute() {
|
||||
const routeId = +elSelected.attr("id").slice(5);
|
||||
return pack.routes.find(route => route.i === routeId);
|
||||
}
|
||||
|
||||
function drawControlPoints(node) {
|
||||
const totalLength = node.getTotalLength();
|
||||
const increment = totalLength / Math.ceil(totalLength / CONTROL_POINST_DISTANCE);
|
||||
for (let i = 0; i <= totalLength; i += increment) {
|
||||
const point = node.getPointAtLength(i);
|
||||
addControlPoint([point.x, point.y]);
|
||||
}
|
||||
routeLength.innerHTML = rn(totalLength * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
function updateRouteData(route) {
|
||||
route.name = route.name || Routes.generateName(route);
|
||||
byId("routeName").value = route.name;
|
||||
|
||||
const routeGroup = byId("routeGroup");
|
||||
routeGroup.options.length = 0;
|
||||
routes.selectAll("g").each(function () {
|
||||
routeGroup.options.add(new Option(this.id, this.id, false, this.id === route.group));
|
||||
});
|
||||
|
||||
updateRouteLength(route);
|
||||
|
||||
const isWater = route.points.some(([x, y, cellId]) => pack.cells.h[cellId] < 20);
|
||||
byId("routeElevationProfile").style.display = isWater ? "none" : "inline-block";
|
||||
}
|
||||
|
||||
function addControlPoint(point, before = null) {
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
.insert("circle", before)
|
||||
.attr("cx", point[0])
|
||||
.attr("cy", point[1])
|
||||
.attr("r", 0.6)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
function updateRouteLength(route) {
|
||||
route.length = Routes.getLength(route.i);
|
||||
byId("routeLength").value = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
|
||||
}
|
||||
|
||||
function addInterimControlPoint() {
|
||||
const point = d3.mouse(this);
|
||||
const controls = document.getElementById("controlPoints").querySelectorAll("circle");
|
||||
const points = Array.from(controls).map(circle => [+circle.getAttribute("cx"), +circle.getAttribute("cy")]);
|
||||
const index = getSegmentId(points, point, 2);
|
||||
addControlPoint(point, ":nth-child(" + (index + 1) + ")");
|
||||
|
||||
redrawRoute();
|
||||
}
|
||||
|
||||
function dragControlPoint() {
|
||||
this.setAttribute("cx", d3.event.x);
|
||||
this.setAttribute("cy", d3.event.y);
|
||||
redrawRoute();
|
||||
}
|
||||
|
||||
function redrawRoute() {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const points = [];
|
||||
function drawControlPoints(points) {
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
.selectAll("circle")
|
||||
.each(function () {
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
});
|
||||
|
||||
elSelected.attr("d", round(lineGen(points)));
|
||||
const l = elSelected.node().getTotalLength();
|
||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||
|
||||
if (modules.elevation) showEPForRoute(elSelected.node());
|
||||
.data(points)
|
||||
.join("circle")
|
||||
.attr("cx", d => d[0])
|
||||
.attr("cy", d => d[1])
|
||||
.attr("r", 0.6)
|
||||
.call(d3.drag().on("start", dragControlPoint))
|
||||
.on("click", handleControlPointClick);
|
||||
}
|
||||
|
||||
function showElevationProfile() {
|
||||
modules.elevation = true;
|
||||
showEPForRoute(elSelected.node());
|
||||
function drawCells(points) {
|
||||
debug
|
||||
.select("#controlCells")
|
||||
.selectAll("polygon")
|
||||
.data(points)
|
||||
.join("polygon")
|
||||
.attr("points", p => getPackPolygon(p[2]));
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "none"));
|
||||
document.getElementById("routeGroupsSelection").style.display = "inline-block";
|
||||
}
|
||||
function dragControlPoint() {
|
||||
const route = getRoute();
|
||||
const initCell = d3.event.subject[2];
|
||||
const pointIndex = route.points.indexOf(d3.event.subject);
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#routeEditor > button").forEach(el => (el.style.display = "inline-block"));
|
||||
document.getElementById("routeGroupsSelection").style.display = "none";
|
||||
document.getElementById("routeGroupName").style.display = "none";
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
document.getElementById("routeGroup").style.display = "inline-block";
|
||||
}
|
||||
d3.event.on("drag", function () {
|
||||
this.setAttribute("cx", d3.event.x);
|
||||
this.setAttribute("cy", d3.event.y);
|
||||
|
||||
function selectRouteGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
const select = document.getElementById("routeGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
const x = rn(d3.event.x, 2);
|
||||
const y = rn(d3.event.y, 2);
|
||||
const cellId = findCell(x, y);
|
||||
|
||||
routes.selectAll("g").each(function () {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
this.__data__ = route.points[pointIndex] = [x, y, cellId];
|
||||
redrawRoute(route);
|
||||
drawCells(route.points);
|
||||
});
|
||||
}
|
||||
|
||||
function changeRouteGroup() {
|
||||
document.getElementById(this.value).appendChild(elSelected.node());
|
||||
}
|
||||
d3.event.on("end", () => {
|
||||
const movedToCell = findCell(d3.event.x, d3.event.y);
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
if (routeGroupName.style.display === "none") {
|
||||
routeGroupName.style.display = "inline-block";
|
||||
routeGroupName.focus();
|
||||
routeGroup.style.display = "none";
|
||||
} else {
|
||||
routeGroupName.style.display = "none";
|
||||
routeGroup.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
if (movedToCell !== initCell) {
|
||||
const prev = route.points[pointIndex - 1];
|
||||
if (prev) {
|
||||
removeConnection(initCell, prev[2]);
|
||||
addConnection(movedToCell, prev[2], route.i);
|
||||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {
|
||||
tip("Please provide a valid group name");
|
||||
return;
|
||||
}
|
||||
const group = this.value
|
||||
.toLowerCase()
|
||||
.replace(/ /g, "_")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Number.isFinite(+group.charAt(0))) {
|
||||
tip("Group name should start with a letter", false, "error");
|
||||
return;
|
||||
}
|
||||
// just rename if only 1 element left
|
||||
const oldGroup = elSelected.node().parentNode;
|
||||
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
|
||||
if (!basic && oldGroup.childElementCount === 1) {
|
||||
document.getElementById("routeGroup").selectedOptions[0].remove();
|
||||
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||
oldGroup.id = group;
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||
document.getElementById("routes").appendChild(newGroup);
|
||||
newGroup.id = group;
|
||||
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||
document.getElementById(group).appendChild(elSelected.node());
|
||||
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
}
|
||||
|
||||
function removeRouteGroup() {
|
||||
const group = elSelected.node().parentNode.id;
|
||||
const basic = ["roads", "trails", "searoutes"].includes(group);
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to remove ${
|
||||
basic ? "all elements in the group" : "the entire route group"
|
||||
}? <br /><br />Routes to be
|
||||
removed: ${count}`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove route group",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
$(this).dialog("close");
|
||||
$("#routeEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
if (basic)
|
||||
routes
|
||||
.select("#" + group)
|
||||
.selectAll("path")
|
||||
.remove();
|
||||
else routes.select("#" + group).remove();
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
const next = route.points[pointIndex + 1];
|
||||
if (next) {
|
||||
removeConnection(initCell, next[2]);
|
||||
addConnection(movedToCell, next[2], route.i);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function editGroupStyle() {
|
||||
const g = elSelected.node().parentNode.id;
|
||||
editStyle("routes", g);
|
||||
function redrawRoute(route) {
|
||||
elSelected.attr("d", Routes.getPath(route));
|
||||
updateRouteLength(route);
|
||||
if (byId("elevationProfile").offsetParent) showRouteElevationProfile();
|
||||
}
|
||||
|
||||
function toggleRouteSplitMode() {
|
||||
document.getElementById("routeNew").classList.remove("pressed");
|
||||
function addControlPoint() {
|
||||
const route = getRoute();
|
||||
const [x, y] = d3.mouse(this);
|
||||
const cellId = findCell(x, y);
|
||||
|
||||
const point = [rn(x, 2), rn(y, 2), cellId];
|
||||
const isNewCell = !route.points.some(p => p[2] === cellId);
|
||||
|
||||
const index = getSegmentId(route.points, point, 2);
|
||||
route.points.splice(index, 0, point);
|
||||
|
||||
// check if added point is in new cell
|
||||
if (isNewCell) {
|
||||
const prev = route.points[index - 1];
|
||||
const next = route.points[index + 1];
|
||||
|
||||
if (!prev) ERROR && console.error("Can't add control point to the start of the route");
|
||||
if (!next) ERROR && console.error("Can't add control point to the end of the route");
|
||||
if (!prev || !next) return;
|
||||
|
||||
removeConnection(prev[2], next[2]);
|
||||
addConnection(prev[2], cellId, route.i);
|
||||
addConnection(cellId, next[2], route.i);
|
||||
|
||||
drawCells(route.points);
|
||||
}
|
||||
|
||||
drawControlPoints(route.points);
|
||||
redrawRoute(route);
|
||||
}
|
||||
|
||||
function handleControlPointClick() {
|
||||
const controlPoint = d3.select(this);
|
||||
|
||||
const point = controlPoint.datum();
|
||||
const route = getRoute();
|
||||
const index = route.points.indexOf(point);
|
||||
|
||||
const isSplitMode = byId("routeSplit").classList.contains("pressed");
|
||||
return isSplitMode ? splitRoute() : removeControlPoint(controlPoint);
|
||||
|
||||
function splitRoute() {
|
||||
const oldRoutePoints = route.points.slice(0, index + 1);
|
||||
const newRoutePoints = route.points.slice(index);
|
||||
|
||||
// update old route
|
||||
route.points = oldRoutePoints;
|
||||
drawControlPoints(route.points);
|
||||
drawCells(route.points);
|
||||
redrawRoute(route);
|
||||
|
||||
// create new route
|
||||
const newRoute = {
|
||||
i: Math.max(...pack.routes.map(route => route.i)) + 1,
|
||||
group: route.group,
|
||||
feature: route.feature,
|
||||
name: route.name,
|
||||
points: newRoutePoints
|
||||
};
|
||||
pack.routes.push(newRoute);
|
||||
|
||||
for (let i = 0; i < newRoute.points.length; i++) {
|
||||
const cellId = newRoute.points[i][2];
|
||||
const nextPoint = newRoute.points[i + 1];
|
||||
if (nextPoint) addConnection(cellId, nextPoint[2], newRoute.i);
|
||||
}
|
||||
|
||||
routes
|
||||
.select("#" + newRoute.group)
|
||||
.append("path")
|
||||
.attr("d", Routes.getPath(newRoute))
|
||||
.attr("id", "route" + newRoute.i);
|
||||
|
||||
byId("routeSplit").classList.remove("pressed");
|
||||
}
|
||||
|
||||
function removeControlPoint(controlPoint) {
|
||||
const isOnlyPointInCell = route.points.filter(p => p[2] === point[2]).length === 1;
|
||||
if (isOnlyPointInCell) {
|
||||
const prev = route.points[index - 1];
|
||||
const next = route.points[index + 1];
|
||||
if (prev) removeConnection(prev[2], point[2]);
|
||||
if (next) removeConnection(point[2], next[2]);
|
||||
if (prev && next) addConnection(prev[2], next[2], route.i);
|
||||
}
|
||||
|
||||
controlPoint.remove();
|
||||
route.points = route.points.filter(p => p !== point);
|
||||
|
||||
drawCells(route.points);
|
||||
redrawRoute(route);
|
||||
}
|
||||
}
|
||||
|
||||
function openJoinRoutesDialog() {
|
||||
const route = getRoute();
|
||||
const firstCell = route.points.at(0)[2];
|
||||
const lastCell = route.points.at(-1)[2];
|
||||
|
||||
const candidateRoutes = pack.routes.filter(r => {
|
||||
if (r.i === route.i) return false;
|
||||
if (r.group !== route.group) return false;
|
||||
if (r.points.at(0)[2] === lastCell) return true;
|
||||
if (r.points.at(-1)[2] === firstCell) return true;
|
||||
if (r.points.at(0)[2] === firstCell) return true;
|
||||
if (r.points.at(-1)[2] === lastCell) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (candidateRoutes.length) {
|
||||
const options = candidateRoutes.map(r => {
|
||||
r.name = r.name || Routes.generateName(r);
|
||||
r.length = r.length || Routes.getLength(r.i);
|
||||
const length = rn(r.length * distanceScale) + " " + distanceUnitInput.value;
|
||||
return `<option value="${r.i}">${r.name} (${length})</option>`;
|
||||
});
|
||||
alertMessage.innerHTML = /* html */ `<div>Route to join with:
|
||||
<select>${options.join("")}</select>
|
||||
</div>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
title: "Join routes",
|
||||
width: fitContent(),
|
||||
position: {my: "left top", at: "left+10 top+150", of: "#map"},
|
||||
buttons: {
|
||||
Cancel: () => {
|
||||
$("#alert").dialog("close");
|
||||
},
|
||||
Join: () => {
|
||||
const selectedRouteId = +alertMessage.querySelector("select").value;
|
||||
const selectedRoute = pack.routes.find(r => r.i === selectedRouteId);
|
||||
joinRoutes(route, selectedRoute);
|
||||
tip("Routes joined", false, "success", 5000);
|
||||
$("#alert").dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
tip("No routes to join with. Route must start or end at current route's start or end cell", false, "error", 4000);
|
||||
}
|
||||
}
|
||||
|
||||
function joinRoutes(route, joinedRoute) {
|
||||
if (route.points.at(-1)[2] === joinedRoute.points.at(0)[2]) {
|
||||
// joinedRoute starts at the end of current route
|
||||
route.points = [...route.points, ...joinedRoute.points.slice(1)];
|
||||
} else if (route.points.at(0)[2] === joinedRoute.points.at(-1)[2]) {
|
||||
// joinedRoute ends at the start of current route
|
||||
route.points = [...joinedRoute.points, ...route.points.slice(1)];
|
||||
} else if (route.points.at(0)[2] === joinedRoute.points.at(0)[2]) {
|
||||
// joinedRoute and current route both start at the same cell
|
||||
route.points = [...route.points.reverse(), ...joinedRoute.points.slice(1)];
|
||||
} else if (route.points.at(-1)[2] === joinedRoute.points.at(-1)[2]) {
|
||||
// joinedRoute and current route both end at the same cell
|
||||
route.points = [...route.points, ...joinedRoute.points.reverse().slice(1)];
|
||||
}
|
||||
|
||||
for (let i = 0; i < route.points.length; i++) {
|
||||
const point = route.points[i];
|
||||
const nextPoint = route.points[i + 1];
|
||||
if (nextPoint) addConnection(point[2], nextPoint[2], route.i);
|
||||
}
|
||||
|
||||
Routes.remove(joinedRoute);
|
||||
drawControlPoints(route.points);
|
||||
redrawRoute(route);
|
||||
drawCells(route.points);
|
||||
}
|
||||
|
||||
function showCreationDialog() {
|
||||
const route = getRoute();
|
||||
createRoute(route.group);
|
||||
}
|
||||
|
||||
function togglePressed() {
|
||||
this.classList.toggle("pressed");
|
||||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
if (routeSplit.classList.contains("pressed")) splitRoute(this);
|
||||
else {
|
||||
this.remove();
|
||||
redrawRoute();
|
||||
}
|
||||
function removeConnection(from, to) {
|
||||
const routes = pack.cells.routes;
|
||||
if (routes[from]) delete routes[from][to];
|
||||
if (routes[to]) delete routes[to][from];
|
||||
}
|
||||
|
||||
function splitRoute(clicked) {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const group = d3.select(elSelected.node().parentNode);
|
||||
routeSplit.classList.remove("pressed");
|
||||
function addConnection(from, to, routeId) {
|
||||
const routes = pack.cells.routes;
|
||||
|
||||
const points1 = [],
|
||||
points2 = [];
|
||||
let points = points1;
|
||||
debug
|
||||
.select("#controlPoints")
|
||||
.selectAll("circle")
|
||||
.each(function () {
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
if (this === clicked) {
|
||||
points = points2;
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
}
|
||||
this.remove();
|
||||
});
|
||||
if (!routes[from]) routes[from] = {};
|
||||
routes[from][to] = routeId;
|
||||
|
||||
elSelected.attr("d", round(lineGen(points1)));
|
||||
const id = getNextId("route");
|
||||
group.append("path").attr("id", id).attr("d", lineGen(points2));
|
||||
debug.select("#controlPoints").selectAll("circle").remove();
|
||||
drawControlPoints(elSelected.node());
|
||||
if (!routes[to]) routes[to] = {};
|
||||
routes[to][from] = routeId;
|
||||
}
|
||||
|
||||
function toggleRouteCreationMode() {
|
||||
document.getElementById("routeSplit").classList.remove("pressed");
|
||||
document.getElementById("routeNew").classList.toggle("pressed");
|
||||
if (document.getElementById("routeNew").classList.contains("pressed")) {
|
||||
tip("Click on map to add control points", true);
|
||||
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
|
||||
elSelected.on("click", null);
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
|
||||
}
|
||||
function changeName() {
|
||||
getRoute().name = this.value;
|
||||
}
|
||||
|
||||
function addPointOnClick() {
|
||||
// create new route
|
||||
if (!elSelected.attr("data-new")) {
|
||||
debug.select("#controlPoints").selectAll("circle").remove();
|
||||
const parent = elSelected.node().parentNode;
|
||||
const id = getNextId("route");
|
||||
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
|
||||
}
|
||||
function changeGroup() {
|
||||
const group = this.value;
|
||||
byId(group).appendChild(elSelected.node());
|
||||
getRoute().group = group;
|
||||
}
|
||||
|
||||
addControlPoint(d3.mouse(this));
|
||||
redrawRoute();
|
||||
function generateName() {
|
||||
const route = getRoute();
|
||||
route.name = routeName.value = Routes.generateName(route);
|
||||
}
|
||||
|
||||
function showRouteElevationProfile() {
|
||||
const route = getRoute();
|
||||
const length = rn(route.length * distanceScale);
|
||||
showElevationProfile(
|
||||
route.points.map(p => p[2]),
|
||||
length,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
function editRouteLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editNotes(id, id);
|
||||
const route = getRoute();
|
||||
editNotes(id, route.name);
|
||||
}
|
||||
|
||||
function editRouteGroupStyle() {
|
||||
const {group} = getRoute();
|
||||
editStyle("routes", group);
|
||||
}
|
||||
|
||||
function toggleLockButton() {
|
||||
const route = getRoute();
|
||||
route.lock = !route.lock;
|
||||
updateLockIcon();
|
||||
}
|
||||
|
||||
function updateLockIcon() {
|
||||
const route = getRoute();
|
||||
if (route.lock) {
|
||||
byId("routeLock").classList.remove("icon-lock-open");
|
||||
byId("routeLock").classList.add("icon-lock");
|
||||
} else {
|
||||
byId("routeLock").classList.remove("icon-lock");
|
||||
byId("routeLock").classList.add("icon-lock-open");
|
||||
}
|
||||
}
|
||||
|
||||
function removeRoute() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the route?";
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the route";
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
width: "22em",
|
||||
title: "Remove route",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
Routes.remove(getRoute());
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#routeEditor").dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
|
|
@ -312,12 +407,16 @@ function editRoute(onClick) {
|
|||
});
|
||||
}
|
||||
|
||||
function closeRoutesEditor() {
|
||||
elSelected.attr("data-new", null).on("click", null);
|
||||
clearMainTip();
|
||||
routeSplit.classList.remove("pressed");
|
||||
routeNew.classList.remove("pressed");
|
||||
function closeRouteEditor() {
|
||||
debug.select("#controlPoints").remove();
|
||||
debug.select("#controlCells").remove();
|
||||
|
||||
elSelected.on("click", null);
|
||||
unselect();
|
||||
clearMainTip();
|
||||
|
||||
const forced = +byId("toggleCells").dataset.forced;
|
||||
byId("toggleCells").dataset.forced = 0;
|
||||
if (forced && layerIsOn("toggleCells")) toggleCells();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
187
modules/ui/routes-overview.js
Normal file
187
modules/ui/routes-overview.js
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"use strict";
|
||||
|
||||
function overviewRoutes() {
|
||||
if (customization) return;
|
||||
closeDialogs("#routesOverview, .stable");
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
|
||||
const body = byId("routesBody");
|
||||
routesOverviewAddLines();
|
||||
$("#routesOverview").dialog();
|
||||
|
||||
if (modules.overviewRoutes) return;
|
||||
modules.overviewRoutes = true;
|
||||
|
||||
$("#routesOverview").dialog({
|
||||
title: "Routes Overview",
|
||||
resizable: false,
|
||||
width: fitContent(),
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
byId("routesOverviewRefresh").on("click", routesOverviewAddLines);
|
||||
byId("routesCreateNew").on("click", createRoute);
|
||||
byId("routesExport").on("click", downloadRoutesData);
|
||||
byId("routesLockAll").on("click", toggleLockAll);
|
||||
byId("routesRemoveAll").on("click", triggerAllRoutesRemove);
|
||||
|
||||
// add line for each route
|
||||
function routesOverviewAddLines() {
|
||||
body.innerHTML = "";
|
||||
let lines = "";
|
||||
|
||||
for (const route of pack.routes) {
|
||||
route.name = route.name || Routes.generateName(route);
|
||||
route.length = route.length || Routes.getLength(route.i);
|
||||
const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value;
|
||||
|
||||
lines += /* html */ `<div
|
||||
class="states"
|
||||
data-id="${route.i}"
|
||||
data-name="${route.name}"
|
||||
data-group="${route.group}"
|
||||
data-length="${route.length}"
|
||||
>
|
||||
<span data-tip="Click to focus on route" class="icon-dot-circled pointer"></span>
|
||||
<div data-tip="Route name" style="width: 15em; margin-left: 0.4em;">${route.name}</div>
|
||||
<div data-tip="Route group" style="width: 8em;">${route.group}</div>
|
||||
<div data-tip="Route length" style="width: 6em;">${length}</div>
|
||||
<span data-tip="Edit route" class="icon-pencil"></span>
|
||||
<span class="locks pointer ${
|
||||
route.lock ? "icon-lock" : "icon-lock-open inactive"
|
||||
}" onmouseover="showElementLockTip(event)"></span>
|
||||
<span data-tip="Remove route" class="icon-trash-empty"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.insertAdjacentHTML("beforeend", lines);
|
||||
|
||||
// update footer
|
||||
routesFooterNumber.innerHTML = pack.routes.length;
|
||||
const averageLength = rn(d3.mean(pack.routes.map(r => r.length)));
|
||||
routesFooterLength.innerHTML = averageLength * distanceScale + " " + distanceUnitInput.value;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseenter", routeHighlightOn));
|
||||
body.querySelectorAll("div.states").forEach(el => el.on("mouseleave", routeHighlightOff));
|
||||
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.on("click", zoomToRoute));
|
||||
body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRouteEditor));
|
||||
body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleLockStatus));
|
||||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove));
|
||||
|
||||
applySorting(routesHeader);
|
||||
}
|
||||
|
||||
function routeHighlightOn(event) {
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
const routeId = +event.target.dataset.id;
|
||||
routes
|
||||
.select("#route" + routeId)
|
||||
.attr("stroke", "red")
|
||||
.attr("stroke-width", 2)
|
||||
.attr("stroke-dasharray", "none");
|
||||
}
|
||||
|
||||
function routeHighlightOff(e) {
|
||||
const routeId = +e.target.dataset.id;
|
||||
routes
|
||||
.select("#route" + routeId)
|
||||
.attr("stroke", null)
|
||||
.attr("stroke-width", null)
|
||||
.attr("stroke-dasharray", null);
|
||||
}
|
||||
|
||||
function zoomToRoute() {
|
||||
const r = +this.parentNode.dataset.id;
|
||||
const route = routes.select("#route" + r).node();
|
||||
highlightElement(route, 3);
|
||||
}
|
||||
|
||||
function downloadRoutesData() {
|
||||
let data = "Id,Route,Group,Length\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||
const d = el.dataset;
|
||||
const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value;
|
||||
data += [d.id, d.name, d.group, length].join(",") + "\n";
|
||||
});
|
||||
|
||||
const name = getFileName("Routes") + ".csv";
|
||||
downloadFile(data, name);
|
||||
}
|
||||
|
||||
function openRouteEditor() {
|
||||
const id = "route" + this.parentNode.dataset.id;
|
||||
editRoute(id);
|
||||
}
|
||||
|
||||
function toggleLockStatus() {
|
||||
const routeId = +this.parentNode.dataset.id;
|
||||
const route = pack.routes[routeId];
|
||||
route.lock = !route.lock;
|
||||
|
||||
if (this.classList.contains("icon-lock")) {
|
||||
this.classList.remove("icon-lock");
|
||||
this.classList.add("icon-lock-open");
|
||||
this.classList.add("inactive");
|
||||
} else {
|
||||
this.classList.remove("icon-lock-open");
|
||||
this.classList.add("icon-lock");
|
||||
this.classList.remove("inactive");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLockAll() {
|
||||
const allLocked = pack.routes.every(route => route.lock);
|
||||
|
||||
pack.routes.forEach(route => {
|
||||
route.lock = !allLocked;
|
||||
});
|
||||
|
||||
routesOverviewAddLines();
|
||||
byId("routesLockAll").className = allLocked ? "icon-lock" : "icon-lock-open";
|
||||
}
|
||||
|
||||
function triggerRouteRemove() {
|
||||
const routeId = +this.parentNode.dataset.id;
|
||||
|
||||
alertMessage.innerHTML = `Are you sure you want to remove the route?`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
width: "22em",
|
||||
title: "Remove route",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
const route = pack.routes.find(r => r.i === routeId);
|
||||
Routes.remove(route);
|
||||
routesOverviewAddLines();
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function triggerAllRoutesRemove() {
|
||||
alertMessage.innerHTML = /* html */ `Are you sure you want to remove all routes? This action can't be undone`;
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Remove all routes",
|
||||
buttons: {
|
||||
Remove: function () {
|
||||
pack.cells.routes = {};
|
||||
pack.routes = [];
|
||||
routes.selectAll("path").remove();
|
||||
|
||||
$(this).dialog("close");
|
||||
$("#routesOverview").dialog("close");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -503,7 +503,7 @@ styleGridScale.addEventListener("input", function () {
|
|||
|
||||
function calculateFriendlyGridSize() {
|
||||
const size = styleGridScale.value * 25;
|
||||
const friendly = `${rn(size * distanceScaleInput.value, 2)} ${distanceUnitInput.value}`;
|
||||
const friendly = `${rn(size * distanceScale, 2)} ${distanceUnitInput.value}`;
|
||||
styleGridSizeFriendly.value = friendly;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -258,11 +258,16 @@ window.UISubmap = (function () {
|
|||
byId("latitudeInput").value = latitudeOutput.value;
|
||||
|
||||
// fix scale
|
||||
distanceScaleInput.value = distanceScaleOutput.value = rn((distanceScale = distanceScaleOutput.value / scale), 2);
|
||||
distanceScale =
|
||||
distanceScaleInput.value =
|
||||
distanceScaleOutput.value =
|
||||
rn((distanceScale = distanceScaleOutput.value / scale), 2);
|
||||
|
||||
populationRateInput.value = populationRateOutput.value = rn(
|
||||
(populationRate = populationRateOutput.value / scale),
|
||||
2
|
||||
);
|
||||
|
||||
customization = 0;
|
||||
startResample(options);
|
||||
}, 1000);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ toolsContent.addEventListener("click", function (event) {
|
|||
else if (button === "editZonesButton") editZones();
|
||||
else if (button === "overviewChartsButton") overviewCharts();
|
||||
else if (button === "overviewBurgsButton") overviewBurgs();
|
||||
else if (button === "overviewRoutesButton") overviewRoutes();
|
||||
else if (button === "overviewRiversButton") overviewRivers();
|
||||
else if (button === "overviewMilitaryButton") overviewMilitary();
|
||||
else if (button === "overviewMarkersButton") overviewMarkers();
|
||||
|
|
@ -66,7 +67,7 @@ toolsContent.addEventListener("click", function (event) {
|
|||
if (button === "addLabel") toggleAddLabel();
|
||||
else if (button === "addBurgTool") toggleAddBurg();
|
||||
else if (button === "addRiver") toggleAddRiver();
|
||||
else if (button === "addRoute") toggleAddRoute();
|
||||
else if (button === "addRoute") createRoute();
|
||||
else if (button === "addMarker") toggleAddMarker();
|
||||
// click to create a new map buttons
|
||||
else if (button === "openSubmapMenu") UISubmap.openSubmapMenu();
|
||||
|
|
@ -79,7 +80,7 @@ function processFeatureRegeneration(event, button) {
|
|||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
} else if (button === "regenerateRoutes") {
|
||||
Routes.regenerate();
|
||||
regenerateRoutes();
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
} else if (button === "regenerateRivers") regenerateRivers();
|
||||
else if (button === "regeneratePopulation") recalculatePopulation();
|
||||
|
|
@ -115,6 +116,14 @@ async function openEmblemEditor() {
|
|||
editEmblem(type, id, el);
|
||||
}
|
||||
|
||||
function regenerateRoutes() {
|
||||
const locked = pack.routes.filter(route => route.lock).map((route, index) => ({...route, i: index}));
|
||||
Routes.generate(locked);
|
||||
|
||||
routes.selectAll("path").remove();
|
||||
if (layerIsOn("toggleRoutes")) drawRoutes();
|
||||
}
|
||||
|
||||
function regenerateRivers() {
|
||||
Rivers.generate();
|
||||
Lakes.defineGroup();
|
||||
|
|
@ -129,7 +138,7 @@ function recalculatePopulation() {
|
|||
if (!b.i || b.removed || b.lock) return;
|
||||
const i = b.cell;
|
||||
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
|
||||
b.population = rn(Math.max(pack.cells.s[i] / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
|
||||
if (b.capital) b.population = b.population * 1.3; // increase capital population
|
||||
if (b.port) b.population = b.population * 1.3; // increase port population
|
||||
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
|
||||
|
|
@ -429,7 +438,7 @@ function regenerateBurgs() {
|
|||
BurgsAndStates.specifyBurgs();
|
||||
BurgsAndStates.defineBurgFeatures();
|
||||
BurgsAndStates.drawBurgs();
|
||||
Routes.regenerate();
|
||||
regenerateRoutes();
|
||||
|
||||
// remove emblems
|
||||
document.querySelectorAll("[id^=burgCOA]").forEach(el => el.remove());
|
||||
|
|
@ -792,34 +801,6 @@ function addRiverOnClick() {
|
|||
}
|
||||
}
|
||||
|
||||
function toggleAddRoute() {
|
||||
const pressed = document.getElementById("addRoute").classList.contains("pressed");
|
||||
if (pressed) {
|
||||
unpressClickToAddButton();
|
||||
return;
|
||||
}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addRoute.classList.add("pressed");
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
|
||||
tip("Click on map to add a first control point", true);
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
}
|
||||
|
||||
function addRouteOnClick() {
|
||||
unpressClickToAddButton();
|
||||
const point = d3.mouse(this);
|
||||
const id = getNextId("route");
|
||||
elSelected = routes
|
||||
.select("g")
|
||||
.append("path")
|
||||
.attr("id", id)
|
||||
.attr("data-new", 1)
|
||||
.attr("d", `M${point[0]},${point[1]}`);
|
||||
editRoute(true);
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
const pressed = document.getElementById("addMarker")?.classList.contains("pressed");
|
||||
if (pressed) {
|
||||
|
|
@ -960,6 +941,6 @@ function viewCellDetails() {
|
|||
}
|
||||
|
||||
async function overviewCharts() {
|
||||
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.89.24");
|
||||
const Overview = await import("../dynamic/overview/charts-overview.js?v=1.99.00");
|
||||
Overview.open();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ function editUnits() {
|
|||
}
|
||||
|
||||
function changeDistanceScale() {
|
||||
distanceScale = +this.value;
|
||||
renderScaleBar();
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
|
@ -90,10 +91,9 @@ function editUnits() {
|
|||
}
|
||||
|
||||
function restoreDefaultUnits() {
|
||||
// distanceScale
|
||||
distanceScale = 3;
|
||||
byId("distanceScaleOutput").value = 3;
|
||||
byId("distanceScaleInput").value = 3;
|
||||
byId("distanceScaleOutput").value = distanceScale;
|
||||
byId("distanceScaleInput").value = distanceScale;
|
||||
unlock("distanceScale");
|
||||
|
||||
// units
|
||||
|
|
@ -179,13 +179,15 @@ function editUnits() {
|
|||
tip("Draw a curve along routes to measure length. Hold Shift to measure away from roads.", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
|
||||
viewbox.style("cursor", "crosshair").call(
|
||||
d3.drag().on("start", function () {
|
||||
const cells = pack.cells;
|
||||
const burgs = pack.burgs;
|
||||
const point = d3.mouse(this);
|
||||
const c = findCell(point[0], point[1]);
|
||||
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
|
||||
|
||||
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
|
||||
const b = cells.burg[c];
|
||||
const x = b ? burgs[b].x : cells.p[c][0];
|
||||
const y = b ? burgs[b].y : cells.p[c][1];
|
||||
|
|
@ -194,7 +196,7 @@ function editUnits() {
|
|||
d3.event.on("drag", function () {
|
||||
const point = d3.mouse(this);
|
||||
const c = findCell(point[0], point[1]);
|
||||
if (cells.road[c] || d3.event.sourceEvent.shiftKey) {
|
||||
if (Routes.isConnected(c) || d3.event.sourceEvent.shiftKey) {
|
||||
routeOpisometer.trackCell(c, true);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -105,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`;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue