mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-22 03:51:23 +01:00
feat: draw state labels start
This commit is contained in:
parent
1bb90251cd
commit
cc7f7cbde2
9 changed files with 362 additions and 33 deletions
|
|
@ -7946,6 +7946,7 @@
|
||||||
<script src="modules/biomes.js"></script>
|
<script src="modules/biomes.js"></script>
|
||||||
<script src="modules/names-generator.js?v=1.87.14"></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/cultures-generator.js?v=1.89.10"></script>
|
||||||
|
<script src="modules/renderers/drawStateLabels.js"></script>
|
||||||
<script src="modules/burgs-and-states.js?v=1.89.37"></script>
|
<script src="modules/burgs-and-states.js?v=1.89.37"></script>
|
||||||
<script src="modules/routes-generator.js"></script>
|
<script src="modules/routes-generator.js"></script>
|
||||||
<script src="modules/religions-generator.js?v=1.89.36"></script>
|
<script src="modules/religions-generator.js?v=1.89.36"></script>
|
||||||
|
|
|
||||||
2
main.js
2
main.js
|
|
@ -648,7 +648,7 @@ async function generate(options) {
|
||||||
|
|
||||||
drawStates();
|
drawStates();
|
||||||
drawBorders();
|
drawBorders();
|
||||||
BurgsAndStates.drawStateLabels();
|
drawStateLabels();
|
||||||
|
|
||||||
Rivers.specify();
|
Rivers.specify();
|
||||||
Lakes.generateName();
|
Lakes.generateName();
|
||||||
|
|
|
||||||
|
|
@ -503,7 +503,7 @@ window.BurgsAndStates = (function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
// calculate and draw curved state labels for a list of states
|
// calculate and draw curved state labels for a list of states
|
||||||
const drawStateLabels = function (list) {
|
const drawStateLabelsOld = function (list) {
|
||||||
TIME && console.time("drawStateLabels");
|
TIME && console.time("drawStateLabels");
|
||||||
const {cells, features, states} = pack;
|
const {cells, features, states} = pack;
|
||||||
const paths = []; // text paths
|
const paths = []; // text paths
|
||||||
|
|
@ -519,8 +519,8 @@ window.BurgsAndStates = (function () {
|
||||||
const hull = getHull(start, s.i, s.cells / 10);
|
const hull = getHull(start, s.i, s.cells / 10);
|
||||||
const points = [...hull].map(v => pack.vertices.p[v]);
|
const points = [...hull].map(v => pack.vertices.p[v]);
|
||||||
const delaunay = Delaunator.from(points);
|
const delaunay = Delaunator.from(points);
|
||||||
const voronoi = new Voronoi(delaunay, points, points.length);
|
|
||||||
const chain = connectCenters(voronoi.vertices, s.pole[1]);
|
const chain = connectCenters(voronoi.vertices, s.pole[1]);
|
||||||
|
const voronoi = new Voronoi(delaunay, points, points.length);
|
||||||
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length);
|
const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length);
|
||||||
paths.push([s.i, relaxed]);
|
paths.push([s.i, relaxed]);
|
||||||
|
|
||||||
|
|
@ -1405,7 +1405,6 @@ window.BurgsAndStates = (function () {
|
||||||
specifyBurgs,
|
specifyBurgs,
|
||||||
defineBurgFeatures,
|
defineBurgFeatures,
|
||||||
getType,
|
getType,
|
||||||
drawStateLabels,
|
|
||||||
collectStatistics,
|
collectStatistics,
|
||||||
generateCampaign,
|
generateCampaign,
|
||||||
generateCampaigns,
|
generateCampaigns,
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,7 @@ function editStateName(state) {
|
||||||
s.name = nameInput.value;
|
s.name = nameInput.value;
|
||||||
s.formName = formSelect.value;
|
s.formName = formSelect.value;
|
||||||
s.fullName = fullNameInput.value;
|
s.fullName = fullNameInput.value;
|
||||||
if (changed && stateNameEditorUpdateLabel.checked) BurgsAndStates.drawStateLabels([s.i]);
|
if (changed && stateNameEditorUpdateLabel.checked) drawStateLabels([s.i]);
|
||||||
refreshStatesEditor();
|
refreshStatesEditor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -877,7 +877,7 @@ function recalculateStates(must) {
|
||||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||||
else drawBorders();
|
else drawBorders();
|
||||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
if (adjustLabels.checked) drawStateLabels();
|
||||||
refreshStatesEditor();
|
refreshStatesEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1022,7 +1022,7 @@ function applyStatesManualAssignent() {
|
||||||
if (affectedStates.length) {
|
if (affectedStates.length) {
|
||||||
refreshStatesEditor();
|
refreshStatesEditor();
|
||||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels([...new Set(affectedStates)]);
|
if (adjustLabels.checked) drawStateLabels([...new Set(affectedStates)]);
|
||||||
adjustProvinces([...new Set(affectedProvinces)]);
|
adjustProvinces([...new Set(affectedProvinces)]);
|
||||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||||
|
|
@ -1459,7 +1459,7 @@ function openStateMergeDialog() {
|
||||||
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
layerIsOn("toggleStates") ? drawStates() : toggleStates();
|
||||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||||
layerIsOn("toggleProvinces") && drawProvinces();
|
layerIsOn("toggleProvinces") && drawProvinces();
|
||||||
BurgsAndStates.drawStateLabels([rulingStateId]);
|
drawStateLabels([rulingStateId]);
|
||||||
|
|
||||||
refreshStatesEditor();
|
refreshStatesEditor();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
278
modules/renderers/drawStatelabels.js
Normal file
278
modules/renderers/drawStatelabels.js
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function drawStateLabels() {
|
||||||
|
console.time("drawStateLabels");
|
||||||
|
|
||||||
|
const {cells, states, features} = pack;
|
||||||
|
const stateIds = cells.state;
|
||||||
|
|
||||||
|
// increase step to 15 or 30 to make it faster and more horyzontal
|
||||||
|
// decrease step to 5 to improve accuracy
|
||||||
|
const ANGLE_STEP = 9;
|
||||||
|
const raycast = precalculateAngles(ANGLE_STEP);
|
||||||
|
|
||||||
|
const INITIAL_DISTANCE = 10;
|
||||||
|
const DISTANCE_STEP = 15;
|
||||||
|
const MAX_ITERATIONS = 100;
|
||||||
|
|
||||||
|
const labelPaths = getLabelPaths();
|
||||||
|
drawLabelPath();
|
||||||
|
|
||||||
|
function getLabelPaths() {
|
||||||
|
const labelPaths = [];
|
||||||
|
|
||||||
|
for (const state of states) {
|
||||||
|
if (!state.i || state.removed || state.locked) continue;
|
||||||
|
|
||||||
|
const offset = getOffsetWidth(state.cells);
|
||||||
|
const maxLakeSize = state.cells / 50;
|
||||||
|
const [x0, y0] = state.pole;
|
||||||
|
|
||||||
|
const offsetPoints = new Map(
|
||||||
|
(offset ? raycast : []).map(({angle, x: x1, y: y1}) => {
|
||||||
|
const [x, y] = [x0 + offset * x1, y0 + offset * y1];
|
||||||
|
return [angle, {x, y}];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const distances = raycast.map(({angle, x: dx, y: dy, modifier}) => {
|
||||||
|
let distanceMin;
|
||||||
|
const distance1 = getMaxDistance(state.i, {x: x0, y: y0}, dx, dy, maxLakeSize);
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
const point2 = offsetPoints.get(angle - 90 < 0 ? angle + 270 : angle - 90);
|
||||||
|
const distance2 = getMaxDistance(state.i, point2, dx, dy, maxLakeSize);
|
||||||
|
|
||||||
|
const point3 = offsetPoints.get(angle + 90 >= 360 ? angle - 270 : angle + 90);
|
||||||
|
const distance3 = getMaxDistance(state.i, point3, dx, dy, maxLakeSize);
|
||||||
|
|
||||||
|
distanceMin = Math.min(distance1, distance2, distance3);
|
||||||
|
} else {
|
||||||
|
distanceMin = distance1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [x, y] = [x0 + distanceMin * dx, y0 + distanceMin * dy];
|
||||||
|
return {angle, distance: distanceMin * modifier, x, y};
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
angle,
|
||||||
|
x: x1,
|
||||||
|
y: y1
|
||||||
|
} = distances.reduce(
|
||||||
|
(acc, {angle, distance, x, y}) => {
|
||||||
|
if (distance > acc.distance) return {angle, distance, x, y};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{angle: 0, distance: 0, x: 0, y: 0}
|
||||||
|
);
|
||||||
|
|
||||||
|
const oppositeAngle = angle >= 180 ? angle - 180 : angle + 180;
|
||||||
|
const {x: x2, y: y2} = distances.reduce(
|
||||||
|
(acc, {angle, distance, x, y}) => {
|
||||||
|
const angleDif = getAnglesDif(angle, oppositeAngle);
|
||||||
|
const score = distance * getAngleModifier(angleDif);
|
||||||
|
if (score > acc.score) return {angle, score, x, y};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{angle: 0, score: 0, x: 0, y: 0}
|
||||||
|
);
|
||||||
|
|
||||||
|
const pathPoints = [[x1, y1], state.pole, [x2, y2]];
|
||||||
|
if (x1 > x2) pathPoints.reverse();
|
||||||
|
labelPaths.push([state.i, pathPoints]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return labelPaths;
|
||||||
|
|
||||||
|
function getMaxDistance(stateId, point, dx, dy, maxLakeSize) {
|
||||||
|
let distance = INITIAL_DISTANCE;
|
||||||
|
|
||||||
|
for (let i = 0; i < MAX_ITERATIONS; i++) {
|
||||||
|
const [x, y] = [point.x + distance * dx, point.y + distance * dy];
|
||||||
|
const cellId = findCell(x, y, DISTANCE_STEP);
|
||||||
|
|
||||||
|
// drawPoint([x, y], {color: cellId && isPassable(cellId) ? "blue" : "red", radius: 0.8});
|
||||||
|
|
||||||
|
if (!cellId || !isPassable(cellId)) break;
|
||||||
|
distance += DISTANCE_STEP;
|
||||||
|
}
|
||||||
|
|
||||||
|
return distance;
|
||||||
|
|
||||||
|
function isPassable(cellId) {
|
||||||
|
const feature = features[cells.f[cellId]];
|
||||||
|
if (feature.type === "lake") return feature.cells <= maxLakeSize;
|
||||||
|
return stateIds[cellId] === stateId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLabelPath() {
|
||||||
|
const mode = options.stateLabelsMode || "auto";
|
||||||
|
const lineGen = d3.line().curve(d3.curveBundle.beta(1));
|
||||||
|
|
||||||
|
const textGroup = d3.select("g#labels > g#states");
|
||||||
|
const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
|
||||||
|
|
||||||
|
const testLabel = textGroup.append("text").attr("x", 0).attr("x", 0).text("Example");
|
||||||
|
const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
|
||||||
|
testLabel.remove();
|
||||||
|
|
||||||
|
for (const [stateId, pathPoints] of labelPaths) {
|
||||||
|
const state = states[stateId];
|
||||||
|
if (!state.i || state.removed) throw new Error("State must not be neutral");
|
||||||
|
if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points");
|
||||||
|
|
||||||
|
textGroup.select("#textPath_stateLabel" + stateId).remove();
|
||||||
|
pathGroup.select("#stateLabel" + stateId).remove();
|
||||||
|
|
||||||
|
const textPath = pathGroup
|
||||||
|
.append("path")
|
||||||
|
.attr("d", round(lineGen(pathPoints)))
|
||||||
|
.attr("id", "textPath_stateLabel" + stateId);
|
||||||
|
|
||||||
|
const pathLength = textPath.node().getTotalLength() / letterLength; // path length in letters
|
||||||
|
const [lines, ratio] = getLinesAndRatio(mode, state.name, state.fullName, pathLength);
|
||||||
|
|
||||||
|
// prolongate path if it's too short
|
||||||
|
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||||
|
if (pathLength && pathLength < longestLineLength) {
|
||||||
|
const [x1, y1] = pathPoints.at(0);
|
||||||
|
const [x2, y2] = pathPoints.at(-1);
|
||||||
|
const [dx, dy] = [(x2 - x1) / 2, (y2 - y1) / 2];
|
||||||
|
|
||||||
|
const mod = longestLineLength / pathLength;
|
||||||
|
pathPoints[0] = [x1 + dx - dx * mod, y1 + dy - dy * mod];
|
||||||
|
pathPoints[pathPoints.length - 1] = [x2 - dx + dx * mod, y2 - dy + dy * mod];
|
||||||
|
|
||||||
|
textPath.attr("d", round(lineGen(pathPoints)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const textElement = textGroup
|
||||||
|
.append("text")
|
||||||
|
.attr("id", "stateLabel" + stateId)
|
||||||
|
.append("textPath")
|
||||||
|
.attr("startOffset", "50%")
|
||||||
|
.attr("font-size", ratio + "%")
|
||||||
|
.node();
|
||||||
|
|
||||||
|
const top = (lines.length - 1) / -2; // y offset
|
||||||
|
const spans = lines.map((line, index) => `<tspan x="0" dy="${index ? 1 : top}em">${line}</tspan>`);
|
||||||
|
textElement.insertAdjacentHTML("afterbegin", spans.join(""));
|
||||||
|
|
||||||
|
const {width, height} = textElement.getBBox();
|
||||||
|
textElement.setAttribute("href", "#textPath_stateLabel" + stateId);
|
||||||
|
|
||||||
|
if (mode === "full" || lines.length === 1) continue;
|
||||||
|
|
||||||
|
// check if label fits state boundaries. If no, replace it with short name
|
||||||
|
const [[x1, y1], [x2, y2]] = [pathPoints.at(0), pathPoints.at(-1)];
|
||||||
|
const angleRad = Math.atan2(y2 - y1, x2 - x1);
|
||||||
|
|
||||||
|
const isInsideState = checkIfInsideState(textElement, angleRad, width / 2, height / 2, stateIds, stateId);
|
||||||
|
if (isInsideState) continue;
|
||||||
|
|
||||||
|
// replace name to one-liner
|
||||||
|
const text = pathLength > state.fullName.length * 1.8 ? state.fullName : state.name;
|
||||||
|
textElement.innerHTML = `<tspan x="0">${text}</tspan>`;
|
||||||
|
|
||||||
|
const correctedRatio = minmax(rn((pathLength / text.length) * 50), 40, 130);
|
||||||
|
textElement.setAttribute("font-size", correctedRatio + "%");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// point offset to reduce label overlap with state borders
|
||||||
|
function getOffsetWidth(cellsNumber) {
|
||||||
|
if (cellsNumber < 80) return 0;
|
||||||
|
if (cellsNumber < 140) return 5;
|
||||||
|
if (cellsNumber < 200) return 15;
|
||||||
|
if (cellsNumber < 300) return 20;
|
||||||
|
if (cellsNumber < 500) return 25;
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
// difference between two angles in range [0, 180]
|
||||||
|
function getAnglesDif(angle1, angle2) {
|
||||||
|
return 180 - Math.abs(Math.abs(angle1 - angle2) - 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
// score multiplier based on angle difference betwee left and right sides
|
||||||
|
function getAngleModifier(angleDif) {
|
||||||
|
if (angleDif === 0) return 1;
|
||||||
|
if (angleDif <= 15) return 0.95;
|
||||||
|
if (angleDif <= 30) return 0.9;
|
||||||
|
if (angleDif <= 45) return 0.6;
|
||||||
|
if (angleDif <= 60) return 0.3;
|
||||||
|
if (angleDif <= 90) return 0.1;
|
||||||
|
return 0; // >90
|
||||||
|
}
|
||||||
|
|
||||||
|
function precalculateAngles(step) {
|
||||||
|
const angles = [];
|
||||||
|
const RAD = Math.PI / 180;
|
||||||
|
|
||||||
|
for (let angle = 0; angle < 360; angle += step) {
|
||||||
|
const x = Math.cos(angle * RAD);
|
||||||
|
const y = Math.sin(angle * RAD);
|
||||||
|
const angleDif = 90 - Math.abs((angle % 180) - 90);
|
||||||
|
const modifier = 1 - angleDif / 120; // [0.25, 1]
|
||||||
|
angles.push({angle, modifier, x, y});
|
||||||
|
}
|
||||||
|
|
||||||
|
return angles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinesAndRatio(mode, name, fullName, pathLength) {
|
||||||
|
// short name
|
||||||
|
if (mode === "short" || (mode === "auto" && pathLength <= name.length)) {
|
||||||
|
const lines = splitInTwo(name);
|
||||||
|
const longestLineLength = d3.max(lines.map(({length}) => length));
|
||||||
|
const ratio = pathLength / longestLineLength;
|
||||||
|
return [lines, minmax(rn(ratio * 60), 50, 150)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// full name: one line
|
||||||
|
if (pathLength > fullName.length * 2) {
|
||||||
|
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 longestLineLength = d3.max(lines.map(({length}) => length));
|
||||||
|
const ratio = pathLength / longestLineLength;
|
||||||
|
return [lines, minmax(rn(ratio * 60), 70, 150)];
|
||||||
|
}
|
||||||
|
|
||||||
|
// check whether multi-lined label is mostly inside the state. If no, replace it with short name label
|
||||||
|
function checkIfInsideState(textElement, angleRad, halfwidth, halfheight, stateIds, stateId) {
|
||||||
|
const bbox = textElement.getBBox();
|
||||||
|
const [cx, cy] = [bbox.x + bbox.width / 2, bbox.y + bbox.height / 2];
|
||||||
|
|
||||||
|
const points = [
|
||||||
|
[-halfwidth, -halfheight],
|
||||||
|
[+halfwidth, -halfheight],
|
||||||
|
[+halfwidth, halfheight],
|
||||||
|
[-halfwidth, halfheight],
|
||||||
|
[0, halfheight],
|
||||||
|
[0, -halfheight]
|
||||||
|
];
|
||||||
|
|
||||||
|
const sin = Math.sin(angleRad);
|
||||||
|
const cos = Math.cos(angleRad);
|
||||||
|
const rotatedPoints = points.map(([x, y]) => [cx + x * cos - y * sin, cy + x * sin + y * cos]);
|
||||||
|
|
||||||
|
let pointsInside = 0;
|
||||||
|
for (const [x, y] of rotatedPoints) {
|
||||||
|
const isInside = stateIds[findCell(x, y)] === stateId;
|
||||||
|
if (isInside) pointsInside++;
|
||||||
|
if (pointsInside > 4) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("drawStateLabels");
|
||||||
|
}
|
||||||
|
|
@ -276,7 +276,7 @@ window.Submap = (function () {
|
||||||
|
|
||||||
drawStates();
|
drawStates();
|
||||||
drawBorders();
|
drawBorders();
|
||||||
BurgsAndStates.drawStateLabels();
|
drawStateLabels();
|
||||||
|
|
||||||
Rivers.specify();
|
Rivers.specify();
|
||||||
Lakes.generateName();
|
Lakes.generateName();
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ function editHeightmap(options) {
|
||||||
|
|
||||||
drawStates();
|
drawStates();
|
||||||
drawBorders();
|
drawBorders();
|
||||||
BurgsAndStates.drawStateLabels();
|
drawStateLabels();
|
||||||
|
|
||||||
Rivers.specify();
|
Rivers.specify();
|
||||||
Lakes.generateName();
|
Lakes.generateName();
|
||||||
|
|
@ -442,7 +442,7 @@ function editHeightmap(options) {
|
||||||
c.center = findCell(c.x, c.y);
|
c.center = findCell(c.x, c.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
BurgsAndStates.drawStateLabels();
|
drawStateLabels();
|
||||||
drawStates();
|
drawStates();
|
||||||
drawBorders();
|
drawBorders();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,9 @@ function editProvinces() {
|
||||||
const rural = p.rural * populationRate;
|
const rural = p.rural * populationRate;
|
||||||
const urban = p.urban * populationRate * urbanization;
|
const urban = p.urban * populationRate * urbanization;
|
||||||
const population = rn(rural + urban);
|
const population = rn(rural + urban);
|
||||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
const populationTip = `Total population: ${si(population)}; Rural population: ${si(
|
||||||
|
rural
|
||||||
|
)}; Urban population: ${si(urban)}`;
|
||||||
totalPopulation += population;
|
totalPopulation += population;
|
||||||
|
|
||||||
const stateName = pack.states[p.state].name;
|
const stateName = pack.states[p.state].name;
|
||||||
|
|
@ -145,9 +147,15 @@ function editProvinces() {
|
||||||
>
|
>
|
||||||
<fill-box fill="${p.color}"></fill-box>
|
<fill-box fill="${p.color}"></fill-box>
|
||||||
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly />
|
<input data-tip="Province name. Click to change" class="name pointer" value="${p.name}" readonly />
|
||||||
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${p.i}"></use></svg>
|
<svg data-tip="Click to show and edit province emblem" class="coaIcon pointer hide" viewBox="0 0 200 200"><use href="#provinceCOA${
|
||||||
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${p.formName}" readonly />
|
p.i
|
||||||
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${p.burg ? "" : "placeholder"}"></span>
|
}"></use></svg>
|
||||||
|
<input data-tip="Province form name. Click to change" class="name pointer hide" value="${
|
||||||
|
p.formName
|
||||||
|
}" readonly />
|
||||||
|
<span data-tip="Province capital. Click to zoom into view" class="icon-star-empty pointer hide ${
|
||||||
|
p.burg ? "" : "placeholder"
|
||||||
|
}"></span>
|
||||||
<select
|
<select
|
||||||
data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital"
|
data-tip="Province capital. Click to select from burgs within the state. No capital means the province is governed from the state capital"
|
||||||
class="cultureBase hide ${p.burgs.length ? "" : "placeholder"}"
|
class="cultureBase hide ${p.burgs.length ? "" : "placeholder"}"
|
||||||
|
|
@ -164,7 +172,7 @@ function editProvinces() {
|
||||||
class="icon-flag-empty ${separable ? "" : "placeholder"} hide"
|
class="icon-flag-empty ${separable ? "" : "placeholder"} hide"
|
||||||
></span>
|
></span>
|
||||||
<span data-tip="Toggle province focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
|
<span data-tip="Toggle province focus" class="icon-pin ${focused ? "" : " inactive"} hide"></span>
|
||||||
<span data-tip="Lock the province" class="icon-lock${p.lock ? '' : '-open'} hide"></span>
|
<span data-tip="Lock the province" class="icon-lock${p.lock ? "" : "-open"} hide"></span>
|
||||||
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
|
<span data-tip="Remove the province" class="icon-trash-empty hide"></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -193,7 +201,9 @@ function editProvinces() {
|
||||||
|
|
||||||
function getCapitalOptions(burgs, capital) {
|
function getCapitalOptions(burgs, capital) {
|
||||||
let options = "";
|
let options = "";
|
||||||
burgs.forEach(b => (options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`));
|
burgs.forEach(
|
||||||
|
b => (options += `<option ${b === capital ? "selected" : ""} value="${b}">${pack.burgs[b].name}</option>`)
|
||||||
|
);
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -267,7 +277,11 @@ function editProvinces() {
|
||||||
const {name, burg: burgId, burgs: provinceBurgs} = province;
|
const {name, burg: burgId, burgs: provinceBurgs} = province;
|
||||||
|
|
||||||
if (provinceBurgs.some(b => burgs[b].capital))
|
if (provinceBurgs.some(b => burgs[b].capital))
|
||||||
return tip("Cannot declare independence of a province having capital burg. Please change capital first", false, "error");
|
return tip(
|
||||||
|
"Cannot declare independence of a province having capital burg. Please change capital first",
|
||||||
|
false,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error");
|
if (!burgId) return tip("Cannot declare independence of a province without burg", false, "error");
|
||||||
|
|
||||||
const oldStateId = province.state;
|
const oldStateId = province.state;
|
||||||
|
|
@ -313,7 +327,10 @@ function editProvinces() {
|
||||||
return relations;
|
return relations;
|
||||||
});
|
});
|
||||||
diplomacy.push("x");
|
diplomacy.push("x");
|
||||||
states[0].diplomacy.push([`Independance declaration`, `${name} declared its independance from ${states[oldStateId].name}`]);
|
states[0].diplomacy.push([
|
||||||
|
`Independance declaration`,
|
||||||
|
`${name} declared its independance from ${states[oldStateId].name}`
|
||||||
|
]);
|
||||||
|
|
||||||
// create new state
|
// create new state
|
||||||
states.push({
|
states.push({
|
||||||
|
|
@ -348,7 +365,7 @@ function editProvinces() {
|
||||||
|
|
||||||
BurgsAndStates.collectStatistics();
|
BurgsAndStates.collectStatistics();
|
||||||
BurgsAndStates.defineStateForms(newStates);
|
BurgsAndStates.defineStateForms(newStates);
|
||||||
BurgsAndStates.drawStateLabels(allStates);
|
drawStateLabels(allStates);
|
||||||
|
|
||||||
// redraw emblems
|
// redraw emblems
|
||||||
allStates.forEach(stateId => {
|
allStates.forEach(stateId => {
|
||||||
|
|
@ -375,8 +392,12 @@ function editProvinces() {
|
||||||
const l = n => Number(n).toLocaleString();
|
const l = n => Number(n).toLocaleString();
|
||||||
|
|
||||||
alertMessage.innerHTML = /* html */ ` Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
|
alertMessage.innerHTML = /* html */ ` Rural: <input type="number" min="0" step="1" id="ruralPop" value=${rural} style="width:6em" /> Urban:
|
||||||
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${p.burgs.length ? "" : "disabled"} />
|
<input type="number" min="0" step="1" id="urbanPop" value=${urban} style="width:6em" ${
|
||||||
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(total)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
p.burgs.length ? "" : "disabled"
|
||||||
|
} />
|
||||||
|
<p>Total population: ${l(total)} ⇒ <span id="totalPop">${l(
|
||||||
|
total
|
||||||
|
)}</span> (<span id="totalPopPerc">100</span>%)</p>`;
|
||||||
|
|
||||||
const update = function () {
|
const update = function () {
|
||||||
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
const totalNew = ruralPop.valueAsNumber + urbanPop.valueAsNumber;
|
||||||
|
|
@ -694,7 +715,13 @@ function editProvinces() {
|
||||||
|
|
||||||
function updateChart() {
|
function updateChart() {
|
||||||
const value =
|
const value =
|
||||||
this.value === "area" ? d => d.area : this.value === "rural" ? d => d.rural : this.value === "urban" ? d => d.urban : d => d.rural + d.urban;
|
this.value === "area"
|
||||||
|
? d => d.area
|
||||||
|
: this.value === "rural"
|
||||||
|
? d => d.rural
|
||||||
|
: this.value === "urban"
|
||||||
|
? d => d.urban
|
||||||
|
: d => d.rural + d.urban;
|
||||||
|
|
||||||
root.sum(value);
|
root.sum(value);
|
||||||
node.data(treeLayout(root).leaves());
|
node.data(treeLayout(root).leaves());
|
||||||
|
|
@ -776,7 +803,13 @@ function editProvinces() {
|
||||||
|
|
||||||
customization = 11;
|
customization = 11;
|
||||||
provs.select("g#provincesBody").append("g").attr("id", "temp");
|
provs.select("g#provincesBody").append("g").attr("id", "temp");
|
||||||
provs.select("g#provincesBody").append("g").attr("id", "centers").attr("fill", "none").attr("stroke", "#ff0000").attr("stroke-width", 1);
|
provs
|
||||||
|
.select("g#provincesBody")
|
||||||
|
.append("g")
|
||||||
|
.attr("id", "centers")
|
||||||
|
.attr("fill", "none")
|
||||||
|
.attr("stroke", "#ff0000")
|
||||||
|
.attr("stroke-width", 1);
|
||||||
|
|
||||||
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
|
document.querySelectorAll("#provincesBottom > *").forEach(el => (el.style.display = "none"));
|
||||||
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
|
document.getElementById("provincesManuallyButtons").style.display = "inline-block";
|
||||||
|
|
@ -788,7 +821,11 @@ function editProvinces() {
|
||||||
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||||
|
|
||||||
tip("Click on a province to select, drag the circle to change province", true);
|
tip("Click on a province to select, drag the circle to change province", true);
|
||||||
viewbox.style("cursor", "crosshair").on("click", selectProvinceOnMapClick).call(d3.drag().on("start", dragBrush)).on("touchmove mousemove", moveBrush);
|
viewbox
|
||||||
|
.style("cursor", "crosshair")
|
||||||
|
.on("click", selectProvinceOnMapClick)
|
||||||
|
.call(d3.drag().on("start", dragBrush))
|
||||||
|
.on("touchmove mousemove", moveBrush);
|
||||||
|
|
||||||
body.querySelector("div").classList.add("selected");
|
body.querySelector("div").classList.add("selected");
|
||||||
selectProvince(+body.querySelector("div").dataset.id);
|
selectProvince(+body.querySelector("div").dataset.id);
|
||||||
|
|
@ -859,7 +896,11 @@ function editProvinces() {
|
||||||
if (i === pack.provinces[provinceOld].center) {
|
if (i === pack.provinces[provinceOld].center) {
|
||||||
const center = centers.select("polygon[data-center='" + i + "']");
|
const center = centers.select("polygon[data-center='" + i + "']");
|
||||||
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i));
|
if (!center.size()) centers.append("polygon").attr("data-center", i).attr("points", getPackPolygon(i));
|
||||||
tip("Province center cannot be assigned to a different region. Please remove the province first", false, "error");
|
tip(
|
||||||
|
"Province center cannot be assigned to a different region. Please remove the province first",
|
||||||
|
false,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -921,7 +962,8 @@ function editProvinces() {
|
||||||
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
|
provincesHeader.querySelector("div[data-sortby='state']").style.left = "22em";
|
||||||
provincesFooter.style.display = "block";
|
provincesFooter.style.display = "block";
|
||||||
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all"));
|
||||||
if (!close) $("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
if (!close)
|
||||||
|
$("#provincesEditor").dialog({position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}});
|
||||||
|
|
||||||
restoreDefaultEvents();
|
restoreDefaultEvents();
|
||||||
clearMainTip();
|
clearMainTip();
|
||||||
|
|
@ -943,14 +985,20 @@ function editProvinces() {
|
||||||
const {cells, provinces} = pack;
|
const {cells, provinces} = pack;
|
||||||
const point = d3.mouse(this);
|
const point = d3.mouse(this);
|
||||||
const center = findCell(point[0], point[1]);
|
const center = findCell(point[0], point[1]);
|
||||||
if (cells.h[center] < 20) return tip("You cannot place province into the water. Please click on a land cell", false, "error");
|
if (cells.h[center] < 20)
|
||||||
|
return tip("You cannot place province into the water. Please click on a land cell", false, "error");
|
||||||
|
|
||||||
const oldProvince = cells.province[center];
|
const oldProvince = cells.province[center];
|
||||||
if (oldProvince && provinces[oldProvince].center === center)
|
if (oldProvince && provinces[oldProvince].center === center)
|
||||||
return tip("The cell is already a center of a different province. Select other cell", false, "error");
|
return tip("The cell is already a center of a different province. Select other cell", false, "error");
|
||||||
|
|
||||||
const state = cells.state[center];
|
const state = cells.state[center];
|
||||||
if (!state) return tip("You cannot create a province in neutral lands. Please assign this land to a state first", false, "error");
|
if (!state)
|
||||||
|
return tip(
|
||||||
|
"You cannot create a province in neutral lands. Please assign this land to a state first",
|
||||||
|
false,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
|
||||||
if (d3.event.shiftKey === false) exitAddProvinceMode();
|
if (d3.event.shiftKey === false) exitAddProvinceMode();
|
||||||
|
|
||||||
|
|
@ -1016,7 +1064,10 @@ function editProvinces() {
|
||||||
|
|
||||||
function downloadProvincesData() {
|
function downloadProvincesData() {
|
||||||
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
const unit = areaUnit.value === "square" ? distanceUnitInput.value + "2" : areaUnit.value;
|
||||||
let data = "Id,Province,Full Name,Form,State,Color,Capital,Area " + unit + ",Total Population,Rural Population,Urban Population\n"; // headers
|
let data =
|
||||||
|
"Id,Province,Full Name,Form,State,Color,Capital,Area " +
|
||||||
|
unit +
|
||||||
|
",Total Population,Rural Population,Urban Population\n"; // headers
|
||||||
|
|
||||||
body.querySelectorAll(":scope > div").forEach(function (el) {
|
body.querySelectorAll(":scope > div").forEach(function (el) {
|
||||||
const key = parseInt(el.dataset.id);
|
const key = parseInt(el.dataset.id);
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ toolsContent.addEventListener("click", function (event) {
|
||||||
});
|
});
|
||||||
|
|
||||||
function processFeatureRegeneration(event, button) {
|
function processFeatureRegeneration(event, button) {
|
||||||
if (button === "regenerateStateLabels") BurgsAndStates.drawStateLabels();
|
if (button === "regenerateStateLabels") drawStateLabels();
|
||||||
else if (button === "regenerateReliefIcons") {
|
else if (button === "regenerateReliefIcons") {
|
||||||
ReliefIcons();
|
ReliefIcons();
|
||||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||||
|
|
@ -154,7 +154,7 @@ function regenerateStates() {
|
||||||
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
layerIsOn("toggleBorders") ? drawBorders() : toggleBorders();
|
||||||
if (layerIsOn("toggleProvinces")) drawProvinces();
|
if (layerIsOn("toggleProvinces")) drawProvinces();
|
||||||
|
|
||||||
BurgsAndStates.drawStateLabels();
|
drawStateLabels();
|
||||||
Military.generate();
|
Military.generate();
|
||||||
if (layerIsOn("toggleEmblems")) drawEmblems();
|
if (layerIsOn("toggleEmblems")) drawEmblems();
|
||||||
|
|
||||||
|
|
@ -836,7 +836,7 @@ function addMarkerOnClick() {
|
||||||
const marker = Markers.add({...baseMarker, x, y, cell});
|
const marker = Markers.add({...baseMarker, x, y, cell});
|
||||||
|
|
||||||
if (selectedConfig && selectedConfig.add) {
|
if (selectedConfig && selectedConfig.add) {
|
||||||
selectedConfig.add("marker"+marker.i, cell);
|
selectedConfig.add("marker" + marker.i, cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
const markersElement = document.getElementById("markers");
|
const markersElement = document.getElementById("markers");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue