mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 17:51:24 +01:00
v 0.8.0b
This commit is contained in:
parent
707913f630
commit
680044ddd6
65 changed files with 14257 additions and 13020 deletions
486
modules/burgs-and-states.js
Normal file
486
modules/burgs-and-states.js
Normal file
|
|
@ -0,0 +1,486 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.BurgsAndStates = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
const generate = function() {
|
||||
console.time("generateBurgsAndStates");
|
||||
|
||||
const cells = pack.cells, vertices = pack.vertices, features = pack.features, cultures = pack.cultures, n = cells.i.length;
|
||||
|
||||
cells.burg = new Uint16Array(n); // cell burg
|
||||
cells.road = new Uint16Array(n); // cell road power
|
||||
|
||||
const burgs = pack.burgs = placeCapitals();
|
||||
pack.states = createStates();
|
||||
const capitalRoutes = Routes.getRoads();
|
||||
|
||||
placeTowns();
|
||||
const townRoutes = Routes.getTrails();
|
||||
specifyBurgs();
|
||||
const oceanRoutes = Routes.getSearoutes();
|
||||
|
||||
expandStates();
|
||||
normalizeStates();
|
||||
|
||||
Routes.draw(capitalRoutes, townRoutes, oceanRoutes);
|
||||
drawBurgsWithLabels();
|
||||
|
||||
function placeCapitals() {
|
||||
console.time('placeCapitals');
|
||||
let count = +regionsInput.value;
|
||||
let burgs = [0];
|
||||
|
||||
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for capitals placement
|
||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||
|
||||
if (sorted.length < count * 10) {
|
||||
count = Math.floor(sorted.length / 10);
|
||||
if (!count) {
|
||||
console.error(`There is no populated cells. Cannot generate states`);
|
||||
return burgs;
|
||||
} else {
|
||||
console.error(`Not enought populated cells (${sorted.length}). Will generate only ${count} states`);
|
||||
}
|
||||
}
|
||||
|
||||
let burgsTree = d3.quadtree();
|
||||
let spacing = (graphWidth + graphHeight) / 2 / count; // min distance between capitals
|
||||
|
||||
for (let i = 0; burgs.length <= count; i++) {
|
||||
const cell = sorted[i], x = cells.p[cell][0], y = cells.p[cell][1];
|
||||
|
||||
if (burgsTree.find(x, y, spacing) === undefined) {
|
||||
burgs.push({cell, x, y});
|
||||
burgsTree.add([x, y]);
|
||||
}
|
||||
|
||||
if (i === sorted.length - 1) {
|
||||
console.error("Cannot place capitals with current spacing. Trying again with reduced spacing");
|
||||
burgsTree = d3.quadtree();
|
||||
i = -1, burgs = [0], spacing /= 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
burgs[0] = burgsTree;
|
||||
console.timeEnd('placeCapitals');
|
||||
return burgs;
|
||||
}
|
||||
|
||||
// For each capital create a state
|
||||
function createStates() {
|
||||
console.time('createStates');
|
||||
const states = [{i:0, name: "Neutrals"}];
|
||||
const colors = getColors(burgs.length-1);
|
||||
|
||||
burgs.forEach(function(b, i) {
|
||||
if (!i) return; // skip first element
|
||||
|
||||
// burgs data
|
||||
b.i = b.state = i;
|
||||
b.culture = cells.culture[b.cell];
|
||||
const base = cultures[b.culture].base;
|
||||
const min = nameBases[base].min-1;
|
||||
const max = Math.max(nameBases[base].max-2, min);
|
||||
b.name = Names.getCulture(b.culture, min, max, "", 0);
|
||||
b.feature = cells.f[b.cell];
|
||||
b.capital = true;
|
||||
|
||||
// states data
|
||||
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||
const basename = b.name.length < 9 && b.cell%5 === 0 ? b.name : Names.getCulture(b.culture, min, 6, "", 0);
|
||||
const name = Names.getState(basename, b.culture);
|
||||
const type = cultures[b.culture].type;
|
||||
states.push({i, color: colors[i-1], name, expansionism, capital: i, type, center: b.cell, culture: b.culture});
|
||||
cells.burg[b.cell] = i;
|
||||
});
|
||||
|
||||
console.timeEnd('createStates');
|
||||
return states;
|
||||
}
|
||||
|
||||
// place secondary settlements based on geo and economical evaluation
|
||||
function placeTowns() {
|
||||
console.time('placeTowns');
|
||||
const score = new Int16Array(cells.s.map(s => s * Math.random())); // cell score for towns placement
|
||||
const sorted = cells.i.filter(i => score[i] > 0 && cells.culture[i]).sort((a, b) => score[b] - score[a]); // filtered and sorted array of indexes
|
||||
|
||||
// burgs number depends on ratio between populated and all cells and burgsDensity input (expected mean ~300))
|
||||
const burgsCount = rn(sorted.length / grid.points.length * manorsInput.value * 1000);
|
||||
const spacing = (graphWidth + graphHeight) * 9 / burgsCount; // base min distance between towns
|
||||
const burgsTree = burgs[0];
|
||||
|
||||
for (let i = 0; burgs.length <= burgsCount && i < sorted.length; i++) {
|
||||
const id = sorted[i], x = cells.p[id][0], y = cells.p[id][1];
|
||||
const s = spacing * Math.random() + 0.5; // randomize to make the placement not uniform
|
||||
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
|
||||
const burg = burgs.length;
|
||||
const culture = cells.culture[id];
|
||||
const name = Names.getCulture(culture);
|
||||
const feature = cells.f[id];
|
||||
burgs.push({cell: id, x, y, state: 0, i: burg, culture, name, capital: false, feature});
|
||||
burgsTree.add([x, y]);
|
||||
cells.burg[id] = burg;
|
||||
}
|
||||
|
||||
if (burgs.length <= burgsCount) console.error(`Cannot place all burgs. Requested ${burgsCount}, placed ${burgs.length-1}`);
|
||||
|
||||
//const min = d3.min(score.filter(s => s)), max = d3.max(score);
|
||||
//terrs.selectAll("polygon").data(sorted).enter().append("polygon").attr("points", d => getPackPolygon(d)).attr("fill", d => color(1 - normalize(score[d], min, max)));
|
||||
//labels.selectAll("text").data(sorted).enter().append("text").attr("x", d => cells.p[d][0]).attr("y", d => cells.p[d][1]).text(d => score[d]).attr("font-size", 2);
|
||||
|
||||
burgs[0] = {name:undefined};
|
||||
console.timeEnd('placeTowns');
|
||||
}
|
||||
|
||||
// define burg coordinates and define details
|
||||
function specifyBurgs() {
|
||||
console.time("specifyBurgs");
|
||||
|
||||
for (const b of burgs) {
|
||||
if (!b.i) continue;
|
||||
const i = b.cell;
|
||||
|
||||
// asign port status: capital with any harbor and towns with good harbors
|
||||
const port = (b.capital && cells.harbor[i]) || cells.harbor[i] === 1;
|
||||
b.port = port ? cells.f[cells.haven[i]] : 0; // port is defined by feature id it lays on
|
||||
|
||||
// define burg population (keep urbanization at about 10% rate)
|
||||
b.population = rn(Math.max((cells.s[i] + cells.road[i]) / 3 + b.i / 1000 + i % 100 / 1000, .1), 3);
|
||||
if (b.capital) b.population = rn(b.population * 1.3, 3); // increase capital population
|
||||
|
||||
if (port) {
|
||||
b.population = rn(b.population * 1.3, 3); // increase port population
|
||||
const e = cells.v[i].filter(v => vertices.c[v].some(c => c === cells.haven[i])); // vertices of common edge
|
||||
b.x = rn((vertices.p[e[0]][0] + vertices.p[e[1]][0]) / 2, 2);
|
||||
b.y = rn((vertices.p[e[0]][1] + vertices.p[e[1]][1]) / 2, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// shift burgs on rivers semi-randomly and just a bit
|
||||
if (cells.r[i]) {
|
||||
const shift = Math.min(cells.fl[i]/150, 1);
|
||||
if (i%2) b.x = rn(b.x + shift, 2); else b.x = rn(b.x - shift, 2);
|
||||
if (cells.r[i]%2) b.y = rn(b.y + shift, 2); else b.y = rn(b.y - shift, 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// de-assign port status if it's the only one on feature
|
||||
for (const f of features) {
|
||||
if (!f.i || f.land) continue;
|
||||
const onFeature = burgs.filter(b => b.port === f.i);
|
||||
if (onFeature.length === 1) {
|
||||
onFeature[0].port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
console.timeEnd("specifyBurgs");
|
||||
}
|
||||
|
||||
function drawBurgsWithLabels() {
|
||||
console.time("drawBurgs");
|
||||
|
||||
// remove old data
|
||||
burgIcons.selectAll("circle").remove();
|
||||
burgLabels.selectAll("text").remove();
|
||||
icons.selectAll("use").remove();
|
||||
|
||||
// capitals
|
||||
const capitals = burgs.filter(b => b.capital);
|
||||
const capitalIcons = burgIcons.select("#cities");
|
||||
const capitalLabels = burgLabels.select("#cities");
|
||||
const capitalSize = capitalIcons.attr("size") || 1;
|
||||
const capitalAnchors = anchors.selectAll("#cities");
|
||||
const caSize = capitalAnchors.attr("size") || 2;
|
||||
|
||||
capitalIcons.selectAll("circle").data(capitals).enter()
|
||||
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", capitalSize);
|
||||
|
||||
capitalLabels.selectAll("text").data(capitals).enter()
|
||||
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${capitalSize * -1.5}px`).text(d => d.name);
|
||||
|
||||
capitalAnchors.selectAll("use").data(capitals.filter(c => c.port)).enter()
|
||||
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - caSize * .47, 2)).attr("y", d => rn(d.y - caSize * .47, 2))
|
||||
.attr("width", caSize).attr("height", caSize);
|
||||
|
||||
// towns
|
||||
const towns = burgs.filter(b => b.capital === false);
|
||||
const townIcons = burgIcons.select("#towns");
|
||||
const townLabels = burgLabels.select("#towns");
|
||||
const townSize = townIcons.attr("size") || 0.5;
|
||||
const townsAnchors = anchors.selectAll("#towns");
|
||||
const taSize = townsAnchors.attr("size") || 1;
|
||||
|
||||
townIcons.selectAll("circle").data(towns).enter()
|
||||
.append("circle").attr("id", d => "burg"+d.i).attr("data-id", d => d.i)
|
||||
.attr("cx", d => d.x).attr("cy", d => d.y).attr("r", townSize);
|
||||
|
||||
townLabels.selectAll("text").data(towns).enter()
|
||||
.append("text").attr("id", d => "burgLabel"+d.i).attr("data-id", d => d.i)
|
||||
.attr("x", d => d.x).attr("y", d => d.y).attr("dy", `${townSize * -1.5}px`).text(d => d.name);
|
||||
|
||||
townsAnchors.selectAll("use").data(towns.filter(c => c.port)).enter()
|
||||
.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", d => d.i)
|
||||
.attr("x", d => rn(d.x - taSize * .47, 2)).attr("y", d => rn(d.y - taSize * .47, 2))
|
||||
.attr("width", taSize).attr("height", taSize);
|
||||
|
||||
console.timeEnd("drawBurgs");
|
||||
}
|
||||
|
||||
console.timeEnd("generateBurgsAndStates");
|
||||
}
|
||||
|
||||
// growth algorithm to assign cells to states like we did for cultures
|
||||
const expandStates = function() {
|
||||
console.time("expandStates");
|
||||
const cells = pack.cells, states = pack.states, cultures = pack.cultures, burgs = pack.burgs;
|
||||
|
||||
cells.state = new Uint8Array(cells.i.length); // cell state
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [];
|
||||
states.filter(s => s.i && !s.removed).forEach(function(s) {
|
||||
cells.state[burgs[s.capital].cell] = s.i;
|
||||
const b = cells.biome[cultures[s.culture].center]; // native biome
|
||||
queue.queue({e:s.center, p:0, s:s.i, b});
|
||||
cost[s.center] = 1;
|
||||
});
|
||||
const neutral = cells.i.length / 5000 * 2000 * neutralInput.value * statesNeutral.value; // limit cost for state growth
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p, s = next.s, b = next.b;
|
||||
const type = states[s].type;
|
||||
|
||||
cells.c[n].forEach(function(e) {
|
||||
const biome = cells.biome[e];
|
||||
const cultureCost = states[s].culture === cells.culture[e] ? 10 : 100;
|
||||
const biomeCost = getBiomeCost(cells.road[e], b, biome, type);
|
||||
const heightCost = getHeightCost(cells.h[e], type);
|
||||
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||
const typeCost = getTypeCost(cells.t[e], type);
|
||||
const totalCost = p + (cultureCost + biomeCost + heightCost + riverCost + typeCost) / states[s].expansionism;
|
||||
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.h[e] >= 20) {
|
||||
cells.state[e] = s; // assign state to cell
|
||||
if (cells.burg[e]) burgs[cells.burg[e]].state = s;
|
||||
}
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p:totalCost, s, b});
|
||||
|
||||
//const points = [cells.p[n][0], cells.p[n][1], (cells.p[n][0]+cells.p[e][0])/2, (cells.p[n][1]+cells.p[e][1])/2, cells.p[e][0], cells.p[e][1]];
|
||||
//debug.append("text").attr("x", (cells.p[n][0]+cells.p[e][0])/2 - 1).attr("y", (cells.p[n][1]+cells.p[e][1])/2 - 1).text(rn(totalCost-p)).attr("font-size", .8);
|
||||
//debug.append("polyline").attr("points", points).attr("marker-mid", "url(#arrow)").attr("opacity", .6);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//debug.selectAll(".text").data(cost).enter().append("text").attr("x", (d, e) => cells.p[e][0]-1).attr("y", (d, e) => cells.p[e][1]-1).text(d => d ? rn(d) : "").attr("font-size", 2);
|
||||
|
||||
function getBiomeCost(r, b, biome, type) {
|
||||
if (r > 5) return 0; // no penalty if there is a road;
|
||||
if (b === biome) return 10; // tiny penalty for native biome
|
||||
if (type === "Hunting") return biomesData.cost[biome] * 2; // non-native biome penalty for hunters
|
||||
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 3; // forest biome penalty for nomads
|
||||
return biomesData.cost[biome]; // general non-native biome penalty
|
||||
}
|
||||
|
||||
function getHeightCost(h, type) {
|
||||
if ((type === "Naval" || type === "Lake") && h < 20) return 200; // low sea crossing penalty for Navals
|
||||
if (type === "Nomadic" && h < 20) return 10000; // giant sea crossing penalty for Navals
|
||||
if (h < 20) return 1000; // general sea crossing penalty
|
||||
if (type === "Highland" && h < 50) return 30; // penalty for highlanders on lowlands
|
||||
if (type === "Highland") return 0; // no penalty for highlanders on highlands
|
||||
if (h >= 70) return 100; // general mountains crossing penalty
|
||||
if (h >= 50) return 30; // general hills crossing penalty
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getRiverCost(r, i, type) {
|
||||
if (type === "River") return r ? 0 : 50; // penalty for river cultures
|
||||
if (!r) return 0; // no penalty for others if there is no river
|
||||
return Math.min(Math.max(cells.fl[i] / 10, 20), 100) // river penalty from 20 to 100 based on flux
|
||||
}
|
||||
|
||||
function getTypeCost(ctype, type) {
|
||||
if (ctype === 1) return type === "Naval" || type === "Lake" ? 0 : type === "Nomadic" ? 60 : 20; // penalty for coastline
|
||||
if (ctype === 2) return type === "Naval" || type === "Nomadic" ? 30 : 0; // low penalty for land level 2 for Navals and nomads
|
||||
if (ctype !== -1) return type === "Naval" || type === "Lake" ? 100 : 0; // penalty for mainland for navals
|
||||
return 0;
|
||||
}
|
||||
|
||||
console.timeEnd("expandStates");
|
||||
}
|
||||
|
||||
const normalizeStates = function() {
|
||||
console.time("normalizeStates");
|
||||
const cells = pack.cells;
|
||||
const burgs = pack.burgs;
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const adversaries = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] !== cells.state[i]);
|
||||
const buddies = cells.c[i].filter(c => cells.h[c] >= 20 && cells.state[c] === cells.state[i]);
|
||||
if (adversaries.length <= buddies.length) continue;
|
||||
if (cells.c[i].some(c => burgs[cells.burg[c]].capital)) continue; // do not overwrite near capital
|
||||
if (burgs[cells.burg[i]].capital) continue; // do not overwrite capital
|
||||
const newState = cells.state[adversaries[0]];
|
||||
cells.state[i] = newState;
|
||||
if (cells.burg[i]) burgs[cells.burg[i]].state = newState;
|
||||
}
|
||||
console.timeEnd("normalizeStates");
|
||||
}
|
||||
|
||||
// calculate and draw curved state labels
|
||||
const drawStateLabels = function() {
|
||||
console.time("drawStateLabels");
|
||||
const cells = pack.cells, features = pack.features, states = pack.states;
|
||||
const paths = []; // text paths
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
|
||||
for (const s of states) {
|
||||
if (!s.i || s.removed) continue;
|
||||
const used = [];
|
||||
const hull = getHull(s.center, s.i);
|
||||
const points = [...hull].map(v => pack.vertices.p[v]);
|
||||
|
||||
//const poly = polylabel([points], 1.0); // pole of inaccessibility
|
||||
//debug.append("circle").attr("r", 3).attr("cx", poly[0]).attr("cy", poly[1]);
|
||||
|
||||
const delaunay = Delaunator.from(points);
|
||||
const voronoi = Voronoi(delaunay, points, points.length);
|
||||
const c = voronoi.vertices;
|
||||
const chain = connectCenters(c, s.i);
|
||||
const relaxed = chain.map(i => c.p[i]).filter((p, i) => i%8 === 0 || i+1 === chain.length);
|
||||
paths.push([s.i, relaxed]);
|
||||
|
||||
// if (s.i == 13) debug.selectAll(".circle").data(points).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", "red");
|
||||
// if (s.i == 13) d3.select("#cells").selectAll(".polygon").data(d3.range(voronoi.cells.v.length)).enter().append("polygon").attr("points", d => voronoi.cells.v[d] ? voronoi.cells.v[d].map(v => c.p[v]) : "");
|
||||
// if (s.i == 13) debug.append("path").attr("d", round(lineGen(relaxed))).attr("fill", "none").attr("stroke", "blue").attr("stroke-width", .5);
|
||||
// if (s.i == 13) debug.selectAll(".circle").data(chain).enter().append("circle").attr("cx", d => c.p[d][0]).attr("cy", d => c.p[d][1]).attr("r", 1);
|
||||
|
||||
function getHull(start, state) {
|
||||
const queue = [start], hull = new Set();
|
||||
|
||||
while (queue.length) {
|
||||
const q = queue.pop();
|
||||
const nQ = cells.c[q].filter(c => cells.state[c] === state);
|
||||
cells.c[q].forEach(function(c, d) {
|
||||
if (features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < 10) return; // ignore small lakes
|
||||
if (cells.b[c]) {hull.add(cells.v[q][d]); return;}
|
||||
if (cells.state[c] !== state) {hull.add(cells.v[q][d]); return;}
|
||||
const nC = cells.c[c].filter(n => cells.state[n] === state);
|
||||
const intersected = intersect(nQ, nC).length
|
||||
if (hull.size > 20 && !intersected) {hull.add(cells.v[q][d]); return;}
|
||||
if (used[c]) return;
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
});
|
||||
}
|
||||
|
||||
return hull;
|
||||
}
|
||||
|
||||
function connectCenters(c, state) {
|
||||
// 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])];
|
||||
});
|
||||
//if (state == 13) debug.selectAll(".circle").data(c.p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", .5).attr("fill", (d, i) => inside[i] ? "green" : "blue");
|
||||
|
||||
const sorted = d3.range(c.p.length).filter(i => inside[i]).sort((a, b) => c.p[a][0] - c.p[b][0]);
|
||||
const left = sorted[0] || 0, right = sorted.pop() || 0;
|
||||
|
||||
// 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: right, p: 0});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||
if (n === left) 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 = [left];
|
||||
let cur = left;
|
||||
while (cur !== right) {
|
||||
cur = from[cur];
|
||||
if (inside[cur]) chain.push(cur);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void function drawLabels() {
|
||||
const g = labels.select("#states"), p = defs.select("#textPaths");
|
||||
g.selectAll("text").remove();
|
||||
p.selectAll("path").remove();
|
||||
|
||||
const data = paths.map(p => [round(lineGen(p[1])), "stateLabel"+p[0], states[p[0]].name, p[1]]);
|
||||
p.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("id", d => "textPath_"+d[1]);
|
||||
|
||||
g.selectAll("text").data(data).enter()
|
||||
.append("text").attr("id", d => d[1])
|
||||
.append("textPath").attr("xlink:href", d => "#textPath_"+d[1])
|
||||
.attr("startOffset", "50%").text(d => d[2]);
|
||||
|
||||
// resize label based on its length
|
||||
g.selectAll("text").each(function(e) {
|
||||
const textPath = document.getElementById("textPath_"+e[1])
|
||||
const pathLength = textPath.getTotalLength();
|
||||
|
||||
// if area is too small to get a path and length is 0
|
||||
if (pathLength === 0) {
|
||||
const x = e[3][0][0], y = e[3][0][1];
|
||||
textPath.setAttribute("d", `M${x-50},${y}h${100}`);
|
||||
this.firstChild.setAttribute("font-size", "60%");
|
||||
return;
|
||||
}
|
||||
|
||||
const copy = g.append("text").text(this.textContent);
|
||||
const textLength = copy.node().getComputedTextLength();
|
||||
copy.remove();
|
||||
|
||||
const size = Math.max(Math.min(rn(pathLength / textLength * 60), 175), 60);
|
||||
this.firstChild.setAttribute("font-size", size+"%");
|
||||
|
||||
// prolongate textPath to not trim labels
|
||||
if (pathLength < 100) {
|
||||
const mod = 25 / pathLength;
|
||||
const points = e[3];
|
||||
const f = points[0], l = points[points.length-1];
|
||||
const dx = l[0] - f[0], dy = l[1] - f[1];
|
||||
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.setAttribute("d", round(lineGen(points)));
|
||||
//debug.append("path").attr("d", round(lineGen(points))).attr("fill", "none").attr("stroke", "red");
|
||||
}
|
||||
|
||||
});
|
||||
}()
|
||||
|
||||
console.timeEnd("drawStateLabels");
|
||||
}
|
||||
|
||||
return {generate, expandStates, normalizeStates, drawStateLabels};
|
||||
|
||||
})));
|
||||
Loading…
Add table
Add a link
Reference in a new issue