Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

State labels: new label placing algorithm #977

Merged
merged 4 commits into from
Aug 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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