Skip to content

Commit

Permalink
State labels: new label placing algorithm (#977)
Browse files Browse the repository at this point in the history
* feat: draw state labels start

* feat: update old .map files

* chore: update version hash

* fear: add change to the user's changelog
  • Loading branch information
Azgaar authored Aug 11, 2023
1 parent 1bb9025 commit 87599d1
Show file tree
Hide file tree
Showing 15 changed files with 396 additions and 282 deletions.
2 changes: 1 addition & 1 deletion index.css
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ i.icon-lock {
}

#labels {
text-anchor: start;
text-anchor: middle;
dominant-baseline: central;
cursor: pointer;
}
Expand Down
21 changes: 11 additions & 10 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@
}
</style>

<link rel="preload" href="index.css?v=1.89.38" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="index.css?v=1.92.00" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="icons.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
<link rel="preload" href="libs/jquery-ui.css" as="style" onload="this.onload=null; this.rel='stylesheet'" />
</head>
Expand Down Expand Up @@ -7946,7 +7946,8 @@
<script src="modules/biomes.js"></script>
<script src="modules/names-generator.js?v=1.87.14"></script>
<script src="modules/cultures-generator.js?v=1.89.10"></script>
<script src="modules/burgs-and-states.js?v=1.89.37"></script>
<script src="modules/renderers/drawStateLabels.js"></script>
<script src="modules/burgs-and-states.js?v=1.92.00"></script>
<script src="modules/routes-generator.js"></script>
<script src="modules/religions-generator.js?v=1.89.36"></script>
<script src="modules/military-generator.js"></script>
Expand All @@ -7963,15 +7964,15 @@

<script src="modules/ui/general.js?v=1.87.03"></script>
<script src="modules/ui/options.js?v=1.91.00"></script>
<script src="main.js?v=1.91.05"></script>
<script src="main.js?v=1.92.00"></script>

<script defer src="modules/relief-icons.js"></script>
<script defer src="modules/ui/style.js"></script>
<script defer src="modules/ui/editors.js?v=1.91.00"></script>
<script defer src="modules/ui/tools.js?v=1.90.00"></script>
<script defer src="modules/ui/editors.js?v=1.92.00"></script>
<script defer src="modules/ui/tools.js?v=1.92.00"></script>
<script defer src="modules/ui/world-configurator.js?v=1.91.05"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.91.05"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.89.00"></script>
<script defer src="modules/ui/heightmap-editor.js?v=1.92.00"></script>
<script defer src="modules/ui/provinces-editor.js?v=1.92.00"></script>
<script defer src="modules/ui/biomes-editor.js?v=1.91.05"></script>
<script defer src="modules/ui/namesbase-editor.js?v=1.89.26"></script>
<script defer src="modules/ui/elevation-profile.js"></script>
Expand All @@ -7980,7 +7981,7 @@
<script defer src="modules/ui/ice-editor.js?v=1.89.08"></script>
<script defer src="modules/ui/lakes-editor.js?v=1.87.10"></script>
<script defer src="modules/ui/coastline-editor.js"></script>
<script defer src="modules/ui/labels-editor.js"></script>
<script defer src="modules/ui/labels-editor.js?v=1.92.00"></script>
<script defer src="modules/ui/rivers-editor.js"></script>
<script defer src="modules/ui/rivers-creator.js?v=1.89.13"></script>
<script defer src="modules/ui/relief-editor.js"></script>
Expand All @@ -7999,14 +8000,14 @@
<script defer src="modules/ui/emblems-editor.js?v=1.91.00"></script>
<script defer src="modules/ui/markers-editor.js"></script>
<script defer src="modules/ui/3d.js?v=1.89.36"></script>
<script defer src="modules/ui/submap.js"></script>
<script defer src="modules/ui/submap.js?v=1.92.00"></script>
<script defer src="modules/ui/hotkeys.js?v=1.88.00"></script>
<script defer src="modules/coa-renderer.js?v=1.91.00"></script>
<script defer src="libs/rgbquant.min.js"></script>
<script defer src="libs/jquery.ui.touch-punch.min.js"></script>

<script defer src="modules/io/save.js?v=1.91.04"></script>
<script defer src="modules/io/load.js?v=1.91.05"></script>
<script defer src="modules/io/load.js?v=1.92.00"></script>
<script defer src="modules/io/cloud.js"></script>
<script defer src="modules/io/export.js?v=1.89.36"></script>
<script defer src="modules/io/formats.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion main.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ async function generate(options) {

drawStates();
drawBorders();
BurgsAndStates.drawStateLabels();
drawStateLabels();

Rivers.specify();
Lakes.generateName();
Expand Down
218 changes: 0 additions & 218 deletions modules/burgs-and-states.js
Original file line number Diff line number Diff line change
Expand Up @@ -502,223 +502,6 @@ window.BurgsAndStates = (function () {
TIME && console.timeEnd("updateCulturesForBurgsAndStates");
};

// calculate and draw curved state labels for a list of states
const drawStateLabels = function (list) {
TIME && console.time("drawStateLabels");
const {cells, features, states} = pack;
const paths = []; // text paths
lineGen.curve(d3.curveBundle.beta(1));
const mode = options.stateLabelsMode || "auto";

for (const s of states) {
if (!s.i || s.removed || s.lock || !s.cells || (list && !list.includes(s.i))) continue;

const used = [];
const visualCenter = findCell(s.pole[0], s.pole[1]);
const start = cells.state[visualCenter] === s.i ? visualCenter : s.center;
const hull = getHull(start, s.i, s.cells / 10);
const points = [...hull].map(v => pack.vertices.p[v]);
const delaunay = Delaunator.from(points);
const voronoi = new Voronoi(delaunay, points, points.length);
const chain = connectCenters(voronoi.vertices, s.pole[1]);
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length);
paths.push([s.i, relaxed]);

function getHull(start, state, maxLake) {
const queue = [start];
const hull = new Set();

while (queue.length) {
const q = queue.pop();
const sameStateNeibs = cells.c[q].filter(c => cells.state[c] === state);

cells.c[q].forEach(function (c, d) {
const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake;
if (cells.b[c] || (cells.state[c] !== state && !passableLake)) return hull.add(cells.v[q][d]);

const hasCoadjacentSameStateCells = sameStateNeibs.some(neib => cells.c[c].includes(neib));
if (hull.size > 20 && !hasCoadjacentSameStateCells && !passableLake) return hull.add(cells.v[q][d]);

if (used[c]) return;
used[c] = 1;
queue.push(c);
});
}

return hull;
}

function connectCenters(c, y) {
// check if vertex is inside the area
const inside = c.p.map(function (p) {
if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen
return used[findCell(p[0], p[1])];
});

const pointsInside = d3.range(c.p.length).filter(i => inside[i]);
if (!pointsInside.length) return [0];
const h = c.p.length < 200 ? 0 : c.p.length < 600 ? 0.5 : 1; // power of horyzontality shift
const end =
pointsInside[
d3.scan(
pointsInside,
(a, b) => c.p[a][0] - c.p[b][0] + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h
)
]; // left point
const start =
pointsInside[
d3.scan(
pointsInside,
(a, b) => c.p[b][0] - c.p[a][0] - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h
)
]; // right point

// connect leftmost and rightmost points with shortest path
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
const cost = [],
from = [];
queue.queue({e: start, p: 0});

while (queue.length) {
const next = queue.dequeue(),
n = next.e,
p = next.p;
if (n === end) break;

for (const v of c.v[n]) {
if (v === -1) continue;
const totalCost = p + (inside[v] ? 1 : 100);
if (from[v] || totalCost >= cost[v]) continue;
cost[v] = totalCost;
from[v] = n;
queue.queue({e: v, p: totalCost});
}
}

// restore path
const chain = [end];
let cur = end;
while (cur !== start) {
cur = from[cur];
if (inside[cur]) chain.push(cur);
}
return chain;
}
}

void (function drawLabels() {
const g = labels.select("#states");
const t = defs.select("#textPaths");
const displayed = layerIsOn("toggleLabels");
if (!displayed) toggleLabels();

// remove state labels to be redrawn
for (const state of pack.states) {
if (!state.i || state.removed || state.lock) continue;
if (list && !list.includes(state.i)) continue;

byId(`stateLabel${state.i}`)?.remove();
byId(`textPath_stateLabel${state.i}`)?.remove();
}

const example = g.append("text").attr("x", 0).attr("x", 0).text("Average");
const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter

paths.forEach(p => {
const id = p[0];
const state = states[p[0]];
const {name, fullName} = state;

const path = p[1].length > 1 ? round(lineGen(p[1])) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`;
const textPath = t
.append("path")
.attr("d", path)
.attr("id", "textPath_stateLabel" + id);
const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters

const [lines, ratio] = getLines(mode, name, fullName, pathLength);

// prolongate path if it's too short
if (pathLength && pathLength < lines[0].length) {
const points = p[1];
const f = points[0];
const l = points[points.length - 1];
const [dx, dy] = [l[0] - f[0], l[1] - f[1]];
const mod = Math.abs((letterLength * lines[0].length) / dx) / 2;
points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)];
points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)];
textPath.attr("d", round(lineGen(points)));
}

example.attr("font-size", ratio + "%");
const top = (lines.length - 1) / -2; // y offset
const spans = lines.map((l, d) => {
example.text(l);
const left = example.node().getBBox().width / -2; // x offset
return `<tspan x=${rn(left, 1)} dy="${d ? 1 : top}em">${l}</tspan>`;
});

const el = g
.append("text")
.attr("id", "stateLabel" + id)
.append("textPath")
.attr("xlink:href", "#textPath_stateLabel" + id)
.attr("startOffset", "50%")
.attr("font-size", ratio + "%")
.node();

el.insertAdjacentHTML("afterbegin", spans.join(""));
if (mode === "full" || lines.length === 1) return;

// check whether multilined label is generally inside the state. If no, replace with short name label
const cs = pack.cells.state;
const b = el.parentNode.getBBox();
const c1 = () => +cs[findCell(b.x, b.y)] === id;
const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id;
const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id;
const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id;
const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id;
const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id;
if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside => exit

// move to one-line name
const text = pathLength > fullName.length * 1.8 ? fullName : name;
example.text(text);
const left = example.node().getBBox().width / -2; // x offset
el.innerHTML = `<tspan x="${left}px">${text}</tspan>`;

const correctedRatio = minmax(rn((pathLength / text.length) * 60), 40, 130);
el.setAttribute("font-size", correctedRatio + "%");
});

example.remove();
if (!displayed) toggleLabels();
})();

function getLines(mode, name, fullName, pathLength) {
// short name
if (mode === "short" || (mode === "auto" && pathLength < name.length)) {
const lines = splitInTwo(name);
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 60), 50, 150)];
}

// full name: one line
if (pathLength > fullName.length * 2.5) {
const lines = [fullName];
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 70), 70, 170)];
}

// full name: two lines
const lines = splitInTwo(fullName);
const ratio = pathLength / lines[0].length;
return [lines, minmax(rn(ratio * 60), 70, 150)];
}

TIME && console.timeEnd("drawStateLabels");
};

// calculate states data like area, population etc.
const collectStatistics = function () {
TIME && console.time("collectStatistics");
Expand Down Expand Up @@ -1405,7 +1188,6 @@ window.BurgsAndStates = (function () {
specifyBurgs,
defineBurgFeatures,
getType,
drawStateLabels,
collectStatistics,
generateCampaign,
generateCampaigns,
Expand Down
7 changes: 7 additions & 0 deletions modules/dynamic/auto-update.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,4 +698,11 @@ export function resolveVersionConflicts(version) {
}
});
}

if (version < 1.92) {
// v1.92 change labels text-anchor from 'start' to 'middle'
labels.selectAll("tspan").each(function () {
this.setAttribute("x", 0);
});
}
}
8 changes: 4 additions & 4 deletions modules/dynamic/editors/states-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ function editStateName(state) {
s.name = nameInput.value;
s.formName = formSelect.value;
s.fullName = fullNameInput.value;
if (changed && stateNameEditorUpdateLabel.checked) BurgsAndStates.drawStateLabels([s.i]);
if (changed && stateNameEditorUpdateLabel.checked) drawStateLabels([s.i]);
refreshStatesEditor();
}
}
Expand Down Expand Up @@ -877,7 +877,7 @@ function recalculateStates(must) {
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
if (adjustLabels.checked) drawStateLabels();
refreshStatesEditor();
}

Expand Down Expand Up @@ -1022,7 +1022,7 @@ function applyStatesManualAssignent() {
if (affectedStates.length) {
refreshStatesEditor();
layerIsOn("toggleStates") ? drawStates() : toggleStates();
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
adjustProvinces([...new Set(affectedProvinces)]);
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
if (layerIsOn("toggleProvinces")) drawProvinces();
Expand Down Expand Up @@ -1459,7 +1459,7 @@ function openStateMergeDialog() {
layerIsOn("toggleStates") ? drawStates() : toggleStates();
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
layerIsOn("toggleProvinces") && drawProvinces();
BurgsAndStates.drawStateLabels([rulingStateId]);
drawStateLabels([rulingStateId]);

refreshStatesEditor();
}
Expand Down
2 changes: 1 addition & 1 deletion modules/io/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ async function parseLoadedData(data) {
{
// dynamically import and run auto-udpdate script
const versionNumber = parseFloat(params[0]);
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.91.00");
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.92.00");
resolveVersionConflicts(versionNumber);
}

Expand Down
Loading

0 comments on commit 87599d1

Please sign in to comment.