diff --git a/public/modules/provinces-generator.js b/public/modules/provinces-generator.js
deleted file mode 100644
index 3276fdf0..00000000
--- a/public/modules/provinces-generator.js
+++ /dev/null
@@ -1,257 +0,0 @@
-"use strict";
-
-window.Provinces = (function () {
- const forms = {
- Monarchy: {County: 22, Earldom: 6, Shire: 2, Landgrave: 2, Margrave: 2, Barony: 2, Captaincy: 1, Seneschalty: 1},
- Republic: {Province: 6, Department: 2, Governorate: 2, District: 1, Canton: 1, Prefecture: 1},
- Theocracy: {Parish: 3, Deanery: 1},
- Union: {Province: 1, State: 1, Canton: 1, Republic: 1, County: 1, Council: 1},
- Anarchy: {Council: 1, Commune: 1, Community: 1, Tribe: 1},
- Wild: {Territory: 10, Land: 5, Region: 2, Tribe: 1, Clan: 1, Dependency: 1, Area: 1}
- };
-
- const generate = (regenerate = false, regenerateLockedStates = false) => {
- TIME && console.time("generateProvinces");
- const localSeed = regenerate ? generateSeed() : seed;
- Math.random = aleaPRNG(localSeed);
-
- const {cells, states, burgs} = pack;
- const provinces = [0]; // 0 index is reserved for "no province"
- const provinceIds = new Uint16Array(cells.i.length);
-
- const isProvinceLocked = province => province.lock || (!regenerateLockedStates && states[province.state]?.lock);
- const isProvinceCellLocked = cell => provinceIds[cell] && isProvinceLocked(provinces[provinceIds[cell]]);
-
- if (regenerate) {
- pack.provinces.forEach(province => {
- if (!province.i || province.removed || !isProvinceLocked(province)) return;
-
- const newId = provinces.length;
- for (const i of cells.i) {
- if (cells.province[i] === province.i) provinceIds[i] = newId;
- }
-
- province.i = newId;
- provinces.push(province);
- });
- }
-
- const provincesRatio = +byId("provincesRatio").value;
- const max = provincesRatio == 100 ? 1000 : gauss(20, 5, 5, 100) * provincesRatio ** 0.5; // max growth
-
- // generate provinces for selected burgs
- states.forEach(s => {
- s.provinces = [];
- if (!s.i || s.removed) return;
- if (provinces.length) s.provinces = provinces.filter(p => p.state === s.i).map(p => p.i); // locked provinces ids
- if (s.lock && !regenerateLockedStates) return; // don't regenerate provinces of a locked state
-
- const stateBurgs = burgs
- .filter(b => b.state === s.i && !b.removed && !provinceIds[b.cell])
- .sort((a, b) => b.population * gauss(1, 0.2, 0.5, 1.5, 3) - a.population)
- .sort((a, b) => b.capital - a.capital);
- if (stateBurgs.length < 2) return; // at least 2 provinces are required
-
- const provincesNumber = Math.max(Math.ceil((stateBurgs.length * provincesRatio) / 100), 2);
- const form = Object.assign({}, forms[s.form]);
-
- for (let i = 0; i < provincesNumber; i++) {
- const provinceId = provinces.length;
- const center = stateBurgs[i].cell;
- const burg = stateBurgs[i];
- const c = stateBurgs[i].culture;
- const nameByBurg = P(0.5);
- const name = nameByBurg ? stateBurgs[i].name : Names.getState(Names.getCultureShort(c), c);
- const formName = rw(form);
- form[formName] += 10;
- const fullName = name + " " + formName;
- const color = getMixedColor(s.color);
- const kinship = nameByBurg ? 0.8 : 0.4;
- const type = Burgs.getType(center, burg.port);
- const coa = COA.generate(stateBurgs[i].coa, kinship, null, type);
- coa.shield = COA.getShield(c, s.i);
-
- s.provinces.push(provinceId);
- provinces.push({i: provinceId, state: s.i, center, burg: burg.i, name, formName, fullName, color, coa});
- }
- });
-
- // expand generated provinces
- const queue = new FlatQueue();
- const cost = [];
-
- provinces.forEach(p => {
- if (!p.i || p.removed || isProvinceLocked(p)) return;
- provinceIds[p.center] = p.i;
- queue.push({e: p.center, province: p.i, state: p.state, p: 0}, 0);
- cost[p.center] = 1;
- });
-
- while (queue.length) {
- const {e, p, province, state} = queue.pop();
-
- cells.c[e].forEach(e => {
- if (isProvinceCellLocked(e)) return; // do not overwrite cell of locked provinces
-
- const land = cells.h[e] >= 20;
- if (!land && !cells.t[e]) return; // cannot pass deep ocean
- if (land && cells.state[e] !== state) return;
- const evevation = cells.h[e] >= 70 ? 100 : cells.h[e] >= 50 ? 30 : cells.h[e] >= 20 ? 10 : 100;
- const totalCost = p + evevation;
-
- if (totalCost > max) return;
- if (!cost[e] || totalCost < cost[e]) {
- if (land) provinceIds[e] = province; // assign province to a cell
- cost[e] = totalCost;
- queue.push({e, province, state, p: totalCost}, totalCost);
- }
- });
- }
-
- // justify provinces shapes a bit
- for (const i of cells.i) {
- if (cells.burg[i]) continue; // do not overwrite burgs
- if (isProvinceCellLocked(i)) continue; // do not overwrite cell of locked provinces
-
- const neibs = cells.c[i]
- .filter(c => cells.state[c] === cells.state[i] && !isProvinceCellLocked(c))
- .map(c => provinceIds[c]);
- const adversaries = neibs.filter(c => c !== provinceIds[i]);
- if (adversaries.length < 2) continue;
-
- const buddies = neibs.filter(c => c === provinceIds[i]).length;
- if (buddies.length > 2) continue;
-
- const competitors = adversaries.map(p => adversaries.reduce((s, v) => (v === p ? s + 1 : s), 0));
- const max = d3.max(competitors);
- if (buddies >= max) continue;
-
- provinceIds[i] = adversaries[competitors.indexOf(max)];
- }
-
- // add "wild" provinces if some cells don't have a province assigned
- const noProvince = Array.from(cells.i).filter(i => cells.state[i] && !provinceIds[i]); // cells without province assigned
- states.forEach(s => {
- if (!s.i || s.removed) return;
- if (s.lock && !regenerateLockedStates) return;
- if (!s.provinces.length) return;
-
- const coreProvinceNames = s.provinces.map(p => provinces[p]?.name);
- const colonyNamePool = [s.name, ...coreProvinceNames].filter(name => name && !/new/i.test(name));
- const getColonyName = () => {
- if (colonyNamePool.length < 1) return null;
-
- const index = rand(colonyNamePool.length - 1);
- const spliced = colonyNamePool.splice(index, 1);
- return spliced[0] ? `New ${spliced[0]}` : null;
- };
-
- let stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
- while (stateNoProvince.length) {
- // add new province
- const provinceId = provinces.length;
- const burgCell = stateNoProvince.find(i => cells.burg[i]);
- const center = burgCell ? burgCell : stateNoProvince[0];
- const burg = burgCell ? cells.burg[burgCell] : 0;
- provinceIds[center] = provinceId;
-
- // expand province
- const cost = [];
- cost[center] = 1;
- queue.push({e: center, p: 0}, 0);
- while (queue.length) {
- const {e, p} = queue.pop();
-
- cells.c[e].forEach(nextCellId => {
- if (provinceIds[nextCellId]) return;
- const land = cells.h[nextCellId] >= 20;
- if (cells.state[nextCellId] && cells.state[nextCellId] !== s.i) return;
- const ter = land ? (cells.state[nextCellId] === s.i ? 3 : 20) : cells.t[nextCellId] ? 10 : 30;
- const totalCost = p + ter;
-
- if (totalCost > max) return;
- if (!cost[nextCellId] || totalCost < cost[nextCellId]) {
- if (land && cells.state[nextCellId] === s.i) provinceIds[nextCellId] = provinceId; // assign province to a cell
- cost[nextCellId] = totalCost;
- queue.push({e: nextCellId, p: totalCost}, totalCost);
- }
- });
- }
-
- // generate "wild" province name
- const c = cells.culture[center];
- const f = pack.features[cells.f[center]];
- const color = getMixedColor(s.color);
-
- const provCells = stateNoProvince.filter(i => provinceIds[i] === provinceId);
- const singleIsle = provCells.length === f.cells && !provCells.find(i => cells.f[i] !== f.i);
- const isleGroup = !singleIsle && !provCells.find(i => pack.features[cells.f[i]].group !== "isle");
- const colony = !singleIsle && !isleGroup && P(0.5) && !isPassable(s.center, center);
-
- const name = (() => {
- const colonyName = colony && P(0.8) && getColonyName();
- if (colonyName) return colonyName;
- if (burgCell && P(0.5)) return burgs[burg].name;
- return Names.getState(Names.getCultureShort(c), c);
- })();
-
- const formName = (() => {
- if (singleIsle) return "Island";
- if (isleGroup) return "Islands";
- if (colony) return "Colony";
- return rw(forms["Wild"]);
- })();
-
- const fullName = name + " " + formName;
-
- const dominion = colony ? P(0.95) : singleIsle || isleGroup ? P(0.7) : P(0.3);
- const kinship = dominion ? 0 : 0.4;
- const type = Burgs.getType(center, burgs[burg]?.port);
- const coa = COA.generate(s.coa, kinship, dominion, type);
- coa.shield = COA.getShield(c, s.i);
-
- provinces.push({i: provinceId, state: s.i, center, burg, name, formName, fullName, color, coa});
- s.provinces.push(provinceId);
-
- // check if there is a land way within the same state between two cells
- function isPassable(from, to) {
- if (cells.f[from] !== cells.f[to]) return false; // on different islands
- const passableQueue = [from],
- used = new Uint8Array(cells.i.length),
- state = cells.state[from];
- while (passableQueue.length) {
- const current = passableQueue.pop();
- if (current === to) return true; // way is found
- cells.c[current].forEach(c => {
- if (used[c] || cells.h[c] < 20 || cells.state[c] !== state) return;
- passableQueue.push(c);
- used[c] = 1;
- });
- }
- return false; // way is not found
- }
-
- // re-check
- stateNoProvince = noProvince.filter(i => cells.state[i] === s.i && !provinceIds[i]);
- }
- });
-
- cells.province = provinceIds;
- pack.provinces = provinces;
-
- TIME && console.timeEnd("generateProvinces");
- };
-
- // calculate pole of inaccessibility for each province
- const getPoles = () => {
- const getType = cellId => pack.cells.province[cellId];
- const poles = getPolesOfInaccessibility(pack, getType);
-
- pack.provinces.forEach(province => {
- if (!province.i || province.removed) return;
- province.pole = poles[province.i] || [0, 0];
- });
- };
-
- return {generate, getPoles};
-})();
diff --git a/public/modules/renderers/draw-borders.js b/public/modules/renderers/draw-borders.js
deleted file mode 100644
index f0f3006e..00000000
--- a/public/modules/renderers/draw-borders.js
+++ /dev/null
@@ -1,120 +0,0 @@
-"use strict";
-
-function drawBorders() {
- TIME && console.time("drawBorders");
- const {cells, vertices} = pack;
-
- const statePath = [];
- const provincePath = [];
- const checked = {};
-
- const isLand = cellId => cells.h[cellId] >= 20;
-
- for (let cellId = 0; cellId < cells.i.length; cellId++) {
- if (!cells.state[cellId]) continue;
- const provinceId = cells.province[cellId];
- const stateId = cells.state[cellId];
-
- // bordering cell of another province
- if (provinceId) {
- const provToCell = cells.c[cellId].find(neibId => {
- const neibProvinceId = cells.province[neibId];
- return (
- neibProvinceId &&
- provinceId > neibProvinceId &&
- !checked[`prov-${provinceId}-${neibProvinceId}-${cellId}`] &&
- cells.state[neibId] === stateId
- );
- });
-
- if (provToCell !== undefined) {
- const addToChecked = cellId => (checked[`prov-${provinceId}-${cells.province[provToCell]}-${cellId}`] = true);
- const border = getBorder({type: "province", fromCell: cellId, toCell: provToCell, addToChecked});
-
- if (border) {
- provincePath.push(border);
- cellId--; // check the same cell again
- continue;
- }
- }
- }
-
- // if cell is on state border
- const stateToCell = cells.c[cellId].find(neibId => {
- const neibStateId = cells.state[neibId];
- return isLand(neibId) && stateId > neibStateId && !checked[`state-${stateId}-${neibStateId}-${cellId}`];
- });
-
- if (stateToCell !== undefined) {
- const addToChecked = cellId => (checked[`state-${stateId}-${cells.state[stateToCell]}-${cellId}`] = true);
- const border = getBorder({type: "state", fromCell: cellId, toCell: stateToCell, addToChecked});
-
- if (border) {
- statePath.push(border);
- cellId--; // check the same cell again
- continue;
- }
- }
- }
-
- svg.select("#borders").selectAll("path").remove();
- svg.select("#stateBorders").append("path").attr("d", statePath.join(" "));
- svg.select("#provinceBorders").append("path").attr("d", provincePath.join(" "));
-
- function getBorder({type, fromCell, toCell, addToChecked}) {
- const getType = cellId => cells[type][cellId];
- const isTypeFrom = cellId => cellId < cells.i.length && getType(cellId) === getType(fromCell);
- const isTypeTo = cellId => cellId < cells.i.length && getType(cellId) === getType(toCell);
-
- addToChecked(fromCell);
- const startingVertex = cells.v[fromCell].find(v => vertices.c[v].some(i => isLand(i) && isTypeTo(i)));
- if (startingVertex === undefined) return null;
-
- const checkVertex = vertex =>
- vertices.c[vertex].some(isTypeFrom) && vertices.c[vertex].some(c => isLand(c) && isTypeTo(c));
- const chain = getVerticesLine({vertices, startingVertex, checkCell: isTypeFrom, checkVertex, addToChecked});
- if (chain.length > 1) return "M" + chain.map(cellId => vertices.p[cellId]).join(" ");
-
- return null;
- }
-
- // connect vertices to chain to form a border
- function getVerticesLine({vertices, startingVertex, checkCell, checkVertex, addToChecked}) {
- let chain = []; // vertices chain to form a path
- let next = startingVertex;
- const MAX_ITERATIONS = vertices.c.length;
-
- for (let run = 0; run < 2; run++) {
- // first run: from any vertex to a border edge
- // second run: from found border edge to another edge
- chain = [];
-
- for (let i = 0; i < MAX_ITERATIONS; i++) {
- const previous = chain.at(-1);
- const current = next;
- chain.push(current);
-
- const neibCells = vertices.c[current];
- neibCells.map(addToChecked);
-
- const [c1, c2, c3] = neibCells.map(checkCell);
- const [v1, v2, v3] = vertices.v[current].map(checkVertex);
- const [vertex1, vertex2, vertex3] = vertices.v[current];
-
- if (v1 && vertex1 !== previous && c1 !== c2) next = vertex1;
- else if (v2 && vertex2 !== previous && c2 !== c3) next = vertex2;
- else if (v3 && vertex3 !== previous && c1 !== c3) next = vertex3;
-
- if (next === current || next === startingVertex) {
- if (next === startingVertex) chain.push(startingVertex);
- startingVertex = next;
- break;
- }
- }
- }
-
- return chain;
- }
-
- TIME && console.timeEnd("drawBorders");
-}
diff --git a/public/modules/renderers/draw-burg-icons.js b/public/modules/renderers/draw-burg-icons.js
deleted file mode 100644
index 66d2dfcb..00000000
--- a/public/modules/renderers/draw-burg-icons.js
+++ /dev/null
@@ -1,108 +0,0 @@
-"use strict";
-
-function drawBurgIcons() {
- TIME && console.time("drawBurgIcons");
- createIconGroups();
-
- for (const {name} of options.burgs.groups) {
- const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
- if (!burgsInGroup.length) continue;
-
- const iconsGroup = document.querySelector("#burgIcons > g#" + name);
- if (!iconsGroup) continue;
-
- const icon = iconsGroup.dataset.icon || "#icon-circle";
- iconsGroup.innerHTML = burgsInGroup
- .map(b => ``)
- .join("");
-
- const portsInGroup = burgsInGroup.filter(b => b.port);
- if (!portsInGroup.length) continue;
-
- const portGroup = document.querySelector("#anchors > g#" + name);
- if (!portGroup) continue;
-
- portGroup.innerHTML = portsInGroup
- .map(b => ``)
- .join("");
- }
-
- TIME && console.timeEnd("drawBurgIcons");
-}
-
-function drawBurgIcon(burg) {
- const iconGroup = burgIcons.select("#" + burg.group);
- if (iconGroup.empty()) {
- drawBurgIcons();
- return; // redraw all icons if group is missing
- }
-
- removeBurgIcon(burg.i);
- const icon = iconGroup.attr("data-icon") || "#icon-circle";
- burgIcons
- .select("#" + burg.group)
- .append("use")
- .attr("href", icon)
- .attr("id", "burg" + burg.i)
- .attr("data-id", burg.i)
- .attr("x", burg.x)
- .attr("y", burg.y);
-
- if (burg.port) {
- anchors
- .select("#" + burg.group)
- .append("use")
- .attr("href", "#icon-anchor")
- .attr("id", "anchor" + burg.i)
- .attr("data-id", burg.i)
- .attr("x", burg.x)
- .attr("y", burg.y);
- }
-}
-
-function removeBurgIcon(burgId) {
- const existingIcon = document.getElementById("burg" + burgId);
- if (existingIcon) existingIcon.remove();
-
- const existingAnchor = document.getElementById("anchor" + burgId);
- if (existingAnchor) existingAnchor.remove();
-}
-
-function createIconGroups() {
- // save existing styles and remove all groups
- document.querySelectorAll("g#burgIcons > g").forEach(group => {
- style.burgIcons[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
- acc[attribute.name] = attribute.value;
- return acc;
- }, {});
- group.remove();
- });
-
- document.querySelectorAll("g#anchors > g").forEach(group => {
- style.anchors[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
- acc[attribute.name] = attribute.value;
- return acc;
- }, {});
- group.remove();
- });
-
- // create groups for each burg group and apply stored or default style
- const defaultIconStyle = style.burgIcons.town || Object.values(style.burgIcons)[0] || {};
- const defaultAnchorStyle = style.anchors.town || Object.values(style.anchors)[0] || {};
- const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order);
- for (const {name} of sortedGroups) {
- const burgGroup = burgIcons.append("g");
- const iconStyles = style.burgIcons[name] || defaultIconStyle;
- Object.entries(iconStyles).forEach(([key, value]) => {
- burgGroup.attr(key, value);
- });
- burgGroup.attr("id", name);
-
- const anchorGroup = anchors.append("g");
- const anchorStyles = style.anchors[name] || defaultAnchorStyle;
- Object.entries(anchorStyles).forEach(([key, value]) => {
- anchorGroup.attr(key, value);
- });
- anchorGroup.attr("id", name);
- }
-}
diff --git a/public/modules/renderers/draw-burg-labels.js b/public/modules/renderers/draw-burg-labels.js
deleted file mode 100644
index c8a43bbb..00000000
--- a/public/modules/renderers/draw-burg-labels.js
+++ /dev/null
@@ -1,84 +0,0 @@
-"use strict";
-
-function drawBurgLabels() {
- TIME && console.time("drawBurgLabels");
- createLabelGroups();
-
- for (const {name} of options.burgs.groups) {
- const burgsInGroup = pack.burgs.filter(b => b.group === name && !b.removed);
- if (!burgsInGroup.length) continue;
-
- const labelGroup = burgLabels.select("#" + name);
- if (labelGroup.empty()) continue;
-
- const dx = labelGroup.attr("data-dx") || 0;
- const dy = labelGroup.attr("data-dy") || 0;
-
- labelGroup
- .selectAll("text")
- .data(burgsInGroup)
- .enter()
- .append("text")
- .attr("text-rendering", "optimizeSpeed")
- .attr("id", d => "burgLabel" + d.i)
- .attr("data-id", d => d.i)
- .attr("x", d => d.x)
- .attr("y", d => d.y)
- .attr("dx", dx + "em")
- .attr("dy", dy + "em")
- .text(d => d.name);
- }
-
- TIME && console.timeEnd("drawBurgLabels");
-}
-
-function drawBurgLabel(burg) {
- const labelGroup = burgLabels.select("#" + burg.group);
- if (labelGroup.empty()) {
- drawBurgLabels();
- return; // redraw all labels if group is missing
- }
-
- const dx = labelGroup.attr("data-dx") || 0;
- const dy = labelGroup.attr("data-dy") || 0;
-
- removeBurgLabel(burg.i);
- labelGroup
- .append("text")
- .attr("text-rendering", "optimizeSpeed")
- .attr("id", "burgLabel" + burg.i)
- .attr("data-id", burg.i)
- .attr("x", burg.x)
- .attr("y", burg.y)
- .attr("dx", dx + "em")
- .attr("dy", dy + "em")
- .text(burg.name);
-}
-
-function removeBurgLabel(burgId) {
- const existingLabel = document.getElementById("burgLabel" + burgId);
- if (existingLabel) existingLabel.remove();
-}
-
-function createLabelGroups() {
- // save existing styles and remove all groups
- document.querySelectorAll("g#burgLabels > g").forEach(group => {
- style.burgLabels[group.id] = Array.from(group.attributes).reduce((acc, attribute) => {
- acc[attribute.name] = attribute.value;
- return acc;
- }, {});
- group.remove();
- });
-
- // create groups for each burg group and apply stored or default style
- const defaultStyle = style.burgLabels.town || Object.values(style.burgLabels)[0] || {};
- const sortedGroups = [...options.burgs.groups].sort((a, b) => a.order - b.order);
- for (const {name} of sortedGroups) {
- const group = burgLabels.append("g");
- const styles = style.burgLabels[name] || defaultStyle;
- Object.entries(styles).forEach(([key, value]) => {
- group.attr(key, value);
- });
- group.attr("id", name);
- }
-}
diff --git a/public/modules/renderers/draw-emblems.js b/public/modules/renderers/draw-emblems.js
deleted file mode 100644
index 13781239..00000000
--- a/public/modules/renderers/draw-emblems.js
+++ /dev/null
@@ -1,129 +0,0 @@
-"use strict";
-
-function drawEmblems() {
- TIME && console.time("drawEmblems");
- const {states, provinces, burgs} = pack;
-
- const validStates = states.filter(s => s.i && !s.removed && s.coa && s.coa.size !== 0);
- const validProvinces = provinces.filter(p => p.i && !p.removed && p.coa && p.coa.size !== 0);
- const validBurgs = burgs.filter(b => b.i && !b.removed && b.coa && b.coa.size !== 0);
-
- const getStateEmblemsSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 40, 10, 100);
- const statesMod = 1 + validStates.length / 100 - (15 - validStates.length) / 200; // states number modifier
- const sizeMod = +emblems.select("#stateEmblems").attr("data-size") || 1;
- return rn((startSize / statesMod) * sizeMod); // target size ~50px on 1536x754 map with 15 states
- };
-
- const getProvinceEmblemsSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 100, 5, 70);
- const provincesMod = 1 + validProvinces.length / 1000 - (115 - validProvinces.length) / 1000; // states number modifier
- const sizeMod = +emblems.select("#provinceEmblems").attr("data-size") || 1;
- return rn((startSize / provincesMod) * sizeMod); // target size ~20px on 1536x754 map with 115 provinces
- };
-
- const getBurgEmblemSize = () => {
- const startSize = minmax((graphHeight + graphWidth) / 185, 2, 50);
- const burgsMod = 1 + validBurgs.length / 1000 - (450 - validBurgs.length) / 1000; // states number modifier
- const sizeMod = +emblems.select("#burgEmblems").attr("data-size") || 1;
- return rn((startSize / burgsMod) * sizeMod); // target size ~8.5px on 1536x754 map with 450 burgs
- };
-
- const sizeBurgs = getBurgEmblemSize();
- const burgCOAs = validBurgs.map(burg => {
- const {x, y} = burg;
- const size = burg.coa.size || 1;
- const shift = (sizeBurgs * size) / 2;
- return {type: "burg", i: burg.i, x: burg.coa.x || x, y: burg.coa.y || y, size, shift};
- });
-
- const sizeProvinces = getProvinceEmblemsSize();
- const provinceCOAs = validProvinces.map(province => {
- const [x, y] = province.pole || pack.cells.p[province.center];
- const size = province.coa.size || 1;
- const shift = (sizeProvinces * size) / 2;
- return {type: "province", i: province.i, x: province.coa.x || x, y: province.coa.y || y, size, shift};
- });
-
- const sizeStates = getStateEmblemsSize();
- const stateCOAs = validStates.map(state => {
- const [x, y] = state.pole || pack.cells.p[state.center];
- const size = state.coa.size || 1;
- const shift = (sizeStates * size) / 2;
- return {type: "state", i: state.i, x: state.coa.x || x, y: state.coa.y || y, size, shift};
- });
-
- const nodes = burgCOAs.concat(provinceCOAs).concat(stateCOAs);
- const simulation = d3
- .forceSimulation(nodes)
- .alphaMin(0.6)
- .alphaDecay(0.2)
- .velocityDecay(0.6)
- .force(
- "collision",
- d3.forceCollide().radius(d => d.shift)
- )
- .stop();
-
- d3.timeout(function () {
- const n = Math.ceil(Math.log(simulation.alphaMin()) / Math.log(1 - simulation.alphaDecay()));
- for (let i = 0; i < n; ++i) {
- simulation.tick();
- }
-
- const burgNodes = nodes.filter(node => node.type === "burg");
- const burgString = burgNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#burgEmblems").attr("font-size", sizeBurgs).html(burgString);
-
- const provinceNodes = nodes.filter(node => node.type === "province");
- const provinceString = provinceNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#provinceEmblems").attr("font-size", sizeProvinces).html(provinceString);
-
- const stateNodes = nodes.filter(node => node.type === "state");
- const stateString = stateNodes
- .map(
- d =>
- ``
- )
- .join("");
- emblems.select("#stateEmblems").attr("font-size", sizeStates).html(stateString);
-
- invokeActiveZooming();
- });
-
- TIME && console.timeEnd("drawEmblems");
-}
-
-const getDataAndType = id => {
- if (id === "burgEmblems") return [pack.burgs, "burg"];
- if (id === "provinceEmblems") return [pack.provinces, "province"];
- if (id === "stateEmblems") return [pack.states, "state"];
- throw new Error(`Unknown emblem type: ${id}`);
-};
-
-async function renderGroupCOAs(g) {
- const [data, type] = getDataAndType(g.id);
-
- for (let use of g.children) {
- const i = +use.dataset.i;
- const id = type + "COA" + i;
- COArenderer.trigger(id, data[i].coa);
- use.setAttribute("href", "#" + id);
- }
-}
diff --git a/public/modules/renderers/draw-features.js b/public/modules/renderers/draw-features.js
deleted file mode 100644
index 0112a0ae..00000000
--- a/public/modules/renderers/draw-features.js
+++ /dev/null
@@ -1,66 +0,0 @@
-"use strict";
-
-function drawFeatures() {
- TIME && console.time("drawFeatures");
-
- const html = {
- paths: [],
- landMask: [],
- waterMask: [''],
- coastline: {},
- lakes: {}
- };
-
- for (const feature of pack.features) {
- if (!feature || feature.type === "ocean") continue;
-
- html.paths.push(``);
-
- if (feature.type === "lake") {
- html.landMask.push(``);
-
- const lakeGroup = feature.group || "freshwater";
- if (!html.lakes[lakeGroup]) html.lakes[lakeGroup] = [];
- html.lakes[lakeGroup].push(``);
- } else {
- html.landMask.push(``);
- html.waterMask.push(``);
-
- const coastlineGroup = feature.group === "lake_island" ? "lake_island" : "sea_island";
- if (!html.coastline[coastlineGroup]) html.coastline[coastlineGroup] = [];
- html.coastline[coastlineGroup].push(``);
- }
- }
-
- defs.select("#featurePaths").html(html.paths.join(""));
- defs.select("#land").html(html.landMask.join(""));
- defs.select("#water").html(html.waterMask.join(""));
-
- coastline.selectAll("g").each(function () {
- const paths = html.coastline[this.id] || [];
- d3.select(this).html(paths.join(""));
- });
-
- lakes.selectAll("g").each(function () {
- const paths = html.lakes[this.id] || [];
- d3.select(this).html(paths.join(""));
- });
-
- TIME && console.timeEnd("drawFeatures");
-}
-
-function getFeaturePath(feature) {
- const points = feature.vertices.map(vertex => pack.vertices.p[vertex]);
- if (points.some(point => point === undefined)) {
- ERROR && console.error("Undefined point in getFeaturePath");
- return "";
- }
-
- const simplifiedPoints = simplify(points, 0.3);
- const clippedPoints = clipPoly(simplifiedPoints, 1);
-
- const lineGen = d3.line().curve(d3.curveBasisClosed);
- const path = round(lineGen(clippedPoints)) + "Z";
-
- return path;
-}
diff --git a/public/modules/renderers/draw-ice.js b/public/modules/renderers/draw-ice.js
deleted file mode 100644
index 4b35f75c..00000000
--- a/public/modules/renderers/draw-ice.js
+++ /dev/null
@@ -1,70 +0,0 @@
-"use strict";
-
-// Ice layer renderer - renders ice from data model to SVG
-function drawIce() {
- TIME && console.time("drawIce");
-
- // Clear existing ice SVG
- ice.selectAll("*").remove();
-
- let html = "";
-
- // Draw all ice elements
- pack.ice.forEach(iceElement => {
- if (iceElement.type === "glacier") {
- html += getGlacierHtml(iceElement);
- } else if (iceElement.type === "iceberg") {
- html += getIcebergHtml(iceElement);
- }
- });
-
- ice.html(html);
-
- TIME && console.timeEnd("drawIce");
-}
-
-function redrawIceberg(id) {
- TIME && console.time("redrawIceberg");
- const iceberg = pack.ice.find(element => element.i === id);
- let el = ice.selectAll(`polygon[data-id="${id}"]:not([type="glacier"])`);
- if (!iceberg && !el.empty()) {
- el.remove();
- } else {
- if (el.empty()) {
- // Create new element if it doesn't exist
- const polygon = getIcebergHtml(iceberg);
- ice.node().insertAdjacentHTML("beforeend", polygon);
- el = ice.selectAll(`polygon[data-id="${id}"]:not([type="glacier"])`);
- }
- el.attr("points", iceberg.points);
- el.attr("transform", iceberg.offset ? `translate(${iceberg.offset[0]},${iceberg.offset[1]})` : null);
- }
- TIME && console.timeEnd("redrawIceberg");
-}
-
-function redrawGlacier(id) {
- TIME && console.time("redrawGlacier");
- const glacier = pack.ice.find(element => element.i === id);
- let el = ice.selectAll(`polygon[data-id="${id}"][type="glacier"]`);
- if (!glacier && !el.empty()) {
- el.remove();
- } else {
- if (el.empty()) {
- // Create new element if it doesn't exist
- const polygon = getGlacierHtml(glacier);
- ice.node().insertAdjacentHTML("beforeend", polygon);
- el = ice.selectAll(`polygon[data-id="${id}"][type="glacier"]`);
- }
- el.attr("points", glacier.points);
- el.attr("transform", glacier.offset ? `translate(${glacier.offset[0]},${glacier.offset[1]})` : null);
- }
- TIME && console.timeEnd("redrawGlacier");
-}
-
-function getGlacierHtml(glacier) {
- return ``;
-}
-
-function getIcebergHtml(iceberg) {
- return ``;
-}
\ No newline at end of file
diff --git a/public/modules/renderers/draw-markers.js b/public/modules/renderers/draw-markers.js
deleted file mode 100644
index f7466a55..00000000
--- a/public/modules/renderers/draw-markers.js
+++ /dev/null
@@ -1,53 +0,0 @@
-"use strict";
-
-function drawMarkers() {
- TIME && console.time("drawMarkers");
-
- const rescale = +markers.attr("rescale");
- const pinned = +markers.attr("pinned");
-
- const markersData = pinned ? pack.markers.filter(({pinned}) => pinned) : pack.markers;
- const html = markersData.map(marker => drawMarker(marker, rescale));
- markers.html(html.join(""));
-
- TIME && console.timeEnd("drawMarkers");
-}
-
-// prettier-ignore
-const pinShapes = {
- bubble: (fill, stroke) => ``,
- pin: (fill, stroke) => ``,
- square: (fill, stroke) => ``,
- squarish: (fill, stroke) => ``,
- diamond: (fill, stroke) => ``,
- hex: (fill, stroke) => ``,
- hexy: (fill, stroke) => ``,
- shieldy: (fill, stroke) => ``,
- shield: (fill, stroke) => ``,
- pentagon: (fill, stroke) => ``,
- heptagon: (fill, stroke) => ``,
- circle: (fill, stroke) => ``,
- no: () => ""
-};
-
-const getPin = (shape = "bubble", fill = "#fff", stroke = "#000") => {
- const shapeFunction = pinShapes[shape] || pinShapes.bubble;
- return shapeFunction(fill, stroke);
-};
-
-function drawMarker(marker, rescale = 1) {
- const {i, icon, x, y, dx = 50, dy = 50, px = 12, size = 30, pin, fill, stroke} = marker;
- const id = `marker${i}`;
- const zoomSize = rescale ? Math.max(rn(size / 5 + 24 / scale, 2), 1) : size;
- const viewX = rn(x - zoomSize / 2, 1);
- const viewY = rn(y - zoomSize, 1);
-
- const isExternal = icon.startsWith("http") || icon.startsWith("data:image");
-
- return /* html */ `
- `;
-}
diff --git a/public/modules/renderers/draw-military.js b/public/modules/renderers/draw-military.js
deleted file mode 100644
index a332130f..00000000
--- a/public/modules/renderers/draw-military.js
+++ /dev/null
@@ -1,155 +0,0 @@
-"use strict";
-
-function drawMilitary() {
- TIME && console.time("drawMilitary");
-
- armies.selectAll("g").remove();
- pack.states.filter(s => s.i && !s.removed).forEach(s => drawRegiments(s.military, s.i));
-
- TIME && console.timeEnd("drawMilitary");
-}
-
-const drawRegiments = function (regiments, s) {
- const size = +armies.attr("box-size");
- const w = d => (d.n ? size * 4 : size * 6);
- const h = size * 2;
- const x = d => rn(d.x - w(d) / 2, 2);
- const y = d => rn(d.y - size, 2);
-
- const baseColor = pack.states[s].color[0] === "#" ? pack.states[s].color : "#999";
- const darkerColor = d3.color(baseColor).darker().hex();
- const army = armies
- .append("g")
- .attr("id", "army" + s)
- .attr("fill", baseColor)
- .attr("color", darkerColor);
-
- const g = army
- .selectAll("g")
- .data(regiments)
- .enter()
- .append("g")
- .attr("id", d => "regiment" + s + "-" + d.i)
- .attr("data-name", d => d.name)
- .attr("data-state", s)
- .attr("data-id", d => d.i)
- .attr("transform", d => (d.angle ? `rotate(${d.angle})` : null))
- .attr("transform-origin", d => `${d.x}px ${d.y}px`);
- g.append("rect")
- .attr("x", d => x(d))
- .attr("y", d => y(d))
- .attr("width", d => w(d))
- .attr("height", h);
- g.append("text")
- .attr("x", d => d.x)
- .attr("y", d => d.y)
- .attr("text-rendering", "optimizeSpeed")
- .text(d => Military.getTotal(d));
- g.append("rect")
- .attr("fill", "currentColor")
- .attr("x", d => x(d) - h)
- .attr("y", d => y(d))
- .attr("width", h)
- .attr("height", h);
- g.append("text")
- .attr("class", "regimentIcon")
- .attr("text-rendering", "optimizeSpeed")
- .attr("x", d => x(d) - size)
- .attr("y", d => d.y)
- .text(d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? "" : d.icon));
- g.append("image")
- .attr("class", "regimentImage")
- .attr("x", d => x(d) - h)
- .attr("y", d => y(d))
- .attr("height", h)
- .attr("width", h)
- .attr("href", d => (d.icon.startsWith("http") || d.icon.startsWith("data:image") ? d.icon : ""));
-};
-
-const drawRegiment = function (reg, stateId) {
- const size = +armies.attr("box-size");
- const w = reg.n ? size * 4 : size * 6;
- const h = size * 2;
- const x1 = rn(reg.x - w / 2, 2);
- const y1 = rn(reg.y - size, 2);
-
- let army = armies.select("g#army" + stateId);
- if (!army.size()) {
- const baseColor = pack.states[stateId].color[0] === "#" ? pack.states[stateId].color : "#999";
- const darkerColor = d3.color(baseColor).darker().hex();
- army = armies
- .append("g")
- .attr("id", "army" + stateId)
- .attr("fill", baseColor)
- .attr("color", darkerColor);
- }
-
- const g = army
- .append("g")
- .attr("id", "regiment" + stateId + "-" + reg.i)
- .attr("data-name", reg.name)
- .attr("data-state", stateId)
- .attr("data-id", reg.i)
- .attr("transform", `rotate(${reg.angle || 0})`)
- .attr("transform-origin", `${reg.x}px ${reg.y}px`);
- g.append("rect").attr("x", x1).attr("y", y1).attr("width", w).attr("height", h);
- g.append("text")
- .attr("x", reg.x)
- .attr("y", reg.y)
- .attr("text-rendering", "optimizeSpeed")
- .text(Military.getTotal(reg));
- g.append("rect")
- .attr("fill", "currentColor")
- .attr("x", x1 - h)
- .attr("y", y1)
- .attr("width", h)
- .attr("height", h);
- g.append("text")
- .attr("class", "regimentIcon")
- .attr("text-rendering", "optimizeSpeed")
- .attr("x", x1 - size)
- .attr("y", reg.y)
- .text(reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? "" : reg.icon);
- g.append("image")
- .attr("class", "regimentImage")
- .attr("x", x1 - h)
- .attr("y", y1)
- .attr("height", h)
- .attr("width", h)
- .attr("href", reg.icon.startsWith("http") || reg.icon.startsWith("data:image") ? reg.icon : "");
-};
-
-// move one regiment to another
-const moveRegiment = function (reg, x, y) {
- const el = armies.select("g#army" + reg.state).select("g#regiment" + reg.state + "-" + reg.i);
- if (!el.size()) return;
-
- const duration = Math.hypot(reg.x - x, reg.y - y) * 8;
- reg.x = x;
- reg.y = y;
- const size = +armies.attr("box-size");
- const w = reg.n ? size * 4 : size * 6;
- const h = size * 2;
- const x1 = x => rn(x - w / 2, 2);
- const y1 = y => rn(y - size, 2);
-
- const move = d3.transition().duration(duration).ease(d3.easeSinInOut);
- el.select("rect").transition(move).attr("x", x1(x)).attr("y", y1(y));
- el.select("text").transition(move).attr("x", x).attr("y", y);
- el.selectAll("rect:nth-of-type(2)")
- .transition(move)
- .attr("x", x1(x) - h)
- .attr("y", y1(y));
- el.select(".regimentIcon")
- .transition(move)
- .attr("x", x1(x) - size)
- .attr("y", y)
- .attr("height", "6")
- .attr("width", "6");
- el.select(".regimentImage")
- .transition(move)
- .attr("x", x1(x) - h)
- .attr("y", y1(y))
- .attr("height", "6")
- .attr("width", "6");
-};
diff --git a/public/modules/renderers/draw-relief-icons.js b/public/modules/renderers/draw-relief-icons.js
deleted file mode 100644
index ffa0b69c..00000000
--- a/public/modules/renderers/draw-relief-icons.js
+++ /dev/null
@@ -1,124 +0,0 @@
-"use strict";
-
-function drawReliefIcons() {
- TIME && console.time("drawRelief");
- terrain.selectAll("*").remove();
-
- const cells = pack.cells;
- const density = terrain.attr("density") || 0.4;
- const size = 2 * (terrain.attr("size") || 1);
- const mod = 0.2 * size; // size modifier
- const relief = [];
-
- for (const i of cells.i) {
- const height = cells.h[i];
- if (height < 20) continue; // no icons on water
- if (cells.r[i]) continue; // no icons on rivers
- const biome = cells.biome[i];
- if (height < 50 && biomesData.iconsDensity[biome] === 0) continue; // no icons for this biome
-
- const polygon = getPackPolygon(i);
- const [minX, maxX] = d3.extent(polygon, p => p[0]);
- const [minY, maxY] = d3.extent(polygon, p => p[1]);
-
- if (height < 50) placeBiomeIcons(i, biome);
- else placeReliefIcons(i);
-
- function placeBiomeIcons() {
- const iconsDensity = biomesData.iconsDensity[biome] / 100;
- const radius = 2 / iconsDensity / density;
- if (Math.random() > iconsDensity * 10) return;
-
- for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
- if (!d3.polygonContains(polygon, [cx, cy])) continue;
- let h = (4 + Math.random()) * size;
- const icon = getBiomeIcon(i, biomesData.icons[biome]);
- if (icon === "#relief-grass-1") h *= 1.2;
- relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
- }
- }
-
- function placeReliefIcons(i) {
- const radius = 2 / density;
- const [icon, h] = getReliefIcon(i, height);
-
- for (const [cx, cy] of poissonDiscSampler(minX, minY, maxX, maxY, radius)) {
- if (!d3.polygonContains(polygon, [cx, cy])) continue;
- relief.push({i: icon, x: rn(cx - h, 2), y: rn(cy - h, 2), s: rn(h * 2, 2)});
- }
- }
-
- function getReliefIcon(i, h) {
- const temp = grid.cells.temp[pack.cells.g[i]];
- const type = h > 70 && temp < 0 ? "mountSnow" : h > 70 ? "mount" : "hill";
- const size = h > 70 ? (h - 45) * mod : minmax((h - 40) * mod, 3, 6);
- return [getIcon(type), size];
- }
- }
-
- // sort relief icons by y+size
- relief.sort((a, b) => a.y + a.s - (b.y + b.s));
-
- const reliefHTML = new Array(relief.length);
- for (const r of relief) {
- reliefHTML.push(``);
- }
- terrain.html(reliefHTML.join(""));
-
- TIME && console.timeEnd("drawRelief");
-
- function getBiomeIcon(i, b) {
- let type = b[Math.floor(Math.random() * b.length)];
- const temp = grid.cells.temp[pack.cells.g[i]];
- if (type === "conifer" && temp < 0) type = "coniferSnow";
- return getIcon(type);
- }
-
- function getVariant(type) {
- switch (type) {
- case "mount":
- return rand(2, 7);
- case "mountSnow":
- return rand(1, 6);
- case "hill":
- return rand(2, 5);
- case "conifer":
- return 2;
- case "coniferSnow":
- return 1;
- case "swamp":
- return rand(2, 3);
- case "cactus":
- return rand(1, 3);
- case "deadTree":
- return rand(1, 2);
- default:
- return 2;
- }
- }
-
- function getOldIcon(type) {
- switch (type) {
- case "mountSnow":
- return "mount";
- case "vulcan":
- return "mount";
- case "coniferSnow":
- return "conifer";
- case "cactus":
- return "dune";
- case "deadTree":
- return "dune";
- default:
- return type;
- }
- }
-
- function getIcon(type) {
- const set = terrain.attr("set") || "simple";
- if (set === "simple") return "#relief-" + getOldIcon(type) + "-1";
- if (set === "colored") return "#relief-" + type + "-" + getVariant(type);
- if (set === "gray") return "#relief-" + type + "-" + getVariant(type) + "-bw";
- return "#relief-" + getOldIcon(type) + "-1"; // simple
- }
-}
diff --git a/public/modules/renderers/draw-state-labels.js b/public/modules/renderers/draw-state-labels.js
deleted file mode 100644
index 9586a9c1..00000000
--- a/public/modules/renderers/draw-state-labels.js
+++ /dev/null
@@ -1,312 +0,0 @@
-"use strict";
-
-// list - an optional array of stateIds to regenerate
-function drawStateLabels(list) {
- TIME && console.time("drawStateLabels");
-
- // temporary make the labels visible
- const layerDisplay = labels.style("display");
- labels.style("display", null);
-
- 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 angles = precalculateAngles(ANGLE_STEP);
-
- const LENGTH_START = 5;
- const LENGTH_STEP = 5;
- const LENGTH_MAX = 300;
-
- const labelPaths = getLabelPaths();
- const letterLength = checkExampleLetterLength();
- drawLabelPath(letterLength);
-
- // restore labels visibility
- labels.style("display", layerDisplay);
-
- function getLabelPaths() {
- const labelPaths = [];
-
- for (const state of states) {
- if (!state.i || state.removed || state.lock) continue;
- if (list && !list.includes(state.i)) continue;
-
- const offset = getOffsetWidth(state.cells);
- const maxLakeSize = state.cells / 20;
- const [x0, y0] = state.pole;
-
- const rays = angles.map(({angle, dx, dy}) => {
- const {length, x, y} = raycast({stateId: state.i, x0, y0, dx, dy, maxLakeSize, offset});
- return {angle, length, x, y};
- });
- const [ray1, ray2] = findBestRayPair(rays);
-
- const pathPoints = [[ray1.x, ray1.y], state.pole, [ray2.x, ray2.y]];
- if (ray1.x > ray2.x) pathPoints.reverse();
-
- if (DEBUG.stateLabels) {
- drawPoint(state.pole, {color: "black", radius: 1});
- drawPath(pathPoints, {color: "black", width: 0.2});
- }
-
- labelPaths.push([state.i, pathPoints]);
- }
-
- return labelPaths;
- }
-
- function checkExampleLetterLength() {
- const textGroup = d3.select("g#labels > g#states");
- const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example");
- const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter
- testLabel.remove();
-
- return letterLength;
- }
-
- function drawLabelPath(letterLength) {
- const mode = options.stateLabelsMode || "auto";
- const lineGen = d3.line().curve(d3.curveNatural);
-
- const textGroup = d3.select("g#labels > g#states");
- const pathGroup = d3.select("defs > g#deftemp > g#textPaths");
-
- for (const [stateId, pathPoints] of labelPaths) {
- const state = states[stateId];
- if (!state.i || state.removed) throw new Error("State must not be neutral or removed");
- if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points");
-
- textGroup.select("#stateLabel" + stateId).remove();
- pathGroup.select("#textPath_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("text-rendering", "optimizeSpeed")
- .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) => `${line}`);
- 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 = `${text}`;
-
- const correctedRatio = minmax(rn((pathLength / text.length) * 50), 50, 130);
- textElement.setAttribute("font-size", correctedRatio + "%");
- }
- }
-
- function getOffsetWidth(cellsNumber) {
- if (cellsNumber < 40) return 0;
- if (cellsNumber < 200) return 5;
- return 10;
- }
-
- function precalculateAngles(step) {
- const angles = [];
- const RAD = Math.PI / 180;
-
- for (let angle = 0; angle < 360; angle += step) {
- const dx = Math.cos(angle * RAD);
- const dy = Math.sin(angle * RAD);
- angles.push({angle, dx, dy});
- }
-
- return angles;
- }
-
- function raycast({stateId, x0, y0, dx, dy, maxLakeSize, offset}) {
- let ray = {length: 0, x: x0, y: y0};
-
- for (let length = LENGTH_START; length < LENGTH_MAX; length += LENGTH_STEP) {
- const [x, y] = [x0 + length * dx, y0 + length * dy];
- // offset points are perpendicular to the ray
- const offset1 = [x + -dy * offset, y + dx * offset];
- const offset2 = [x + dy * offset, y + -dx * offset];
-
- if (DEBUG.stateLabels) {
- drawPoint([x, y], {color: isInsideState(x, y) ? "blue" : "red", radius: 0.8});
- drawPoint(offset1, {color: isInsideState(...offset1) ? "blue" : "red", radius: 0.4});
- drawPoint(offset2, {color: isInsideState(...offset2) ? "blue" : "red", radius: 0.4});
- }
-
- const inState = isInsideState(x, y) && isInsideState(...offset1) && isInsideState(...offset2);
- if (!inState) break;
- ray = {length, x, y};
- }
-
- return ray;
-
- function isInsideState(x, y) {
- if (x < 0 || x > graphWidth || y < 0 || y > graphHeight) return false;
- const cellId = findCell(x, y);
-
- const feature = features[cells.f[cellId]];
- if (feature.type === "lake") return isInnerLake(feature) || isSmallLake(feature);
-
- return stateIds[cellId] === stateId;
- }
-
- function isInnerLake(feature) {
- return feature.shoreline.every(cellId => stateIds[cellId] === stateId);
- }
-
- function isSmallLake(feature) {
- return feature.cells <= maxLakeSize;
- }
- }
-
- function findBestRayPair(rays) {
- let bestPair = null;
- let bestScore = -Infinity;
-
- for (let i = 0; i < rays.length; i++) {
- const score1 = rays[i].length * scoreRayAngle(rays[i].angle);
-
- for (let j = i + 1; j < rays.length; j++) {
- const score2 = rays[j].length * scoreRayAngle(rays[j].angle);
- const pairScore = (score1 + score2) * scoreCurvature(rays[i].angle, rays[j].angle);
-
- if (pairScore > bestScore) {
- bestScore = pairScore;
- bestPair = [rays[i], rays[j]];
- }
- }
- }
-
- return bestPair;
- }
-
- function scoreRayAngle(angle) {
- const normalizedAngle = Math.abs(angle % 180); // [0, 180]
- const horizontality = Math.abs(normalizedAngle - 90) / 90; // [0, 1]
-
- if (horizontality === 1) return 1; // Best: horizontal
- if (horizontality >= 0.75) return 0.9; // Very good: slightly slanted
- if (horizontality >= 0.5) return 0.6; // Good: moderate slant
- if (horizontality >= 0.25) return 0.5; // Acceptable: more slanted
- if (horizontality >= 0.15) return 0.2; // Poor: almost vertical
- return 0.1; // Very poor: almost vertical
- }
-
- function scoreCurvature(angle1, angle2) {
- const delta = getAngleDelta(angle1, angle2);
- const similarity = evaluateArc(angle1, angle2);
-
- if (delta === 180) return 1; // straight line: best
- if (delta < 90) return 0; // acute: not allowed
- if (delta < 120) return 0.6 * similarity;
- if (delta < 140) return 0.7 * similarity;
- if (delta < 160) return 0.8 * similarity;
-
- return similarity;
- }
-
- function getAngleDelta(angle1, angle2) {
- let delta = Math.abs(angle1 - angle2) % 360;
- if (delta > 180) delta = 360 - delta; // [0, 180]
- return delta;
- }
-
- // compute arc similarity towards x-axis
- function evaluateArc(angle1, angle2) {
- const proximity1 = Math.abs((angle1 % 180) - 90);
- const proximity2 = Math.abs((angle2 % 180) - 90);
- return 1 - Math.abs(proximity1 - proximity2) / 90;
- }
-
- function getLinesAndRatio(mode, name, fullName, pathLength) {
- if (mode === "short") return getShortOneLine();
- if (pathLength > fullName.length * 2) return getFullOneLine();
- return getFullTwoLines();
-
- function getShortOneLine() {
- const ratio = pathLength / name.length;
- return [[name], minmax(rn(ratio * 60), 50, 150)];
- }
-
- function getFullOneLine() {
- const ratio = pathLength / fullName.length;
- return [[fullName], minmax(rn(ratio * 70), 70, 170)];
- }
-
- function getFullTwoLines() {
- 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;
- }
-
- TIME && console.timeEnd("drawStateLabels");
-}
diff --git a/public/modules/renderers/draw-temperature.js b/public/modules/renderers/draw-temperature.js
deleted file mode 100644
index 51dc32f5..00000000
--- a/public/modules/renderers/draw-temperature.js
+++ /dev/null
@@ -1,104 +0,0 @@
-"use strict";
-
-function drawTemperature() {
- TIME && console.time("drawTemperature");
-
- temperature.selectAll("*").remove();
- lineGen.curve(d3.curveBasisClosed);
- const scheme = d3.scaleSequential(d3.interpolateSpectral);
-
- const tMax = +byId("temperatureEquatorOutput").max;
- const tMin = +byId("temperatureEquatorOutput").min;
- const delta = tMax - tMin;
-
- const {cells, vertices} = grid;
- const n = cells.i.length;
-
- const checkedCells = new Uint8Array(n);
- const addToChecked = cellId => (checkedCells[cellId] = 1);
-
- const min = d3.min(cells.temp);
- const max = d3.max(cells.temp);
- const step = Math.max(Math.round(Math.abs(min - max) / 5), 1);
-
- const isolines = d3.range(min + step, max, step);
- const chains = [];
- const labels = []; // store label coordinates
-
- for (const cellId of cells.i) {
- const t = cells.temp[cellId];
- if (checkedCells[cellId] || !isolines.includes(t)) continue;
-
- const startingVertex = findStart(cellId, t);
- if (!startingVertex) continue;
- checkedCells[cellId] = 1;
-
- const ofSameType = cellId => cells.temp[cellId] >= t;
- const chain = connectVertices({vertices, startingVertex, ofSameType, addToChecked});
- const relaxed = chain.filter((v, i) => i % 4 === 0 || vertices.c[v].some(c => c >= n));
- if (relaxed.length < 6) continue;
-
- const points = relaxed.map(v => vertices.p[v]);
- chains.push([t, points]);
- addLabel(points, t);
- }
-
- // min temp isoline covers all graph
- temperature
- .append("path")
- .attr("d", `M0,0 h${graphWidth} v${graphHeight} h${-graphWidth} Z`)
- .attr("fill", scheme(1 - (min - tMin) / delta))
- .attr("stroke", "none");
-
- for (const t of isolines) {
- const path = chains
- .filter(c => c[0] === t)
- .map(c => round(lineGen(c[1])))
- .join("");
- if (!path) continue;
- const fill = scheme(1 - (t - tMin) / delta),
- stroke = d3.color(fill).darker(0.2);
- temperature.append("path").attr("d", path).attr("fill", fill).attr("stroke", stroke);
- }
-
- const tempLabels = temperature.append("g").attr("id", "tempLabels").attr("fill-opacity", 1);
- tempLabels
- .selectAll("text")
- .data(labels)
- .enter()
- .append("text")
- .attr("x", d => d[0])
- .attr("y", d => d[1])
- .text(d => convertTemperature(d[2]));
-
- // find cell with temp < isotherm and find vertex to start path detection
- function findStart(i, t) {
- if (cells.b[i]) return cells.v[i].find(v => vertices.c[v].some(c => c >= n)); // map border cell
- return cells.v[i][cells.c[i].findIndex(c => cells.temp[c] < t || !cells.temp[c])];
- }
-
- function addLabel(points, t) {
- const xCenter = svgWidth / 2;
-
- // add label on isoline top center
- const tc =
- points[d3.scan(points, (a, b) => a[1] - b[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
- pushLabel(tc[0], tc[1], t);
-
- // add label on isoline bottom center
- if (points.length > 20) {
- const bc =
- points[d3.scan(points, (a, b) => b[1] - a[1] + (Math.abs(a[0] - xCenter) - Math.abs(b[0] - xCenter)) / 2)];
- const dist2 = (tc[1] - bc[1]) ** 2 + (tc[0] - bc[0]) ** 2; // square distance between this and top point
- if (dist2 > 100) pushLabel(bc[0], bc[1], t);
- }
- }
-
- function pushLabel(x, y, t) {
- if (x < 20 || x > svgWidth - 20) return;
- if (y < 20 || y > svgHeight - 20) return;
- labels.push([x, y, t]);
- }
-
- TIME && console.timeEnd("drawTemperature");
-}
diff --git a/public/modules/ui/biomes-editor.js b/public/modules/ui/biomes-editor.js
index 8c50993d..125aa0da 100644
--- a/public/modules/ui/biomes-editor.js
+++ b/public/modules/ui/biomes-editor.js
@@ -136,11 +136,13 @@ function editBiomes() {
body.innerHTML = lines;
// update footer
+ const totalMapArea = getArea(d3.sum(pack.cells.area));
biomesFooterBiomes.innerHTML = body.querySelectorAll(":scope > div").length;
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
biomesFooterArea.innerHTML = si(totalArea) + unit;
biomesFooterPopulation.innerHTML = si(totalPopulation);
biomesFooterArea.dataset.area = totalArea;
+ biomesFooterArea.dataset.mapArea = totalMapArea;
biomesFooterPopulation.dataset.population = totalPopulation;
// add listeners
@@ -255,6 +257,7 @@ function editBiomes() {
body.dataset.type = "percentage";
const totalCells = +biomesFooterCells.innerHTML;
const totalArea = +biomesFooterArea.dataset.area;
+ const totalMapArea = +biomesFooterArea.dataset.mapArea;
const totalPopulation = +biomesFooterPopulation.dataset.population;
body.querySelectorAll(":scope> div").forEach(function (el) {
@@ -262,6 +265,9 @@ function editBiomes() {
el.querySelector(".biomeArea").innerHTML = rn((+el.dataset.area / totalArea) * 100) + "%";
el.querySelector(".biomePopulation").innerHTML = rn((+el.dataset.population / totalPopulation) * 100) + "%";
});
+
+ // update footer to show land percentage of total map
+ biomesFooterArea.innerHTML = rn((totalArea / totalMapArea) * 100) + "%";
} else {
body.dataset.type = "absolute";
biomesEditorAddLines();
diff --git a/public/versioning.js b/public/versioning.js
index 8069c818..fc81870d 100644
--- a/public/versioning.js
+++ b/public/versioning.js
@@ -13,7 +13,7 @@
* Example: 1.102.2 -> Major version 1, Minor version 102, Patch version 2
*/
-const VERSION = "1.111.0";
+const VERSION = "1.112.0";
if (parseMapVersion(VERSION) !== VERSION) alert("versioning.js: Invalid format or parsing function");
{
diff --git a/src/index.html b/src/index.html
index d6605dfb..98549419 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8490,11 +8490,11 @@
+
-
@@ -8518,7 +8518,7 @@
-
+
@@ -8560,19 +8560,5 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-