mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 01:41:22 +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};
|
||||
|
||||
})));
|
||||
210
modules/cultures-generator.js
Normal file
210
modules/cultures-generator.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Cultures = factory());
|
||||
}(this, (function () {'use strict';
|
||||
|
||||
let cells;
|
||||
|
||||
const generate = function() {
|
||||
console.time('generateCultures');
|
||||
cells = pack.cells;
|
||||
cells.culture = new Int8Array(cells.i.length); // cell cultures
|
||||
let count = +culturesInput.value;
|
||||
|
||||
const populated = cells.i.filter(i => cells.s[i]).sort((a, b) => cells.s[b] - cells.s[a]); // cells sorted by population
|
||||
if (populated.length < count * 25) {
|
||||
count = Math.floor(populated.length / 50);
|
||||
if (!count) {
|
||||
console.error(`There is no populated cells. Cannot generate cultures`);
|
||||
pack.cultures = [{name:"Wildlands", i:0, base:1}];
|
||||
alertMessage.innerHTML = `
|
||||
The climate is harsh and people cannot live in this world.<br>
|
||||
No cultures, states and burgs will be created.<br>
|
||||
Please consider changing the World Configurator settings`;
|
||||
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
console.error(`Not enought populated cells (${populated.length}). Will generate only ${count} cultures`);
|
||||
alertMessage.innerHTML = `
|
||||
There is only ${populated.length} populated cells and it's insufficient livable area.<br>
|
||||
Only ${count} out of ${culturesInput.value} requiested cultures will be generated.<br>
|
||||
Please consider changing the World Configurator settings`;
|
||||
$("#alert").dialog({resizable: false, title: "Extreme climate warning",
|
||||
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pack.cultures = d3.shuffle(getDefault()).slice(0, count);
|
||||
const centers = d3.quadtree();
|
||||
const colors = getColors(count);
|
||||
|
||||
pack.cultures.forEach(function(culture, i) {
|
||||
const c = culture.center = placeCultureCenter();
|
||||
centers.add(cells.p[c]);
|
||||
culture.i = i+1;
|
||||
culture.color = colors[i];
|
||||
culture.type = defineCultureType(c);
|
||||
culture.expansionism = defineCultureExpansionism(culture.type);
|
||||
cells.culture[c] = i+1;
|
||||
//debug.append("text").attr("stroke", "#000").attr("font-size", "10").attr("font-family", "Almendra SC").attr("x", cells.p[c][0]).attr("y", cells.p[c][1]).text(culture.type);
|
||||
});
|
||||
|
||||
// the first culture with id 0 is for wildlands
|
||||
pack.cultures.unshift({name:"Wildlands", i:0, base:1});
|
||||
|
||||
// check whether all bases are valid. If not, load default namesbase
|
||||
const invalidBase = pack.cultures.some(c => !nameBase[c.base]);
|
||||
if (invalidBase) applyDefaultNamesData();
|
||||
|
||||
// culture center tends to be placed in a density populated cell
|
||||
function placeCultureCenter() {
|
||||
let center, spacing = (graphWidth + graphHeight) / count;
|
||||
do {
|
||||
center = populated[biased(0, populated.length-1, 3)];
|
||||
spacing = spacing * .8;
|
||||
}
|
||||
while (centers.find(cells.p[center][0], cells.p[center][1], spacing) !== undefined);
|
||||
return center;
|
||||
}
|
||||
|
||||
function defineCultureType(i) {
|
||||
if (cells.h[i] > 50) return "Highland"; // no penalty for hills and moutains, high for other elevations
|
||||
const f = cells.f[cells.haven[i]];
|
||||
if (pack.features[f].type === "lake" && pack.features[f].cells > 5) return "Lake" // low water cross penalty and high for non-along-coastline growth
|
||||
if (cells.harbor[i] === 1) return "Naval"; // low water cross penalty and high for non-along-coastline growth
|
||||
if (cells.r[i] && cells.fl[i] > 100) return "River"; // no River cross penalty, penalty for non-River growth
|
||||
const b = cells.biome[i];
|
||||
if (b === 4 || b === 1 || b === 2) return "Nomadic"; // high penalty in forest biomes and near coastline
|
||||
if (b === 3 || b === 9 || b === 10) return "Hunting"; // high penalty in non-native biomes
|
||||
return "Generic";
|
||||
}
|
||||
|
||||
function defineCultureExpansionism(type) {
|
||||
let base = 1; // Generic
|
||||
if (type === "Lake") base = .8; else
|
||||
if (type === "Naval") base = 1.5; else
|
||||
if (type === "River") base = .9; else
|
||||
if (type === "Nomadic") base = 1.8; else
|
||||
if (type === "Hunting") base = .7; else
|
||||
if (type === "Highland") base = .5;
|
||||
return rn((Math.random() * powerInput.value / 2 + 1) * base, 1);
|
||||
}
|
||||
|
||||
console.timeEnd('generateCultures');
|
||||
}
|
||||
|
||||
const getDefault = function() {
|
||||
return [
|
||||
{name:"Shwazen", base:0},
|
||||
{name:"Angshire", base:1},
|
||||
{name:"Luari", base:2},
|
||||
{name:"Tallian", base:3},
|
||||
{name:"Astellian", base:4},
|
||||
{name:"Slovan", base:5},
|
||||
{name:"Norse", base:6},
|
||||
{name:"Elladan", base:7},
|
||||
{name:"Romian", base:8},
|
||||
{name:"Soumi", base:9},
|
||||
{name:"Koryo", base:10},
|
||||
{name:"Hantzu", base:11},
|
||||
{name:"Yamoto", base:12},
|
||||
{name:"Portuzian", base:13},
|
||||
{name:"Nawatli", base:14},
|
||||
{name:"Vengrian", base: 15},
|
||||
{name:"Turchian", base: 16},
|
||||
{name:"Berberan", base: 17},
|
||||
{name:"Eurabic", base: 18},
|
||||
{name:"Inuk", base: 19},
|
||||
{name:"Euskati", base: 20},
|
||||
{name:"Negarian", base: 21},
|
||||
{name:"Keltan", base: 22},
|
||||
{name:"Efratic", base: 23},
|
||||
{name:"Tehrani", base: 24},
|
||||
{name:"Maui", base: 25},
|
||||
{name:"Carnatic", base: 26},
|
||||
{name:"Inqan", base: 27},
|
||||
{name:"Kiswaili", base: 28},
|
||||
{name:"Vietic", base: 29}
|
||||
];
|
||||
}
|
||||
|
||||
// expand cultures across the map (Dijkstra-like algorithm)
|
||||
const expand = function() {
|
||||
console.time('expandCultures');
|
||||
cells = pack.cells;
|
||||
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
pack.cultures.forEach(function(c) {
|
||||
if (!c.i || c.removed) return;
|
||||
queue.queue({e:c.center, p:0, c:c.i});
|
||||
});
|
||||
|
||||
const neutral = cells.i.length / 5000 * 3000 * neutralInput.value; // limit cost for culture growth
|
||||
const cost = [];
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p, c = next.c;
|
||||
const type = pack.cultures[c].type;
|
||||
cells.c[n].forEach(function(e) {
|
||||
const biome = cells.biome[e];
|
||||
const biomeCost = getBiomeCost(c, biome, type);
|
||||
const biomeChangeCost = biome === cells.biome[n] ? 0 : 5 * Math.abs(biome - cells.biome[n]); // penalty on biome change
|
||||
const heightCost = getHeightCost(e, cells.h[e], type);
|
||||
const riverCost = getRiverCost(cells.r[e], e, type);
|
||||
const typeCost = getTypeCost(cells.t[e], type);
|
||||
const totalCost = p + (biomeCost + biomeChangeCost + heightCost + riverCost + typeCost) / pack.cultures[c].expansionism;
|
||||
|
||||
if (totalCost > neutral) return;
|
||||
|
||||
if (!cost[e] || totalCost < cost[e]) {
|
||||
if (cells.s[e] > 0) cells.culture[e] = c; // assign culture to populated cell
|
||||
cost[e] = totalCost;
|
||||
queue.queue({e, p:totalCost, c});
|
||||
|
||||
//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);
|
||||
//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("polyline").attr("points", points.toString()).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);
|
||||
console.timeEnd('expandCultures');
|
||||
}
|
||||
|
||||
function getBiomeCost(c, biome, type) {
|
||||
if (cells.biome[pack.cultures[c].center] === biome) return biomesData.cost[biome] / 2; // tiny penalty for native biome
|
||||
if (type === "Hunting") return biomesData.cost[biome] * 5; // non-native biome penalty for hunters
|
||||
if (type === "Nomadic" && biome > 4 && biome < 10) return biomesData.cost[biome] * 10; // forest biome penalty for nomads
|
||||
return biomesData.cost[biome] * 2; // general non-native biome penalty
|
||||
}
|
||||
|
||||
function getHeightCost(i, h, type) {
|
||||
if ((type === "Naval" || type === "Lake") && h < 20) return cells.area[i]; // low sea crossing penalty for Navals
|
||||
if (type === "Nomadic" && h < 20) return cells.area[i] * 50; // giant sea crossing penalty for Navals
|
||||
if (h < 20) return cells.area[i] * 5; // 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;
|
||||
}
|
||||
|
||||
return {generate, expand, getDefault};
|
||||
|
||||
})));
|
||||
475
modules/heightmap-generator.js
Normal file
475
modules/heightmap-generator.js
Normal file
|
|
@ -0,0 +1,475 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.HeightmapGenerator = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
let cells, p;
|
||||
|
||||
const generate = function() {
|
||||
console.time('generateHeightmap');
|
||||
cells = grid.cells;
|
||||
p = grid.points;
|
||||
cells.h = new Uint8Array(grid.points.length);
|
||||
|
||||
const input = document.getElementById("templateInput");
|
||||
if (!locked("template")) {
|
||||
const rnd = Math.random();
|
||||
if (rnd < .05) input.value = "Volcano"; else // 5%
|
||||
if (rnd < .25) input.value = "High Island"; else // 20%
|
||||
if (rnd < .35) input.value = "Low Island"; else // 10%
|
||||
if (rnd < .55) input.value = "Continents"; else // 20%
|
||||
if (rnd < .85) input.value = "Archipelago"; else // 30%
|
||||
if (rnd < .90) input.value = "Mediterranean"; else // 5%
|
||||
if (rnd < .95) input.value = "Peninsula"; else // 5%
|
||||
if (rnd < .99) input.value = "Pangea"; else // 4%
|
||||
input.value = "Atoll"; // 1%
|
||||
}
|
||||
|
||||
switch (input.value) {
|
||||
case "Volcano": templateVolcano(); break;
|
||||
case "High Island": templateHighIsland(); break;
|
||||
case "Low Island": templateLowIsland(); break;
|
||||
case "Continents": templateContinents(); break;
|
||||
case "Archipelago": templateArchipelago(); break;
|
||||
case "Atoll": templateAtoll(); break;
|
||||
case "Mediterranean": templateMediterranean(); break;
|
||||
case "Peninsula": templatePeninsula(); break;
|
||||
case "Pangea": templatePangea(); break;
|
||||
}
|
||||
|
||||
console.timeEnd('generateHeightmap');
|
||||
}
|
||||
|
||||
// parse template step
|
||||
function addStep(a1, a2, a3, a4, a5) {
|
||||
if (a1 === "Hill") addHill(a2, a3, a4, a5); else
|
||||
if (a1 === "Pit") addPit(a2, a3, a4, a5); else
|
||||
if (a1 === "Range") addRange(a2, a3, a4, a5); else
|
||||
if (a1 === "Trough") addTrough(a2, a3, a4, a5); else
|
||||
if (a1 === "Strait") addStrait(a2, a3); else
|
||||
if (a1 === "Add") modify(a3, a2, 1); else
|
||||
if (a1 === "Multiply") modify(a3, 0, a2); else
|
||||
if (a1 === "Smooth") smooth(a2);
|
||||
}
|
||||
|
||||
// Heighmap Template: Volcano
|
||||
function templateVolcano() {
|
||||
addStep("Hill", "1", "90-100", "44-56", "40-60");
|
||||
addStep("Multiply", .8, "50-100");
|
||||
addStep("Range", "1.5", "30-55", "45-55", "40-60");
|
||||
addStep("Smooth", 2);
|
||||
addStep("Hill", "1.5", "25-35", "25-30", "20-75");
|
||||
addStep("Hill", "1", "25-35", "75-80", "25-75");
|
||||
addStep("Hill", "0.5", "20-25", "10-15", "20-25");
|
||||
}
|
||||
|
||||
// Heighmap Template: High Island
|
||||
function templateHighIsland() {
|
||||
addStep("Hill", "1", "90-100", "65-75", "47-53");
|
||||
addStep("Add", 5, "all");
|
||||
addStep("Hill", "6", "20-23", "25-55", "45-55");
|
||||
addStep("Range", "1", "40-50", "45-55", "45-55");
|
||||
addStep("Smooth", 2);
|
||||
addStep("Trough", "2-3", "20-30", "20-30", "20-30");
|
||||
addStep("Trough", "2-3", "20-30", "60-80", "70-80");
|
||||
addStep("Hill", "1", "10-15", "60-60", "50-50");
|
||||
addStep("Hill", "1.5", "13-16", "15-20", "20-75");
|
||||
addStep("Multiply", .8, "20-100");
|
||||
addStep("Range", "1.5", "30-40", "15-85", "30-40");
|
||||
addStep("Range", "1.5", "30-40", "15-85", "60-70");
|
||||
addStep("Pit", "2-3", "10-15", "15-85", "20-80");
|
||||
}
|
||||
|
||||
// Heighmap Template: Low Island
|
||||
function templateLowIsland() {
|
||||
addStep("Hill", "1", "90-99", "60-80", "45-55");
|
||||
addStep("Hill", "4-5", "25-35", "20-65", "40-60");
|
||||
addStep("Range", "1", "40-50", "45-55", "45-55");
|
||||
addStep("Smooth", 3);
|
||||
addStep("Trough", "1.5", "20-30", "15-85", "20-30");
|
||||
addStep("Trough", "1.5", "20-30", "15-85", "70-80");
|
||||
addStep("Hill", "1.5", "10-15", "5-15", "20-80");
|
||||
addStep("Hill", "1", "10-15", "85-95", "70-80");
|
||||
addStep("Pit", "3-5", "10-15", "15-85", "20-80");
|
||||
addStep("Multiply", .4, "20-100");
|
||||
}
|
||||
|
||||
// Heighmap Template: Continents
|
||||
function templateContinents() {
|
||||
addStep("Hill", "1", "80-85", "75-80", "40-60");
|
||||
addStep("Hill", "1", "80-85", "20-25", "40-60");
|
||||
addStep("Multiply", .22, "20-100");
|
||||
addStep("Hill", "5-6", "15-20", "25-75", "20-82");
|
||||
addStep("Range", ".8", "30-60", "5-15", "20-45");
|
||||
addStep("Range", ".8", "30-60", "5-15", "55-80");
|
||||
addStep("Range", "0-3", "30-60", "80-90", "20-80");
|
||||
addStep("Trough", "3-4", "15-20", "15-85", "20-80");
|
||||
addStep("Strait", "2", "vertical");
|
||||
addStep("Smooth", 2);
|
||||
addStep("Trough", "1-2", "5-10", "45-55", "45-55");
|
||||
addStep("Pit", "3-4", "10-15", "15-85", "20-80");
|
||||
addStep("Hill", "1", "5-10", "40-60", "40-60");
|
||||
}
|
||||
|
||||
// Heighmap Template: Archipelago
|
||||
function templateArchipelago() {
|
||||
addStep("Add", 11, "all");
|
||||
addStep("Range", "2-3", "40-60", "20-80", "20-80");
|
||||
addStep("Hill", "5", "15-20", "10-90", "30-70");
|
||||
addStep("Hill", "2", "10-15", "10-30", "20-80");
|
||||
addStep("Hill", "2", "10-15", "60-90", "20-80");
|
||||
addStep("Smooth", 3);
|
||||
addStep("Trough", "10", "20-30", "5-95", "5-95");
|
||||
addStep("Strait", "2", "vertical");
|
||||
addStep("Strait", "2", "horizontal");
|
||||
}
|
||||
|
||||
// Heighmap Template: Atoll
|
||||
function templateAtoll() {
|
||||
addStep("Hill", "1", "75-80", "50-60", "45-55");
|
||||
addStep("Hill", "1.5", "30-50", "25-75", "30-70");
|
||||
addStep("Hill", ".5", "30-50", "25-35", "30-70");
|
||||
addStep("Smooth", 1);
|
||||
addStep("Multiply", .2, "25-100");
|
||||
addStep("Hill", ".5", "10-20", "50-55", "48-52");
|
||||
}
|
||||
|
||||
// Heighmap Template: Mediterranean
|
||||
function templateMediterranean() {
|
||||
addStep("Range", "3-4", "30-50", "0-100", "0-10");
|
||||
addStep("Range", "3-4", "30-50", "0-100", "90-100");
|
||||
addStep("Hill", "5-6", "30-70", "0-100", "0-5");
|
||||
addStep("Hill", "5-6", "30-70", "0-100", "95-100");
|
||||
addStep("Smooth", 1);
|
||||
addStep("Hill", "2-3", "30-70", "0-5", "20-80");
|
||||
addStep("Hill", "2-3", "30-70", "95-100", "20-80");
|
||||
addStep("Multiply", .8, "land");
|
||||
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
|
||||
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
|
||||
}
|
||||
|
||||
// Heighmap Template: Peninsula
|
||||
function templatePeninsula() {
|
||||
addStep("Range", "2-3", "20-35", "40-50", "0-15");
|
||||
addStep("Add", 5, "all");
|
||||
addStep("Hill", "1", "90-100", "10-90", "0-5");
|
||||
addStep("Add", 13, "all");
|
||||
addStep("Hill", "3-4", "3-5", "5-95", "80-100");
|
||||
addStep("Hill", "1-2", "3-5", "5-95", "40-60");
|
||||
addStep("Trough", "5-6", "10-25", "5-95", "5-95");
|
||||
addStep("Smooth", 3);
|
||||
}
|
||||
|
||||
// Heighmap Template: Pangea
|
||||
function templatePangea() {
|
||||
addStep("Hill", "1-2", "25-40", "15-50", "0-10");
|
||||
addStep("Hill", "1-2", "5-40", "50-85", "0-10");
|
||||
addStep("Hill", "1-2", "25-40", "50-85", "90-100");
|
||||
addStep("Hill", "1-2", "5-40", "15-50", "90-100");
|
||||
addStep("Hill", "8-12", "20-40", "20-80", "48-52");
|
||||
addStep("Smooth", 2);
|
||||
addStep("Multiply", .7, "land");
|
||||
addStep("Trough", "3-4", "25-35", "5-95", "10-20");
|
||||
addStep("Trough", "3-4", "25-35", "5-95", "80-90");
|
||||
addStep("Range", "5-6", "30-40", "10-90", "35-65");
|
||||
}
|
||||
|
||||
const addHill = function(count, height, rangeX, rangeY) {
|
||||
count = getNumberInRange(count);
|
||||
while (count >= 1 || Math.random() < count) {addOneHill(); count--;}
|
||||
|
||||
function addOneHill() {
|
||||
const change = new Uint8Array( cells.h.length);
|
||||
let limit = 0, start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
do {
|
||||
const x = getPointInRange(rangeX, graphWidth);
|
||||
const y = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(x, y);
|
||||
limit++;
|
||||
} while (cells.h[start] + h > 90 && limit < 50)
|
||||
|
||||
change[start] = h;
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
|
||||
for (const c of cells.c[q]) {
|
||||
if (change[c]) continue;
|
||||
change[c] = change[q] ** .98 * (Math.random() * .2 + .9);
|
||||
if (change[c] > 1) queue.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
cells.h = cells.h.map((h, i) => lim(h + change[i]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const addPit = function(count, height, rangeX, rangeY) {
|
||||
count = getNumberInRange(count);
|
||||
while (count >= 1 || Math.random() < count) {addOnePit(); count--;}
|
||||
|
||||
function addOnePit() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
let limit = 0, start;
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
do {
|
||||
const x = getPointInRange(rangeX, graphWidth);
|
||||
const y = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(x, y);
|
||||
limit++;
|
||||
} while (cells.h[start] < 20 && limit < 50)
|
||||
|
||||
const queue = [start];
|
||||
while (queue.length) {
|
||||
const q = queue.shift();
|
||||
h = h ** .98 * (Math.random() * .2 + .9);
|
||||
if (h < 1) return;
|
||||
|
||||
cells.c[q].forEach(function(c, i) {
|
||||
if (used[c]) return;
|
||||
cells.h[c] = lim(cells.h[c] - h * (Math.random() * .2 + .9));
|
||||
used[c] = 1;
|
||||
queue.push(c);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addRange = function(count, height, rangeX, rangeY) {
|
||||
count = getNumberInRange(count);
|
||||
while (count >= 1 || Math.random() < count) {addOneRange(); count--;}
|
||||
|
||||
function addOneRange() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
// find start and end points
|
||||
const startX = getPointInRange(rangeX, graphWidth);
|
||||
const startY = getPointInRange(rangeY, graphHeight);
|
||||
|
||||
let dist = 0, limit = 0, endX, endY;
|
||||
do {
|
||||
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
||||
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||
limit++;
|
||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 3) && limit < 50)
|
||||
|
||||
let range = getRange(findGridCell(startX, startY), findGridCell(endX, endY));
|
||||
|
||||
// get main ridge
|
||||
function getRange(cur, end) {
|
||||
const range = [cur];
|
||||
used[cur] = 1;
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function(e) {
|
||||
if (used[e]) return;
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > .85) diff = diff / 2;
|
||||
if (diff < min) {min = diff; cur = e;}
|
||||
});
|
||||
if (min === Infinity) return range;
|
||||
range.push(cur);
|
||||
used[cur] = 1;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
// add height to ridge and cells around
|
||||
let queue = range.slice(), i = 0;
|
||||
while (queue.length) {
|
||||
const frontier = queue.slice();
|
||||
queue = [], i++;
|
||||
frontier.forEach(i => {
|
||||
cells.h[i] = lim(cells.h[i] + h * (Math.random() * .3 + .85));
|
||||
});
|
||||
h = h ** .82 - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
cells.c[f].forEach(i => {
|
||||
if (!used[i]) {queue.push(i); used[i] = 1;}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// generate prominences
|
||||
range.forEach((cur, d) => {
|
||||
if (d%6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||
cur = min;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const addTrough = function(count, height, rangeX, rangeY) {
|
||||
count = getNumberInRange(count);
|
||||
while (count >= 1 || Math.random() < count) {addOneTrough(); count--;}
|
||||
|
||||
function addOneTrough() {
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
let h = lim(getNumberInRange(height));
|
||||
|
||||
// find start and end points
|
||||
let limit = 0, startX, startY, start, dist = 0, endX, endY;
|
||||
do {
|
||||
startX = getPointInRange(rangeX, graphWidth);
|
||||
startY = getPointInRange(rangeY, graphHeight);
|
||||
start = findGridCell(startX, startY);
|
||||
limit++;
|
||||
} while (cells.h[start] < 20 && limit < 50)
|
||||
|
||||
limit = 0;
|
||||
do {
|
||||
endX = Math.random() * graphWidth * .8 + graphWidth * .1;
|
||||
endY = Math.random() * graphHeight * .7 + graphHeight * .15;
|
||||
dist = Math.abs(endY - startY) + Math.abs(endX - startX);
|
||||
limit++;
|
||||
} while ((dist < graphWidth / 8 || dist > graphWidth / 2) && limit < 50)
|
||||
|
||||
let range = getRange(start, findGridCell(endX, endY));
|
||||
|
||||
// get main ridge
|
||||
function getRange(cur, end) {
|
||||
const range = [cur];
|
||||
used[cur] = 1;
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function(e) {
|
||||
if (used[e]) return;
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > .8) diff = diff / 2;
|
||||
if (diff < min) {min = diff; cur = e;}
|
||||
});
|
||||
if (min === Infinity) return range;
|
||||
range.push(cur);
|
||||
used[cur] = 1;
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
// add height to ridge and cells around
|
||||
let queue = range.slice(), i = 0;
|
||||
while (queue.length) {
|
||||
const frontier = queue.slice();
|
||||
queue = [], i++;
|
||||
frontier.forEach(i => {
|
||||
cells.h[i] = lim(cells.h[i] - h * (Math.random() * .3 + .85));
|
||||
});
|
||||
h = h ** .8 - 1;
|
||||
if (h < 2) break;
|
||||
frontier.forEach(f => {
|
||||
cells.c[f].forEach(i => {
|
||||
if (!used[i]) {queue.push(i); used[i] = 1;}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// generate prominences
|
||||
range.forEach((cur, d) => {
|
||||
if (d%6 !== 0) return;
|
||||
for (const l of d3.range(i)) {
|
||||
const min = cells.c[cur][d3.scan(cells.c[cur], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
//debug.append("circle").attr("cx", p[min][0]).attr("cy", p[min][1]).attr("r", 1);
|
||||
cells.h[min] = (cells.h[cur] * 2 + cells.h[min]) / 3;
|
||||
cur = min;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const addStrait = function(width, direction = "vertical") {
|
||||
width = Math.min(getNumberInRange(width), grid.cellsX/3);
|
||||
if (width < 1 && Math.random() < width) return;
|
||||
const used = new Uint8Array(cells.h.length);
|
||||
const vert = direction === "vertical";
|
||||
const startX = vert ? Math.floor(Math.random() * graphWidth * .4 + graphWidth * .3) : 5;
|
||||
const startY = vert ? 5 : Math.floor(Math.random() * graphHeight * .4 + graphHeight * .3);
|
||||
const endX = vert ? Math.floor((graphWidth - startX) - (graphWidth * .1) + (Math.random() * graphWidth * .2)) : graphWidth - 5;
|
||||
const endY = vert ? graphHeight - 5 : Math.floor((graphHeight - startY) - (graphHeight * .1) + (Math.random() * graphHeight * .2));
|
||||
|
||||
const start = findGridCell(startX, startY), end = findGridCell(endX, endY);
|
||||
let range = getRange(start, end);
|
||||
const query = [];
|
||||
|
||||
function getRange(cur, end) {
|
||||
const range = [];
|
||||
|
||||
while (cur !== end) {
|
||||
let min = Infinity;
|
||||
cells.c[cur].forEach(function(e) {
|
||||
let diff = (p[end][0] - p[e][0]) ** 2 + (p[end][1] - p[e][1]) ** 2;
|
||||
if (Math.random() > 0.8) diff = diff / 2;
|
||||
if (diff < min) {min = diff; cur = e;}
|
||||
});
|
||||
range.push(cur);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
const step = .1 / width;
|
||||
|
||||
while (width > 0) {
|
||||
const exp = .9 - step * width;
|
||||
range.forEach(function(r) {
|
||||
cells.c[r].forEach(function(e) {
|
||||
if (used[e]) return;
|
||||
used[e] = 1;
|
||||
query.push(e);
|
||||
cells.h[e] **= exp;
|
||||
if (cells.h[e] > 100) cells.h[e] = 5;
|
||||
});
|
||||
|
||||
range = query.slice();
|
||||
});
|
||||
width--;
|
||||
}
|
||||
}
|
||||
|
||||
const modify = function(range, add, mult, power) {
|
||||
const min = range === "land" ? 20 : range === "all" ? 0 : +range.split("-")[0];
|
||||
const max = range === "land" || range === "all" ? 100 : +range.split("-")[1];
|
||||
grid.cells.h = grid.cells.h.map(h => h >= min && h <= max ? mod(h) : h);
|
||||
|
||||
function mod(v) {
|
||||
if (add) v = min === 20 ? Math.max(v + add, 20) : v + add;
|
||||
if (mult !== 1) v = min === 20 ? (v-20) * mult + 20 : v * mult;
|
||||
if (power) v = min === 20 ? (v-20) ** power + 20 : v ** power;
|
||||
return lim(v);
|
||||
}
|
||||
}
|
||||
|
||||
const smooth = function(fr = 2) {
|
||||
cells.h = cells.h.map((h, i) => {
|
||||
const a = [h];
|
||||
cells.c[i].forEach(c => a.push(cells.h[c]));
|
||||
return lim((h * (fr-1) + d3.mean(a)) / fr);
|
||||
});
|
||||
}
|
||||
|
||||
function getPointInRange(range, length) {
|
||||
if (typeof range !== "string") {console.error("Range should be a string"); return;}
|
||||
const min = range.split("-")[0]/100 || 0;
|
||||
const max = range.split("-")[1]/100 || 100;
|
||||
return rand(min * length, max * length);
|
||||
}
|
||||
|
||||
return {generate, addHill, addRange, addTrough, addStrait, addPit, smooth, modify};
|
||||
|
||||
})));
|
||||
152
modules/names-generator.js
Normal file
152
modules/names-generator.js
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Names = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
const chains = [];
|
||||
|
||||
// calculate Markov chain for a namesbase
|
||||
const calculateChain = function(b) {
|
||||
const chain = [];
|
||||
const d = nameBase[b].join(" ").toLowerCase();
|
||||
|
||||
for (let i = -1, prev = " ", str = ""; i < d.length - 2; prev = str, i += str.length, str = "") {
|
||||
let v = 0, f = " ";
|
||||
|
||||
for (let c=i+1; str.length < 5; c++) {
|
||||
if (d[c] === undefined) break;
|
||||
str += d[c];
|
||||
if (str === " ") break;
|
||||
if (d[c] !== "o" && d[c] !== "e" && vowel(d[c]) && d[c+1] === d[c]) break;
|
||||
if (d[c+2] === " ") {str += d[c+1]; break;}
|
||||
if (vowel(d[c])) v++;
|
||||
if (v && vowel(d[c+2])) break;
|
||||
}
|
||||
|
||||
if (i >= 0) f = d[i];
|
||||
if (chain[f] === undefined) chain[f] = [];
|
||||
chain[f].push(str);
|
||||
}
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
// update chain for specific base
|
||||
const updateChain = (b) => chains[b] = nameBase[b] ? calculateChain(b) : null;
|
||||
|
||||
// update chains for all used bases
|
||||
const updateChains = () => chains.forEach((c, i) => chains[i] = nameBase[i] ? calculateChain(i) : null);
|
||||
|
||||
// generate name using Markov's chain
|
||||
const getBase = function(base, min, max, dupl, multi) {
|
||||
if (base === undefined) {console.error("Please define a base"); return;}
|
||||
if (!chains[base]) chains[base] = nameBase[base] ? calculateChain(base) : null;
|
||||
|
||||
const data = chains[base];
|
||||
if (!data || data[" "] === undefined) {
|
||||
tip("Namesbase " + base + " is incorrect. Please checl in namesbase editor", false, "error");
|
||||
console.error("nameBase " + base + " is incorrect!");
|
||||
return "ERROR";
|
||||
}
|
||||
|
||||
if (!min) min = nameBases[base].min;
|
||||
if (!max) max = nameBases[base].max;
|
||||
if (!dupl) dupl = nameBases[base].d;
|
||||
if (!multi) multi = nameBases[base].m;
|
||||
|
||||
let v = data[" "], cur = v[rand(v.length-1)], w = "";
|
||||
for (let i=0; i < 21; i++) {
|
||||
if (cur === " " && Math.random() > multi) {
|
||||
if (w.length < min) {cur = ""; w = ""; v = data[" "];} else break;
|
||||
} else {
|
||||
if ((w+cur).length > max) {
|
||||
if (w.length < min) w += cur;
|
||||
break;
|
||||
} else if (cur === " " && w.length+1 < min) {
|
||||
cur = "";
|
||||
v = data[" "];
|
||||
} else {
|
||||
v = data[cur.slice(-1)];
|
||||
}
|
||||
}
|
||||
|
||||
w += cur;
|
||||
cur = v[rand(v.length - 1)];
|
||||
}
|
||||
|
||||
// parse word to get a final name
|
||||
let name = [...w].reduce(function(r, c, i, d) {
|
||||
if (c === d[i+1] && !dupl.includes(c)) return r; // duplication is not allowed
|
||||
if (!r.length) return c.toUpperCase();
|
||||
if (r.slice(-1) === " ") return r + c.toUpperCase();
|
||||
if (c === "a" && d[i+1] === "e") return r; // "ae" => "e"
|
||||
if (c === " " && i+1 === d.length) return r;
|
||||
// remove consonant before 2 consonants
|
||||
if (i+2 < d.length && !vowel(c) && !vowel(d[i+1]) && !vowel(d[i+2])) return r;
|
||||
if (i+2 < d.length && c === d[i+1] && c === d[i+2]) return r; // remove tree same letters in a row
|
||||
return r + c;
|
||||
}, "");
|
||||
|
||||
if (name.length < 2) name = nameBase[base][rand(nameBase[base].length-1)]; // rare case when no name generated
|
||||
return name;
|
||||
}
|
||||
|
||||
// generate name for culture
|
||||
const getCulture = function(culture, min, max, dupl, multi) {
|
||||
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||
const base = pack.cultures[culture].base;
|
||||
return getBase(base, min, max, dupl, multi);
|
||||
}
|
||||
|
||||
// generate state name based on capital or random name and culture-specific suffix
|
||||
const getState = function(name, culture) {
|
||||
if (name === undefined) {console.error("Please define a base name"); return;}
|
||||
if (culture === undefined) {console.error("Please define a culture"); return;}
|
||||
const base = pack.cultures[culture].base;
|
||||
|
||||
// exclude endings inappropriate for states name
|
||||
if (name.includes(" ")) name = capitalize(name.replace(/ /g, "").toLowerCase()); // don't allow multiword state names
|
||||
if (name.length > 6 && name.slice(-4) === "berg") name = name.slice(0,-4); // remove -berg for any
|
||||
if (base === 5 && ["sk", "ev", "ov"].includes(name.slice(-2))) name = name.slice(0,-2); // remove -sk/-ev/-ov for Ruthenian
|
||||
else if (base === 1 && name.length > 5 && name.slice(-3) === "ton") name = name.slice(0,-3); // remove -ton for English
|
||||
else if (base === 12) return vowel(name.slice(-1)) ? name : name + "u"; // Japanese ends on any vowel or -u
|
||||
else if (base === 18 && Math.random() < .4) name = vowel(name.slice(0,1).toLowerCase()) ? "Al" + name.toLowerCase() : "Al " + name; // Arabic starts with -Al
|
||||
|
||||
// define if suffix should be used
|
||||
if (name.length > 3 && vowel(name.slice(-1))) {
|
||||
if (vowel(name.slice(-2,-1)) && Math.random() < .85) name = name.slice(0,-2); // 85% for vv
|
||||
else if (Math.random() < .7) name = name.slice(0,-1); // ~60% for cv
|
||||
else return name;
|
||||
} else if (Math.random() < .4) return name; // 60% for cc and vc
|
||||
|
||||
// define suffix
|
||||
let suffix = "";
|
||||
const rnd = Math.random(), l = name.length;
|
||||
if (base === 3) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Italian
|
||||
else if (base === 4) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Spanish
|
||||
else if (base === 13) suffix = rnd < .03 && l < 7 ? "terra" : "ia"; // Portuguese
|
||||
else if (base === 2) suffix = rnd < .03 && l < 7 ? "terre" : "ia"; // French
|
||||
else if (base === 0) suffix = rnd < .5 && l < 7 ? "land" : "ia"; // German
|
||||
else if (base === 1) suffix = rnd < .4 && l < 7 ? "land" : "ia"; // English
|
||||
else if (base === 6) suffix = rnd < .3 && l < 7 ? "land" : "ia"; // Nordic
|
||||
else if (base === 7) suffix = rnd < .1 ? "eia" : "ia"; // Greek
|
||||
else if (base === 9) suffix = rnd < .35 ? "maa" : "ia"; // Finnic
|
||||
else if (base === 15) suffix = rnd < .6 && l < 6 ? "orszag" : "ia"; // Hungarian
|
||||
else if (base === 16) suffix = rnd < .5 ? "stan" : "ya"; // Turkish
|
||||
else if (base === 10) suffix = "guk"; // Korean
|
||||
else if (base === 11) suffix = " Guo"; // Chinese
|
||||
else if (base === 14) suffix = rnd < .6 && l < 7 ? "tlan" : "co"; // Nahuatl
|
||||
else if (base === 17) suffix = rnd < .8 ? "a" : "ia"; // Berber
|
||||
else if (base === 18) suffix = rnd < .8 ? "a" : "ia"; // Arabic
|
||||
else suffix = "ia" // other
|
||||
|
||||
if (name.slice(-1 * suffix.length) === suffix) return name; // no suffix if name already ends with it
|
||||
const s1 = suffix.charAt(0);
|
||||
if (name.slice(-1) === s1) name = name.slice(0, -1); // remove name last letter if it's a suffix first letter
|
||||
if (vowel(s1) === vowel(name.slice(-1)) && vowel(s1) === vowel(name.slice(-2,-1))) name = name.slice(0, -1); // remove name last char if 2 last chars are the same type as suffix's 1st
|
||||
return name + suffix;
|
||||
}
|
||||
|
||||
return {getBase, getCulture, getState, updateChain, updateChains};
|
||||
})));
|
||||
95
modules/ocean-layers.js
Normal file
95
modules/ocean-layers.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.OceanLayers = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
let cells, vertices, pointsN, used;
|
||||
|
||||
var OceanLayers = function OceanLayers() {
|
||||
const outline = outlineLayersInput.value;
|
||||
if (outline === "none") return;
|
||||
console.time("drawOceanLayers");
|
||||
|
||||
cells = grid.cells, pointsN = grid.cells.i.length, vertices = grid.vertices;
|
||||
const limits = outline === "random" ? randomizeOutline() : outline.split(",").map(s => +s);
|
||||
markupOcean(limits);
|
||||
|
||||
const chains = [];
|
||||
const opacity = rn(0.4 / limits.length, 2);
|
||||
used = new Uint8Array(pointsN); // to detect already passed cells
|
||||
|
||||
for (const i of cells.i) {
|
||||
const t = cells.t[i];
|
||||
if (used[i] || !limits.includes(t)) continue;
|
||||
const start = findStart(i, t);
|
||||
if (!start) continue;
|
||||
used[i] = 1;
|
||||
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start.c][0]).attr("cy", vertices.p[start.c][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
|
||||
|
||||
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||
const relaxation = 1 + t * -2; // select only n-th point
|
||||
const relaxed = chain.filter((v, i) => i % relaxation === 0 || vertices.c[v].some(c => c >= pointsN));
|
||||
if (relaxed.length >= 3) chains.push([t, relaxed.map(v => vertices.p[v])]);
|
||||
}
|
||||
//debug.selectAll("text").data(cells.i).enter().append("text").attr("font-size", 2).attr("x", d => grid.points[d][0]).attr("y", d => grid.points[d][1]).text(d => cells.t[d]+","+used[d]);
|
||||
|
||||
for (const t of limits) {
|
||||
const path = chains.filter(c => c[0] === t).map(c => round(lineGen(c[1]))).join();
|
||||
if (path) oceanLayers.append("path").attr("d", path).attr("fill", "#ecf2f9").style("opacity", opacity);
|
||||
// For each layer there should outer ring. If no, layer will be upside down. Need to fix it in the future
|
||||
}
|
||||
|
||||
// find eligible cell 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 >= pointsN)); // map border cell
|
||||
return cells.v[i][cells.c[i].findIndex(c => cells.t[c] < t || !cells.t[c])];
|
||||
}
|
||||
|
||||
console.timeEnd("drawOceanLayers");
|
||||
}
|
||||
|
||||
function randomizeOutline() {
|
||||
const limits = [];
|
||||
let odd = 0.2
|
||||
for (let l = -9; l < 0; l++) {
|
||||
if (Math.random() < odd) {odd = 0.2; limits.push(l);}
|
||||
else {odd *= 2;}
|
||||
}
|
||||
return limits;
|
||||
}
|
||||
|
||||
function markupOcean(limits) {
|
||||
// Define ocean cells type based on distance form land
|
||||
for (let t = -2; t >= limits[0]-1; t--) {
|
||||
for (let i = 0; i < pointsN; i++) {
|
||||
if (cells.t[i] !== t+1) continue;
|
||||
cells.c[i].forEach(function(e) {if (!cells.t[e]) cells.t[e] = t;});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 10000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.t[c] === t).forEach(c => used[c] = 1);
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
const c0 = !cells.t[c[0]] || cells.t[c[0]] === t-1;
|
||||
const c1 = !cells.t[c[1]] || cells.t[c[1]] === t-1;
|
||||
const c2 = !cells.t[c[2]] || cells.t[c[2]] === t-1;
|
||||
if (v[0] !== undefined && v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== undefined && v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== undefined && v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
chain.push(chain[0]); // push first vertex as the last one
|
||||
return chain;
|
||||
}
|
||||
|
||||
return OceanLayers;
|
||||
|
||||
})));
|
||||
80
modules/relief-icons.js
Normal file
80
modules/relief-icons.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.ReliefIcons = factory());
|
||||
}(this, (function () {'use strict';
|
||||
|
||||
var ReliefIcons = function ReliefIcons() {
|
||||
console.time('drawRelief');
|
||||
terrain.selectAll("*").remove();
|
||||
const density = +styleReliefDensityInput.value;
|
||||
if (!density) return;
|
||||
|
||||
const size = 1.6, mod = .2 * size; // size modifier;s
|
||||
const relief = []; // t: type, c: cell, x: centerX, y: centerY, s: size;
|
||||
const cells = pack.cells;
|
||||
|
||||
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 b = cells.biome[i];
|
||||
if (height < 50 && biomesData.iconsDensity[b] === 0) continue; // no icons for this biome
|
||||
const polygon = getPackPolygon(i);
|
||||
const x = d3.extent(polygon, p => p[0]), y = d3.extent(polygon, p => p[1]);
|
||||
const e = [Math.ceil(x[0]), Math.ceil(y[0]), Math.floor(x[1]), Math.floor(y[1])]; // polygon box
|
||||
|
||||
if (height < 50) placeBiomeIcons(i, b); else placeReliefIcons(i);
|
||||
|
||||
function placeBiomeIcons() {
|
||||
const iconsDensity = biomesData.iconsDensity[b] / 100;
|
||||
const radius = 2 / iconsDensity / density;
|
||||
if (Math.random() > iconsDensity * 10) return;
|
||||
|
||||
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
|
||||
if (!d3.polygonContains(polygon, [cx, cy])) continue;
|
||||
let h = rn((4 + Math.random()) * size, 2);
|
||||
const icon = getBiomeIcon(biomesData.icons[b]);
|
||||
if (icon === "#relief-grass-1") h *= 1.3;
|
||||
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
}
|
||||
}
|
||||
|
||||
function placeReliefIcons() {
|
||||
const radius = 2 / density;
|
||||
const [icon, h] = getReliefIcon(height);
|
||||
|
||||
for (const [cx, cy] of poissonDiscSampler(e[0], e[1], e[2], e[3], radius)) {
|
||||
if (!d3.polygonContains(polygon, [cx, cy])) continue;
|
||||
relief.push({t: icon, c: i, x: rn(cx-h, 2), y: rn(cy-h, 2), s: h*2});
|
||||
}
|
||||
}
|
||||
|
||||
function getReliefIcon(h) {
|
||||
const type = h > 70 ? "mount" : "hill";
|
||||
const size = h > 70 ? (h - 45) * mod : Math.min(Math.max((h - 40) * mod, 3), 6);
|
||||
return ["#relief-" + type + "-1", size];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sort relief icons by y+size
|
||||
relief.sort((a, b) => (a.y + a.s) - (b.y + b.s));
|
||||
|
||||
// append relief icons at once using pure js
|
||||
void function renderRelief() {
|
||||
let reliefHTML = "";
|
||||
for (const r of relief) {reliefHTML += `<use xlink:href="${r.t}" data-type="${r.t}" x=${r.x} y=${r.y} data-size=${r.s} width=${r.s} height=${r.s}></use>`;}
|
||||
terrain.html(reliefHTML);
|
||||
}()
|
||||
|
||||
console.timeEnd('drawRelief');
|
||||
}
|
||||
|
||||
function getBiomeIcon(i) {
|
||||
return "#relief-" + i[Math.floor(Math.random() * i.length)] + "-1";
|
||||
}
|
||||
|
||||
return ReliefIcons;
|
||||
|
||||
})));
|
||||
204
modules/river-generator.js
Normal file
204
modules/river-generator.js
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Rivers = factory());
|
||||
}(this, (function () {'use strict';
|
||||
|
||||
const generate = function Rivers() {
|
||||
console.time('generateRivers');
|
||||
Math.seedrandom(seed);
|
||||
const cells = pack.cells, p = cells.p, features = pack.features;
|
||||
features.forEach(f => {delete f.river; delete f.flux;});
|
||||
|
||||
const riversData = []; // rivers data
|
||||
cells.fl = new Uint16Array(cells.i.length); // water flux array
|
||||
cells.r = new Uint16Array(cells.i.length); // rivers array
|
||||
cells.conf = new Uint8Array(cells.i.length); // confluences array
|
||||
let riverNext = 1; // first river id is 1, not 0
|
||||
|
||||
void function drainWater() {
|
||||
const land = cells.i.filter(isLand).sort(highest);
|
||||
|
||||
land.forEach(function(i) {
|
||||
cells.fl[i] += grid.cells.prec[cells.g[i]]; // flux from precipitation
|
||||
const x = p[i][0], y = p[i][1];
|
||||
|
||||
// near-border cell: pour out of the screen
|
||||
if (cells.b[i]) {
|
||||
if (cells.r[i]) {
|
||||
const to = [];
|
||||
const min = Math.min(y, graphHeight - y, x, graphWidth - x);
|
||||
if (min === y) {to[0] = x; to[1] = 0;} else
|
||||
if (min === graphHeight - y) {to[0] = x; to[1] = graphHeight;} else
|
||||
if (min === x) {to[0] = 0; to[1] = y;} else
|
||||
if (min === graphWidth - x) {to[0] = graphWidth; to[1] = y;}
|
||||
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1]});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
|
||||
// allow only one river can flow thought a lake
|
||||
const cf = features[cells.f[i]]; // current cell feature
|
||||
if (cf.river && cf.river !== cells.r[i]) {
|
||||
cells.fl[i] = 0;
|
||||
}
|
||||
|
||||
if (cells.fl[i] < 30) {
|
||||
if (cells.h[min] >= 20) cells.fl[min] += cells.fl[i];
|
||||
return; // flux is too small to operate as river
|
||||
}
|
||||
|
||||
// Proclaim a new river
|
||||
if (!cells.r[i]) {
|
||||
cells.r[i] = riverNext;
|
||||
riversData.push({river: riverNext, cell: i, x, y});
|
||||
riverNext++;
|
||||
}
|
||||
|
||||
if (cells.r[min]) { // downhill cell already has river assigned
|
||||
if (cells.fl[min] < cells.fl[i]) {
|
||||
cells.conf[min] = cells.fl[min]; // confluence
|
||||
cells.r[min] = cells.r[i]; // re-assign river if downhill part has less flux
|
||||
} else cells.conf[min] += cells.fl[i]; // confluence
|
||||
} else cells.r[min] = cells.r[i]; // assign the river to the downhill cell
|
||||
|
||||
const nx = p[min][0], ny = p[min][1];
|
||||
if (cells.h[min] < 20) {
|
||||
// pour water to the sea haven
|
||||
riversData.push({river: cells.r[i], cell: cells.haven[i], x: nx, y: ny});
|
||||
} else {
|
||||
const mf = features[cells.f[min]]; // feature of min cell
|
||||
if (mf.type === "lake") {
|
||||
if (!mf.river || cells.fl[i] > mf.flux) {
|
||||
mf.river = cells.r[i]; // pour water to temporaly elevated lake
|
||||
mf.flux = cells.fl[i]; // entering flux
|
||||
}
|
||||
}
|
||||
cells.fl[min] += cells.fl[i]; // propagate flux
|
||||
riversData.push({river: cells.r[i], cell: min, x: nx, y: ny}); // add next River segment
|
||||
}
|
||||
|
||||
});
|
||||
}()
|
||||
|
||||
void function drawRivers() {
|
||||
const riverPaths = []; // to store data for all rivers
|
||||
|
||||
for (let r = 1; r <= riverNext; r++) {
|
||||
const riverSegments = riversData.filter(d => d.river === r);
|
||||
|
||||
if (riverSegments.length > 2) {
|
||||
const riverEnhanced = addMeandring(riverSegments);
|
||||
const width = rn(0.8 + Math.random() * 0.4, 1); // river width modifier
|
||||
const increment = rn(0.8 + Math.random() * 0.6, 1); // river bed widening modifier
|
||||
const path = getPath(riverEnhanced, width, increment);
|
||||
riverPaths.push([r, path, width, increment]);
|
||||
} else {
|
||||
// remove too short rivers
|
||||
riverSegments.filter(s => cells.r[s.cell] === r).forEach(s => cells.r[s.cell] = 0);
|
||||
}
|
||||
}
|
||||
|
||||
rivers.selectAll("path").remove();
|
||||
rivers.selectAll("path").data(riverPaths).enter()
|
||||
.append("path").attr("d", d => d[1]).attr("id", d => "river"+d[0])
|
||||
.attr("data-width", d => d[2]).attr("data-increment", d => d[3]);
|
||||
}()
|
||||
|
||||
console.timeEnd('generateRivers');
|
||||
}
|
||||
|
||||
// add more river points on 1/3 and 2/3 of length
|
||||
const addMeandring = function(segments, rndFactor = 0.3) {
|
||||
const riverEnhanced = []; // to store enhanced segments
|
||||
let side = 1; // to control meandring direction
|
||||
|
||||
for (let s = 0; s < segments.length; s++) {
|
||||
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
|
||||
const c = pack.cells.conf[segments[s].cell] || 0; // if segment is river confluence
|
||||
riverEnhanced.push([sX, sY, c]);
|
||||
|
||||
if (s+1 === segments.length) break; // do not enhance last segment
|
||||
|
||||
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
|
||||
const angle = Math.atan2(eY - sY, eX - sX);
|
||||
const sin = Math.sin(angle), cos = Math.cos(angle);
|
||||
const serpentine = 1 / (s + 1) + 0.3;
|
||||
const meandr = serpentine + Math.random() * rndFactor;
|
||||
if (Math.random() < 0.5) side *= -1; // change meandring direction in 50%
|
||||
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2;
|
||||
// if dist2 is big or river is small add extra points at 1/3 and 2/3 of segment
|
||||
if (dist2 > 64 || (dist2 > 16 && segments.length < 6)) {
|
||||
const p1x = (sX * 2 + eX) / 3 + side * -sin * meandr;
|
||||
const p1y = (sY * 2 + eY) / 3 + side * cos * meandr;
|
||||
if (Math.random() < 0.2) side *= -1; // change 2nd extra point meandring direction in 20%
|
||||
const p2x = (sX + eX * 2) / 3 + side * sin * meandr;
|
||||
const p2y = (sY + eY * 2) / 3 + side * cos * meandr;
|
||||
riverEnhanced.push([p1x, p1y], [p2x, p2y]);
|
||||
// if dist is medium or river is small add 1 extra middlepoint
|
||||
} else if (dist2 > 16 || segments.length < 6) {
|
||||
const p1x = (sX + eX) / 2 + side * -sin * meandr;
|
||||
const p1y = (sY + eY) / 2 + side * cos * meandr;
|
||||
riverEnhanced.push([p1x, p1y]);
|
||||
}
|
||||
|
||||
}
|
||||
return riverEnhanced;
|
||||
}
|
||||
|
||||
const getPath = function(points, width = 1, increment = 1) {
|
||||
let offset, extraOffset = .1; // starting river width (to make river source visible)
|
||||
const riverLength = points.reduce((s, v, i, p) => s + (i ? Math.hypot(v[0] - p[i-1][0], v[1] - p[i-1][1]) : 0), 0); // summ of segments length
|
||||
const widening = rn((1000 + (riverLength * 30)) * increment);
|
||||
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
|
||||
const last = points.length - 1;
|
||||
const factor = riverLength / points.length;
|
||||
|
||||
// first point
|
||||
let x = points[0][0], y = points[0][1], c;
|
||||
let angle = Math.atan2(y - points[1][1], x - points[1][0]);
|
||||
let sin = Math.sin(angle), cos = Math.cos(angle);
|
||||
let xLeft = x + -sin * extraOffset, yLeft = y + cos * extraOffset;
|
||||
riverPointsLeft.push([xLeft, yLeft]);
|
||||
let xRight = x + sin * extraOffset, yRight = y + -cos * extraOffset;
|
||||
riverPointsRight.unshift([xRight, yRight]);
|
||||
|
||||
// middle points
|
||||
for (let p = 1; p < last; p++) {
|
||||
x = points[p][0], y = points[p][1], c = points[p][2] || 0;
|
||||
const xPrev = points[p-1][0], yPrev = points[p - 1][1];
|
||||
const xNext = points[p+1][0], yNext = points[p + 1][1];
|
||||
angle = Math.atan2(yPrev - yNext, xPrev - xNext);
|
||||
sin = Math.sin(angle), cos = Math.cos(angle);
|
||||
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2 * width) + extraOffset;
|
||||
const confOffset = Math.atan(c * 5 / widening);
|
||||
extraOffset += confOffset;
|
||||
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
|
||||
riverPointsLeft.push([xLeft, yLeft]);
|
||||
xRight = x + sin * offset, yRight = y + -cos * offset;
|
||||
riverPointsRight.unshift([xRight, yRight]);
|
||||
}
|
||||
|
||||
// end point
|
||||
x = points[last][0], y = points[last][1], c = points[last][2];
|
||||
if (c) extraOffset += Math.atan(c * 10 / widening); // add extra width on river confluence
|
||||
angle = Math.atan2(points[last-1][1] - y, points[last-1][0] - x);
|
||||
sin = Math.sin(angle), cos = Math.cos(angle);
|
||||
xLeft = x + -sin * offset, yLeft = y + cos * offset;
|
||||
riverPointsLeft.push([xLeft, yLeft]);
|
||||
xRight = x + sin * offset, yRight = y + -cos * offset;
|
||||
riverPointsRight.unshift([xRight, yRight]);
|
||||
|
||||
// generate polygon path and return
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
const right = lineGen(riverPointsRight);
|
||||
let left = lineGen(riverPointsLeft);
|
||||
left = left.substring(left.indexOf("C"));
|
||||
return round(right + left, 2);
|
||||
}
|
||||
|
||||
return {generate, addMeandring, getPath};
|
||||
|
||||
})));
|
||||
252
modules/routes-generator.js
Normal file
252
modules/routes-generator.js
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Routes = factory());
|
||||
}(this, (function () {'use strict';
|
||||
|
||||
const getRoads = function() {
|
||||
console.time("generateMainRoads");
|
||||
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
const capitals = burgs.filter(b => b.capital);
|
||||
if (capitals.length < 2) return []; // not enought capitals to build main roads
|
||||
const paths = []; // array to store path segments
|
||||
|
||||
for (const b of capitals) {
|
||||
const connect = capitals.filter(c => c.i > b.i && c.feature === b.feature);
|
||||
if (!connect.length) continue;
|
||||
const farthest = d3.scan(connect, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||
const [from, exit] = findLandPath(b.cell, connect[farthest].cell, null);
|
||||
const segments = restorePath(b.cell, exit, "main", from);
|
||||
segments.forEach(s => paths.push(s));
|
||||
}
|
||||
|
||||
cells.i.forEach(i => cells.s[i] += cells.road[i] / 2); // add roads to suitability score
|
||||
console.timeEnd("generateMainRoads");
|
||||
return paths;
|
||||
}
|
||||
|
||||
const getTrails = function() {
|
||||
console.time("generateTrails");
|
||||
const cells = pack.cells, burgs = pack.burgs.filter(b => b.i && !b.removed);
|
||||
if (burgs.length < 2) return []; // not enought capitals to build main roads
|
||||
|
||||
let paths = []; // array to store path segments
|
||||
for (const f of pack.features.filter(f => f.land)) {
|
||||
const isle = burgs.filter(b => b.feature === f.i); // burgs on island
|
||||
if (isle.length < 2) continue;
|
||||
|
||||
isle.forEach(function(b, i) {
|
||||
let path = [];
|
||||
if (!i) {
|
||||
const farthest = d3.scan(isle, (a, c) => ((c.y - b.y) ** 2 + (c.x - b.x) ** 2) - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2));
|
||||
const to = isle[farthest].cell;
|
||||
if (cells.road[to]) return;
|
||||
const [from, exit] = findLandPath(b.cell, to, null);
|
||||
path = restorePath(b.cell, exit, "small", from);
|
||||
} else {
|
||||
if (cells.road[b.cell]) return;
|
||||
const [from, exit] = findLandPath(b.cell, null, true);
|
||||
if (exit === null) return;
|
||||
path = restorePath(b.cell, exit, "small", from);
|
||||
}
|
||||
if (path) paths = paths.concat(path);
|
||||
});
|
||||
}
|
||||
|
||||
console.timeEnd("generateTrails");
|
||||
return paths;
|
||||
}
|
||||
|
||||
const getSearoutes = function() {
|
||||
console.time("generateSearoutes");
|
||||
const cells = pack.cells, allPorts = pack.burgs.filter(b => b.port != 0 && !b.removed);
|
||||
if (allPorts.length < 2) return [];
|
||||
const bodies = new Set(allPorts.map(b => b.port)); // features with ports
|
||||
let from = [], exit = null, path = [], paths = []; // array to store path segments
|
||||
|
||||
bodies.forEach(function(f) {
|
||||
const ports = allPorts.filter(b => b.port === f);
|
||||
if (ports.length < 2) return;
|
||||
const first = ports[0].cell;
|
||||
|
||||
// directly connect first port with the farthest one on the same island to remove gap
|
||||
if (pack.features[f].type !== "lake") {
|
||||
const portsOnIsland = ports.filter(b => cells.f[b.cell] === cells.f[first]);
|
||||
if (portsOnIsland.length > 3) {
|
||||
const opposite = ports[d3.scan(portsOnIsland, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||
//debug.append("circle").attr("r", 1).attr("fill", "blue").attr("cx", pack.cells.p[first][0]).attr("cy", pack.cells.p[first][1])
|
||||
//debug.append("circle").attr("r", 1).attr("fill", "green").attr("cx", pack.cells.p[opposite][0]).attr("cy", pack.cells.p[opposite][1])
|
||||
[from, exit] = findOceanPath(opposite, first);
|
||||
from[first] = cells.haven[first];
|
||||
path = restorePath(opposite, first, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
}
|
||||
}
|
||||
|
||||
// directly connect first port with the farthest one
|
||||
const farthest = ports[d3.scan(ports, (a, b) => ((b.y - ports[0].y) ** 2 + (b.x - ports[0].x) ** 2) - ((a.y - ports[0].y) ** 2 + (a.x - ports[0].x) ** 2))].cell;
|
||||
[from, exit] = findOceanPath(farthest, first);
|
||||
from[first] = cells.haven[first];
|
||||
path = restorePath(farthest, first, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
|
||||
// indirectly connect first port with all other ports
|
||||
if (ports.length < 3) return;
|
||||
for (const p of ports) {
|
||||
if (p.cell === first || p.cell === farthest) continue;
|
||||
[from, exit] = findOceanPath(p.cell, first, true);
|
||||
//from[exit] = cells.haven[exit];
|
||||
const path = restorePath(p.cell, exit, "ocean", from);
|
||||
paths = paths.concat(path);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
console.timeEnd("generateSearoutes");
|
||||
return paths;
|
||||
}
|
||||
|
||||
const draw = function(main, small, ocean) {
|
||||
console.time("drawRoutes");
|
||||
const cells = pack.cells, burgs = pack.burgs;
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
|
||||
// main routes
|
||||
roads.selectAll("path").data(main).enter().append("path")
|
||||
.attr("id", (d, i) => "road" + i)
|
||||
.attr("d", d => round(lineGen(d.map(c => {
|
||||
const b = cells.burg[c];
|
||||
const x = b ? burgs[b].x : cells.p[c][0];
|
||||
const y = b ? burgs[b].y : cells.p[c][1];
|
||||
return [x, y];
|
||||
})), 1));
|
||||
|
||||
// small routes
|
||||
trails.selectAll("path").data(small).enter().append("path")
|
||||
.attr("id", (d, i) => "trail" + i)
|
||||
.attr("d", d => round(lineGen(d.map(c => {
|
||||
const b = cells.burg[c];
|
||||
const x = b ? burgs[b].x : cells.p[c][0];
|
||||
const y = b ? burgs[b].y : cells.p[c][1];
|
||||
return [x, y];
|
||||
})), 1));
|
||||
|
||||
// ocean routes
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
searoutes.selectAll("path").data(ocean).enter().append("path")
|
||||
.attr("id", (d, i) => "searoute" + i)
|
||||
.attr("d", d => round(lineGen(d.map(c => {
|
||||
const b = cells.burg[c];
|
||||
const x = b ? burgs[b].x : cells.p[c][0];
|
||||
const y = b ? burgs[b].y : cells.p[c][1];
|
||||
return [x, y];
|
||||
})), 1));
|
||||
|
||||
console.timeEnd("drawRoutes");
|
||||
}
|
||||
|
||||
const regenerate = function() {
|
||||
routes.selectAll("path").remove();
|
||||
pack.cells.road = new Uint16Array(pack.cells.i.length);
|
||||
const main = getRoads();
|
||||
const small = getTrails();
|
||||
const ocean = getSearoutes();
|
||||
draw(main, small, ocean);
|
||||
}
|
||||
|
||||
return {getRoads, getTrails, getSearoutes, draw, regenerate};
|
||||
|
||||
// Dijkstra's algorithm to find a land path
|
||||
function findLandPath(start, exit = null, toRoad = null) {
|
||||
const cells = pack.cells;
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [], from = [];
|
||||
const basicCost = 10;
|
||||
queue.queue({e: start, p: 0});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||
if (toRoad && cells.road[n]) return [from, n];
|
||||
|
||||
for (const c of cells.c[n]) {
|
||||
if (cells.h[c] < 20) continue; // ignore water cells
|
||||
const habitedCost = 100 - biomesData.habitability[cells.biome[c]];
|
||||
const heightCost = Math.abs(cells.h[c] - cells.h[n]) * 10;
|
||||
const cellCoast = basicCost + habitedCost + heightCost;
|
||||
const totalCost = p + (cells.road[c] || cells.burg[c] ? cellCoast / 3 : cellCoast);
|
||||
|
||||
if (from[c] || totalCost >= cost[c]) continue;
|
||||
from[c] = n;
|
||||
if (c === exit) return [from, exit];
|
||||
cost[c] = totalCost;
|
||||
queue.queue({e: c, p: totalCost});
|
||||
}
|
||||
|
||||
}
|
||||
return [from, exit];
|
||||
}
|
||||
|
||||
function restorePath(start, end, type, from) {
|
||||
const cells = pack.cells;
|
||||
const path = []; // to store all segments;
|
||||
let segment = [], current = end, prev = end;
|
||||
const score = type === "main" ? 5 : 1; // to incrade road score at cell
|
||||
|
||||
if (type === "ocean" || !cells.road[prev]) segment.push(end);
|
||||
if (!cells.road[prev]) cells.road[prev] = score;
|
||||
|
||||
for (let i = 0, limit = 1000; i < limit; i++) {
|
||||
if (!from[current]) break;
|
||||
current = from[current];
|
||||
|
||||
if (cells.road[current]) {
|
||||
if (segment.length) {
|
||||
segment.push(current);
|
||||
path.push(segment);
|
||||
if (segment[0] !== end) cells.road[segment[0]] += score; // crossroad
|
||||
if (current !== start) cells.road[current] += score; // crossroad
|
||||
}
|
||||
segment = [];
|
||||
prev = current;
|
||||
} else {
|
||||
if (prev) segment.push(prev);
|
||||
prev = null;
|
||||
segment.push(current);
|
||||
}
|
||||
|
||||
cells.road[current] += score;
|
||||
if (current === start) break;
|
||||
}
|
||||
|
||||
if (segment.length > 1) path.push(segment);
|
||||
return path;
|
||||
}
|
||||
|
||||
// find water paths
|
||||
function findOceanPath(start, exit = null, toRoute = null) {
|
||||
const cells = pack.cells;
|
||||
const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p});
|
||||
const cost = [], from = [];
|
||||
queue.queue({e: start, p: 0});
|
||||
|
||||
while (queue.length) {
|
||||
const next = queue.dequeue(), n = next.e, p = next.p;
|
||||
if (toRoute && n !== start && cells.road[n]) return [from, n];
|
||||
|
||||
for (const c of cells.c[n]) {
|
||||
if (cells.h[c] >= 20) continue; // ignore land cells
|
||||
const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2;
|
||||
const totalCost = p + (cells.road[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100));
|
||||
|
||||
if (from[c] || totalCost >= cost[c]) continue;
|
||||
from[c] = n;
|
||||
if (c === exit) return [from, exit];
|
||||
cost[c] = totalCost;
|
||||
queue.queue({e: c, p: totalCost});
|
||||
}
|
||||
|
||||
}
|
||||
return [from, exit];
|
||||
}
|
||||
|
||||
})));
|
||||
377
modules/save-and-load.js
Normal file
377
modules/save-and-load.js
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
// Functions to save and load the map
|
||||
"use strict";
|
||||
|
||||
// download map as SVG or PNG file
|
||||
function saveAsImage(type) {
|
||||
console.time("saveAsImage");
|
||||
|
||||
// clone svg
|
||||
const cloneEl = document.getElementById("map").cloneNode(true);
|
||||
cloneEl.id = "fantasyMap";
|
||||
document.getElementsByTagName("body")[0].appendChild(cloneEl);
|
||||
const clone = d3.select("#fantasyMap");
|
||||
|
||||
if (type === "svg") clone.select("#viewbox").attr("transform", null); // reset transform to show whole map
|
||||
if (layerIsOn("texture") && type === "png") clone.select("#texture").remove(); // no texture for png
|
||||
|
||||
// for each g element get inline style
|
||||
const emptyG = clone.append("g").node();
|
||||
const defaultStyles = window.getComputedStyle(emptyG);
|
||||
clone.selectAll("g, #ruler > g > *, #scaleBar > text").each(function(d) {
|
||||
const compStyle = window.getComputedStyle(this);
|
||||
let style = "";
|
||||
for (let i=0; i < compStyle.length; i++) {
|
||||
const key = compStyle[i];
|
||||
const value = compStyle.getPropertyValue(key);
|
||||
// Firefox mask hack
|
||||
if (key === "mask-image" && value !== defaultStyles.getPropertyValue(key)) {
|
||||
style += "mask-image: url('#land');";
|
||||
continue;
|
||||
}
|
||||
if (key === "cursor") continue; // cursor should be default
|
||||
if (this.hasAttribute(key)) continue; // don't add style if there is the same attribute
|
||||
if (value === defaultStyles.getPropertyValue(key)) continue;
|
||||
style += key + ':' + value + ';';
|
||||
}
|
||||
if (style != "") this.setAttribute('style', style);
|
||||
});
|
||||
emptyG.remove();
|
||||
|
||||
// load fonts as dataURI so they will be available in downloaded svg/png
|
||||
GFontToDataURI(getFontsToLoad()).then(cssRules => {
|
||||
clone.select("defs").append("style").text(cssRules.join('\n'));
|
||||
const svg_xml = (new XMLSerializer()).serializeToString(clone.node());
|
||||
clone.remove();
|
||||
const blob = new Blob([svg_xml], {type: 'image/svg+xml;charset=utf-8'});
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.target = "_blank";
|
||||
|
||||
if (type === "png") {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
canvas.width = svgWidth * pngResolutionInput.value;
|
||||
canvas.height = svgHeight * pngResolutionInput.value;
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = function() {
|
||||
window.URL.revokeObjectURL(url);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
link.download = "fantasy_map_" + Date.now() + ".png";
|
||||
canvas.toBlob(function(blob) {
|
||||
link.href = window.URL.createObjectURL(blob);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
window.setTimeout(function() {
|
||||
canvas.remove();
|
||||
window.URL.revokeObjectURL(link.href);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
link.download = "fantasy_map_" + Date.now() + ".svg";
|
||||
link.href = url;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
console.timeEnd("saveAsImage");
|
||||
});
|
||||
}
|
||||
|
||||
// get non-standard fonts used for labels to fetch them from web
|
||||
function getFontsToLoad() {
|
||||
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"];
|
||||
|
||||
const fontsInUse = []; // to store fonts currently in use
|
||||
labels.selectAll("g").each(function() {
|
||||
const font = this.dataset.font;
|
||||
if (!font) return;
|
||||
if (webSafe.includes(font)) return; // do not fetch web-safe fonts
|
||||
if (!fontsInUse.includes(font)) fontsInUse.push(font);
|
||||
});
|
||||
return "https://fonts.googleapis.com/css?family=" + fontsInUse.join("|");
|
||||
}
|
||||
|
||||
// code from Kaiido's answer https://stackoverflow.com/questions/42402584/how-to-use-google-fonts-in-canvas-when-drawing-dom-objects-in-svg
|
||||
function GFontToDataURI(url) {
|
||||
return fetch(url) // first fecth the embed stylesheet page
|
||||
.then(resp => resp.text()) // we only need the text of it
|
||||
.then(text => {
|
||||
let s = document.createElement('style');
|
||||
s.innerHTML = text;
|
||||
document.head.appendChild(s);
|
||||
let styleSheet = Array.prototype.filter.call(
|
||||
document.styleSheets,
|
||||
sS => sS.ownerNode === s)[0];
|
||||
let FontRule = rule => {
|
||||
let src = rule.style.getPropertyValue('src');
|
||||
let url = src.split('url(')[1].split(')')[0];
|
||||
return {rule: rule, src: src, url: url.substring(url.length - 1, 1)};
|
||||
};
|
||||
let fontRules = [], fontProms = [];
|
||||
|
||||
for (let r of styleSheet.cssRules) {
|
||||
let fR = FontRule(r);
|
||||
fontRules.push(fR);
|
||||
fontProms.push(
|
||||
fetch(fR.url) // fetch the actual font-file (.woff)
|
||||
.then(resp => resp.blob())
|
||||
.then(blob => {
|
||||
return new Promise(resolve => {
|
||||
let f = new FileReader();
|
||||
f.onload = e => resolve(f.result);
|
||||
f.readAsDataURL(blob);
|
||||
})
|
||||
})
|
||||
.then(dataURL => {
|
||||
return fR.rule.cssText.replace(fR.url, dataURL);
|
||||
})
|
||||
)
|
||||
}
|
||||
document.head.removeChild(s); // clean up
|
||||
return Promise.all(fontProms); // wait for all this has been done
|
||||
});
|
||||
}
|
||||
|
||||
// Save in .map format
|
||||
function saveMap() {
|
||||
if (customization) {tip("Map cannot be saved when is in edit mode, please exit the mode and re-try", false, "error"); return;}
|
||||
console.time("saveMap");
|
||||
const date = new Date();
|
||||
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
|
||||
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
|
||||
const params = [version, license, dateString, seed, graphWidth, graphHeight].join("|");
|
||||
const options = [distanceUnit.value, distanceScale.value, areaUnit.value, heightUnit.value, heightExponent.value, temperatureScale.value,
|
||||
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value,
|
||||
equatorOutput.value, equidistanceOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(winds)].join("|");
|
||||
const coords = JSON.stringify(mapCoordinates);
|
||||
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
|
||||
const notesData = JSON.stringify(notes);
|
||||
|
||||
// set transform values to default
|
||||
svg.attr("width", graphWidth).attr("height", graphHeight);
|
||||
const transform = d3.zoomTransform(svg.node());
|
||||
viewbox.attr("transform", null);
|
||||
const svg_xml = (new XMLSerializer()).serializeToString(svg.node());
|
||||
|
||||
const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features});
|
||||
const features = JSON.stringify(pack.features);
|
||||
const cultures = JSON.stringify(pack.cultures);
|
||||
const states = JSON.stringify(pack.states);
|
||||
const burgs = JSON.stringify(pack.burgs);
|
||||
|
||||
const data = [params, options, coords, biomes, notesData, svg_xml,
|
||||
gridGeneral, grid.cells.h, grid.cells.prec, grid.cells.f, grid.cells.t, grid.cells.temp,
|
||||
features, cultures, states, burgs,
|
||||
pack.cells.biome, pack.cells.burg, pack.cells.conf, pack.cells.culture, pack.cells.fl,
|
||||
pack.cells.pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state].join("\r\n");
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const dataURL = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.download = "fantasy_map_" + Date.now() + ".map";
|
||||
link.href = dataURL;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// restore initial values
|
||||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||
zoom.transform(svg, transform);
|
||||
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(dataURL);}, 2000);
|
||||
console.timeEnd("saveMap");
|
||||
}
|
||||
|
||||
function uploadFile(file, callback) {
|
||||
console.time("loadMap");
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function(fileLoadedEvent) {
|
||||
const dataLoaded = fileLoadedEvent.target.result;
|
||||
const data = dataLoaded.split("\r\n");
|
||||
|
||||
const mapVersion = data[0].split("|")[0] || data[0];
|
||||
if (mapVersion === version) {parseLoadedData(data); return;}
|
||||
|
||||
const archive = "<a href='https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog' target='_blank'>archived version</a>";
|
||||
const parsed = parseFloat(mapVersion);
|
||||
let message = "", load = false;
|
||||
if (isNaN(parsed) || data.length < 26 || !data[5]) {
|
||||
message = `The file you are trying to load is not a valid .map file`;
|
||||
} else if (parsed < 0.7) {
|
||||
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.
|
||||
<br>Please keep using an ${archive}`;
|
||||
} else {
|
||||
load = true;
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}). The map will be auto-updated.
|
||||
<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
}
|
||||
alertMessage.innerHTML = message;
|
||||
$("#alert").dialog({title: "Version conflict", buttons: {
|
||||
OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
|
||||
}});
|
||||
};
|
||||
|
||||
fileReader.readAsText(file, "UTF-8");
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
function parseLoadedData(data) {
|
||||
closeDialogs();
|
||||
|
||||
void function parseParameters() {
|
||||
const params = data[0].split("|");
|
||||
if (params[3]) {seed = params[3]; optionsSeed.value = seed;}
|
||||
if (params[4]) graphWidth = +params[4];
|
||||
if (params[5]) graphHeight = +params[5];
|
||||
}()
|
||||
|
||||
void function parseOptions() {
|
||||
const options = data[1].split("|");
|
||||
if (options[0]) distanceUnit.value = distanceUnitOutput.innerHTML = options[0];
|
||||
if (options[1]) distanceScale.value = distanceScaleSlider.value = options[1];
|
||||
if (options[2]) areaUnit.value = options[2];
|
||||
if (options[3]) heightUnit.value= options[3];
|
||||
if (options[4]) heightExponent.value = heightExponentSlider.value = options[4];
|
||||
if (options[5]) temperatureScale.value = options[5];
|
||||
if (options[6]) barSize.value = barSizeSlider.value = options[6];
|
||||
if (options[7] !== undefined) barLabel.value = options[7];
|
||||
if (options[8] !== undefined) barBackOpacity.value = options[8];
|
||||
if (options[9]) barBackColor.value = options[9];
|
||||
if (options[10]) barPosX.value = options[10];
|
||||
if (options[11]) barPosY.value = options[11];
|
||||
if (options[12]) populationRate.value = populationRateSlider.value = options[12];
|
||||
if (options[13]) urbanization.value = urbanizationSlider.value = options[13];
|
||||
if (options[14]) equatorInput.value = equatorOutput.value = options[14];
|
||||
if (options[15]) equidistanceInput.value = equidistanceOutput.value = options[15];
|
||||
if (options[16]) temperatureEquatorInput.value = temperatureEquatorOutput.value = options[16];
|
||||
if (options[17]) temperaturePoleInput.value = temperaturePoleOutput.value = options[17];
|
||||
if (options[18]) precInput.value = precOutput.value = options[18];
|
||||
if (options[19]) winds = JSON.parse(options[19]);
|
||||
}()
|
||||
|
||||
void function parseConfiguration() {
|
||||
if (data[2]) mapCoordinates = JSON.parse(data[2]);
|
||||
if (data[4]) notes = JSON.parse(data[4]);
|
||||
|
||||
const biomes = data[3].split("|");
|
||||
const name = biomes[2].split(",");
|
||||
if (name.length !== biomesData.name.length) {
|
||||
console.error("Biomes data is not correct and will not be loaded");
|
||||
return;
|
||||
}
|
||||
biomesData.color = biomes[0].split(",");
|
||||
biomesData.habitability = biomes[1].split(",");
|
||||
biomesData.name = name;
|
||||
}()
|
||||
|
||||
void function replaceSVG() {
|
||||
svg.remove();
|
||||
document.body.insertAdjacentHTML("afterbegin", data[5]);
|
||||
}()
|
||||
|
||||
void function redefineElements() {
|
||||
svg = d3.select("#map");
|
||||
defs = svg.select("#deftemp");
|
||||
viewbox = svg.select("#viewbox");
|
||||
scaleBar = svg.select("#scaleBar");
|
||||
ocean = viewbox.select("#ocean");
|
||||
oceanLayers = ocean.select("#oceanLayers");
|
||||
oceanPattern = ocean.select("#oceanPattern");
|
||||
lakes = viewbox.select("#lakes");
|
||||
landmass = viewbox.select("#landmass");
|
||||
texture = viewbox.select("#texture");
|
||||
terrs = viewbox.select("#terrs");
|
||||
biomes = viewbox.select("#biomes");
|
||||
cells = viewbox.select("#cells");
|
||||
gridOverlay = viewbox.select("#gridOverlay");
|
||||
coordinates = viewbox.select("#coordinates");
|
||||
compass = viewbox.select("#compass");
|
||||
rivers = viewbox.select("#rivers");
|
||||
terrain = viewbox.select("#terrain");
|
||||
cults = viewbox.select("#cults");
|
||||
regions = viewbox.select("#regions");
|
||||
statesBody = regions.select("#statesBody");
|
||||
statesHalo = regions.select("#statesHalo");
|
||||
borders = viewbox.select("#borders");
|
||||
routes = viewbox.select("#routes");
|
||||
roads = routes.select("#roads");
|
||||
trails = routes.select("#trails");
|
||||
searoutes = routes.select("#searoutes");
|
||||
temperature = viewbox.select("#temperature");
|
||||
coastline = viewbox.select("#coastline");
|
||||
prec = viewbox.select("#prec");
|
||||
population = viewbox.select("#population");
|
||||
labels = viewbox.select("#labels");
|
||||
icons = viewbox.select("#icons");
|
||||
burgIcons = icons.select("#burgIcons");
|
||||
anchors = icons.select("#anchors");
|
||||
markers = viewbox.select("#markers");
|
||||
ruler = viewbox.select("#ruler");
|
||||
debug = viewbox.select("#debug");
|
||||
freshwater = lakes.select("#freshwater");
|
||||
salt = lakes.select("#salt");
|
||||
burgLabels = labels.select("#burgLabels");
|
||||
}()
|
||||
|
||||
void function parseGridData() {
|
||||
grid = JSON.parse(data[6]);
|
||||
calculateVoronoi(grid, grid.points);
|
||||
grid.cells.h = Uint8Array.from(data[7].split(","));
|
||||
grid.cells.prec = Uint8Array.from(data[8].split(","));
|
||||
grid.cells.f = Uint16Array.from(data[9].split(","));
|
||||
grid.cells.t = Int8Array.from(data[10].split(","));
|
||||
grid.cells.temp = Int8Array.from(data[11].split(","));
|
||||
}()
|
||||
|
||||
void function parsePackData() {
|
||||
pack = {};
|
||||
reGraph();
|
||||
reMarkFeatures();
|
||||
pack.features = JSON.parse(data[12]);
|
||||
pack.cultures = JSON.parse(data[13]);
|
||||
pack.states = JSON.parse(data[14]);
|
||||
pack.burgs = JSON.parse(data[15]);
|
||||
|
||||
pack.cells.biome = Uint8Array.from(data[16].split(","));
|
||||
pack.cells.burg = Uint16Array.from(data[17].split(","));
|
||||
pack.cells.conf = Uint8Array.from(data[18].split(","));
|
||||
pack.cells.culture = Uint8Array.from(data[19].split(","));
|
||||
pack.cells.fl = Uint16Array.from(data[20].split(","));
|
||||
pack.cells.pop = Uint16Array.from(data[21].split(","));
|
||||
pack.cells.r = Uint16Array.from(data[22].split(","));
|
||||
pack.cells.road = Uint16Array.from(data[23].split(","));
|
||||
pack.cells.s = Uint16Array.from(data[24].split(","));
|
||||
pack.cells.state = Uint8Array.from(data[25].split(","));
|
||||
}()
|
||||
|
||||
void function restoreLayersState() {
|
||||
if (texture.style("display") !== "none" && texture.select("image").size()) turnButtonOn("toggleTexture"); else turnButtonOff("toggleTexture");
|
||||
if (terrs.selectAll("*").size()) turnButtonOn("toggleHeight"); else turnButtonOff("toggleHeight");
|
||||
if (biomes.selectAll("*").size()) turnButtonOn("toggleBiomes"); else turnButtonOff("toggleBiomes");
|
||||
if (cells.selectAll("*").size()) turnButtonOn("toggleCells"); else turnButtonOff("toggleCells");
|
||||
if (gridOverlay.selectAll("*").size()) turnButtonOn("toggleGrid"); else turnButtonOff("toggleGrid");
|
||||
if (coordinates.selectAll("*").size()) turnButtonOn("toggleCoordinates"); else turnButtonOff("toggleCoordinates");
|
||||
if (compass.style("display") !== "none" && compass.select("use").size()) turnButtonOn("toggleCompass"); else turnButtonOff("toggleCompass");
|
||||
if (rivers.style("display") !== "none") turnButtonOn("toggleRivers"); else turnButtonOff("toggleRivers");
|
||||
if (terrain.style("display") !== "none" && terrain.selectAll("*").size()) turnButtonOn("toggleRelief"); else turnButtonOff("toggleRelief");
|
||||
if (cults.selectAll("*").size()) turnButtonOn("toggleCultures"); else turnButtonOff("toggleCultures");
|
||||
if (statesBody.selectAll("*").size()) turnButtonOn("toggleStates"); else turnButtonOff("toggleStates");
|
||||
if (borders.style("display") !== "none" && borders.selectAll("*").size()) turnButtonOn("toggleBorders"); else turnButtonOff("toggleBorders");
|
||||
if (routes.style("display") !== "none" && routes.selectAll("path").size()) turnButtonOn("toggleRoutes"); else turnButtonOff("toggleRoutes");
|
||||
if (temperature.selectAll("*").size()) turnButtonOn("toggleTemp"); else turnButtonOff("toggleTemp");
|
||||
if (population.select("#rural").selectAll("*").size()) turnButtonOn("togglePopulation"); else turnButtonOff("togglePopulation");
|
||||
if (prec.selectAll("circle").size()) turnButtonOn("togglePrec"); else turnButtonOff("togglePrec");
|
||||
if (labels.style("display") !== "none") turnButtonOn("toggleLabels"); else turnButtonOff("toggleLabels");
|
||||
if (icons.style("display") !== "none") turnButtonOn("toggleIcons"); else turnButtonOff("toggleIcons");
|
||||
if (markers.style("display") !== "none") turnButtonOn("toggleMarkers"); else turnButtonOff("toggleMarkers");
|
||||
if (ruler.style("display") !== "none") turnButtonOn("toggleRulers"); else turnButtonOff("toggleRulers");
|
||||
if (scaleBar.style("display") !== "none") turnButtonOn("toggleScaleBar"); else turnButtonOff("toggleScaleBar");
|
||||
}()
|
||||
|
||||
changeMapSize();
|
||||
restoreDefaultEvents();
|
||||
invokeActiveZooming();
|
||||
tip("Map is loaded");
|
||||
console.timeEnd("loadMap");
|
||||
}
|
||||
303
modules/ui/biomes-editor.js
Normal file
303
modules/ui/biomes-editor.js
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
"use strict";
|
||||
function editBiomes() {
|
||||
if (customization) return;
|
||||
closeDialogs("#biomesEditor, .stable");
|
||||
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||
|
||||
const body = document.getElementById("biomesBody");
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
refreshBiomesEditor();
|
||||
|
||||
if (modules.editBiomes) return;
|
||||
modules.editBiomes = true;
|
||||
|
||||
$("#biomesEditor").dialog({
|
||||
title: "Biomes Editor", width: fitContent(), close: closeBiomesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("biomesEditorRefresh").addEventListener("click", refreshBiomesEditor);
|
||||
document.getElementById("biomesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("biomesManually").addEventListener("click", enterBiomesCustomizationMode);
|
||||
document.getElementById("biomesManuallyApply").addEventListener("click", applyBiomesChange);
|
||||
document.getElementById("biomesManuallyCancel").addEventListener("click", exitBiomesCustomizationMode);
|
||||
document.getElementById("biomesRestore").addEventListener("click", restoreInitialBiomes);
|
||||
document.getElementById("biomesRegenerateReliefIcons").addEventListener("click", regenerateIcons);
|
||||
document.getElementById("biomesExport").addEventListener("click", downloadBiomesData);
|
||||
|
||||
function refreshBiomesEditor() {
|
||||
biomesCollectStatistics();
|
||||
biomesEditorAddLines();
|
||||
}
|
||||
|
||||
function biomesCollectStatistics() {
|
||||
const cells = pack.cells;
|
||||
biomesData.cells = new Uint32Array(biomesData.i.length);
|
||||
biomesData.area = new Uint32Array(biomesData.i.length);
|
||||
biomesData.rural = new Uint32Array(biomesData.i.length);
|
||||
biomesData.urban = new Uint32Array(biomesData.i.length);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const b = cells.biome[i];
|
||||
biomesData.cells[b] += 1;
|
||||
biomesData.area[b] += cells.area[i];
|
||||
biomesData.rural[b] += cells.pop[i];
|
||||
if (cells.burg[i]) biomesData.urban[b] += pack.burgs[cells.burg[i]].population;
|
||||
}
|
||||
}
|
||||
|
||||
function biomesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const b = biomesData;
|
||||
let lines = "", totalArea = 0, totalPopulation = 0;;
|
||||
|
||||
for (const i of b.i) {
|
||||
if (!i) continue; // ignore marine (water) biome
|
||||
const area = b.area[i] * distanceScale.value ** 2;
|
||||
const rural = b.rural[i] * populationRate.value;
|
||||
const urban = b.urban[i] * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
lines += `<div class="states biomes" data-id="${i}" data-name="${b.name[i]}" data-habitability="${b.habitability[i]}"
|
||||
data-cells=${b.cells[i]} data-area=${area} data-population=${population} data-color=${b.color[i]}>
|
||||
<input data-tip="Biome color. Click to change" class="stateColor" type="color" value="${b.color[i]}">
|
||||
<input data-tip="Biome name. Click and type to change" class="biomeName" value="${b.name[i]}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Biome habitability percent">%</span>
|
||||
<input data-tip="Biome habitability percent. Click and set new value to change" type="number" min=0 max=999 step=1 class="biomeHabitability" value=${b.habitability[i]}>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="biomeCells">${b.cells[i]}</div>
|
||||
<span data-tip="Biome area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Biome area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="biomePopulation">${si(population)}</div>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
biomesFooterBiomes.innerHTML = b.i.length - 1;
|
||||
biomesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||
biomesFooterArea.innerHTML = si(totalArea) + unit;
|
||||
biomesFooterPopulation.innerHTML = si(totalPopulation);
|
||||
biomesFooterArea.dataset.area = totalArea;
|
||||
biomesFooterPopulation.dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseenter", ev => biomeHighlightOn(ev)));
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("mouseleave", ev => biomeHighlightOff(ev)));
|
||||
body.querySelectorAll("div.biomes").forEach(el => el.addEventListener("click", selectBiomeOnLineClick));
|
||||
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", biomeChangeColor));
|
||||
body.querySelectorAll("div > input.biomeName").forEach(el => el.addEventListener("input", biomeChangeName));
|
||||
body.querySelectorAll("div > input.biomeHabitability").forEach(el => el.addEventListener("change", biomeChangeHabitability));
|
||||
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
applySorting(biomesHeader);
|
||||
$("#biomesEditor").dialog();
|
||||
}
|
||||
|
||||
function biomeHighlightOn(event) {
|
||||
if (customization === 6) return;
|
||||
const biome = +event.target.dataset.id;
|
||||
biomes.select("#biome"+biome).raise().transition(animate).attr("stroke-width", 2).attr("stroke", "#cd4c11");
|
||||
}
|
||||
|
||||
function biomeHighlightOff(event) {
|
||||
if (customization === 6) return;
|
||||
const biome = +event.target.dataset.id;
|
||||
const color = biomesData.color[biome];
|
||||
biomes.select("#biome"+biome).transition().attr("stroke-width", .7).attr("stroke", color);
|
||||
}
|
||||
|
||||
function biomeChangeColor() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
biomesData.color[biome] = this.value;
|
||||
biomes.select("#biome"+biome).attr("fill", this.value).attr("stroke", this.value);
|
||||
}
|
||||
|
||||
function biomeChangeName() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
biomesData.name[biome] = this.value;
|
||||
}
|
||||
|
||||
function biomeChangeHabitability() {
|
||||
const biome = +this.parentNode.dataset.id;
|
||||
const failed = isNaN(+this.value) || +this.value < 0 || +this.value > 999;
|
||||
if (failed) {
|
||||
this.value = biomesData.habitability[biome];
|
||||
tip("Please provide a valid number in range 0-999", false, "error");
|
||||
return;
|
||||
}
|
||||
biomesData.habitability[biome] = +this.value;
|
||||
this.parentNode.dataset.habitability = this.value;
|
||||
recalculatePopulation();
|
||||
refreshBiomesEditor();
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
const totalCells = +biomesFooterCells.innerHTML;
|
||||
const totalArea = +biomesFooterArea.dataset.area;
|
||||
const totalPopulation = +biomesFooterPopulation.dataset.population;
|
||||
|
||||
body.querySelectorAll(":scope> div").forEach(function(el) {
|
||||
el.querySelector(".biomeCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||
el.querySelector(".biomePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||
});
|
||||
} else {
|
||||
body.dataset.type = "absolute";
|
||||
biomesEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
function regenerateIcons() {
|
||||
ReliefIcons();
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
}
|
||||
|
||||
function downloadBiomesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
let data = "Id,Biome,Color,Habitability,Cells,Area "+unit+",Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.habitability + "%,";
|
||||
data += el.dataset.cells + ",";
|
||||
data += el.dataset.area + ",";
|
||||
data += el.dataset.population + "\n";
|
||||
});
|
||||
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
document.body.appendChild(link);
|
||||
link.download = "states_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function enterBiomesCustomizationMode() {
|
||||
if (!layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
customization = 6;
|
||||
biomes.append("g").attr("id", "temp");
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
|
||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "block");
|
||||
body.querySelector("div.biomes").classList.add("selected");
|
||||
|
||||
tip("Click on biome to select, drag the circle to change biome", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragBiomeBrush))
|
||||
.on("click", selectBiomeOnMapClick)
|
||||
.on("touchmove mousemove", moveBiomeBrush);
|
||||
}
|
||||
|
||||
function selectBiomeOnLineClick() {
|
||||
if (customization !== 6) return;
|
||||
const selected = body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectBiomeOnMapClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) {tip("You cannot reassign water via biomes. Please edit the Heightmap to change water", false, "error"); return;}
|
||||
|
||||
const assigned = biomes.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||
const biome = assigned.size() ? +assigned.attr("data-biome") : pack.cells.biome[i];
|
||||
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
body.querySelector("div[data-id='"+biome+"']").classList.add("selected");
|
||||
}
|
||||
|
||||
function dragBiomeBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +biomesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeBiomeForSelection(selection);
|
||||
}
|
||||
|
||||
// change region within selection
|
||||
function changeBiomeForSelection(selection) {
|
||||
const temp = biomes.select("#temp");
|
||||
const selected = body.querySelector("div.selected");
|
||||
|
||||
const biomeNew = selected.dataset.id;
|
||||
const color = biomesData.color[biomeNew];
|
||||
|
||||
selection.forEach(function(i) {
|
||||
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||
const biomeOld = exists.size() ? +exists.attr("data-biome") : pack.cells.biome[i];
|
||||
if (biomeNew === biomeOld) return;
|
||||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-biome", biomeNew).attr("fill", color).attr("stroke", color);
|
||||
else temp.append("polygon").attr("data-cell", i).attr("data-biome", biomeNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||
});
|
||||
}
|
||||
|
||||
function moveBiomeBrush() {
|
||||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +biomesManuallyBrush.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
}
|
||||
|
||||
function applyBiomesChange() {
|
||||
const changed = biomes.select("#temp").selectAll("polygon");
|
||||
changed.each(function() {
|
||||
const i = +this.dataset.cell;
|
||||
const b = +this.dataset.biome;
|
||||
pack.cells.biome[i] = b;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
drawBiomes();
|
||||
refreshBiomesEditor();
|
||||
}
|
||||
exitBiomesCustomizationMode();
|
||||
}
|
||||
|
||||
function exitBiomesCustomizationMode() {
|
||||
customization = 0;
|
||||
biomes.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#biomesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.querySelectorAll("#biomesBottom > div").forEach(el => el.style.display = "none");
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = document.querySelector("#biomesBody > div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function restoreInitialBiomes() {
|
||||
biomesData = applyDefaultBiomesSystem();
|
||||
defineBiomes();
|
||||
drawBiomes();
|
||||
recalculatePopulation();
|
||||
refreshBiomesEditor();
|
||||
}
|
||||
|
||||
function closeBiomesEditor() {
|
||||
//biomes.on("mousemove", null).on("mouseleave", null);
|
||||
exitBiomesCustomizationMode();
|
||||
}
|
||||
}
|
||||
336
modules/ui/burg-editor.js
Normal file
336
modules/ui/burg-editor.js
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
"use strict";
|
||||
function editBurg() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const id = +d3.event.target.dataset.id;
|
||||
elSelected = burgLabels.select("[data-id='" + id + "']");
|
||||
burgLabels.selectAll("text").call(d3.drag().on("start", dragBurgLabel)).classed("draggable", true);
|
||||
|
||||
selectBurgGroup(event.target);
|
||||
document.getElementById("burgNameInput").value = elSelected.text();
|
||||
const my = elSelected.attr("id") == d3.event.target.id ? "center bottom" : "center top+10";
|
||||
const at = elSelected.attr("id") == d3.event.target.id ? "top" : "bottom";
|
||||
|
||||
$("#burgEditor").dialog({
|
||||
title: "Edit Burg: " + elSelected.text(), resizable: false,
|
||||
position: {my, at, of: d3.event.target, collision: "fit"},
|
||||
close: closeBurgEditor
|
||||
});
|
||||
|
||||
if (modules.editBurg) return;
|
||||
modules.editBurg = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("burgGroupShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("burgGroupHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("burgSelectGroup").addEventListener("change", changeGroup);
|
||||
document.getElementById("burgInputGroup").addEventListener("change", createNewGroup);
|
||||
document.getElementById("burgAddGroup").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("burgRemoveGroup").addEventListener("click", removeBurgsGroup);
|
||||
|
||||
document.getElementById("burgNameShow").addEventListener("click", showNameSection);
|
||||
document.getElementById("burgNameHide").addEventListener("click", hideNameSection);
|
||||
document.getElementById("burgNameInput").addEventListener("input", changeName);
|
||||
document.getElementById("burgNameReCulture").addEventListener("click", generateNameCulture);
|
||||
document.getElementById("burgNameReRandom").addEventListener("click", generateNameRandom);
|
||||
|
||||
document.getElementById("burgSeeInMFCG").addEventListener("click", openInMFCG);
|
||||
document.getElementById("burgRelocate").addEventListener("click", toggleRelocateBurg);
|
||||
document.getElementById("burglLegend").addEventListener("click", editBurgLegend);
|
||||
document.getElementById("burgRemove").addEventListener("click", removeSelectedBurg);
|
||||
|
||||
function dragBurgLabel() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
this.setAttribute("transform", `translate(${(dx+x)},${(dy+y)})`);
|
||||
tip('Use dragging for fine-tuning only, to actually move burg use "Relocate" button', false, "warning");
|
||||
});
|
||||
}
|
||||
|
||||
function selectBurgGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
const select = document.getElementById("burgSelectGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
burgLabels.selectAll("g").each(function() {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
});
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("burgGroupSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("burgGroupSection").style.display = "none";
|
||||
document.getElementById("burgInputGroup").style.display = "none";
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
document.getElementById("burgSelectGroup").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function changeGroup() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
moveBurgToGroup(id, this.value);
|
||||
}
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
if (burgInputGroup.style.display === "none") {
|
||||
burgInputGroup.style.display = "inline-block";
|
||||
burgInputGroup.focus();
|
||||
burgSelectGroup.style.display = "none";
|
||||
} else {
|
||||
burgInputGroup.style.display = "none";
|
||||
burgSelectGroup.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = +elSelected.attr("data-id");
|
||||
const oldGroup = elSelected.node().parentNode.id;
|
||||
|
||||
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
|
||||
|
||||
const labelG = document.querySelector("#burgLabels > #"+oldGroup);
|
||||
const iconG = document.querySelector("#burgIcons > #"+oldGroup);
|
||||
const anchorG = document.querySelector("#anchors > #"+oldGroup);
|
||||
|
||||
// just rename if only 1 element left
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
if (oldGroup !== "cities" && oldGroup !== "towns" && count === 1) {
|
||||
document.getElementById("burgSelectGroup").selectedOptions[0].remove();
|
||||
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
labelG.id = group;
|
||||
iconG.id = group;
|
||||
if (anchor) anchorG.id = group;
|
||||
return;
|
||||
}
|
||||
|
||||
// create new groups
|
||||
document.getElementById("burgSelectGroup").options.add(new Option(group, group, false, true));
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("burgInputGroup").value = "";
|
||||
|
||||
const newLabelG = document.querySelector("#burgLabels").appendChild(labelG.cloneNode(false));
|
||||
newLabelG.id = group;
|
||||
const newIconG = document.querySelector("#burgIcons").appendChild(iconG.cloneNode(false));
|
||||
newIconG.id = group;
|
||||
if (anchor) {
|
||||
const newAnchorG = document.querySelector("#anchors").appendChild(anchorG.cloneNode(false));
|
||||
newAnchorG.id = group;
|
||||
}
|
||||
moveBurgToGroup(id, group);
|
||||
}
|
||||
|
||||
function removeBurgsGroup() {
|
||||
const group = elSelected.node().parentNode;
|
||||
const basic = group.id === "cities" || group.id === "towns";
|
||||
|
||||
const burgsInGroup = [];
|
||||
for (let i=0; i < group.children.length; i++) {
|
||||
burgsInGroup.push(+group.children[i].dataset.id);
|
||||
}
|
||||
const burgsToRemove = burgsInGroup.filter(b => !pack.burgs[b].capital);
|
||||
const capital = burgsToRemove.length < burgsInGroup.length;
|
||||
|
||||
alertMessage.innerHTML = `Are you sure you want to remove
|
||||
${basic || capital ? "all elements in the group" : "the entire burg group"}?
|
||||
<br>Please note that capital burgs will not be deleted.
|
||||
<br><br>Burgs to be removed: ${burgsToRemove.length}`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
$("#burgEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
burgsToRemove.forEach(b => removeBurg(b));
|
||||
|
||||
if (!basic && !capital) {
|
||||
// entirely remove group
|
||||
const labelG = document.querySelector("#burgLabels > #"+group.id);
|
||||
const iconG = document.querySelector("#burgIcons > #"+group.id);
|
||||
const anchorG = document.querySelector("#anchors > #"+group.id);
|
||||
if (labelG) labelG.remove();
|
||||
if (iconG) iconG.remove();
|
||||
if (anchorG) anchorG.remove();
|
||||
}
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showNameSection() {
|
||||
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("burgNameSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideNameSection() {
|
||||
document.querySelectorAll("#burgEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("burgNameSection").style.display = "none";
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
pack.burgs[id].name = burgNameInput.value;
|
||||
elSelected.text(burgNameInput.value);
|
||||
}
|
||||
|
||||
function generateNameCulture() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const culture = pack.burgs[id].culture;
|
||||
burgNameInput.value = Names.getCulture(culture);
|
||||
changeName();
|
||||
}
|
||||
|
||||
function generateNameRandom() {
|
||||
const base = rand(nameBase.length-1);
|
||||
burgNameInput.value = Names.getBase(base);
|
||||
changeName();
|
||||
}
|
||||
|
||||
function openInMFCG() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const name = elSelected.text();
|
||||
const cell = pack.burgs[id].cell;
|
||||
const pop = rn(pack.burgs[id].population);
|
||||
const size = Math.max(Math.min(pop, 65), 6);
|
||||
|
||||
// MFCG seed is FMG map seed + burg id padded to 4 chars with zeros
|
||||
const s = seed + id.padStart(4, 0);
|
||||
const hub = +pack.cells.road[cell] > 50;
|
||||
const river = pack.cells.r[cell] ? 1 : 0;
|
||||
const coast = +pack.burgs[id].port;
|
||||
|
||||
const half = rn(pop) % 2;
|
||||
const most = (+id + rn(pop)) % 3 ? 1 : 0;
|
||||
const walls = pop > 10 && half || pop > 20 && most || pop > 30 ? 1 : 0;;
|
||||
const shanty = pop > 40 && half || pop > 60 && most || pop > 80 ? 1 : 0;
|
||||
const temple = pop > 50 && half || pop > 80 && most || pop > 100 ? 1 : 0;
|
||||
|
||||
const url = `http://fantasycities.watabou.ru/?name=${name}&size=${size}&seed=${s}&hub=${hub}&random=0&continuous=0&river=${river}&coast=${coast}&citadel=${half}&plaza=${half}&temple=${temple}&walls=${walls}&shantytown=${shanty}`;
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
function toggleRelocateBurg() {
|
||||
const toggler = document.getElementById("toggleCells");
|
||||
document.getElementById("burgRelocate").classList.toggle("pressed");
|
||||
if (document.getElementById("burgRelocate").classList.contains("pressed")) {
|
||||
viewbox.style("cursor", "crosshair").on("click", relocateBurgOnClick);
|
||||
tip("Click on map to relocate burg. Hold Shift for continuous move", true);
|
||||
if (!layerIsOn("toggleCells")) {toggleCells(); toggler.dataset.forced = true;}
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
if (layerIsOn("toggleCells") && toggler.dataset.forced) {toggleCells(); toggler.dataset.forced = false;}
|
||||
}
|
||||
}
|
||||
|
||||
function relocateBurgOnClick() {
|
||||
const cells = pack.cells;
|
||||
const point = d3.mouse(this);
|
||||
const cell = findCell(point[0], point[1]);
|
||||
const id = +elSelected.attr("data-id");
|
||||
const burg = pack.burgs[id];
|
||||
|
||||
if (cells.h[cell] < 20) {
|
||||
tip("Cannot place burg into the water! Select a land cell", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (cells.burg[cell] && cells.burg[cell] !== id) {
|
||||
tip("There is already a burg in this cell. Please select a free cell", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const newState = cells.state[cell];
|
||||
const oldState = burg.state;
|
||||
|
||||
if (newState !== oldState && burg.capital) {
|
||||
tip("Capital cannot be relocated into another state!", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// change UI
|
||||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||
burgIcons.select("[data-id='" + id + "']").attr("transform", null).attr("cx", x).attr("cy", y);
|
||||
burgLabels.select("text[data-id='" + id + "']").attr("transform", null).attr("x", x).attr("y", y);
|
||||
const anchor = anchors.select("use[data-id='" + id+ "']");
|
||||
if (anchor.size()) {
|
||||
const size = anchor.attr("width");
|
||||
const xa = rn(x - size * 0.47, 2);
|
||||
const ya = rn(y - size * 0.47, 2);
|
||||
anchor.attr("transform", null).attr("x", xa).attr("y", ya);
|
||||
}
|
||||
|
||||
// change data
|
||||
cells.burg[burg.cell] = 0;
|
||||
cells.burg[cell] = id;
|
||||
burg.cell = cell;
|
||||
burg.state = newState;
|
||||
burg.x = x;
|
||||
burg.y = y;
|
||||
if (burg.capital) pack.states[newState].center = burg.cell;
|
||||
|
||||
if (d3.event.shiftKey === false) toggleRelocateBurg();
|
||||
}
|
||||
|
||||
function editBurgLegend() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const name = elSelected.text();
|
||||
editLegends("burg"+id, name);
|
||||
}
|
||||
|
||||
function removeSelectedBurg() {
|
||||
const id = +elSelected.attr("data-id");
|
||||
const capital = pack.burgs[id].capital;
|
||||
|
||||
if (capital) {
|
||||
alertMessage.innerHTML = `You cannot remove the burg as it is a capital.<br><br>
|
||||
You can change the capital using the Burgs Editor`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove burg",
|
||||
buttons: {Ok: function() {$(this).dialog("close");}}
|
||||
});
|
||||
} else {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the burg?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove burg",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
removeBurg(id); // see Editors module
|
||||
$("#burgEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function closeBurgEditor() {
|
||||
document.getElementById("burgRelocate").classList.remove("pressed");
|
||||
burgLabels.selectAll("text").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||||
unselect();
|
||||
}
|
||||
|
||||
}
|
||||
332
modules/ui/burgs-editor.js
Normal file
332
modules/ui/burgs-editor.js
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
"use strict";
|
||||
function editBurgs() {
|
||||
if (customization) return;
|
||||
closeDialogs("#burgsEditor, .stable");
|
||||
if (!layerIsOn("toggleIcons")) toggleIcons();
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const body = document.getElementById("burgsBody");
|
||||
updateFilter();
|
||||
burgsEditorAddLines();
|
||||
|
||||
if (modules.editBurgs) return;
|
||||
modules.editBurgs = true;
|
||||
|
||||
$("#burgsEditor").dialog({title: "Burgs Editor", width: fitContent(), close: exitAddBurgMode,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("burgsEditorRefresh").addEventListener("click", burgsEditorAddLines);
|
||||
document.getElementById("burgsFilterState").addEventListener("change", burgsEditorAddLines);
|
||||
document.getElementById("burgsFilterCulture").addEventListener("change", burgsEditorAddLines);
|
||||
document.getElementById("regenerateBurgNames").addEventListener("click", regenerateNames);
|
||||
document.getElementById("addNewBurg").addEventListener("click", enterAddBurgMode);
|
||||
document.getElementById("burgsExport").addEventListener("click", downloadBurgsData);
|
||||
document.getElementById("burgNamesImport").addEventListener("click", e => burgsListToLoad.click());
|
||||
document.getElementById("burgsListToLoad").addEventListener("change", importBurgNames);
|
||||
document.getElementById("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove);
|
||||
|
||||
function updateFilter() {
|
||||
const stateFilter = document.getElementById("burgsFilterState");
|
||||
const selectedState = stateFilter.value || 1;
|
||||
stateFilter.options.length = 0; // remove all options
|
||||
stateFilter.options.add(new Option("all", -1, false, selectedState == -1));
|
||||
pack.states.forEach(s => stateFilter.options.add(new Option(s.name, s.i, false, s.i == selectedState)));
|
||||
|
||||
const cultureFilter = document.getElementById("burgsFilterCulture");
|
||||
const selectedCulture = cultureFilter.value || -1;
|
||||
cultureFilter.options.length = 0; // remove all options
|
||||
cultureFilter.options.add(new Option("all", -1, false, selectedCulture == -1));
|
||||
pack.cultures.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture)));
|
||||
}
|
||||
|
||||
// add line for each state
|
||||
function burgsEditorAddLines() {
|
||||
const selectedState = +document.getElementById("burgsFilterState").value;
|
||||
const selectedCulture = +document.getElementById("burgsFilterCulture").value;
|
||||
let filtered = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
|
||||
if (selectedState != -1) filtered = filtered.filter(b => b.state === selectedState); // filtered by state
|
||||
if (selectedCulture != -1) filtered = filtered.filter(b => b.culture === selectedCulture); // filtered by culture
|
||||
|
||||
const showState = selectedState == -1 ? "visible" : "hidden";
|
||||
document.getElementById("burgStateHeader").style.display = `${selectedState == -1 ? "inline-block" : "none"}`;
|
||||
body.innerHTML = "";
|
||||
let lines = "", totalPopulation = 0;
|
||||
|
||||
for (const b of filtered) {
|
||||
const population = rn(b.population * populationRate.value * urbanization.value);
|
||||
totalPopulation += population;
|
||||
const type = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg";
|
||||
const state = pack.states[b.state].name;
|
||||
const culture = pack.cultures[b.culture].name;
|
||||
|
||||
lines += `<div class="states" data-id=${b.i} data-name=${b.name} data-state=${state} data-culture=${culture} data-population=${population} data-type=${type}>
|
||||
<span data-tip="Click to zoom into view" class="icon-dot-circled pointer"></span>
|
||||
<input data-tip="Burg name. Click and type to change" class="burgName" value="${b.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Burg state" class="burgState ${showState}">${state}</span>
|
||||
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(b.culture)}</select>
|
||||
<span data-tip="Burg population" class="icon-male"></span>
|
||||
<input data-tip="Burg population. Type to change" class="burgPopulation" value=${population}>
|
||||
<div class="burgType">
|
||||
<span data-tip="${b.capital ? ' This burg is a state capital' : 'Click to assign a capital status'}" class="icon-star-empty${b.capital ? '' : ' inactive pointer'}"></span>
|
||||
<span data-tip="Click to toggle port status" class="icon-anchor pointer${b.port ? '' : ' inactive'}" style="font-size:.9em"></span>
|
||||
</div>
|
||||
<span data-tip="Remove burg" class="icon-trash-empty"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.insertAdjacentHTML("beforeend", lines);
|
||||
|
||||
// update footer
|
||||
burgsFooterBurgs.innerHTML = filtered.length;
|
||||
burgsFooterPopulation.innerHTML = filtered.length ? rn(totalPopulation / filtered.length) : 0;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev)));
|
||||
body.querySelectorAll("div > input.burgName").forEach(el => el.addEventListener("input", changeBurgName));
|
||||
body.querySelectorAll("div > span.icon-dot-circled").forEach(el => el.addEventListener("click", zoomIntoBurg));
|
||||
body.querySelectorAll("div > select.burgCulture").forEach(el => el.addEventListener("click", updateCulturesList));
|
||||
body.querySelectorAll("div > input.burgPopulation").forEach(el => el.addEventListener("change", changeBurgPopulation));
|
||||
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", toggleCapitalStatus));
|
||||
body.querySelectorAll("div > span.icon-anchor").forEach(el => el.addEventListener("click", togglePortStatus));
|
||||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove));
|
||||
|
||||
applySorting(burgsHeader);
|
||||
$("#burgsEditor").dialog();
|
||||
}
|
||||
|
||||
function getCultureOptions(culture) {
|
||||
let options = "";
|
||||
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function burgHighlightOn(event) {
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
const burg = +event.target.dataset.id;
|
||||
burgLabels.select("[data-id='" + burg + "']").classed("drag", true);
|
||||
}
|
||||
|
||||
function burgHighlightOff() {
|
||||
burgLabels.selectAll("text.drag").classed("drag", false);
|
||||
}
|
||||
|
||||
function changeBurgName() {
|
||||
if (this.value == "")tip("Please provide a name", false, "error");
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
pack.burgs[burg].name = this.value;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
|
||||
if (label) label.innerHTML = this.value;
|
||||
}
|
||||
|
||||
function zoomIntoBurg() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
const label = document.querySelector("#burgLabels [data-id='" + burg + "']");
|
||||
const x = +label.getAttribute("x"), y = +label.getAttribute("y");
|
||||
zoomTo(x, y, 8, 2000);
|
||||
}
|
||||
|
||||
function updateCulturesList() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
const v = +this.value;
|
||||
pack.burgs[burg].culture = v;
|
||||
this.parentNode.dataset.culture = pack.cultures[v].name;
|
||||
this.options.length = 0;
|
||||
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
|
||||
}
|
||||
|
||||
function changeBurgPopulation() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
if (this.value == "" || isNaN(+this.value)) {
|
||||
tip("Please provide a valid number", false, "error");
|
||||
this.value = pack.burgs[burg].population * populationRate.value * urbanization.value;
|
||||
return;
|
||||
}
|
||||
pack.burgs[burg].population = this.value / populationRate.value / urbanization.value;
|
||||
this.parentNode.dataset.population = this.value;
|
||||
|
||||
const population = [];
|
||||
body.querySelectorAll(":scope > div").forEach(el => population.push(+el.dataset.population));
|
||||
pack.burgsFooterPopulation.innerHTML = rn(d3.mean(population));
|
||||
}
|
||||
|
||||
function toggleCapitalStatus() {
|
||||
const burg = +this.parentNode.parentNode.dataset.id, state = pack.burgs[burg].state;
|
||||
if (pack.burgs[burg].capital) {tip("To change capital please assign capital status to another burg", false, "error"); return;}
|
||||
if (!state) {tip("Neutral lands cannot have a capital", false, "error"); return;}
|
||||
const old = pack.states[state].capital;
|
||||
|
||||
// change statuses
|
||||
pack.states[state].capital = burg;
|
||||
pack.burgs[burg].capital = true;
|
||||
pack.burgs[old].capital = false;
|
||||
moveBurgToGroup(burg, "cities");
|
||||
moveBurgToGroup(old, "towns");
|
||||
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
|
||||
function togglePortStatus() {
|
||||
const burg = +this.parentNode.parentNode.dataset.id;
|
||||
const anchor = document.querySelector("#anchors [data-id='" + burg + "']");
|
||||
if (anchor) anchor.remove();
|
||||
|
||||
if (!pack.burgs[burg].port) {
|
||||
const haven = pack.cells.haven[pack.burgs[burg].cell];
|
||||
const port = haven ? pack.cells.f[haven] : -1;
|
||||
if (!haven) tip("Port haven is not found, system won't be able to make a searoute", false, "warning");
|
||||
pack.burgs[burg].port = port;
|
||||
|
||||
const g = pack.burgs[burg].capital ? "cities" : "towns";
|
||||
const group = anchors.select("g#"+g);
|
||||
const size = +group.attr("size");
|
||||
group.append("use").attr("xlink:href", "#icon-anchor").attr("data-id", burg)
|
||||
.attr("x", rn(pack.burgs[burg].x - size * .47, 2)).attr("y", rn(pack.burgs[burg].y - size * .47, 2))
|
||||
.attr("width", size).attr("height", size);
|
||||
} else {
|
||||
pack.burgs[burg].port = 0;
|
||||
}
|
||||
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
|
||||
function triggerBurgRemove() {
|
||||
const burg = +this.parentNode.dataset.id;
|
||||
if (pack.burgs[burg].capital) {tip("You cannot remove the capital. Please change the capital first", false, "error"); return;}
|
||||
removeBurg(burg);
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
|
||||
function regenerateNames() {
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
const burg = +el.dataset.id;
|
||||
const culture = pack.burgs[burg].culture;
|
||||
const name = Names.getCulture(culture);
|
||||
el.querySelector(".burgName").value = name;
|
||||
pack.burgs[burg].name = el.dataset.name = name;
|
||||
burgLabels.select("[data-id='" + burg + "']").text(name);
|
||||
});
|
||||
}
|
||||
|
||||
function enterAddBurgMode() {
|
||||
if (this.classList.contains("pressed")) {exitAddBurgMode(); return;};
|
||||
customization = 3;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to create a new burg. Hold Shift to add multiple", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addBurgOnClick);
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
}
|
||||
|
||||
function addBurgOnClick() {
|
||||
const point = d3.mouse(this);
|
||||
const cell = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[cell] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
|
||||
if (pack.cells.burg[cell]) {tip("There is already a burg in this cell. Please select a free cell", false, "error"); return;}
|
||||
addBurg(point); // add new burg
|
||||
|
||||
if (d3.event.shiftKey === false) {
|
||||
exitAddBurgMode();
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
function exitAddBurgMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
if (addBurgTool.classList.contains("pressed")) addBurgTool.classList.remove("pressed");
|
||||
if (addNewBurg.classList.contains("pressed")) addNewBurg.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function downloadBurgsData() {
|
||||
let data = "Id,Burg,State,Culture,Population,Capital,Port\n"; // headers
|
||||
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
|
||||
|
||||
valid.forEach(b => {
|
||||
data += b.i + ",";
|
||||
data += b.name + ",";
|
||||
data += pack.states[b.state].name + ",";
|
||||
data += pack.cultures[b.culture].name + ",";
|
||||
data += rn(b.population * populationRate.value * urbanization.value) + ",";
|
||||
data += b.capital ? "capital," : ",";
|
||||
data += b.port ? "port\n" : "\n";
|
||||
});
|
||||
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
document.body.appendChild(link);
|
||||
link.download = "burgs_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function importBurgNames() {
|
||||
const el = document.getElementById("burgsListToLoad");
|
||||
const fileToLoad = el.files[0];
|
||||
el.value = "";
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = function(e) {
|
||||
const dataLoaded = e.target.result;
|
||||
const data = dataLoaded.split("\r\n");
|
||||
if (!data.length) {tip("Cannot parse the list, please check the file format", false, "error"); return;}
|
||||
|
||||
let change = [];
|
||||
let message = `Burgs will be renamed as below. Please confirm`;
|
||||
message += `<div class="overflow-div"><table class="overflow-table"><tr><th>Id</th><th>Current name</th><th>New Name</th></tr>`;
|
||||
|
||||
for (let i=1; i < data.length && i < pack.burgs.length; i++) {
|
||||
const v = data[i];
|
||||
if (!v || v == pack.burgs[i].name) continue;
|
||||
change.push({i, name: v});
|
||||
message += `<tr><td style="width:20%">${i}</td><td style="width:40%">${pack.burgs[i].name}</td><td style="width:40%">${v}</td></tr>`;
|
||||
}
|
||||
message += `</tr></table></div>`;
|
||||
alertMessage.innerHTML = message;
|
||||
|
||||
$("#alert").dialog({title: "Burgs bulk renaming", position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
Cancel: function() {$(this).dialog("close");},
|
||||
Confirm: function() {
|
||||
for (let i=0; i < change.length; i++) {
|
||||
const id = change[i].i;
|
||||
pack.burgs[id].name = change[i].name;
|
||||
burgLabels.select("[data-id='" + id + "']").text(change[i].name);
|
||||
}
|
||||
$(this).dialog("close");
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||
}
|
||||
|
||||
function triggerAllBurgsRemove() {
|
||||
alertMessage.innerHTML = `Are you sure you want to remove all burgs except of capitals?
|
||||
<br>To remove a capital you have to remove its state first`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove all burgs",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
removeAllBurgs();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeAllBurgs() {
|
||||
pack.burgs.filter(b => b.i && !b.capital).forEach(b => removeBurg(b.i));
|
||||
burgsEditorAddLines();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
435
modules/ui/cultures-editor.js
Normal file
435
modules/ui/cultures-editor.js
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
"use strict";
|
||||
function editCultures() {
|
||||
if (customization) return;
|
||||
closeDialogs("#culturesEditor, .stable");
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleStates")) toggleStates();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
|
||||
const body = document.getElementById("culturesBody");
|
||||
const animate = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
drawCultureCenters();
|
||||
refreshCulturesEditor();
|
||||
|
||||
if (modules.editCultures) return;
|
||||
modules.editCultures = true;
|
||||
|
||||
$("#culturesEditor").dialog({
|
||||
title: "Cultures Editor", width: fitContent(), close: closeCulturesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("culturesEditorRefresh").addEventListener("click", refreshCulturesEditor);
|
||||
document.getElementById("culturesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("culturesRecalculate").addEventListener("click", recalculateCultures);
|
||||
document.getElementById("culturesManually").addEventListener("click", enterCultureManualAssignent);
|
||||
document.getElementById("culturesManuallyApply").addEventListener("click", applyCultureManualAssignent);
|
||||
document.getElementById("culturesManuallyCancel").addEventListener("click", exitCulturesManualAssignment);
|
||||
document.getElementById("culturesEditNamesBase").addEventListener("click", editNamesbase);
|
||||
document.getElementById("culturesAdd").addEventListener("click", addCulture);
|
||||
document.getElementById("culturesExport").addEventListener("click", downloadCulturesData);
|
||||
|
||||
function refreshCulturesEditor() {
|
||||
culturesCollectStatistics();
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
|
||||
function culturesCollectStatistics() {
|
||||
const cells = pack.cells, cultures = pack.cultures;
|
||||
cultures.forEach(c => c.cells = c.area = c.rural = c.urban = 0);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const c = cells.culture[i];
|
||||
cultures[c].cells += 1;
|
||||
cultures[c].area += cells.area[i];
|
||||
cultures[c].rural += cells.pop[i];
|
||||
if (cells.burg[i]) cultures[c].urban += pack.burgs[cells.burg[i]].population;
|
||||
}
|
||||
}
|
||||
|
||||
// add line for each culture
|
||||
function culturesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
let lines = "", totalArea = 0, totalPopulation = 0;
|
||||
|
||||
for (const c of pack.cultures) {
|
||||
if (c.removed) continue;
|
||||
const area = c.area * (distanceScale.value ** 2);
|
||||
const rural = c.rural * populationRate.value;
|
||||
const urban = c.urban * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
|
||||
if (!c.i) {
|
||||
// Uncultured (neutral) line
|
||||
lines += `<div class="states" data-id=${c.i} data-name="${c.name}" data-color="" data-cells=${c.cells}
|
||||
data-area=${area} data-population=${population} data-base=${c.base} data-type="" data-expansionism="">
|
||||
<input class="stateColor placeholder" type="color">
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName italic" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||
<span class="icon-resize-full placeholder"></span>
|
||||
<input class="statePower placeholder" type="number">
|
||||
<select class="cultureType placeholder">${getTypeOptions(c.type)}</select>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||
<select data-tip="Culture namesbase. Click to change" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
</div>`;
|
||||
continue;
|
||||
}
|
||||
|
||||
lines += `<div class="states cultures" data-id=${c.i} data-name="${c.name}" data-color="${c.color}" data-cells=${c.cells}
|
||||
data-area=${area} data-population=${population} data-base=${c.base} data-type=${c.type} data-expansionism=${c.expansionism}>
|
||||
<input data-tip="Culture color. Click to change" class="stateColor" type="color" value="${c.color}">
|
||||
<input data-tip="Culture name. Click and type to change" class="cultureName" value="${c.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${c.cells}</div>
|
||||
<span data-tip="Culture expansionism (defines competitive size)" class="icon-resize-full"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate cultures based on new value" class="statePower" type="number" min=0 max=99 step=.1 value=${c.expansionism}>
|
||||
<select data-tip="Culture type. Change to re-calculate cultures based on new value" class="cultureType">${getTypeOptions(c.type)}</select>
|
||||
<span data-tip="Culture area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="Culture area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Click to re-generate names for burgs with this culture assigned" class="icon-arrows-cw"></span>
|
||||
<select data-tip="Culture namesbase. Change and then click on the Re-generate button to get new names" class="cultureBase">${getBaseOptions(c.base)}</select>
|
||||
<span data-tip="Remove culture" class="icon-trash-empty"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
culturesFooterCultures.innerHTML = pack.cultures.filter(c => c.i && !c.removed).length;
|
||||
culturesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||
culturesFooterArea.innerHTML = si(totalArea) + unit;
|
||||
culturesFooterPopulation.innerHTML = si(totalPopulation);
|
||||
culturesFooterArea.dataset.area = totalArea;
|
||||
culturesFooterPopulation.dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseenter", ev => cultureHighlightOn(ev)));
|
||||
body.querySelectorAll("div.cultures").forEach(el => el.addEventListener("mouseleave", ev => cultureHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectCultureOnLineClick));
|
||||
body.querySelectorAll("div > input[type='color']").forEach(el => el.addEventListener("input", cultureChangeColor));
|
||||
body.querySelectorAll("div > input.cultureName").forEach(el => el.addEventListener("input", cultureChangeName));
|
||||
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", cultureChangeExpansionism));
|
||||
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("change", cultureChangeType));
|
||||
body.querySelectorAll("div > select.cultureBase").forEach(el => el.addEventListener("click", updateBaseOptions));
|
||||
body.querySelectorAll("div > span.icon-arrows-cw").forEach(el => el.addEventListener("click", cultureRegenerateBurgs));
|
||||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", cultureRemove));
|
||||
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
applySorting(culturesHeader);
|
||||
$("#culturesEditor").dialog();
|
||||
}
|
||||
|
||||
function getTypeOptions(type) {
|
||||
let options = "";
|
||||
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
|
||||
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function getBaseOptions(base) {
|
||||
let options = "";
|
||||
nameBases.forEach((n, i) => options += `<option ${base === i ? "selected" : ""} value="${i}">${n.name}</option>`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function cultureHighlightOn(event) {
|
||||
if (customization === 4) return;
|
||||
const culture = +event.target.dataset.id;
|
||||
const color = d3.interpolateLab(pack.cultures[culture].color, "#ff0000")(.8)
|
||||
cults.select("#culture"+culture).raise().transition(animate).attr("stroke-width", 3).attr("stroke", color);
|
||||
debug.select("#cultureCenter"+culture).raise().transition(animate).attr("r", 8);
|
||||
}
|
||||
|
||||
function cultureHighlightOff(event) {
|
||||
if (customization === 4) return;
|
||||
const culture = +event.target.dataset.id;
|
||||
cults.select("#culture"+culture).transition().attr("stroke-width", .7).attr("stroke", pack.cultures[culture].color);
|
||||
debug.select("#cultureCenter"+culture).transition().attr("r", 6);
|
||||
}
|
||||
|
||||
function cultureChangeColor() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
pack.cultures[culture].color = this.value;
|
||||
cults.select("#culture"+culture).attr("fill", this.value).attr("stroke", this.value);
|
||||
debug.select("#cultureCenter"+culture).attr("fill", this.value);
|
||||
}
|
||||
|
||||
function cultureChangeName() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.cultures[culture].name = this.value;
|
||||
}
|
||||
|
||||
function cultureChangeExpansionism() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.expansionism = this.value;
|
||||
pack.cultures[culture].expansionism = +this.value;
|
||||
recalculateCultures();
|
||||
}
|
||||
|
||||
function cultureChangeType() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.type = this.value;
|
||||
pack.cultures[culture].type = this.value;
|
||||
recalculateCultures();
|
||||
}
|
||||
|
||||
function updateBaseOptions() {
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const v = +this.value;
|
||||
this.parentNode.dataset.base = pack.cultures[culture].base = v;
|
||||
this.options.length = 0;
|
||||
nameBases.forEach((b, i) => this.options.add(new Option(b.name, i, false, i === v)));
|
||||
}
|
||||
|
||||
function cultureRegenerateBurgs() {
|
||||
if (customization === 4) return;
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
const cBurgs = pack.burgs.filter(b => b.culture === culture);
|
||||
cBurgs.forEach(b => {
|
||||
b.name = Names.getCulture(culture);
|
||||
labels.select("[data-id='" + b.i +"']").text(b.name);
|
||||
});
|
||||
tip(`Names for ${cBurgs.length} burgs are re-generated`);
|
||||
}
|
||||
|
||||
function cultureRemove() {
|
||||
if (customization === 4) return;
|
||||
const culture = +this.parentNode.dataset.id;
|
||||
cults.select("#culture"+culture).remove();
|
||||
debug.select("#cultureCenter"+culture).remove();
|
||||
|
||||
pack.burgs.filter(b => b.culture === culture).forEach(b => b.culture = 0);
|
||||
pack.cells.culture.forEach((c, i) => {if(c === culture) pack.cells.culture[i] = 0;});
|
||||
pack.cultures[culture].removed = true;
|
||||
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
function drawCultureCenters() {
|
||||
const tooltip = 'Drag to move the culture center (ancestral home)';
|
||||
debug.select("#cultureCenters").remove();
|
||||
const cultureCenters = debug.append("g").attr("id", "cultureCenters");
|
||||
|
||||
const data = pack.cultures.filter(c => c.i && !c.removed);
|
||||
cultureCenters.selectAll("circle").data(data).enter().append("circle")
|
||||
.attr("id", d => "cultureCenter"+d.i).attr("data-id", d => d.i)
|
||||
.attr("r", 6).attr("fill", d => d.color)
|
||||
.attr("cx", d => pack.cells.p[d.center][0]).attr("cy", d => pack.cells.p[d.center][1])
|
||||
.on("mouseenter", d => {tip(tooltip, true); body.querySelector(`div[data-id='${d.i}']`).classList.add("selected"); cultureHighlightOn(event);})
|
||||
.on("mouseleave", d => {tip('', true); body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); cultureHighlightOff(event);})
|
||||
.call(d3.drag().on("start", cultureCenterDrag));
|
||||
}
|
||||
|
||||
function cultureCenterDrag() {
|
||||
const el = d3.select(this);
|
||||
const c = +this.id.slice(13);
|
||||
d3.event.on("drag", () => {
|
||||
el.attr("cx", d3.event.x).attr("cy", d3.event.y);
|
||||
const cell = findCell(d3.event.x, d3.event.y);
|
||||
if (pack.cells.h[cell] < 20) return; // ignore dragging on water
|
||||
pack.cultures[c].center = cell;
|
||||
recalculateCultures();
|
||||
});
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
const totalCells = +culturesFooterCells.innerHTML;
|
||||
const totalArea = +culturesFooterArea.dataset.area;
|
||||
const totalPopulation = +culturesFooterPopulation.dataset.population;
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||
});
|
||||
} else {
|
||||
body.dataset.type = "absolute";
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
// re-calculate cultures
|
||||
function recalculateCultures() {
|
||||
pack.cells.culture = new Int8Array(pack.cells.i.length);
|
||||
pack.cultures.forEach(function(c) {
|
||||
if (!c.i || c.removed) return;
|
||||
pack.cells.culture[c.center] = c.i;
|
||||
});
|
||||
Cultures.expand();
|
||||
drawCultures();
|
||||
pack.burgs.forEach(b => b.culture = pack.cells.culture[b.cell]);
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
|
||||
function enterCultureManualAssignent() {
|
||||
if (!layerIsOn("toggleCultures")) toggleCultures();
|
||||
customization = 4;
|
||||
cults.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("culturesManuallyButtons").style.display = "inline-block";
|
||||
debug.select("#cultureCenters").style("display", "none");
|
||||
|
||||
tip("Click on culture to select, drag the circle to change culture", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragCultureBrush))
|
||||
.on("click", selectCultureOnMapClick)
|
||||
.on("touchmove mousemove", moveCultureBrush);
|
||||
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnLineClick(i) {
|
||||
if (customization !== 4) return;
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectCultureOnMapClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) return;
|
||||
|
||||
const assigned = cults.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||
const culture = assigned.size() ? +assigned.attr("data-culture") : pack.cells.culture[i];
|
||||
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
body.querySelector("div[data-id='"+culture+"']").classList.add("selected");
|
||||
}
|
||||
|
||||
function dragCultureBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +culturesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeCultureForSelection(selection);
|
||||
}
|
||||
|
||||
// change culture within selection
|
||||
function changeCultureForSelection(selection) {
|
||||
const temp = cults.select("#temp");
|
||||
const selected = body.querySelector("div.selected");
|
||||
|
||||
const cultureNew = +selected.dataset.id;
|
||||
const color = pack.cultures[cultureNew].color;
|
||||
|
||||
selection.forEach(function(i) {
|
||||
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||
const cultureOld = exists.size() ? +exists.attr("data-culture") : pack.cells.culture[i];
|
||||
if (cultureNew === cultureOld) return;
|
||||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-culture", cultureNew).attr("fill", color).attr("stroke", color);
|
||||
else temp.append("polygon").attr("data-cell", i).attr("data-culture", cultureNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||
});
|
||||
}
|
||||
|
||||
function moveCultureBrush() {
|
||||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +culturesManuallyBrush.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
}
|
||||
|
||||
function applyCultureManualAssignent() {
|
||||
const changed = cults.select("#temp").selectAll("polygon");
|
||||
changed.each(function() {
|
||||
const i = +this.dataset.cell;
|
||||
const c = +this.dataset.culture;
|
||||
pack.cells.culture[i] = c;
|
||||
if (pack.cells.burg[i]) pack.burgs[pack.cells.burg[i]].culture = c;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
drawCultures();
|
||||
refreshCulturesEditor();
|
||||
}
|
||||
exitCulturesManualAssignment();
|
||||
}
|
||||
|
||||
function exitCulturesManualAssignment() {
|
||||
customization = 0;
|
||||
cults.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#culturesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("culturesManuallyButtons").style.display = "none";
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
debug.select("#cultureCenters").style("display", null);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function addCulture() {
|
||||
const defaultCultures = Cultures.getDefault();
|
||||
let culture, base, name;
|
||||
if (pack.cultures.length < defaultCultures.length) {
|
||||
// add one of the default cultures
|
||||
culture = pack.cultures.length;
|
||||
base = defaultCultures[culture].base;
|
||||
name = defaultCultures[culture].name;
|
||||
} else {
|
||||
// add random culture besed on one of the current ones
|
||||
culture = rand(pack.cultures.length - 1);
|
||||
name = Names.getCulture(culture, 5, 8, "");
|
||||
base = pack.cultures[culture].base;
|
||||
}
|
||||
const i = pack.cultures.length;
|
||||
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||
const land = pack.cells.i.filter(isLand);
|
||||
const center = land[Math.floor(Math.random() * land.length - 1)];
|
||||
pack.cultures.push({name, color, base, center, i, expansionism:1, type:"Generic", cells:0, area:0, rural:0, urban:0});
|
||||
drawCultureCenters();
|
||||
culturesEditorAddLines();
|
||||
}
|
||||
|
||||
function downloadCulturesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
let data = "Id,Culture,Color,Cells,Expansionism,Type,Area "+unit+",Population,Namesbase\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.cells + ",";
|
||||
data += el.dataset.expansionism + ",";
|
||||
data += el.dataset.type + ",";
|
||||
data += el.dataset.area + ",";
|
||||
data += el.dataset.population + ",";
|
||||
const base = +el.dataset.base;
|
||||
data += nameBases[base].name + "\n";
|
||||
});
|
||||
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
document.body.appendChild(link);
|
||||
link.download = "cultures_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function closeCulturesEditor() {
|
||||
debug.select("#cultureCenters").remove();
|
||||
exitCulturesManualAssignment();
|
||||
}
|
||||
|
||||
}
|
||||
189
modules/ui/editors.js
Normal file
189
modules/ui/editors.js
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
// module stub to store common functions for ui editors
|
||||
"use strict";
|
||||
|
||||
restoreDefaultEvents(); // apply default viewbox events on load
|
||||
|
||||
// restore default viewbox events
|
||||
function restoreDefaultEvents() {
|
||||
svg.call(zoom);
|
||||
viewbox.style("cursor", "default")
|
||||
.on(".drag", null)
|
||||
.on("click", clicked)
|
||||
.on("touchmove mousemove", moved);
|
||||
}
|
||||
|
||||
// on viewbox click event - run function based on target
|
||||
function clicked() {
|
||||
const el = d3.event.target;
|
||||
if (!el || !el.parentElement || !el.parentElement.parentElement) return;
|
||||
const parent = el.parentElement, grand = parent.parentElement;
|
||||
if (parent.id === "rivers") editRiver(); else
|
||||
if (grand.id === "routes") editRoute(); else
|
||||
if (el.tagName === "textPath" && grand.parentNode.id === "labels") editLabel(); else
|
||||
if (grand.id === "burgLabels") editBurg(); else
|
||||
if (grand.id === "burgIcons") editBurg(); else
|
||||
if (parent.id === "terrain") editReliefIcon(); else
|
||||
if (parent.id === "markers") editMarker();
|
||||
}
|
||||
|
||||
// clear elSelected variable
|
||||
function unselect() {
|
||||
restoreDefaultEvents();
|
||||
if (!elSelected) return;
|
||||
elSelected.call(d3.drag().on("drag", null)).attr("class", null);
|
||||
debug.selectAll("*").remove();
|
||||
viewbox.style("cursor", "default");
|
||||
elSelected = null;
|
||||
}
|
||||
|
||||
// close all dialogs except stated
|
||||
function closeDialogs(except = "#except") {
|
||||
$(".dialog:visible").not(except).each(function() {
|
||||
$(this).dialog("close");
|
||||
});
|
||||
}
|
||||
|
||||
// move brush radius circle
|
||||
function moveCircle(x, y, r = 20) {
|
||||
let circle = document.getElementById("brushCircle");
|
||||
if (!circle) {
|
||||
const html = `<circle id="brushCircle" cx=${x} cy=${y} r=${r}></circle>`;
|
||||
document.getElementById("debug").insertAdjacentHTML("afterBegin", html);
|
||||
} else {
|
||||
circle.setAttribute("cx", x);
|
||||
circle.setAttribute("cy", y);
|
||||
circle.setAttribute("r", r);
|
||||
}
|
||||
}
|
||||
|
||||
function removeCircle() {
|
||||
if (document.getElementById("brushCircle")) document.getElementById("brushCircle").remove();
|
||||
}
|
||||
|
||||
// get browser-defined fit-content
|
||||
function fitContent() {
|
||||
return !window.chrome ? "-moz-max-content" : "fit-content";
|
||||
}
|
||||
|
||||
// DOM elements sorting on header click
|
||||
$(".sortable").on("click", function() {
|
||||
const el = $(this);
|
||||
// remove sorting for all siblings except of clicked element
|
||||
el.siblings().removeClass("icon-sort-name-up icon-sort-name-down icon-sort-number-up icon-sort-number-down");
|
||||
const type = el.hasClass("alphabetically") ? "name" : "number";
|
||||
let state = "no";
|
||||
if (el.is("[class*='down']")) state = "asc";
|
||||
if (el.is("[class*='up']")) state = "desc";
|
||||
const sortby = el.attr("data-sortby");
|
||||
const list = el.parent().next(); // get list container element (e.g. "countriesBody")
|
||||
const lines = list.children("div"); // get list elements
|
||||
if (state === "no" || state === "asc") { // sort desc
|
||||
el.removeClass("icon-sort-" + type + "-down");
|
||||
el.addClass("icon-sort-" + type + "-up");
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
if (an === "bottom") {return 1;}
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (bn === "bottom") {return -1;}
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
if (an > bn) {return 1;}
|
||||
if (an < bn) {return -1;}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
if (state === "desc") { // sort asc
|
||||
el.removeClass("icon-sort-" + type + "-up");
|
||||
el.addClass("icon-sort-" + type + "-down");
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
if (an === "bottom") {return 1;}
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (bn === "bottom") {return -1;}
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
if (an < bn) {return 1;}
|
||||
if (an > bn) {return -1;}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
lines.detach().appendTo(list);
|
||||
});
|
||||
|
||||
function applySorting(headers) {
|
||||
const header = headers.querySelector("[class*='icon-sort']");
|
||||
if (!header) return;
|
||||
const sortby = header.dataset.sortby;
|
||||
const type = header.classList.contains("alphabetically") ? "name" : "number";
|
||||
const desc = headers.querySelector("[class*='-down']") ? -1 : 1;
|
||||
const list = headers.nextElementSibling;
|
||||
const lines = Array.from(list.children);
|
||||
|
||||
lines.sort(function(a, b) {
|
||||
let an = a.getAttribute("data-" + sortby);
|
||||
let bn = b.getAttribute("data-" + sortby);
|
||||
if (type === "number") {an = +an; bn = +bn;}
|
||||
return (an - bn) * desc;
|
||||
}).forEach(line => list.appendChild(line));
|
||||
}
|
||||
|
||||
// trigger trash button click on "Delete" keypress
|
||||
function removeElementOnKey() {
|
||||
$(".dialog:visible .icon-trash").click();
|
||||
$("button:visible:contains('Remove')").click();
|
||||
}
|
||||
|
||||
function addBurg(point) {
|
||||
const cells = pack.cells;
|
||||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||
const cell = findCell(x, point[1]);
|
||||
const i = pack.burgs.length;
|
||||
const culture = cells.culture[cell];
|
||||
const name = Names.getCulture(culture);
|
||||
const state = cells.state[cell];
|
||||
const feature = cells.f[cell];
|
||||
|
||||
const population = Math.max((cells.s[cell] + cells.road[cell]) / 3 + i / 1000 + cell % 100 / 1000, .1);
|
||||
pack.burgs.push({name, cell, x, y, state, i, culture, feature, capital: false, port: 0, population});
|
||||
|
||||
const townSize = burgIcons.select("#towns").attr("size") || 0.5;
|
||||
burgIcons.select("#towns").append("circle").attr("id", "burg"+i).attr("data-id", i)
|
||||
.attr("cx", x).attr("cy", y).attr("r", townSize);
|
||||
burgLabels.select("#towns").append("text").attr("id", "burgLabel"+i).attr("data-id", i)
|
||||
.attr("x", x).attr("y", y).attr("dy", `${townSize * -1.5}px`).text(name);
|
||||
|
||||
return i;
|
||||
}
|
||||
|
||||
function moveBurgToGroup(id, g) {
|
||||
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||
if (!label || !icon) {console.error("Cannot find label or icon elements"); return;}
|
||||
|
||||
document.querySelector("#burgLabels > #"+g).appendChild(label);
|
||||
document.querySelector("#burgIcons > #"+g).appendChild(icon);
|
||||
|
||||
const iconSize = icon.parentNode.getAttribute("size");
|
||||
icon.setAttribute("r", iconSize);
|
||||
label.setAttribute("dy", `${iconSize * -1.5}px`);
|
||||
|
||||
if (anchor) {
|
||||
document.querySelector("#anchors > #"+g).appendChild(anchor);
|
||||
const anchorSize = +anchor.parentNode.getAttribute("size");
|
||||
anchor.setAttribute("width", anchorSize);
|
||||
anchor.setAttribute("height", anchorSize);
|
||||
anchor.setAttribute("x", rn(pack.burgs[id].x - anchorSize * 0.47, 2));
|
||||
anchor.setAttribute("y", rn(pack.burgs[id].y - anchorSize * 0.47, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function removeBurg(id) {
|
||||
const label = document.querySelector("#burgLabels [data-id='" + id + "']");
|
||||
const icon = document.querySelector("#burgIcons [data-id='" + id + "']");
|
||||
const anchor = document.querySelector("#anchors [data-id='" + id + "']");
|
||||
if (label) label.remove();
|
||||
if (icon) icon.remove();
|
||||
if (anchor) anchor.remove();
|
||||
pack.burgs[id].removed = true;
|
||||
const cell = pack.burgs[id].cell;
|
||||
pack.cells.burg[cell] = 0;
|
||||
}
|
||||
267
modules/ui/general.js
Normal file
267
modules/ui/general.js
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
// Module to store general UI functions
|
||||
"use strict";
|
||||
|
||||
// ask before closing the window
|
||||
window.onbeforeunload = () => "Are you sure you want to navigate away?";
|
||||
|
||||
// fit full-screen map if window is resized
|
||||
$(window).resize(function(e) {
|
||||
// trick to prevent resize on download bar opening
|
||||
if (autoResize === false) return;
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
changeMapSize();
|
||||
});
|
||||
|
||||
// Tooltips
|
||||
const tooltip = document.getElementById("tooltip");
|
||||
|
||||
// show tip for non-svg elemets with data-tip
|
||||
document.getElementById("dialogs").addEventListener("mousemove", showDataTip);
|
||||
document.getElementById("optionsContainer").addEventListener("mousemove", showDataTip);
|
||||
|
||||
function tip(tip = "Tip is undefined", main = false, error = false) {
|
||||
const reg = "linear-gradient(0.1turn, #ffffff00, #5e5c5c66, #ffffff00)";
|
||||
const red = "linear-gradient(0.1turn, #ffffff00, #e3141499, #ffffff00)";
|
||||
tooltip.innerHTML = tip;
|
||||
tooltip.style.background = error ? red : reg;
|
||||
if (main) tooltip.dataset.main = tip;
|
||||
}
|
||||
|
||||
function showMainTip() {
|
||||
tooltip.style.background = "linear-gradient(0.1turn, #aaffff00, #3a26264d, #ffffff00)";
|
||||
tooltip.innerHTML = tooltip.dataset.main;
|
||||
}
|
||||
|
||||
function clearMainTip() {
|
||||
tooltip.dataset.main = "";
|
||||
tooltip.innerHTML = "";
|
||||
}
|
||||
|
||||
function showDataTip(e) {
|
||||
if (!e.target) return;
|
||||
if (e.target.dataset.tip) {tip(e.target.dataset.tip); return;};
|
||||
if (e.target.parentNode.dataset.tip) tip(e.target.parentNode.dataset.tip);
|
||||
}
|
||||
|
||||
function moved() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]); // pack ell id
|
||||
if (i === undefined) return;
|
||||
showLegend(d3.event, i);
|
||||
const g = findGridCell(point[0], point[1]); // grid cell id
|
||||
if (tooltip.dataset.main) showMainTip(); else showMapTooltip(d3.event, i, g);
|
||||
if (toolsContent.style.display === "block" && cellInfo.style.display === "block") updateCellInfo(point, i, g);
|
||||
}
|
||||
|
||||
// show legend on hover (if any)
|
||||
function showLegend(e, i) {
|
||||
let id = e.target.id || e.target.parentNode.id;
|
||||
if (e.target.parentNode.parentNode.id === "burgLabels") id = "burg" + e.target.dataset.id; else
|
||||
if (e.target.parentNode.parentNode.id === "burgIcons") id = "burg" + e.target.dataset.id;
|
||||
|
||||
const note = notes.find(note => note.id === id);
|
||||
if (note !== undefined && note.legend !== "") {
|
||||
document.getElementById("legend").style.display = "block";
|
||||
document.getElementById("legendHeader").innerHTML = note.name;
|
||||
document.getElementById("legendBody").innerHTML = note.legend;
|
||||
} else {
|
||||
document.getElementById("legend").style.display = "none";
|
||||
document.getElementById("legendHeader").innerHTML = "";
|
||||
document.getElementById("legendBody").innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
// show viewbox tooltip if main tooltip is blank
|
||||
function showMapTooltip(e, i, g) {
|
||||
tip(""); // clear tip
|
||||
const tag = e.target.tagName;
|
||||
const path = e.composedPath ? e.composedPath() : getComposedPath(e.target); // apply polyfill
|
||||
const group = path[path.length - 7].id;
|
||||
const subgroup = path[path.length - 8].id;
|
||||
const land = pack.cells.h[i] >= 20;
|
||||
|
||||
// specific elements
|
||||
if (group === "rivers") {tip("Click to edit the River"); return;}
|
||||
if (group === "routes") {tip("Click to edit the Route"); return;}
|
||||
if (group === "terrain") {tip("Click to edit the Relief Icon"); return;}
|
||||
if (subgroup === "burgLabels" || subgroup === "burgIcons") {tip("Click to open Burg Editor"); return;}
|
||||
if (group === "labels") {tip("Click to edit the Label"); return;}
|
||||
if (group === "markers") {tip("Click to edit the Marker"); return;}
|
||||
if (group === "ruler") {
|
||||
if (tag === "rect") {tip("Drag to split the ruler into 2 parts"); return;}
|
||||
if (tag === "circle") {tip("Drag to adjust the measurer"); return;}
|
||||
if (tag === "path" || tag === "line") {tip("Drag to move the measurer"); return;}
|
||||
if (tag === "text") {tip("Click to remove the measurer"); return;}
|
||||
}
|
||||
if (subgroup === "burgIcons") {tip("Click to edit the Burg"); return;}
|
||||
if (subgroup === "burgLabels") {tip("Click to edit the Burg"); return;}
|
||||
if (subgroup === "freshwater" && !land) {tip("Freshwater lake"); return;}
|
||||
if (subgroup === "salt" && !land) {tip("Salt lake"); return;}
|
||||
|
||||
// covering elements
|
||||
if (layerIsOn("togglePrec") && land) tip("Annual Precipitation: "+ getFriendlyPrecipitation(i)); else
|
||||
if (layerIsOn("togglePopulation")) tip("Population: "+ getFriendlyPopulation(i)); else
|
||||
if (layerIsOn("toggleTemp")) tip("Temperature: " + convertTemperature(grid.cells.temp[g])); else
|
||||
if (layerIsOn("toggleBiomes") && pack.cells.biome[i]) tip("Biome: " + biomesData.name[pack.cells.biome[i]]); else
|
||||
if (layerIsOn("toggleStates") && pack.cells.state[i]) tip("State: " + pack.states[pack.cells.state[i]].name); else
|
||||
if (layerIsOn("toggleCultures") && pack.cells.culture[i]) tip("Culture: " + pack.cultures[pack.cells.culture[i]].name); else
|
||||
if (layerIsOn("toggleHeight")) tip("Height: " + getFriendlyHeight(pack.cells.h[i]));
|
||||
}
|
||||
|
||||
// get cell info on mouse move
|
||||
function updateCellInfo(point, i, g) {
|
||||
const cells = pack.cells;
|
||||
infoX.innerHTML = rn(point[0]);
|
||||
infoY.innerHTML = rn(point[1]);
|
||||
infoCell.innerHTML = i;
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
infoArea.innerHTML = cells.area[i] ? si(cells.area[i] * distanceScale.value ** 2) + unit : "n/a";
|
||||
infoHeight.innerHTML = getFriendlyHeight(cells.h[i]) + " (" + cells.h[i] + ")";
|
||||
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
|
||||
infoPrec.innerHTML = pack.cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
|
||||
infoState.innerHTML = ifDefined(cells.state[i]) !== "no" ? pack.states[cells.state[i]].name + " (" + cells.state[i] + ")" : "n/a";
|
||||
infoCulture.innerHTML = ifDefined(cells.culture[i]) !== "no" ? pack.cultures[cells.culture[i]].name + " (" + cells.culture[i] + ")" : "n/a";
|
||||
infoPopulation.innerHTML = getFriendlyPopulation(i);
|
||||
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
|
||||
const f = cells.f[i];
|
||||
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
|
||||
infoBiome.innerHTML = biomesData.name[cells.biome[i]];
|
||||
}
|
||||
|
||||
// return value (v) if defined with number of decimals (d), else return "no" or attribute (r)
|
||||
function ifDefined(v, r = "no", d) {
|
||||
if (v === null || v === undefined) return r;
|
||||
if (d) return v.toFixed(d);
|
||||
return v;
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) height value from map data
|
||||
function getFriendlyHeight(h) {
|
||||
const unit = heightUnit.value;
|
||||
let unitRatio = 3.281; // default calculations are in feet
|
||||
if (unit === "m") unitRatio = 1; // if meter
|
||||
else if (unit === "f") unitRatio = 0.5468; // if fathom
|
||||
|
||||
let height = -990;
|
||||
if (h >= 20) height = Math.pow(h - 18, +heightExponent.value);
|
||||
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
|
||||
|
||||
return rn(height * unitRatio) + " " + unit;
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) precipitation value from map data
|
||||
function getFriendlyPrecipitation(i) {
|
||||
const prec = grid.cells.prec[pack.cells.g[i]];
|
||||
return prec * 100 + " mm";
|
||||
}
|
||||
|
||||
// get user-friendly (real-world) population value from map data
|
||||
function getFriendlyPopulation(i) {
|
||||
const rural = pack.cells.pop[i] * populationRate.value;
|
||||
const urban = pack.cells.burg[i] ? pack.burgs[pack.cells.burg[i]].population * populationRate.value * urbanization.value : 0;
|
||||
return si(rural+urban);
|
||||
}
|
||||
|
||||
// assign lock behavior
|
||||
document.querySelectorAll("[data-locked]").forEach(function(e) {
|
||||
e.addEventListener("mouseover", function(event) {
|
||||
if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
|
||||
else tip("Click to lock the option and always use the current value on new map generation");
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
e.addEventListener("click", function(event) {
|
||||
const id = (this.id).slice(5);
|
||||
if (this.className === "icon-lock") unlock(id);
|
||||
else lock(id);
|
||||
});
|
||||
});
|
||||
|
||||
// lock option
|
||||
function lock(id) {
|
||||
const input = document.querySelector("[data-stored='"+id+"']");
|
||||
if (input) localStorage.setItem(id, input.value);
|
||||
const el = document.getElementById("lock_" + id);
|
||||
if(!el) return;
|
||||
el.dataset.locked = 1;
|
||||
el.className = "icon-lock";
|
||||
}
|
||||
|
||||
// unlock option
|
||||
function unlock(id) {
|
||||
localStorage.removeItem(id);
|
||||
const el = document.getElementById("lock_" + id);
|
||||
if(!el) return;
|
||||
el.dataset.locked = 0;
|
||||
el.className = "icon-lock-open";
|
||||
}
|
||||
|
||||
// check if option is locked
|
||||
function locked(id) {
|
||||
const lockEl = document.getElementById("lock_" + id);
|
||||
return lockEl.dataset.locked == 1;
|
||||
}
|
||||
|
||||
// Hotkeys, see github.com/Azgaar/Fantasy-Map-Generator/wiki/Hotkeys
|
||||
document.addEventListener("keydown", function(event) {
|
||||
const active = document.activeElement.tagName;
|
||||
if (active === "INPUT" || active === "SELECT" || active === "TEXTAREA") return; // don't trigger if user inputs a text
|
||||
const key = event.keyCode, ctrl = event.ctrlKey, shift = event.shiftKey;
|
||||
if (key === 118) regenerateMap(); // "F7" for new map
|
||||
else if (key === 27) {closeDialogs(); hideOptions();} // Escape to close all dialogs
|
||||
else if (key === 9) {toggleOptions(event); event.preventDefault();} // Tab to toggle options
|
||||
else if (ctrl && key === 80) saveAsImage("png"); // Ctrl + "P" to save as PNG
|
||||
else if (ctrl && key === 83) saveAsImage("svg"); // Ctrl + "S" to save as SVG
|
||||
else if (ctrl && key === 77) saveMap(); // Ctrl + "M" to save MAP file
|
||||
else if (ctrl && key === 76) mapToLoad.click(); // Ctrl + "L" to load MAP
|
||||
else if (key === 46) removeElementOnKey(); // "Delete" to remove the selected element
|
||||
|
||||
else if (shift && key === 192) console.log(pack.cells); // Shift + "`" to log cells data
|
||||
else if (shift && key === 66) console.table(pack.burgs); // Shift + "B" to log burgs data
|
||||
else if (shift && key === 83) console.table(pack.states); // Shift + "S" to log states data
|
||||
else if (shift && key === 67) console.table(pack.cultures); // Shift + "C" to log cultures data
|
||||
else if (shift && key === 70) console.table(pack.features); // Shift + "F" to log features data
|
||||
|
||||
else if (key === 88) toggleTexture(); // "X" to toggle Texture layer
|
||||
else if (key === 72) toggleHeight(); // "H" to toggle Heightmap layer
|
||||
else if (key === 66) toggleBiomes(); // "B" to toggle Biomes layer
|
||||
else if (key === 69) toggleCells(); // "E" to toggle Cells layer
|
||||
else if (key === 71) toggleGrid(); // "G" to toggle Grid layer
|
||||
else if (key === 79) toggleCoordinates(); // "O" to toggle Coordinates layer
|
||||
else if (key === 87) toggleCompass(); // "W" to toggle Compass Rose layer
|
||||
else if (key === 86) toggleRivers(); // "V" to toggle Rivers layer
|
||||
else if (key === 82) toggleRelief(); // "R" to toggle Relief icons layer
|
||||
else if (key === 67) toggleCultures(); // "C" to toggle Cultures layer
|
||||
else if (key === 83) toggleStates(); // "S" to toggle States layer
|
||||
else if (key === 68) toggleBorders(); // "D" to toggle Borders layer
|
||||
else if (key === 85) toggleRoutes(); // "U" to toggle Routes layer
|
||||
else if (key === 84) toggleTemp(); // "T" to toggle Temperature layer
|
||||
else if (key === 80) togglePopulation(); // "P" to toggle Population layer
|
||||
else if (key === 65) togglePrec(); // "A" to toggle Precipitation layer
|
||||
else if (key === 76) toggleLabels(); // "L" to toggle Labels layer
|
||||
else if (key === 73) toggleIcons(); // "I" to toggle Icons layer
|
||||
else if (key === 77) toggleMarkers(); // "M" to toggle Markers layer
|
||||
else if (key === 187) toggleRulers(); // Equal (=) to toggle Rulers
|
||||
else if (key === 189) toggleScaleBar(); // Minus (-) to toggle Scale bar
|
||||
|
||||
else if (key === 37) zoom.translateBy(svg, 10, 0); // Left to scroll map left
|
||||
else if (key === 39) zoom.translateBy(svg, -10, 0); // Right to scroll map right
|
||||
else if (key === 38) zoom.translateBy(svg, 0, 10); // Up to scroll map up
|
||||
else if (key === 40) zoom.translateBy(svg, 0, -10); // Up to scroll map up
|
||||
else if (key === 107) zoom.scaleBy(svg, 1.2); // Numpad Plus to zoom map up
|
||||
else if (key === 109) zoom.scaleBy(svg, 0.8); // Numpad Minus to zoom map out
|
||||
else if (key === 48 || key === 96) resetZoom(1000); // 0 to reset zoom
|
||||
else if (key === 49 || key === 97) zoom.scaleTo(svg, 1); // 1 to zoom to 1
|
||||
else if (key === 50 || key === 98) zoom.scaleTo(svg, 2); // 2 to zoom to 2
|
||||
else if (key === 51 || key === 99) zoom.scaleTo(svg, 3); // 3 to zoom to 3
|
||||
else if (key === 52 || key === 100) zoom.scaleTo(svg, 4); // 4 to zoom to 4
|
||||
else if (key === 53 || key === 101) zoom.scaleTo(svg, 5); // 5 to zoom to 5
|
||||
else if (key === 54 || key === 102) zoom.scaleTo(svg, 6); // 6 to zoom to 6
|
||||
else if (key === 55 || key === 103) zoom.scaleTo(svg, 7); // 7 to zoom to 7
|
||||
else if (key === 56 || key === 104) zoom.scaleTo(svg, 8); // 8 to zoom to 8
|
||||
else if (key === 57 || key === 105) zoom.scaleTo(svg, 9); // 9 to zoom to 9
|
||||
|
||||
else if (ctrl && key === 90) undo.click(); // Ctrl + "Z" to undo
|
||||
else if (ctrl && key === 89) redo.click(); // Ctrl + "Y" to redo
|
||||
});
|
||||
1098
modules/ui/heightmap-editor.js
Normal file
1098
modules/ui/heightmap-editor.js
Normal file
File diff suppressed because it is too large
Load diff
312
modules/ui/labels-editor.js
Normal file
312
modules/ui/labels-editor.js
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
"use strict";
|
||||
function editLabel() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
|
||||
const node = d3.event.target;
|
||||
elSelected = d3.select(node.parentNode).call(d3.drag().on("start", dragLabel)).classed("draggable", true);
|
||||
viewbox.on("touchmove mousemove", showEditorTips);
|
||||
|
||||
$("#labelEditor").dialog({
|
||||
title: "Edit Label: " + node.innerHTML, resizable: false,
|
||||
position: {my: "center top+10", at: "bottom", of: node, collision: "fit"},
|
||||
close: closeLabelEditor
|
||||
});
|
||||
|
||||
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||
drawControlPointsAndLine();
|
||||
selectLabelGroup(node);
|
||||
updateValues(node);
|
||||
|
||||
if (modules.editLabel) return;
|
||||
modules.editLabel = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("labelGroupShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("labelGroupHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("labelGroupSelect").addEventListener("click", changeGroup);
|
||||
document.getElementById("labelGroupInput").addEventListener("change", createNewGroup);
|
||||
document.getElementById("labelGroupNew").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("labelGroupRemove").addEventListener("click", removeLabelsGroup);
|
||||
|
||||
document.getElementById("labelTextShow").addEventListener("click", showTextSection);
|
||||
document.getElementById("labelTextHide").addEventListener("click", hideTextSection);
|
||||
document.getElementById("labelText").addEventListener("input", changeText);
|
||||
document.getElementById("labelTextRandom").addEventListener("click", generateRandomName);
|
||||
|
||||
document.getElementById("labelSizeShow").addEventListener("click", showSizeSection);
|
||||
document.getElementById("labelSizeHide").addEventListener("click", hideSizeSection);
|
||||
document.getElementById("labelStartOffset").addEventListener("input", changeStartOffset);
|
||||
document.getElementById("labelRelativeSize").addEventListener("input", changeRelativeSize);
|
||||
|
||||
document.getElementById("labelLegend").addEventListener("click", editLabelLegend);
|
||||
document.getElementById("labelRemoveSingle").addEventListener("click", removeLabel);
|
||||
|
||||
function showEditorTips() {
|
||||
showMainTip();
|
||||
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to shift the label"); else
|
||||
if (d3.event.target.parentNode.id === "controlPoints") {
|
||||
if (d3.event.target.tagName === "circle") tip("Drag to move, click to delete the control point");
|
||||
if (d3.event.target.tagName === "path") tip("Click to add a control point");
|
||||
}
|
||||
}
|
||||
|
||||
function selectLabelGroup(node) {
|
||||
const group = node.parentNode.parentNode.id;
|
||||
const select = document.getElementById("labelGroupSelect");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
labels.selectAll(":scope > g").each(function() {
|
||||
if (this.id === "burgLabels") return;
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
});
|
||||
}
|
||||
|
||||
function updateValues(node) {
|
||||
document.getElementById("labelText").value = node.innerHTML;
|
||||
document.getElementById("labelStartOffset").value = parseFloat(node.getAttribute("startOffset"));
|
||||
document.getElementById("labelRelativeSize").value = parseFloat(node.getAttribute("font-size"));
|
||||
}
|
||||
|
||||
function drawControlPointsAndLine() {
|
||||
const path = document.getElementById("textPath_" + elSelected.attr("id"));
|
||||
debug.select("#controlPoints").append("path").attr("d", path.getAttribute("d")).on("click", addInterimControlPoint);
|
||||
const l = path.getTotalLength();
|
||||
const increment = l / Math.max(Math.ceil(l / 100), 2);
|
||||
for (let i=0; i <= l; i += increment) {addControlPoint(path.getPointAtLength(i));}
|
||||
}
|
||||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", 1)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
||||
function dragControlPoint() {
|
||||
this.setAttribute("cx", d3.event.x);
|
||||
this.setAttribute("cy", d3.event.y);
|
||||
redrawLabelPath();
|
||||
}
|
||||
|
||||
function redrawLabelPath() {
|
||||
const path = document.getElementById("textPath_" + elSelected.attr("id"));
|
||||
lineGen.curve(d3.curveBundle.beta(1));
|
||||
const points = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
});
|
||||
const d = round(lineGen(points));
|
||||
path.setAttribute("d", d);
|
||||
debug.select("#controlPoints > path").attr("d", d);
|
||||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
this.remove();
|
||||
redrawLabelPath();
|
||||
}
|
||||
|
||||
function addInterimControlPoint() {
|
||||
const point = d3.mouse(this);
|
||||
|
||||
const dists = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
const x = +this.getAttribute("cx");
|
||||
const y = +this.getAttribute("cy");
|
||||
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||
});
|
||||
|
||||
let index = dists.length;
|
||||
if (dists.length > 1) {
|
||||
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||
const closest = dists.indexOf(sorted[0]);
|
||||
const next = dists.indexOf(sorted[1]);
|
||||
if (closest <= next) index = closest+1; else index = next+1;
|
||||
}
|
||||
|
||||
const before = ":nth-child(" + (index + 2) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", 1)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
redrawLabelPath();
|
||||
}
|
||||
|
||||
function dragLabel() {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
const dx = +tr[0] - d3.event.x, dy = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
const transform = `translate(${(dx+x)},${(dy+y)})`;
|
||||
elSelected.attr("transform", transform);
|
||||
debug.select("#controlPoints").attr("transform", transform);
|
||||
});
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("labelGroupSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("labelGroupSection").style.display = "none";
|
||||
document.getElementById("labelGroupInput").style.display = "none";
|
||||
document.getElementById("labelGroupInput").value = "";
|
||||
document.getElementById("labelGroupSelect").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function changeGroup() {
|
||||
document.getElementById(this.value).appendChild(elSelected.node());
|
||||
}
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
if (labelGroupInput.style.display === "none") {
|
||||
labelGroupInput.style.display = "inline-block";
|
||||
labelGroupInput.focus();
|
||||
labelGroupSelect.style.display = "none";
|
||||
} else {
|
||||
labelGroupInput.style.display = "none";
|
||||
labelGroupSelect.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// just rename if only 1 element left
|
||||
const oldGroup = elSelected.node().parentNode;
|
||||
if (oldGroup !== "states" && oldGroup !== "addedLabels" && oldGroup.childElementCount === 1) {
|
||||
document.getElementById("labelGroupSelect").selectedOptions[0].remove();
|
||||
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
|
||||
oldGroup.id = group;
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("labelGroupInput").value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||
document.getElementById("labels").appendChild(newGroup);
|
||||
newGroup.id = group;
|
||||
document.getElementById("labelGroupSelect").options.add(new Option(group, group, false, true));
|
||||
document.getElementById(group).appendChild(elSelected.node());
|
||||
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("labelGroupInput").value = "";
|
||||
}
|
||||
|
||||
function removeLabelsGroup() {
|
||||
const group = elSelected.node().parentNode.id;
|
||||
const basic = group === "states" || group === "addedLabels";
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
alertMessage.innerHTML = `Are you sure you want to remove
|
||||
${basic ? "all elements in the group" : "the entire label group"}?
|
||||
<br><br>Labels to be removed: ${count}`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
$("#labelEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
labels.select("#"+group).selectAll("text").each(function() {
|
||||
document.getElementById("textPath_" + this.id).remove();
|
||||
this.remove();
|
||||
});
|
||||
if (!basic) labels.select("#"+group).remove();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showTextSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("labelTextSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideTextSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("labelTextSection").style.display = "none";
|
||||
}
|
||||
|
||||
function changeText() {
|
||||
const text = document.getElementById("labelText").value;
|
||||
elSelected.select("textPath").text(text);
|
||||
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
|
||||
const id = +elSelected.attr("id").slice(10);
|
||||
pack.states[id].name = text;
|
||||
}
|
||||
}
|
||||
|
||||
function generateRandomName() {
|
||||
let name = "";
|
||||
if (elSelected.attr("id").slice(0,10) === "stateLabel") {
|
||||
const id = +elSelected.attr("id").slice(10);
|
||||
const culture = pack.states[id].culture;
|
||||
name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
|
||||
} else {
|
||||
const box = elSelected.node().getBBox();
|
||||
const cell = findCell((box.x + box.width) / 2, (box.y + box.height) / 2);
|
||||
const culture = pack.cells.culture[cell];
|
||||
name = Names.getCulture(culture);
|
||||
}
|
||||
document.getElementById("labelText").value = name;
|
||||
changeText();
|
||||
}
|
||||
|
||||
function showSizeSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("labelSizeSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideSizeSection() {
|
||||
document.querySelectorAll("#labelEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("labelSizeSection").style.display = "none";
|
||||
}
|
||||
|
||||
function changeStartOffset() {
|
||||
elSelected.select("textPath").attr("startOffset", this.value + "%");
|
||||
tip("Label offset: " + this.value + "%");
|
||||
}
|
||||
|
||||
function changeRelativeSize() {
|
||||
elSelected.select("textPath").attr("font-size", this.value + "%");
|
||||
tip("Label relative size: " + this.value + "%");
|
||||
}
|
||||
|
||||
function editLabelLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
const name = elSelected.text();
|
||||
editLegends(id, name);
|
||||
}
|
||||
|
||||
function removeLabel() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the label?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove label",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
defs.select("#textPath_" + elSelected.attr("id")).remove();
|
||||
elSelected.remove();
|
||||
$("#labelEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeLabelEditor() {
|
||||
debug.select("#controlPoints").remove();
|
||||
unselect();
|
||||
}
|
||||
}
|
||||
813
modules/ui/layers.js
Normal file
813
modules/ui/layers.js
Normal file
|
|
@ -0,0 +1,813 @@
|
|||
// UI module stub to control map layers
|
||||
"use strict";
|
||||
|
||||
// on map regeneration restore layers if they was turned on
|
||||
function restoreLayers() {
|
||||
if (layerIsOn("toggleHeight")) drawHeightmap();
|
||||
if (layerIsOn("toggleCells")) drawCells();
|
||||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
if (layerIsOn("toggleCompass")) compass.attr("display", "block");
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("togglePopulation")) drawPopulation();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleRelief")) ReliefIcons();
|
||||
if (layerIsOn("toggleStates") || layerIsOn("toggleBorders")) drawStatesWithBorders();
|
||||
if (layerIsOn("toggleCultures")) drawCultures();
|
||||
}
|
||||
|
||||
restoreLayers(); // run on-load
|
||||
|
||||
// toggle layers on preset change
|
||||
function changePreset(preset) {
|
||||
const layers = getLayers(preset); // layers to be turned on
|
||||
const ignore = ["toggleTexture", "toggleScaleBar"]; // never toggle this layers
|
||||
|
||||
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
|
||||
if (ignore.includes(e.id)) return; // ignore
|
||||
if (layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
|
||||
else if (!layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
|
||||
});
|
||||
layersPreset.value = preset;
|
||||
}
|
||||
|
||||
// retrun list of layers to be turned on
|
||||
function getLayers(preset) {
|
||||
switch(preset) {
|
||||
case "political": return ["toggleStates", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||
case "cultural": return ["toggleCultures", "toggleRivers", "toggleBorders", "toggleRoutes", "toggleLabels", "toggleIcons"];
|
||||
case "heightmap": return ["toggleHeight", "toggleRivers"];
|
||||
case "biomes": return ["toggleBiomes", "toggleRivers"];
|
||||
case "landmass": return [];
|
||||
}
|
||||
}
|
||||
|
||||
function toggleHeight() {
|
||||
if (!terrs.selectAll("*").size()) {
|
||||
turnButtonOn("toggleHeight");
|
||||
drawHeightmap();
|
||||
} else {
|
||||
if (customization === 1) {tip("You cannot turn off the layer when heightmap is in edit mode", false, "error"); return;}
|
||||
turnButtonOff("toggleHeight");
|
||||
terrs.selectAll("*").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawHeightmap() {
|
||||
console.time("drawHeightmap");
|
||||
terrs.selectAll("*").remove();
|
||||
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(101).fill("");
|
||||
|
||||
const scheme = getColorScheme();
|
||||
const terracing = +styleHeightmapTerracingInput.value / 10; // add additional shifted darker layer for pseudo-3d effect
|
||||
const skip = +styleHeightmapSkipOutput.value + 1;
|
||||
const simplification = +styleHeightmapSimplificationInput.value;
|
||||
switch (+styleHeightmapCurveInput.value) {
|
||||
case 0: lineGen.curve(d3.curveBasisClosed); break;
|
||||
case 1: lineGen.curve(d3.curveLinear); break;
|
||||
case 2: lineGen.curve(d3.curveStep); break;
|
||||
default: lineGen.curve(d3.curveBasisClosed);
|
||||
}
|
||||
|
||||
let currentLayer = 20;
|
||||
const heights = cells.i.sort((a, b) => cells.h[a] - cells.h[b]);
|
||||
for (const i of heights) {
|
||||
const h = cells.h[i];
|
||||
if (h > currentLayer) currentLayer += skip;
|
||||
if (currentLayer > 100) break; // no layers possible with height > 100
|
||||
if (h < currentLayer) continue;
|
||||
if (used[i]) continue; // already marked
|
||||
const onborder = cells.c[i].some(n => cells.h[n] < h);
|
||||
if (!onborder) continue;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.h[i] < h));
|
||||
const chain = connectVertices(vertex, h);
|
||||
if (chain.length < 3) continue;
|
||||
const points = simplifyLine(chain).map(v => vertices.p[v]);
|
||||
paths[h] += round(lineGen(points));
|
||||
}
|
||||
|
||||
terrs.append("rect").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%").attr("fill", scheme(.8)); // draw base layer
|
||||
for (const i of d3.range(20, 101)) {
|
||||
if (paths[i].length < 10) continue;
|
||||
const color = getColor(i, scheme);
|
||||
if (terracing) terrs.append("path").attr("d", paths[i]).attr("transform", "translate(.7,1.4)").attr("fill", d3.color(color).darker(terracing)).attr("data-height", i);
|
||||
terrs.append("path").attr("d", paths[i]).attr("fill", color).attr("data-height", i);
|
||||
}
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, h) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.h[c] === h).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.h[c[0]] < h;
|
||||
const c1 = c[1] >= n || cells.h[c[1]] < h;
|
||||
const c2 = c[2] >= n || cells.h[c[2]] < h;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
function simplifyLine(chain) {
|
||||
if (!simplification) return chain;
|
||||
const n = simplification + 1; // filter each nth element
|
||||
return chain.filter((d, i) => i % n === 0);
|
||||
}
|
||||
|
||||
console.timeEnd("drawHeightmap");
|
||||
}
|
||||
|
||||
function getColorScheme() {
|
||||
const scheme = styleHeightmapSchemeInput.value;
|
||||
if (scheme === "bright") return d3.scaleSequential(d3.interpolateSpectral);
|
||||
if (scheme === "light") return d3.scaleSequential(d3.interpolateRdYlGn);
|
||||
if (scheme === "green") return d3.scaleSequential(d3.interpolateGreens);
|
||||
if (scheme === "monochrome") return d3.scaleSequential(d3.interpolateGreys);
|
||||
}
|
||||
|
||||
function getColor(value, scheme = getColorScheme()) {
|
||||
return scheme(1 - (value < 20 ? value - 5 : value) / 100);
|
||||
}
|
||||
|
||||
function toggleTemp() {
|
||||
if (!temperature.selectAll("*").size()) {
|
||||
turnButtonOn("toggleTemp");
|
||||
drawTemp();
|
||||
} else {
|
||||
turnButtonOff("toggleTemp");
|
||||
temperature.selectAll("*").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawTemp() {
|
||||
console.time("drawTemp");
|
||||
temperature.selectAll("*").remove();
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const scheme = d3.scaleSequential(d3.interpolateSpectral);
|
||||
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min, delta = tMax - tMin;
|
||||
|
||||
const cells = grid.cells, vertices = grid.vertices, n = cells.i.length;
|
||||
const used = new Uint8Array(n); // to detect already passed cells
|
||||
const min = d3.min(cells.temp), 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 = [], labels = []; // store label coordinates
|
||||
|
||||
for (const i of cells.i) {
|
||||
const t = cells.temp[i];
|
||||
if (used[i] || !isolines.includes(t)) continue;
|
||||
const start = findStart(i, t);
|
||||
if (!start) continue;
|
||||
used[i] = 1;
|
||||
//debug.append("circle").attr("r", 3).attr("cx", vertices.p[start][0]).attr("cy", vertices.p[start][1]).attr("fill", "red").attr("stroke", "black").attr("stroke-width", .3);
|
||||
|
||||
const chain = connectVertices(start, t); // vertices chain to form a path
|
||||
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 map
|
||||
temperature.append("path").attr("d", `M0,0 h${svgWidth} v${svgHeight} h${-svgWidth} 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(.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 c = svgWidth / 2; // map center x coordinate
|
||||
// add label on isoline top center
|
||||
const tc = points[d3.scan(points, (a, b) => (a[1] - b[1]) + (Math.abs(a[0] - c) - Math.abs(b[0] - c)) / 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] - c) - Math.abs(b[0] - c)) / 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]);
|
||||
}
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.temp[c] === t).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.temp[c[0]] < t;
|
||||
const c1 = c[1] >= n || cells.temp[c[1]] < t;
|
||||
const c2 = c[2] >= n || cells.temp[c[2]] < t;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
chain.push(start);
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawTemp");
|
||||
}
|
||||
|
||||
function toggleBiomes() {
|
||||
if (!biomes.selectAll("path").size()) {
|
||||
turnButtonOn("toggleBiomes");
|
||||
drawBiomes();
|
||||
} else {
|
||||
biomes.selectAll("path").remove();
|
||||
turnButtonOff("toggleBiomes");
|
||||
}
|
||||
}
|
||||
|
||||
function drawBiomes() {
|
||||
biomes.selectAll("path").remove();
|
||||
const cells = pack.cells, vertices = pack.vertices, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(biomesData.i.length).fill("");
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.biome[i]) continue; // no need to mark water
|
||||
if (used[i]) continue; // already marked
|
||||
const b = cells.biome[i];
|
||||
const onborder = cells.c[i].some(n => cells.biome[n] !== b);
|
||||
if (!onborder) continue;
|
||||
const edgeVerticle = cells.v[i].find(v => vertices.c[v].some(i => cells.biome[i] !== b));
|
||||
const chain = connectVertices(edgeVerticle, b);
|
||||
if (chain.length < 3) continue;
|
||||
const points = chain.map(v => vertices.p[v]);
|
||||
paths[b] += "M" + points.join("L") + "Z";
|
||||
}
|
||||
|
||||
paths.forEach(function(d, i) {
|
||||
if (d.length < 10) return;
|
||||
const color = biomesData.color[i];
|
||||
biomes.append("path").attr("d", d).attr("fill", color).attr("stroke", color).attr("id", "biome"+i);
|
||||
});
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, b) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.biome[c] === b).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.biome[c[0]] !== b;
|
||||
const c1 = c[1] >= n || cells.biome[c[1]] !== b;
|
||||
const c2 = c[2] >= n || cells.biome[c[2]] !== b;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePrec() {
|
||||
if (!prec.selectAll("circle").size()) {
|
||||
turnButtonOn("togglePrec");
|
||||
drawPrec();
|
||||
} else {
|
||||
turnButtonOff("togglePrec");
|
||||
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
|
||||
prec.selectAll("text").attr("opacity", 1).transition(hide).attr("opacity", 0);
|
||||
prec.selectAll("circle").transition(hide).attr("r", 0).remove();
|
||||
prec.transition().delay(1000).attr("display", "none");
|
||||
}
|
||||
}
|
||||
|
||||
function drawPrec() {
|
||||
prec.selectAll("circle").remove();
|
||||
const cells = grid.cells, p = grid.points;
|
||||
prec.attr("display", "block");
|
||||
const show = d3.transition().duration(800).ease(d3.easeSinIn);
|
||||
prec.selectAll("text").attr("opacity", 0).transition(show).attr("opacity", 1);
|
||||
|
||||
const data = cells.i.filter(i => cells.h[i] >= 20 && cells.prec[i]);
|
||||
prec.selectAll("circle").data(data).enter().append("circle")
|
||||
.attr("cx", d => p[d][0]).attr("cy", d => p[d][1]).attr("r", 0)
|
||||
.transition(show).attr("r", d => rn(Math.max(Math.sqrt(cells.prec[d] * .5), .8),2));
|
||||
}
|
||||
|
||||
function togglePopulation() {
|
||||
if (!population.selectAll("line").size()) {
|
||||
turnButtonOn("togglePopulation");
|
||||
drawPopulation();
|
||||
} else {
|
||||
turnButtonOff("togglePopulation");
|
||||
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
|
||||
population.select("#rural").selectAll("line").transition(hide).attr("y2", d => d[1]).remove();
|
||||
population.select("#urban").selectAll("line").transition(hide).delay(1000).attr("y2", d => d[1]).remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawPopulation() {
|
||||
population.selectAll("line").remove();
|
||||
const cells = pack.cells, p = cells.p, burgs = pack.burgs;
|
||||
|
||||
// pack.cells.pop.reduce((s=0,v) => s+v)
|
||||
// pack.burgs.map(b => b.population).reduce((s=0,v) => s+v)
|
||||
const show = d3.transition().duration(2000).ease(d3.easeSinIn);
|
||||
|
||||
const rural = Array.from(cells.i.filter(i => cells.pop[i] > 0), i => [p[i][0], p[i][1], p[i][1] - cells.pop[i] / 8]);
|
||||
population.select("#rural").selectAll("line").data(rural).enter().append("line")
|
||||
.attr("x1", d => d[0]).attr("y1", d => d[1])
|
||||
.attr("x2", d => d[0]).attr("y2", d => d[1])
|
||||
.transition(show).attr("y2", d => d[2]);
|
||||
|
||||
const urban = burgs.filter(b => b.i).map(b => [b.x, b.y, b.y - b.population / 8 * urbanization.value]);
|
||||
population.select("#urban").selectAll("line").data(urban).enter().append("line")
|
||||
.attr("x1", d => d[0]).attr("y1", d => d[1])
|
||||
.attr("x2", d => d[0]).attr("y2", d => d[1])
|
||||
.transition(show).delay(500).attr("y2", d => d[2]);
|
||||
}
|
||||
|
||||
function toggleCells() {
|
||||
if (!cells.selectAll("path").size()) {
|
||||
turnButtonOn("toggleCells");
|
||||
drawCells();
|
||||
} else {
|
||||
cells.selectAll("path").remove();
|
||||
turnButtonOff("toggleCells");
|
||||
}
|
||||
}
|
||||
|
||||
function drawCells() {
|
||||
cells.selectAll("path").remove();
|
||||
const data = customization === 1 ? grid.cells.i : pack.cells.i;
|
||||
const polygon = customization === 1 ? getGridPolygon : getPackPolygon;
|
||||
let path = "";
|
||||
data.forEach(i => path += "M" + polygon(i));
|
||||
cells.append("path").attr("d", path);
|
||||
}
|
||||
|
||||
function toggleCultures() {
|
||||
if (!cults.selectAll("path").size()) {
|
||||
turnButtonOn("toggleCultures");
|
||||
drawCultures();
|
||||
} else {
|
||||
cults.selectAll("path").remove();
|
||||
turnButtonOff("toggleCultures");
|
||||
}
|
||||
}
|
||||
|
||||
function drawCultures() {
|
||||
console.time("drawCultures");
|
||||
|
||||
cults.selectAll("path").remove();
|
||||
const cells = pack.cells, vertices = pack.vertices, cultures = pack.cultures, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const paths = new Array(cultures.length).fill("");
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.culture[i]) continue;
|
||||
if (used[i]) continue;
|
||||
used[i] = 1;
|
||||
const c = cells.culture[i];
|
||||
const onborder = cells.c[i].some(n => cells.culture[n] !== c);
|
||||
if (!onborder) continue;
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.culture[i] !== c));
|
||||
const chain = connectVertices(vertex, c);
|
||||
if (chain.length < 3) continue;
|
||||
const points = chain.map(v => vertices.p[v]);
|
||||
paths[c] += "M" + points.join("L") + "Z";
|
||||
}
|
||||
|
||||
const data = paths.map((p, i) => [p, i, cultures[i].color]).filter(d => d[0].length > 10);
|
||||
cults.selectAll("path").data(data).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("id", d => "culture"+d[1]);
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1]; // previous vertex in chain
|
||||
chain.push(current); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.culture[c] === t).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.culture[c[0]] !== t;
|
||||
const c1 = c[1] >= n || cells.culture[c[1]] !== t;
|
||||
const c2 = c[2] >= n || cells.culture[c[2]] !== t;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) current = v[0];
|
||||
else if (v[1] !== prev && c1 !== c2) current = v[1];
|
||||
else if (v[2] !== prev && c0 !== c2) current = v[2];
|
||||
if (current === chain[chain.length - 1]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawCultures");
|
||||
}
|
||||
|
||||
function toggleStates() {
|
||||
if (!layerIsOn("toggleStates")) {
|
||||
turnButtonOn("toggleStates");
|
||||
regions.attr("display", null);
|
||||
drawStatesWithBorders();
|
||||
} else {
|
||||
regions.attr("display", "none").selectAll("path").remove();
|
||||
turnButtonOff("toggleStates");
|
||||
}
|
||||
}
|
||||
|
||||
function drawStatesWithBorders() {
|
||||
console.time("drawStatesWithBorders");
|
||||
regions.selectAll("path").remove();
|
||||
borders.selectAll("path").remove();
|
||||
|
||||
const cells = pack.cells, vertices = pack.vertices, states = pack.states, n = cells.i.length;
|
||||
const used = new Uint8Array(cells.i.length);
|
||||
const body = new Array(states.length).fill(""); // store path around each state
|
||||
const gap = new Array(states.length).fill(""); // store path along water for each state to fill the gaps
|
||||
const border = new Array(states.length).fill(""); // store path along land for all states to render borders
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (!cells.state[i] || used[i]) continue;
|
||||
used[i] = 1;
|
||||
const s = cells.state[i];
|
||||
const onborder = cells.c[i].some(n => cells.state[n] !== s);
|
||||
if (!onborder) continue;
|
||||
|
||||
const borderWith = cells.c[i].map(c => cells.state[c]).find(n => n !== s);
|
||||
const vertex = cells.v[i].find(v => vertices.c[v].some(i => cells.state[i] === borderWith));
|
||||
const chain = connectVertices(vertex, s, borderWith);
|
||||
if (chain.length < 3) continue;
|
||||
body[s] += "M" + chain.map(v => vertices.p[v[0]]).join("L");
|
||||
gap[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : !v[2] ? r + "L" + vertices.p[v[0]] : d[i+1] && !d[i+1][2] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||
border[s] += "M" + vertices.p[chain[0][0]] + chain.reduce((r,v,i,d) => !i ? r : v[2] && s > v[1] ? r + "L" + vertices.p[v[0]] : d[i+1] && d[i+1][2] && s > d[i+1][1] ? r + "M" + vertices.p[v[0]] : r, "");
|
||||
|
||||
// debug.append("circle").attr("r", 2).attr("cx", cells.p[i][0]).attr("cy", cells.p[i][1]).attr("fill", "blue");
|
||||
// const p = chain.map(v => vertices.p[v[0]])
|
||||
// debug.selectAll(".circle").data(p).enter().append("circle").attr("cx", d => d[0]).attr("cy", d => d[1]).attr("r", 1).attr("fill", "red");
|
||||
// const poly = polylabel([p], 1.0); // pole of inaccessibility
|
||||
// debug.append("circle").attr("r", 2).attr("cx", poly[0]).attr("cy", poly[1]).attr("fill", "green");
|
||||
}
|
||||
|
||||
const bodyData = body.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||
statesBody.selectAll("path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("fill", d => d[2]).attr("stroke", "none").attr("id", d => "state"+d[1]);
|
||||
const gapData = gap.map((p, i) => [p.length > 10 ? p : null, i, states[i].color]).filter(d => d[0]);
|
||||
statesBody.selectAll(".path").data(gapData).enter().append("path").attr("d", d => d[0]).attr("fill", "none").attr("stroke", d => d[2]).attr("id", d => "state-gap"+d[1]);
|
||||
|
||||
defs.select("#statePaths").selectAll("clipPath").remove();
|
||||
defs.select("#statePaths").selectAll("clipPath").data(bodyData).enter().append("clipPath").attr("id", d => "state-clip"+d[1]).append("use").attr("href", d => "#state"+d[1]);
|
||||
statesHalo.selectAll(".path").data(bodyData).enter().append("path").attr("d", d => d[0]).attr("stroke", d => d3.color(d[2]).darker().hex()).attr("id", d => "state-border"+d[1]).attr("clip-path", d => "url(#state-clip"+d[1]+")");
|
||||
|
||||
const borderData = border.map((p, i) => [p.length > 10 ? p : null, i]).filter(d => d[0]);
|
||||
borders.selectAll("path").data(borderData).enter().append("path").attr("d", d => d[0]).attr("id", d => "border"+d[1]);
|
||||
|
||||
// connect vertices to chain
|
||||
function connectVertices(start, t, state) {
|
||||
const chain = []; // vertices chain to form a path
|
||||
let land = vertices.c[start].some(c => cells.h[c] >= 20 && cells.state[c] !== t);
|
||||
function check(i) {state = cells.state[i]; land = cells.h[i] >= 20;}
|
||||
|
||||
for (let i=0, current = start; i === 0 || current !== start && i < 20000; i++) {
|
||||
const prev = chain[chain.length - 1] ? chain[chain.length - 1][0] : -1; // previous vertex in chain
|
||||
chain.push([current, state, land]); // add current vertex to sequence
|
||||
const c = vertices.c[current]; // cells adjacent to vertex
|
||||
c.filter(c => cells.state[c] === t).forEach(c => used[c] = 1);
|
||||
const c0 = c[0] >= n || cells.state[c[0]] !== t;
|
||||
const c1 = c[1] >= n || cells.state[c[1]] !== t;
|
||||
const c2 = c[2] >= n || cells.state[c[2]] !== t;
|
||||
const v = vertices.v[current]; // neighboring vertices
|
||||
if (v[0] !== prev && c0 !== c1) {current = v[0]; check(c0 ? c[0] : c[1]);} else
|
||||
if (v[1] !== prev && c1 !== c2) {current = v[1]; check(c1 ? c[1] : c[2]);} else
|
||||
if (v[2] !== prev && c0 !== c2) {current = v[2]; check(c2 ? c[2] : c[0]);}
|
||||
if (current === chain[chain.length - 1][0]) {console.error("Next vertex is not found"); break;}
|
||||
}
|
||||
chain.push([start, state, land]); // add starting vertex to sequence to close the path
|
||||
return chain;
|
||||
}
|
||||
console.timeEnd("drawStatesWithBorders");
|
||||
}
|
||||
|
||||
function toggleBorders() {
|
||||
if (!layerIsOn("toggleBorders")) {
|
||||
turnButtonOn("toggleBorders");
|
||||
$('#borders').fadeIn();
|
||||
} else {
|
||||
turnButtonOff("toggleBorders");
|
||||
$('#borders').fadeOut();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleGrid() {
|
||||
if (!gridOverlay.selectAll("*").size()) {
|
||||
turnButtonOn("toggleGrid");
|
||||
drawGrid();
|
||||
calculateFriendlyGridSize();
|
||||
} else {
|
||||
turnButtonOff("toggleGrid");
|
||||
gridOverlay.selectAll("*").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid() {
|
||||
console.time("drawGrid");
|
||||
gridOverlay.selectAll("*").remove();
|
||||
const type = styleGridType.value;
|
||||
const size = +styleGridSize.value;
|
||||
if (type === "pointyHex" || type === "flatHex") {
|
||||
const points = getHexGridPoints(size, type);
|
||||
const hex = "m" + getHex(size, type).slice(0, 4).join("l");
|
||||
const d = points.map(p => "M" + p + hex).join("");
|
||||
gridOverlay.append("path").attr("d", d);
|
||||
} else if (type === "square") {
|
||||
const pathX = d3.range(size, svgWidth, size).map(x => "M" + rn(x, 2) + ",0v" + svgHeight);
|
||||
const pathY = d3.range(size, svgHeight, size).map(y => "M0," + rn(y, 2) + "h" + svgWidth);
|
||||
gridOverlay.append("path").attr("d", pathX + pathY);
|
||||
}
|
||||
|
||||
// calculate hexes centers
|
||||
function getHexGridPoints(size, type) {
|
||||
const points = [];
|
||||
const rt3 = Math.sqrt(3);
|
||||
const off = type === "pointyHex" ? rn(rt3 * size / 2, 2) : rn(size * 3 / 2, 2);
|
||||
const ySpace = type === "pointyHex" ? rn(size * 3 / 2, 2) : rn(rt3 * size / 2, 2);
|
||||
const xSpace = type === "pointyHex" ? rn(rt3 * size, 2) : rn(size * 3, 2);
|
||||
for (let y = 0, l = 0; y < graphHeight+ySpace; y += ySpace, l++) {
|
||||
for (let x = l % 2 ? 0 : off; x < graphWidth+xSpace; x += xSpace) {points.push([rn(x, 2), rn(y, 2)]);}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// calculate hex points
|
||||
function getHex(radius, type) {
|
||||
let x0 = 0, y0 = 0;
|
||||
const s = type === "pointyHex" ? 0 : Math.PI / -6;
|
||||
const thirdPi = Math.PI / 3;
|
||||
let angles = [s, s + thirdPi, s + 2 * thirdPi, s + 3 * thirdPi, s + 4 * thirdPi, s + 5 * thirdPi];
|
||||
return angles.map(function(a) {
|
||||
const x1 = Math.sin(a) * radius;
|
||||
const y1 = -Math.cos(a) * radius;
|
||||
const dx = rn(x1 - x0, 2);
|
||||
const dy = rn(y1 - y0, 2);
|
||||
x0 = x1, y0 = y1;
|
||||
return [rn(dx, 2), rn(dy, 2)];
|
||||
});
|
||||
}
|
||||
|
||||
console.timeEnd("drawGrid");
|
||||
}
|
||||
|
||||
function toggleCoordinates() {
|
||||
if (!coordinates.selectAll("*").size()) {
|
||||
turnButtonOn("toggleCoordinates");
|
||||
drawCoordinates();
|
||||
} else {
|
||||
turnButtonOff("toggleCoordinates");
|
||||
coordinates.selectAll("*").remove();
|
||||
}
|
||||
}
|
||||
|
||||
function drawCoordinates() {
|
||||
if (!layerIsOn("toggleCoordinates")) return;
|
||||
coordinates.selectAll("*").remove(); // remove every time
|
||||
const eqY = +document.getElementById("equatorOutput").value;
|
||||
const eqD = +document.getElementById("equidistanceOutput").value;
|
||||
const merX = svgWidth / 2; // x of zero meridian
|
||||
const steps = [.5, 1, 2, 5, 10, 15, 30]; // possible steps
|
||||
const goal = merX / eqD / scale ** 0.4 * 12;
|
||||
const step = steps.reduce((p, c) => Math.abs(c - goal) < Math.abs(p - goal) ? c : p);
|
||||
const p = getViewPoint(2 + scale, 2 + scale); // on border point on viexBox
|
||||
const desired = +coordinates.attr("data-size")
|
||||
const size = Math.max(desired + 1 - scale, 2);
|
||||
coordinates.attr("font-size", size);
|
||||
|
||||
// map coordinates extent
|
||||
const extent = getViewBoxExtent();
|
||||
const latS = mapCoordinates.latS + (1 - extent[1][1] / svgHeight) * mapCoordinates.latT;
|
||||
const latN = mapCoordinates.latN - (extent[0][1] / svgHeight) * mapCoordinates.latT;
|
||||
const lonW = mapCoordinates.lonW + (extent[0][0] / svgWidth) * mapCoordinates.lonT;
|
||||
const lonE = mapCoordinates.lonE - (1 - extent[1][0] / svgWidth) * mapCoordinates.lonT;
|
||||
|
||||
const grid = coordinates.append("g").attr("id", "coordinateGrid");
|
||||
const lalitude = coordinates.append("g").attr("id", "lalitude");
|
||||
const longitude = coordinates.append("g").attr("id", "longitude");
|
||||
|
||||
// rander lalitude lines
|
||||
d3.range(nextStep(latS), nextStep(latN)+0.01, step).forEach(function(l) {
|
||||
const c = eqY - l / 90 * eqD;
|
||||
const lat = l < 0 ? Math.abs(l) + "°S" : l + "°N";
|
||||
grid.append("line").attr("x1", 0).attr("x2", svgWidth).attr("y1", c).attr("y2", c).attr("l", l);
|
||||
const nearBorder = c - size <= extent[0][1] || c + size / 2 >= extent[1][1];
|
||||
if (nearBorder || !Number.isInteger(l)) return;
|
||||
lalitude.append("text").attr("x", p.x).attr("y", c).text(lat);
|
||||
});
|
||||
|
||||
// rander longitude lines
|
||||
d3.range(nextStep(lonW), nextStep(lonE)+0.01, step).forEach(function(l) {
|
||||
const c = merX + l / 90 * eqD;
|
||||
const lon = l < 0 ? Math.abs(l) + "°W" : l + "°E";
|
||||
grid.append("line").attr("x1", c).attr("x2", c).attr("y1", 0).attr("y2", svgHeight).attr("l", l);
|
||||
const nearBorder = c - size * 1.5 <= extent[0][0] || c + size >= extent[1][0];
|
||||
if (nearBorder || !Number.isInteger(l)) return;
|
||||
longitude.append("text").attr("x", c).attr("y", p.y).text(lon);
|
||||
});
|
||||
|
||||
function nextStep(v) {return (v / step | 0) * step;}
|
||||
}
|
||||
|
||||
// conver svg point into viewBox point
|
||||
function getViewPoint(x, y) {
|
||||
const view = document.getElementById('viewbox');
|
||||
const svg = document.getElementById('map');
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = x, pt.y = y;
|
||||
return pt.matrixTransform(view.getScreenCTM().inverse());
|
||||
}
|
||||
|
||||
function toggleCompass() {
|
||||
if (!layerIsOn("toggleCompass")) {
|
||||
turnButtonOn("toggleCompass");
|
||||
$('#compass').fadeIn();
|
||||
if (!compass.selectAll("*").size()) {
|
||||
const tr = `translate(80 80) scale(.25)`;
|
||||
d3.select("#rose").attr("transform", tr);
|
||||
compass.append("use").attr("xlink:href","#rose");
|
||||
}
|
||||
} else {
|
||||
$('#compass').fadeOut();
|
||||
turnButtonOff("toggleCompass");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRelief() {
|
||||
if (!layerIsOn("toggleRelief")) {
|
||||
turnButtonOn("toggleRelief");
|
||||
if (!terrain.selectAll("*").size()) ReliefIcons();
|
||||
$('#terrain').fadeIn();
|
||||
} else {
|
||||
$('#terrain').fadeOut();
|
||||
turnButtonOff("toggleRelief");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTexture() {
|
||||
if (!layerIsOn("toggleTexture")) {
|
||||
turnButtonOn("toggleTexture");
|
||||
// append default texture image selected by default. Don't append on load to not harm performance
|
||||
if (!texture.selectAll("*").size()) {
|
||||
const link = getAbsolutePath(styleTextureInput.value);
|
||||
texture.append("image").attr("x", 0).attr("y", 0).attr("width", "100%").attr("height", "100%")
|
||||
.attr('xlink:href', link).attr('preserveAspectRatio', "xMidYMid slice");
|
||||
}
|
||||
$('#texture').fadeIn();
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
} else {
|
||||
$('#texture').fadeOut();
|
||||
turnButtonOff("toggleTexture");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRivers() {
|
||||
if (!layerIsOn("toggleRivers")) {
|
||||
turnButtonOn("toggleRivers");
|
||||
$('#rivers').fadeIn();
|
||||
} else {
|
||||
$('#rivers').fadeOut();
|
||||
turnButtonOff("toggleRivers");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRoutes() {
|
||||
if (!layerIsOn("toggleRoutes")) {
|
||||
turnButtonOn("toggleRoutes");
|
||||
$('#routes').fadeIn();
|
||||
} else {
|
||||
$('#routes').fadeOut();
|
||||
turnButtonOff("toggleRoutes");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleMarkers() {
|
||||
if (!layerIsOn("toggleMarkers")) {
|
||||
turnButtonOn("toggleMarkers");
|
||||
$('#markers').fadeIn();
|
||||
} else {
|
||||
$('#markers').fadeOut();
|
||||
turnButtonOff("toggleMarkers");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLabels() {
|
||||
if (!layerIsOn("toggleLabels")) {
|
||||
turnButtonOn("toggleLabels");
|
||||
$('#labels').fadeIn();
|
||||
} else {
|
||||
turnButtonOff("toggleLabels");
|
||||
$('#labels').fadeOut();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleIcons() {
|
||||
if (!layerIsOn("toggleIcons")) {
|
||||
turnButtonOn("toggleIcons");
|
||||
$('#icons').fadeIn();
|
||||
} else {
|
||||
turnButtonOff("toggleIcons");
|
||||
$('#icons').fadeOut();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRulers() {
|
||||
if (!layerIsOn("toggleRulers")) {
|
||||
turnButtonOn("toggleRulers");
|
||||
$('#ruler').fadeIn();
|
||||
} else {
|
||||
$('#ruler').fadeOut();
|
||||
turnButtonOff("toggleRulers");
|
||||
}
|
||||
}
|
||||
|
||||
function toggleScaleBar() {
|
||||
if (!layerIsOn("toggleScaleBar")) {
|
||||
turnButtonOn("toggleScaleBar");
|
||||
$('#scaleBar').fadeIn();
|
||||
} else {
|
||||
$('#scaleBar').fadeOut();
|
||||
turnButtonOff("toggleScaleBar");
|
||||
}
|
||||
}
|
||||
|
||||
function layerIsOn(el) {
|
||||
const buttonoff = document.getElementById(el).classList.contains("buttonoff");
|
||||
return !buttonoff;
|
||||
}
|
||||
|
||||
function turnButtonOff(el) {
|
||||
document.getElementById(el).classList.add("buttonoff");
|
||||
layersPreset.value = "custom";
|
||||
}
|
||||
|
||||
function turnButtonOn(el) {
|
||||
document.getElementById(el).classList.remove("buttonoff");
|
||||
layersPreset.value = "custom";
|
||||
}
|
||||
|
||||
// move layers on mapLayers dragging (jquery sortable)
|
||||
$("#mapLayers").sortable({items: "li:not(.solid)", cancel: ".solid", update: moveLayer});
|
||||
function moveLayer(event, ui) {
|
||||
const el = getLayer(ui.item.attr("id"));
|
||||
if (el) {
|
||||
const prev = getLayer(ui.item.prev().attr("id"));
|
||||
const next = getLayer(ui.item.next().attr("id"));
|
||||
if (prev) el.insertAfter(prev); else if (next) el.insertBefore(next);
|
||||
}
|
||||
}
|
||||
|
||||
// define connection between option layer buttons and actual svg groups to move the element
|
||||
function getLayer(id) {
|
||||
if (id === "toggleHeight") return $("#terrs");
|
||||
if (id === "toggleBiomes") return $("#biomes");
|
||||
if (id === "toggleCells") return $("#cells");
|
||||
if (id === "toggleGrid") return $("#gridOverlay");
|
||||
if (id === "toggleCoordinates") return $("#coordinates");
|
||||
if (id === "toggleCompass") return $("#compass");
|
||||
if (id === "toggleRivers") return $("#rivers");
|
||||
if (id === "toggleRelief") return $("#terrain");
|
||||
if (id === "toggleCultures") return $("#cults");
|
||||
if (id === "toggleStates") return $("#regions");
|
||||
if (id === "toggleBorders") return $("#borders");
|
||||
if (id === "toggleRoutes") return $("#routes");
|
||||
if (id === "toggleTemp") return $("#temperature");
|
||||
if (id === "togglePrec") return $("#prec");
|
||||
if (id === "togglePopulation") return $("#population");
|
||||
if (id === "toggleTexture") return $("#texture");
|
||||
if (id === "toggleLabels") return $("#labels");
|
||||
if (id === "toggleIcons") return $("#icons");
|
||||
if (id === "toggleMarkers") return $("#markers");
|
||||
if (id === "toggleRulers") return $("#ruler");
|
||||
}
|
||||
147
modules/ui/legends-editor.js
Normal file
147
modules/ui/legends-editor.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
"use strict";
|
||||
function editLegends(id, name) {
|
||||
// update list of objects
|
||||
const select = document.getElementById("legendSelect");
|
||||
for (let i = select.options.length; i < notes.length; i++) {
|
||||
select.options.add(new Option(notes[i].id, notes[i].id));
|
||||
}
|
||||
|
||||
// select an object
|
||||
if (id) {
|
||||
let note = notes.find(note => note.id === id);
|
||||
if (note === undefined) {
|
||||
if (!name) name = id;
|
||||
note = {id, name, legend: ""};
|
||||
notes.push(note);
|
||||
select.options.add(new Option(id, id));
|
||||
}
|
||||
select.value = id;
|
||||
legendName.value = note.name;
|
||||
legendText.value = note.legend;
|
||||
}
|
||||
|
||||
// open a dialog
|
||||
$("#legendEditor").dialog({
|
||||
title: "Legends Editor", minWidth: Math.min(svgWidth, 400),
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
if (modules.editLegends) return;
|
||||
modules.editLegends = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("legendSelect").addEventListener("change", changeObject);
|
||||
document.getElementById("legendName").addEventListener("input", changeName);
|
||||
document.getElementById("legendText").addEventListener("input", changeText);
|
||||
document.getElementById("legendFocus").addEventListener("click", validateHighlightElement);
|
||||
document.getElementById("legendDownload").addEventListener("click", downloadLegends);
|
||||
document.getElementById("legendUpload").addEventListener("click", () => legendsToLoad.click());
|
||||
document.getElementById("legendsToLoad").addEventListener("change", uploadLegends);
|
||||
document.getElementById("legendRemove").addEventListener("click", triggerLegendRemove);
|
||||
|
||||
function changeObject() {
|
||||
const note = notes.find(note => note.id === this.value);
|
||||
legendName.value = note.name;
|
||||
legendText.value = note.legend;
|
||||
}
|
||||
|
||||
function changeName() {
|
||||
const id = document.getElementById("legendSelect").value;
|
||||
const note = notes.find(note => note.id === id);
|
||||
note.name = this.value;
|
||||
}
|
||||
|
||||
function changeText() {
|
||||
const id = document.getElementById("legendSelect").value;
|
||||
const note = notes.find(note => note.id === id);
|
||||
note.legend = this.value;
|
||||
}
|
||||
|
||||
function validateHighlightElement() {
|
||||
const select = document.getElementById("legendSelect");
|
||||
const element = document.getElementById(select.value);
|
||||
|
||||
// if element is not found
|
||||
if (element === null) {
|
||||
alertMessage.innerHTML = "Related element is not found. Would you like to remove the note (legend item)?";
|
||||
$("#alert").dialog({resizable: false, title: "Element not found",
|
||||
buttons: {
|
||||
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||
Keep: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
highlightElement(element); // if element is found
|
||||
}
|
||||
|
||||
function highlightElement(element) {
|
||||
if (debug.select(".highlighted").size()) return; // allow only 1 highlight element simultaniosly
|
||||
const box = element.getBBox();
|
||||
const transform = element.getAttribute("transform") || null;
|
||||
const t = d3.transition().duration(1000).ease(d3.easeBounceOut);
|
||||
const r = d3.transition().duration(500).ease(d3.easeLinear);
|
||||
|
||||
const highlight = debug.append("rect").attr("x", box.x).attr("y", box.y)
|
||||
.attr("width", box.width).attr("height", box.height).attr("transform", transform);
|
||||
|
||||
highlight.classed("highlighted", 1)
|
||||
.transition(t).style("outline-offset", "0px")
|
||||
.transition(r).style("outline-color", "transparent").remove();
|
||||
|
||||
const tr = parseTransform(transform);
|
||||
let x = box.x + box.width / 2;
|
||||
if (tr[0]) x += tr[0];
|
||||
let y = box.y + box.height / 2;
|
||||
if (tr[1]) y += tr[1];
|
||||
if (scale >= 2) zoomTo(x, y, scale, 1600);
|
||||
}
|
||||
|
||||
function downloadLegends() {
|
||||
const legendString = JSON.stringify(notes);
|
||||
const dataBlob = new Blob([legendString],{type:"text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.download = "legends" + Date.now() + ".txt";
|
||||
link.href = url;
|
||||
link.click();
|
||||
}
|
||||
|
||||
function uploadLegends() {
|
||||
const fileToLoad = this.files[0];
|
||||
this.value = "";
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function(fileLoadedEvent) {
|
||||
const dataLoaded = fileLoadedEvent.target.result;
|
||||
if (dataLoaded) {
|
||||
notes = JSON.parse(dataLoaded);
|
||||
document.getElementById("legendSelect").options.length = 0;
|
||||
editLegends(notes[0].id, notes[0].name);
|
||||
} else {
|
||||
tip("Cannot load a file. Please check the data format", false, "error")
|
||||
}
|
||||
}
|
||||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||
}
|
||||
|
||||
function triggerLegendRemove() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the selected legend?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove legend element",
|
||||
buttons: {
|
||||
Remove: function() {$(this).dialog("close"); removeLegend();},
|
||||
Keep: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function removeLegend() {
|
||||
const select = document.getElementById("legendSelect");
|
||||
const index = notes.findIndex(n => n.id === select.value);
|
||||
notes.splice(index, 1);
|
||||
select.options.length = 0;
|
||||
if (!notes.length) {$("#legendEditor").dialog("close"); return;}
|
||||
editLegends(notes[0].id, notes[0].name);
|
||||
}
|
||||
|
||||
}
|
||||
474
modules/ui/markers-editor.js
Normal file
474
modules/ui/markers-editor.js
Normal file
|
|
@ -0,0 +1,474 @@
|
|||
"use strict";
|
||||
function editMarker() {
|
||||
if (customization) return;
|
||||
closeDialogs("#markerEditor, .stable");
|
||||
$("#markerEditor").dialog();
|
||||
|
||||
elSelected = d3.select(d3.event.target).call(d3.drag().on("start", dragMarker)).classed("draggable", true);
|
||||
updateInputs();
|
||||
|
||||
if (modules.editMarker) return;
|
||||
modules.editMarker = true;
|
||||
|
||||
$("#markerEditor").dialog({
|
||||
title: "Edit Marker", resizable: false,
|
||||
position: {my: "center top+30", at: "bottom", of: d3.event, collision: "fit"},
|
||||
close: closeMarkerEditor
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("markerGroup").addEventListener("click", toggleGroupSection);
|
||||
document.getElementById("markerAddGroup").addEventListener("click", toggleGroupInput);
|
||||
document.getElementById("markerSelectGroup").addEventListener("change", changeGroup);
|
||||
document.getElementById("markerInputGroup").addEventListener("change", createGroup);
|
||||
document.getElementById("markerRemoveGroup").addEventListener("click", removeGroup);
|
||||
|
||||
document.getElementById("markerIcon").addEventListener("click", toggleIconSection);
|
||||
document.getElementById("markerIconSize").addEventListener("input", changeIconSize);
|
||||
document.getElementById("markerIconShiftX").addEventListener("input", changeIconShiftX);
|
||||
document.getElementById("markerIconShiftY").addEventListener("input", changeIconShiftY);
|
||||
document.getElementById("markerIconCustom").addEventListener("input", applyCustomUnicodeIcon);
|
||||
|
||||
document.getElementById("markerStyle").addEventListener("click", toggleStyleSection);
|
||||
document.getElementById("markerSize").addEventListener("input", changeMarkerSize);
|
||||
document.getElementById("markerBaseStroke").addEventListener("input", changePinStroke);
|
||||
document.getElementById("markerBaseFill").addEventListener("input", changePinFill);
|
||||
document.getElementById("markerIconStrokeWidth").addEventListener("input", changeIconStrokeWidth);
|
||||
document.getElementById("markerIconStroke").addEventListener("input", changeIconStroke);
|
||||
document.getElementById("markerIconFill").addEventListener("input", changeIconFill);
|
||||
|
||||
document.getElementById("markerToggleBubble").addEventListener("click", togglePinVisibility);
|
||||
document.getElementById("markerLegendButton").addEventListener("click", editMarkerLegend);
|
||||
document.getElementById("markerAdd").addEventListener("click", toggleAddMarker);
|
||||
document.getElementById("markerRemove").addEventListener("click", removeMarker);
|
||||
|
||||
updateGroupOptions();
|
||||
|
||||
function dragMarker() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||
this.setAttribute("transform", transform);
|
||||
});
|
||||
}
|
||||
|
||||
function updateInputs() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const symbol = d3.select("#defs-markers").select(id);
|
||||
const icon = symbol.select("text");
|
||||
|
||||
markerSelectGroup.value = id.slice(1);
|
||||
markerIconSize.value = parseFloat(icon.attr("font-size"));
|
||||
markerIconShiftX.value = parseFloat(icon.attr("x"));
|
||||
markerIconShiftY.value = parseFloat(icon.attr("y"));
|
||||
|
||||
markerSize.value = elSelected.attr("data-size");
|
||||
markerBaseStroke.value = symbol.select("path").attr("fill");
|
||||
markerBaseFill.value = symbol.select("circle").attr("fill");
|
||||
|
||||
markerIconStrokeWidth.value = icon.attr("stroke-width");
|
||||
markerIconStroke.value = icon.attr("stroke");
|
||||
markerIconFill.value = icon.attr("fill");
|
||||
|
||||
markerToggleBubble.className = symbol.select("circle").attr("opacity") === "0" ? "icon-info" : "icon-info-circled";
|
||||
|
||||
const table = document.getElementById("markerIconTable");
|
||||
let selected = table.getElementsByClassName("selected");
|
||||
if (selected.length) selected[0].removeAttribute("class");
|
||||
selected = document.querySelectorAll("#markerIcon" + icon.text().codePointAt());
|
||||
if (selected.length) selected[0].className = "selected";
|
||||
markerIconCustom.value = selected.length ? "" : icon.text();
|
||||
}
|
||||
|
||||
function toggleGroupSection() {
|
||||
if (markerGroupSection.style.display === "inline-block") {
|
||||
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "inline-block");
|
||||
markerGroupSection.style.display = "none";
|
||||
} else {
|
||||
markerEditor.querySelectorAll("button:not(#markerGroup)").forEach(b => b.style.display = "none");
|
||||
markerGroupSection.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function updateGroupOptions() {
|
||||
markerSelectGroup.innerHTML = "";
|
||||
d3.select("#defs-markers").selectAll("symbol").each(function() {
|
||||
markerSelectGroup.options.add(new Option(this.id, this.id));
|
||||
});
|
||||
markerSelectGroup.value = elSelected.attr("data-id").slice(1);
|
||||
}
|
||||
|
||||
function toggleGroupInput() {
|
||||
if (markerInputGroup.style.display === "inline-block") {
|
||||
markerSelectGroup.style.display = "inline-block";
|
||||
markerInputGroup.style.display = "none";
|
||||
} else {
|
||||
markerSelectGroup.style.display = "none";
|
||||
markerInputGroup.style.display = "inline-block";
|
||||
markerInputGroup.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function changeGroup() {
|
||||
elSelected.attr("xlink:href", "#"+this.value);
|
||||
elSelected.attr("data-id", "#"+this.value);
|
||||
}
|
||||
|
||||
function createGroup() {
|
||||
let newGroup = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||
if (Number.isFinite(+newGroup.charAt(0))) newGroup = "m" + newGroup;
|
||||
if (document.getElementById(newGroup)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
markerInputGroup.value = "";
|
||||
// clone old group assigning new id
|
||||
const id = elSelected.attr("data-id");
|
||||
const clone = d3.select("#defs-markers").select(id).node().cloneNode(true);
|
||||
clone.id = newGroup;
|
||||
document.getElementById("defs-markers").insertBefore(clone, null);
|
||||
elSelected.attr("xlink:href", "#"+newGroup).attr("data-id", "#"+newGroup);
|
||||
|
||||
// select new group
|
||||
markerSelectGroup.options.add(new Option(newGroup, newGroup, false, true));
|
||||
toggleGroupInput();
|
||||
}
|
||||
|
||||
function removeGroup() {
|
||||
const id = elSelected.attr("data-id");
|
||||
const used = document.querySelectorAll("use[data-id='"+id+"']");
|
||||
const count = used.length === 1 ? "1 element" : used.length + " elements";
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the marker (" + count + ")?";
|
||||
|
||||
$("#alert").dialog({resizable: false, title: "Remove marker",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
if (id !== "#marker0") d3.select("#defs-markers").select(id).remove();
|
||||
used.forEach(e => e.remove());
|
||||
updateGroupOptions();
|
||||
updateGroupOptions();
|
||||
$("#markerEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleIconSection() {
|
||||
if (markerIconSection.style.display === "inline-block") {
|
||||
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "inline-block");
|
||||
markerIconSection.style.display = "none";
|
||||
} else {
|
||||
markerEditor.querySelectorAll("button:not(#markerIcon)").forEach(b => b.style.display = "none");
|
||||
markerIconSection.style.display = "inline-block";
|
||||
if (!markerIconTable.innerHTML) drawIconsList();
|
||||
}
|
||||
}
|
||||
|
||||
function drawIconsList() {
|
||||
let icons = [
|
||||
// emoticons in FF:
|
||||
["2693", "⚓", "Anchor"],
|
||||
["26EA", "⛪", "Church"],
|
||||
["1F3EF", "🏯", "Japanese Castle"],
|
||||
["1F3F0", "🏰", "Castle"],
|
||||
["1F5FC", "🗼", "Tower"],
|
||||
["1F3E0", "🏠", "House"],
|
||||
["1F3AA", "🎪", "Tent"],
|
||||
["1F3E8", "🏨", "Hotel"],
|
||||
["1F4B0", "💰", "Money bag"],
|
||||
["1F4A8", "💨", "Dashing away"],
|
||||
["1F334", "🌴", "Palm"],
|
||||
["1F335", "🌵", "Cactus"],
|
||||
["1F33E", "🌾", "Sheaf"],
|
||||
["1F5FB", "🗻", "Mountain"],
|
||||
["1F30B", "🌋", "Volcano"],
|
||||
["1F40E", "🐎", "Horse"],
|
||||
["1F434", "🐴", "Horse Face"],
|
||||
["1F42E", "🐮", "Cow"],
|
||||
["1F43A", "🐺", "Wolf Face"],
|
||||
["1F435", "🐵", "Monkey face"],
|
||||
["1F437", "🐷", "Pig face"],
|
||||
["1F414", "🐔", "Chiken"],
|
||||
["1F411", "🐑", "Eve"],
|
||||
["1F42B", "🐫", "Camel"],
|
||||
["1F418", "🐘", "Elephant"],
|
||||
["1F422", "🐢", "Turtle"],
|
||||
["1F40C", "🐌", "Snail"],
|
||||
["1F40D", "🐍", "Snake"],
|
||||
["1F433", "🐳", "Whale"],
|
||||
["1F42C", "🐬", "Dolphin"],
|
||||
["1F420", "🐟", "Fish"],
|
||||
["1F432", "🐲", "Dragon Head"],
|
||||
["1F479", "👹", "Ogre"],
|
||||
["1F47B", "👻", "Ghost"],
|
||||
["1F47E", "👾", "Alien"],
|
||||
["1F480", "💀", "Skull"],
|
||||
["1F374", "🍴", "Fork and knife"],
|
||||
["1F372", "🍲", "Food"],
|
||||
["1F35E", "🍞", "Bread"],
|
||||
["1F357", "🍗", "Poultry leg"],
|
||||
["1F347", "🍇", "Grapes"],
|
||||
["1F34F", "🍏", "Apple"],
|
||||
["1F352", "🍒", "Cherries"],
|
||||
["1F36F", "🍯", "Honey pot"],
|
||||
["1F37A", "🍺", "Beer"],
|
||||
["1F377", "🍷", "Wine glass"],
|
||||
["1F3BB", "🎻", "Violin"],
|
||||
["1F3B8", "🎸", "Guitar"],
|
||||
["26A1", "⚡", "Electricity"],
|
||||
["1F320", "🌠", "Shooting star"],
|
||||
["1F319", "🌙", "Crescent moon"],
|
||||
["1F525", "🔥", "Fire"],
|
||||
["1F4A7", "💧", "Droplet"],
|
||||
["1F30A", "🌊", "Wave"],
|
||||
["231B", "⌛", "Hourglass"],
|
||||
["1F3C6", "🏆", "Goblet"],
|
||||
["26F2", "⛲", "Fountain"],
|
||||
["26F5", "⛵", "Sailboat"],
|
||||
["26FA", "⛺", "Tend"],
|
||||
["1F489", "💉", "Syringe"],
|
||||
["1F4D6", "📚", "Books"],
|
||||
["1F3AF", "🎯", "Archery"],
|
||||
["1F52E", "🔮", "Magic ball"],
|
||||
["1F3AD", "🎭", "Performing arts"],
|
||||
["1F3A8", "🎨", "Artist palette"],
|
||||
["1F457", "👗", "Dress"],
|
||||
["1F451", "👑", "Crown"],
|
||||
["1F48D", "💍", "Ring"],
|
||||
["1F48E", "💎", "Gem"],
|
||||
["1F514", "🔔", "Bell"],
|
||||
["1F3B2", "🎲", "Die"],
|
||||
// black and white icons in FF:
|
||||
["26A0", "⚠", "Alert"],
|
||||
["2317", "⌗", "Hash"],
|
||||
["2318", "⌘", "POI"],
|
||||
["2307", "⌇", "Wavy"],
|
||||
["21E6", "⇦", "Left arrow"],
|
||||
["21E7", "⇧", "Top arrow"],
|
||||
["21E8", "⇨", "Right arrow"],
|
||||
["21E9", "⇩", "Left arrow"],
|
||||
["21F6", "⇶", "Three arrows"],
|
||||
["2699", "⚙", "Gear"],
|
||||
["269B", "⚛", "Atom"],
|
||||
["0024", "$", "Dollar"],
|
||||
["2680", "⚀", "Die1"],
|
||||
["2681", "⚁", "Die2"],
|
||||
["2682", "⚂", "Die3"],
|
||||
["2683", "⚃", "Die4"],
|
||||
["2684", "⚄", "Die5"],
|
||||
["2685", "⚅", "Die6"],
|
||||
["26B4", "⚴", "Pallas"],
|
||||
["26B5", "⚵", "Juno"],
|
||||
["26B6", "⚶", "Vesta"],
|
||||
["26B7", "⚷", "Chiron"],
|
||||
["26B8", "⚸", "Lilith"],
|
||||
["263F", "☿", "Mercury"],
|
||||
["2640", "♀", "Venus"],
|
||||
["2641", "♁", "Earth"],
|
||||
["2642", "♂", "Mars"],
|
||||
["2643", "♃", "Jupiter"],
|
||||
["2644", "♄", "Saturn"],
|
||||
["2645", "♅", "Uranus"],
|
||||
["2646", "♆", "Neptune"],
|
||||
["2647", "♇", "Pluto"],
|
||||
["26B3", "⚳", "Ceres"],
|
||||
["2654", "♔", "Chess king"],
|
||||
["2655", "♕", "Chess queen"],
|
||||
["2656", "♖", "Chess rook"],
|
||||
["2657", "♗", "Chess bishop"],
|
||||
["2658", "♘", "Chess knight"],
|
||||
["2659", "♙", "Chess pawn"],
|
||||
["2660", "♠", "Spade"],
|
||||
["2663", "♣", "Club"],
|
||||
["2665", "♥", "Heart"],
|
||||
["2666", "♦", "Diamond"],
|
||||
["2698", "⚘", "Flower"],
|
||||
["2625", "☥", "Ankh"],
|
||||
["2626", "☦", "Orthodox"],
|
||||
["2627", "☧", "Chi Rho"],
|
||||
["2628", "☨", "Lorraine"],
|
||||
["2629", "☩", "Jerusalem"],
|
||||
["2670", "♰", "Syriac cross"],
|
||||
["2020", "†", "Dagger"],
|
||||
["262A", "☪", "Muslim"],
|
||||
["262D", "☭", "Soviet"],
|
||||
["262E", "☮", "Peace"],
|
||||
["262F", "☯", "Yin yang"],
|
||||
["26A4", "⚤", "Heterosexuality"],
|
||||
["26A2", "⚢", "Female homosexuality"],
|
||||
["26A3", "⚣", "Male homosexuality"],
|
||||
["26A5", "⚥", "Male and female"],
|
||||
["26AD", "⚭", "Rings"],
|
||||
["2690", "⚐", "White flag"],
|
||||
["2691", "⚑", "Black flag"],
|
||||
["263C", "☼", "Sun"],
|
||||
["263E", "☾", "Moon"],
|
||||
["2668", "♨", "Hot springs"],
|
||||
["2600", "☀", "Black sun"],
|
||||
["2601", "☁", "Cloud"],
|
||||
["2602", "☂", "Umbrella"],
|
||||
["2603", "☃", "Snowman"],
|
||||
["2604", "☄", "Comet"],
|
||||
["2605", "★", "Black star"],
|
||||
["2606", "☆", "White star"],
|
||||
["269D", "⚝", "Outlined star"],
|
||||
["2618", "☘", "Shamrock"],
|
||||
["21AF", "↯", "Lightning"],
|
||||
["269C", "⚜", "FleurDeLis"],
|
||||
["2622", "☢", "Radiation"],
|
||||
["2623", "☣", "Biohazard"],
|
||||
["2620", "☠", "Skull"],
|
||||
["2638", "☸", "Dharma"],
|
||||
["2624", "☤", "Caduceus"],
|
||||
["2695", "⚕", "Aeculapius staff"],
|
||||
["269A", "⚚", "Hermes staff"],
|
||||
["2697", "⚗", "Alembic"],
|
||||
["266B", "♫", "Music"],
|
||||
["2702", "✂", "Scissors"],
|
||||
["2696", "⚖", "Scales"],
|
||||
["2692", "⚒", "Hammer and pick"],
|
||||
["2694", "⚔", "Swords"]
|
||||
];
|
||||
|
||||
const table = document.getElementById("markerIconTable");
|
||||
table.addEventListener("click", selectIcon, false);
|
||||
table.addEventListener("mouseover", hoverIcon, false);
|
||||
let row = "";
|
||||
|
||||
for (let i=0; i < icons.length; i++) {
|
||||
if (i%16 === 0) row = table.insertRow(0);
|
||||
const cell = row.insertCell(0);
|
||||
const icon = String.fromCodePoint(parseInt(icons[i][0], 16));
|
||||
cell.innerHTML = icon;
|
||||
cell.id = "markerIcon" + icon.codePointAt();
|
||||
cell.dataset.desc = icons[i][2];
|
||||
}
|
||||
}
|
||||
|
||||
function selectIcon(e) {
|
||||
if (e.target !== e.currentTarget) {
|
||||
const table = document.getElementById("markerIconTable");
|
||||
const selected = table.getElementsByClassName("selected");
|
||||
if (selected.length) selected[0].removeAttribute("class");
|
||||
e.target.className = "selected";
|
||||
const id = elSelected.attr("data-id");
|
||||
const icon = e.target.innerHTML;
|
||||
d3.select("#defs-markers").select(id).select("text").text(icon);
|
||||
}
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function hoverIcon(e) {
|
||||
if (e.target !== e.currentTarget) tip(e.target.innerHTML + " " + e.target.dataset.desc);
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function changeIconSize() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("font-size", this.value + "px");
|
||||
}
|
||||
|
||||
function changeIconShiftX() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("x", this.value + "%");
|
||||
}
|
||||
|
||||
function changeIconShiftY() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("y", this.value + "%");
|
||||
}
|
||||
|
||||
function applyCustomUnicodeIcon() {
|
||||
if (!this.value) return;
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").text(this.value);
|
||||
}
|
||||
|
||||
function toggleStyleSection() {
|
||||
if (markerStyleSection.style.display === "inline-block") {
|
||||
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "inline-block");
|
||||
markerStyleSection.style.display = "none";
|
||||
} else {
|
||||
markerEditor.querySelectorAll("button:not(#markerStyle)").forEach(b => b.style.display = "none");
|
||||
markerStyleSection.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function changeMarkerSize() {
|
||||
const id = elSelected.attr("data-id");
|
||||
document.querySelectorAll("use[data-id='"+id+"']").forEach(e => e.dataset.size = markerSize.value);
|
||||
invokeActiveZooming();
|
||||
}
|
||||
|
||||
function changePinStroke() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select(id).select("path").attr("fill", this.value);
|
||||
d3.select(id).select("circle").attr("stroke", this.value);
|
||||
}
|
||||
|
||||
function changePinFill() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select(id).select("circle").attr("fill", this.value);
|
||||
}
|
||||
|
||||
function changeIconStrokeWidth() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("stroke-width", this.value);
|
||||
}
|
||||
|
||||
function changeIconStroke() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("stroke", this.value);
|
||||
}
|
||||
|
||||
function changeIconFill() {
|
||||
const id = elSelected.attr("data-id");
|
||||
d3.select("#defs-markers").select(id).select("text").attr("fill", this.value);
|
||||
}
|
||||
|
||||
function togglePinVisibility() {
|
||||
const id = elSelected.attr("data-id");
|
||||
let show = 1;
|
||||
if (this.className === "icon-info-circled") {this.className = "icon-info"; show = 0; }
|
||||
else this.className = "icon-info-circled";
|
||||
d3.select(id).select("circle").attr("opacity", show);
|
||||
d3.select(id).select("path").attr("opacity", show);
|
||||
}
|
||||
|
||||
function editMarkerLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
document.getElementById("addMarker").click();
|
||||
}
|
||||
|
||||
function removeMarker() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the marker?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove marker",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#markerEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeMarkerEditor() {
|
||||
unselect();
|
||||
if (addMarker.classList.contains("pressed")) addMarker.classList.remove("pressed");
|
||||
if (markerAdd.classList.contains("pressed")) markerAdd.classList.remove("pressed");
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
}
|
||||
}
|
||||
|
||||
277
modules/ui/measurers.js
Normal file
277
modules/ui/measurers.js
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
// UI measurers: rulers (linear, curve, area) and Scale Bar
|
||||
"use strict";
|
||||
|
||||
// Linear measurer (one is added by default)
|
||||
function addRuler(x1, y1, x2, y2) {
|
||||
const cx = rn((x1 + x2) / 2, 2), cy = rn((y1 + y2) / 2, 2);
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const dash = rn(30 / distanceScale.value, 2);
|
||||
|
||||
// body
|
||||
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "white").attr("stroke-width", size);
|
||||
rulerNew.append("line").attr("x1", x1).attr("y1", y1).attr("x2", x2).attr("y2", y2).attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x1).attr("cy", y1).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
|
||||
rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("cx", x2).attr("cy", y2).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
|
||||
|
||||
// label and center
|
||||
const angle = Math.atan2(y2 - y1, x2 - x1) * 180 / Math.PI;
|
||||
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||
const dist = rn(Math.hypot(x1 - x2, y1 - y2));
|
||||
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
rulerNew.append("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", rotate).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
|
||||
rulerNew.append("text").attr("x", cx).attr("y", cy).attr("dx", ".3em").attr("dy", "-.3em").attr("transform", rotate).attr("font-size", 10 * size).text(label).on("click", removeParent);
|
||||
}
|
||||
|
||||
function dragRuler() {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
const x = +tr[0] - d3.event.x, y = +tr[1] - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const transform = `translate(${(x + d3.event.x)},${(y + d3.event.y)})`;
|
||||
this.setAttribute("transform", transform);
|
||||
});
|
||||
}
|
||||
|
||||
function dragRulerEdge() {
|
||||
const ruler = d3.select(this.parentNode);
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
|
||||
d3.select(this).attr("cx", x).attr("cy", y);
|
||||
const line = ruler.selectAll("line");
|
||||
const left = this.dataset.edge === "left";
|
||||
const x0 = left ? +line.attr("x2") : +line.attr("x1");
|
||||
const y0 = left ? +line.attr("y2") : +line.attr("y1");
|
||||
if (left) line.attr("x1", x).attr("y1", y); else line.attr("x2", x).attr("y2", y);
|
||||
|
||||
const cx = rn((x + x0) / 2, 2), cy = rn((y + y0) / 2, 2);
|
||||
const dist = Math.hypot(x0 - x, y0 - y);
|
||||
const label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
const atan = x0 > x ? Math.atan2(y0 - y, x0 - x) : Math.atan2(y - y0, x - x0);
|
||||
const angle = rn(atan * 180 / Math.PI, 3);
|
||||
const rotate = `rotate(${angle} ${cx} ${cy})`;
|
||||
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
ruler.select("rect").attr("x", cx - size * 1.5).attr("y", cy - size * 1.5).attr("transform", rotate);
|
||||
ruler.select("text").attr("x", cx).attr("y", cy).attr("transform", rotate).text(label);
|
||||
}
|
||||
|
||||
function rulerCenterDrag() {
|
||||
let xc1, yc1, xc2, yc2, r1, r2;
|
||||
const rulerOld = d3.select(this.parentNode); // current ruler
|
||||
let x = d3.event.x, y = d3.event.y; // current coords
|
||||
const line = rulerOld.selectAll("line"); // current lines
|
||||
const x1 = +line.attr("x1"), y1 = +line.attr("y1"), x2 = +line.attr("x2"), y2 = +line.attr("y2"); // initial line edge points
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const dash = +rulerOld.select(".gray").attr("stroke-dasharray");
|
||||
|
||||
const rulerNew = ruler.insert("g", ":first-child");
|
||||
rulerNew.attr("transform", rulerOld.attr("transform")).call(d3.drag().on("start", dragRuler));
|
||||
rulerNew.append("line").attr("class", "white").attr("stroke-width", size);
|
||||
rulerNew.append("line").attr("class", "gray").attr("stroke-dasharray", dash).attr("stroke-width", size);
|
||||
rulerNew.append("text").attr("dx", ".3em").attr("dy", "-.3em").on("click", removeParent).attr("font-size", 10 * size).attr("stroke-width", size);
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
x = d3.event.x, y = d3.event.y;
|
||||
|
||||
// change first part
|
||||
let dist = rn(Math.hypot(x1 - x, y1 - y));
|
||||
let label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
let atan = x1 > x ? Math.atan2(y1 - y, x1 - x) : Math.atan2(y - y1, x - x1);
|
||||
xc1 = rn((x + x1) / 2, 2), yc1 = rn((y + y1) / 2, 2);
|
||||
r1 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc1} ${yc1})`;
|
||||
line.attr("x1", x1).attr("y1", y1).attr("x2", x).attr("y2", y);
|
||||
rulerOld.select("rect").attr("x", x - size * 1.5).attr("y", y - size * 1.5).attr("transform", null);
|
||||
rulerOld.select("text").attr("x", xc1).attr("y", yc1).attr("transform", r1).text(label);
|
||||
|
||||
// change second (new) part
|
||||
dist = rn(Math.hypot(x2 - x, y2 - y));
|
||||
label = rn(dist * distanceScale.value) + " " + distanceUnit.value;
|
||||
atan = x2 > x ? Math.atan2(y2 - y, x2 - x) : Math.atan2(y - y2, x - x2);
|
||||
xc2 = rn((x + x2) / 2, 2), yc2 = rn((y + y2) / 2, 2);
|
||||
r2 = `rotate(${rn(atan * 180 / Math.PI, 3)} ${xc2} ${yc2})`;
|
||||
rulerNew.selectAll("line").attr("x1", x).attr("y1", y).attr("x2", x2).attr("y2", y2);
|
||||
rulerNew.select("text").attr("x", xc2).attr("y", yc2).attr("transform", r2).text(label);
|
||||
});
|
||||
|
||||
d3.event.on("end", function() {
|
||||
// contols for 1st part
|
||||
rulerOld.select("circle[data-edge='left']").attr("cx", x1).attr("cy", y1);
|
||||
rulerOld.select("circle[data-edge='right']").attr("cx", x).attr("cy", y);
|
||||
rulerOld.select("rect").attr("x", xc1 - size * 1.5).attr("y", yc1 - size * 1.5).attr("transform", r1);
|
||||
|
||||
// contols for 2nd part
|
||||
rulerNew.append("circle").attr("cx", x).attr("cy", y).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "left").call(d3.drag().on("drag", dragRulerEdge));
|
||||
rulerNew.append("circle").attr("cx", x2).attr("cy", y2).attr("r", 2 * size).attr("stroke-width", 0.5 * size).attr("data-edge", "right").call(d3.drag().on("drag", dragRulerEdge));
|
||||
rulerNew.append("rect").attr("x", xc2 - size * 1.5).attr("y", yc2 - size * 1.5).attr("width", size * 3).attr("height", size * 3).attr("transform", r2).attr("stroke-width", .5 * size).call(d3.drag().on("start", rulerCenterDrag));
|
||||
});
|
||||
}
|
||||
|
||||
function drawOpisometer() {
|
||||
lineGen.curve(d3.curveBasis);
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const dash = rn(30 / distanceScale.value, 2);
|
||||
const p0 = d3.mouse(this);
|
||||
const points = [[p0[0], p0[1]]];
|
||||
let length = 0;
|
||||
|
||||
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||
const curve = rulerNew.append("path").attr("class", "white").attr("stroke-width", size);
|
||||
const curveGray = rulerNew.append("path").attr("class", "gray").attr("stroke-width", size).attr("stroke-dasharray", dash);
|
||||
const text = rulerNew.append("text").attr("dy", "-.3em").attr("font-size", 10 * size).on("click", removeParent);
|
||||
const start = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "start").call(d3.drag().on("start", dragOpisometerEnd));
|
||||
const end = rulerNew.append("circle").attr("r", 2 * size).attr("stroke-width", .5 * size).attr("data-edge", "end").call(d3.drag().on("start", dragOpisometerEnd));
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const p = d3.mouse(this);
|
||||
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||
if (diff > 3) points.push([p[0], p[1]]); else return;
|
||||
|
||||
const path = round(lineGen(points));
|
||||
curve.attr("d", path);
|
||||
curveGray.attr("d", path);
|
||||
length = curve.node().getTotalLength();
|
||||
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||
text.attr("x", p[0]).attr("y", p[1]).text(label);
|
||||
});
|
||||
|
||||
d3.event.on("end", function() {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addOpisometer.classList.remove("pressed");
|
||||
|
||||
const c = curve.node().getPointAtLength(length / 2);
|
||||
const p = curve.node().getPointAtLength(length / 2 - 1);
|
||||
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||||
const angle = rn(atan * 180 / Math.PI, 3);
|
||||
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
|
||||
|
||||
rulerNew.attr("data-points", JSON.stringify(points));
|
||||
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
|
||||
start.attr("cx", points[0][0]).attr("cy", points[0][1]);
|
||||
end.attr("cx", last(points)[0]).attr("cy", last(points)[1]);
|
||||
});
|
||||
}
|
||||
|
||||
function dragOpisometerEnd() {
|
||||
const ruler = d3.select(this.parentNode);
|
||||
const curve = ruler.select(".white");
|
||||
const curveGray = ruler.select(".gray");
|
||||
const text = ruler.select("text");
|
||||
|
||||
const points = JSON.parse(ruler.attr("data-points"));
|
||||
const x0 = +this.getAttribute("cx"), y0 = +this.getAttribute("cy");
|
||||
if (x0 === points[0][0] && y0 === points[0][1]) points.reverse();
|
||||
lineGen.curve(d3.curveBasis);
|
||||
let length = 0;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const p = d3.mouse(this);
|
||||
d3.select(this).attr("cx", p[0]).attr("cy", p[1]);
|
||||
|
||||
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||
if (diff > 3) points.push([p[0], p[1]]); else return;
|
||||
|
||||
const path = round(lineGen(points));
|
||||
curve.attr("d", path);
|
||||
curveGray.attr("d", path);
|
||||
length = curve.node().getTotalLength();
|
||||
const label = rn(length * distanceScale.value) + " " + distanceUnit.value;
|
||||
text.text(label);
|
||||
});
|
||||
|
||||
d3.event.on("end", function() {
|
||||
const c = curve.node().getPointAtLength(length / 2);
|
||||
const p = curve.node().getPointAtLength(length / 2 - 1);
|
||||
const atan = p.x > c.x ? Math.atan2(p.y - c.y, p.x - c.x) : Math.atan2(c.y - p.y, c.x - p.x);
|
||||
const angle = rn(atan * 180 / Math.PI, 3);
|
||||
const rotate = `rotate(${angle} ${c.x} ${c.y})`;
|
||||
|
||||
ruler.attr("data-points", JSON.stringify(points));
|
||||
text.attr("x", c.x).attr("y", c.y).attr("transform", rotate);
|
||||
});
|
||||
}
|
||||
|
||||
function drawPlanimeter() {
|
||||
lineGen.curve(d3.curveBasisClosed);
|
||||
const size = rn(1 / scale ** .3 * 2, 1);
|
||||
const p0 = d3.mouse(this);
|
||||
const points = [[p0[0], p0[1]]];
|
||||
|
||||
const rulerNew = ruler.append("g").call(d3.drag().on("start", dragRuler));
|
||||
const curve = rulerNew.append("path").attr("class", "planimeter").attr("stroke-width", size);
|
||||
const text = rulerNew.append("text").attr("font-size", 10 * size).on("click", removeParent);
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const p = d3.mouse(this);
|
||||
const diff = Math.hypot(last(points)[0] - p[0], last(points)[1] - p[1]);
|
||||
if (diff > 5) points.push([p[0], p[1]]); else return;
|
||||
curve.attr("d", round(lineGen(points)));
|
||||
});
|
||||
|
||||
d3.event.on("end", function() {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
addPlanimeter.classList.remove("pressed");
|
||||
|
||||
const polygonArea = rn(Math.abs(d3.polygonArea(points)));
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const area = si(polygonArea * distanceScale.value ** 2) + " " + unit;
|
||||
const c = polylabel([points], 1.0); // pole of inaccessibility
|
||||
text.attr("x", c[0]).attr("y", c[1]).text(area);
|
||||
});
|
||||
}
|
||||
|
||||
// draw default scale bar
|
||||
function drawScaleBar() {
|
||||
if (scaleBar.style("display") === "none") return; // no need to re-draw hidden element
|
||||
scaleBar.selectAll("*").remove(); // fully redraw every time
|
||||
|
||||
const dScale = distanceScale.value;
|
||||
const unit = distanceUnit.value;
|
||||
|
||||
// calculate size
|
||||
const init = 100; // actual length in pixels if scale, dScale and size = 1;
|
||||
const size = +barSize.value;
|
||||
let val = init * size * dScale / scale; // bar length in distance unit
|
||||
if (val > 900) val = rn(val, -3); // round to 1000
|
||||
else if (val > 90) val = rn(val, -2); // round to 100
|
||||
else if (val > 9) val = rn(val, -1); // round to 10
|
||||
else val = rn(val) // round to 1
|
||||
const l = val * scale / dScale; // actual length in pixels on this scale
|
||||
|
||||
scaleBar.append("line").attr("x1", 0.5).attr("y1", 0).attr("x2", l+size-0.5).attr("y2", 0).attr("stroke-width", size).attr("stroke", "white");
|
||||
scaleBar.append("line").attr("x1", 0).attr("y1", size).attr("x2", l+size).attr("y2", size).attr("stroke-width", size).attr("stroke", "#3d3d3d");
|
||||
const dash = size + " " + rn(l / 5 - size, 2);
|
||||
scaleBar.append("line").attr("x1", 0).attr("y1", 0).attr("x2", l+size).attr("y2", 0)
|
||||
.attr("stroke-width", rn(size * 3, 2)).attr("stroke-dasharray", dash).attr("stroke", "#3d3d3d");
|
||||
|
||||
const fontSize = rn(5 * size, 1);
|
||||
scaleBar.selectAll("text").data(d3.range(0,6)).enter().append("text")
|
||||
.attr("x", d => rn(d * l/5, 2)).attr("y", 0).attr("dy", "-.5em")
|
||||
.attr("font-size", fontSize).text(d => rn(d * l/5 * dScale / scale) + (d<5 ? "" : " " + unit));
|
||||
|
||||
if (barLabel.value !== "") {
|
||||
scaleBar.append("text").attr("x", (l+1) / 2).attr("y", 2 * size)
|
||||
.attr("dominant-baseline", "text-before-edge")
|
||||
.attr("font-size", fontSize).text(barLabel.value);
|
||||
}
|
||||
|
||||
const bbox = scaleBar.node().getBBox();
|
||||
// append backbround rectangle
|
||||
scaleBar.insert("rect", ":first-child").attr("x", -10).attr("y", -20).attr("width", bbox.width + 10).attr("height", bbox.height + 15)
|
||||
.attr("stroke-width", size).attr("stroke", "none").attr("filter", "url(#blur5)")
|
||||
.attr("fill", barBackColor.value).attr("opacity", +barBackOpacity.value);
|
||||
|
||||
fitScaleBar();
|
||||
}
|
||||
|
||||
// fit ScaleBar to map size
|
||||
function fitScaleBar() {
|
||||
if (!scaleBar.select("rect").size()) return;
|
||||
const px = isNaN(+barPosX.value) ? 100 : barPosX.value / 100;
|
||||
const py = isNaN(+barPosY.value) ? 100 : barPosY.value / 100;
|
||||
const bbox = scaleBar.select("rect").node().getBBox();
|
||||
const x = rn(svgWidth * px - bbox.width + 10), y = rn(svgHeight * py - bbox.height + 20);
|
||||
scaleBar.attr("transform", `translate(${x},${y})`);
|
||||
}
|
||||
177
modules/ui/namesbase-editor.js
Normal file
177
modules/ui/namesbase-editor.js
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
"use strict";
|
||||
function editNamesbase() {
|
||||
if (customization) return;
|
||||
closeDialogs("#namesbaseEditor, .stable");
|
||||
$("#namesbaseEditor").dialog();
|
||||
|
||||
if (modules.editNamesbase) return;
|
||||
modules.editNamesbase = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("namesbaseSelect").addEventListener("change", updateInputs);
|
||||
document.getElementById("namesbaseTextarea").addEventListener("change", updateNamesData);
|
||||
document.getElementById("namesbaseUpdateExamples").addEventListener("click", updateExamples);
|
||||
document.getElementById("namesbaseExamples").addEventListener("click", updateExamples);
|
||||
document.getElementById("namesbaseName").addEventListener("input", updateBaseName);
|
||||
document.getElementById("namesbaseMin").addEventListener("input", updateBaseMin);
|
||||
document.getElementById("namesbaseMax").addEventListener("input", updateBaseMax);
|
||||
document.getElementById("namesbaseDouble").addEventListener("input", updateBaseDublication);
|
||||
document.getElementById("namesbaseMulti").addEventListener("input", updateBaseMiltiwordRate);
|
||||
document.getElementById("namesbaseAdd").addEventListener("click", namesbaseAdd);
|
||||
document.getElementById("namesbaseDefault").addEventListener("click", namesbaseRestoreDefault);
|
||||
document.getElementById("namesbaseDownload").addEventListener("click", namesbaseDownload);
|
||||
document.getElementById("namesbaseUpload").addEventListener("click", e => namesbaseToLoad.click());
|
||||
document.getElementById("namesbaseToLoad").addEventListener("change", namesbaseUpload);
|
||||
|
||||
createBasesList();
|
||||
updateInputs();
|
||||
|
||||
$("#namesbaseEditor").dialog({
|
||||
title: "Namesbase Editor", width: 468,
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
|
||||
function createBasesList() {
|
||||
const select = document.getElementById("namesbaseSelect");
|
||||
select.innerHTML = "";
|
||||
nameBases.forEach(function(b, i) {
|
||||
const option = new Option(b.name, i);
|
||||
select.options.add(option);
|
||||
});
|
||||
}
|
||||
|
||||
function updateInputs() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (!nameBases[base]) {tip(`Namesbase ${base} is not defined`, false, "error"); return;}
|
||||
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||
document.getElementById("namesbaseName").value = nameBases[base].name;
|
||||
document.getElementById("namesbaseMin").value = nameBases[base].min;
|
||||
document.getElementById("namesbaseMax").value = nameBases[base].max;
|
||||
document.getElementById("namesbaseDouble").value = nameBases[base].d;
|
||||
document.getElementById("namesbaseMulti").value = nameBases[base].m;
|
||||
updateExamples();
|
||||
}
|
||||
|
||||
function updateExamples() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
let examples = "";
|
||||
for (let i=0; i < 10; i++) {
|
||||
const example = Names.getBase(base);
|
||||
if (example === undefined) {
|
||||
examples = "Cannot generate examples. Please verify the data";
|
||||
break;
|
||||
}
|
||||
if (i) examples += ", ";
|
||||
examples += example;
|
||||
}
|
||||
document.getElementById("namesbaseExamples").innerHTML = examples;
|
||||
}
|
||||
|
||||
function updateNamesData() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
const data = document.getElementById("namesbaseTextarea").value.replace(/ /g, "").split(",");
|
||||
if (data.length < 3) {
|
||||
tip("The names data provided is not correct", false, "error");
|
||||
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||
return;
|
||||
}
|
||||
nameBase[base] = data;
|
||||
Names.updateChain(base);
|
||||
}
|
||||
|
||||
function updateBaseName() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
const select = document.getElementById("namesbaseSelect");
|
||||
select.options[namesbaseSelect.selectedIndex].innerHTML = this.value;
|
||||
nameBases[base].name = this.value;
|
||||
}
|
||||
|
||||
function updateBaseMin() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (+this.value > nameBases[base].max) {tip("Minimal length cannot be greater than maximal", false, "error"); return;}
|
||||
nameBases[base].min = +this.value;
|
||||
}
|
||||
|
||||
function updateBaseMax() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
if (+this.value < nameBases[base].min) {tip("Maximal length should be greater than minimal", false, "error"); return;}
|
||||
nameBases[base].max = +this.value;
|
||||
}
|
||||
|
||||
function updateBaseDublication() {
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
nameBases[base].d = this.value;
|
||||
}
|
||||
|
||||
function updateBaseMiltiwordRate() {
|
||||
if (isNaN(+this.value) || +this.value < 0 || +this.value > 1) {tip("Please provide a number within [0-1] range", false, "error"); return;}
|
||||
const base = +document.getElementById("namesbaseSelect").value;
|
||||
nameBases[base].m = +this.value;
|
||||
}
|
||||
|
||||
function namesbaseAdd() {
|
||||
const base = nameBases.length;
|
||||
nameBases.push({name: "Base" + base, min: 5, max: 12, d: "", m: 0});
|
||||
nameBase[base] = ["This", "is", "an", "example", "data", "Please", "replace", "with", "an", "actual", "names", "data", "with", "at", "least", "100", "names"];
|
||||
document.getElementById("namesbaseSelect").add(new Option("Base" + base, base));
|
||||
document.getElementById("namesbaseSelect").value = base;
|
||||
document.getElementById("namesbaseTextarea").value = nameBase[base].join(", ");
|
||||
document.getElementById("namesbaseName").value = "Base" + base;
|
||||
document.getElementById("namesbaseMin").value = 5;
|
||||
document.getElementById("namesbaseMax").value = 12;
|
||||
document.getElementById("namesbaseDouble").value = "";
|
||||
document.getElementById("namesbaseMulti").value = 0;
|
||||
document.getElementById("namesbaseExamples").innerHTML = "Please provide names data";
|
||||
}
|
||||
|
||||
function namesbaseRestoreDefault() {
|
||||
alertMessage.innerHTML = `Are you sure you want to restore default namesbase?`;
|
||||
$("#alert").dialog({resizable: false, title: "Restore default data",
|
||||
buttons: {
|
||||
Restore: function() {
|
||||
$(this).dialog("close");
|
||||
applyDefaultNamesData();
|
||||
createBasesList();
|
||||
updateInputs();
|
||||
Names.updateChains();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function namesbaseDownload() {
|
||||
const data = nameBases.map((b,i) => `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${nameBase[i]}`);
|
||||
const dataBlob = new Blob([data.join("\r\n")], {type:"text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
link.download = "namesbase" + Date.now() + ".txt";
|
||||
link.href = url;
|
||||
link.click();
|
||||
}
|
||||
|
||||
function namesbaseUpload() {
|
||||
const fileToLoad = this.files[0];
|
||||
this.value = "";
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = function(fileLoadedEvent) {
|
||||
const dataLoaded = fileLoadedEvent.target.result;
|
||||
const data = dataLoaded.split("\r\n");
|
||||
if (!data || !data[0]) {tip("Cannot load a namesbase. Please check the data format", false, "error"); return;}
|
||||
|
||||
nameBases = [], nameBase = [];
|
||||
data.forEach(d => {
|
||||
const e = d.split("|");
|
||||
nameBases.push({name:e[0], min:e[1], max:e[2], d:e[3], m:d[4]});
|
||||
nameBase.push(e[5].split(","));
|
||||
});
|
||||
|
||||
createBasesList();
|
||||
updateInputs();
|
||||
Names.updateChains();
|
||||
};
|
||||
|
||||
fileReader.readAsText(fileToLoad, "UTF-8");
|
||||
}
|
||||
}
|
||||
954
modules/ui/options.js
Normal file
954
modules/ui/options.js
Normal file
|
|
@ -0,0 +1,954 @@
|
|||
// UI module to control the options (style, preferences)
|
||||
"use strict";
|
||||
|
||||
$("#optionsContainer").draggable({handle: ".drag-trigger", snap: "svg", snapMode: "both"});
|
||||
$("#mapLayers").disableSelection();
|
||||
|
||||
// show control elements and remove loading screen on map load
|
||||
d3.select("#loading").transition().duration(5000).style("opacity", 0).remove();
|
||||
d3.select("#initial").transition().duration(5000).attr("opacity", 0).remove();
|
||||
d3.select("#optionsContainer").transition().duration(5000).style("opacity", 1);
|
||||
d3.select("#tooltip").transition().duration(5000).style("opacity", 1);
|
||||
|
||||
// remove glow if tip is aknowledged
|
||||
if (localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||
clearMainTip();
|
||||
optionsTrigger.classList.remove("glow");
|
||||
}
|
||||
|
||||
// Show options pane on trigger click
|
||||
function showOptions(event) {
|
||||
if (!localStorage.getItem("disable_click_arrow_tooltip")) {
|
||||
clearMainTip();
|
||||
localStorage.setItem("disable_click_arrow_tooltip", true);
|
||||
optionsTrigger.classList.remove("glow");
|
||||
}
|
||||
|
||||
regenerate.style.display = "none";
|
||||
options.style.display = "block";
|
||||
optionsTrigger.style.display = "none";
|
||||
|
||||
if (event) event.stopPropagation();
|
||||
}
|
||||
|
||||
// Hide options pane on trigger click
|
||||
function hideOptions(event) {
|
||||
options.style.display = "none";
|
||||
optionsTrigger.style.display = "block";
|
||||
if (event) event.stopPropagation();
|
||||
}
|
||||
|
||||
// To toggle options on hotkey press
|
||||
function toggleOptions(event) {
|
||||
if (options.style.display === "none") showOptions(event);
|
||||
else hideOptions(event);
|
||||
}
|
||||
|
||||
// Toggle "New Map!" pane on hover
|
||||
optionsTrigger.addEventListener("mouseenter", function() {
|
||||
if (optionsTrigger.classList.contains("glow")) return;
|
||||
if (options.style.display === "none") regenerate.style.display = "block";
|
||||
});
|
||||
|
||||
collapsible.addEventListener("mouseleave", function() {
|
||||
regenerate.style.display = "none";
|
||||
});
|
||||
|
||||
// Activate options tab on click
|
||||
options.querySelector("div.tab").addEventListener("click", function(event) {
|
||||
if (event.target.tagName !== "BUTTON") return;
|
||||
const id = event.target.id;
|
||||
const active = options.querySelector(".tab > button.active");
|
||||
if (active && id === active.id) return; // already active tab is clicked
|
||||
|
||||
if (active) active.classList.remove("active");
|
||||
document.getElementById(id).classList.add("active");
|
||||
options.querySelectorAll(".tabcontent").forEach(e => e.style.display = "none");
|
||||
|
||||
if (id === "styleTab") styleContent.style.display = "block"; else
|
||||
if (id === "optionsTab") optionsContent.style.display = "block"; else
|
||||
if (id === "toolsTab" && !customization) toolsContent.style.display = "block"; else
|
||||
if (id === "toolsTab" && customization) customizationMenu.style.display = "block"; else
|
||||
if (id === "aboutTab") aboutContent.style.display = "block";
|
||||
});
|
||||
|
||||
options.querySelectorAll("i.collapsible").forEach(el => el.addEventListener("click", collapse));
|
||||
function collapse(e) {
|
||||
const trigger = e.target;
|
||||
const section = trigger.parentElement.nextElementSibling;
|
||||
|
||||
if (section.style.display === "none") {
|
||||
section.style.display = "block";
|
||||
trigger.classList.replace("icon-down-open", "icon-up-open");
|
||||
} else {
|
||||
section.style.display = "none";
|
||||
trigger.classList.replace("icon-up-open", "icon-down-open");
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle style sections on element select
|
||||
styleElementSelect.addEventListener("change", selectStyleElement);
|
||||
function selectStyleElement() {
|
||||
const sel = styleElementSelect.value;
|
||||
let el = viewbox.select("#"+sel);
|
||||
|
||||
styleElements.querySelectorAll("tbody").forEach(e => e.style.display = "none"); // hide all sections
|
||||
const off = el.style("display") === "none" || !el.selectAll("*").size(); // check if layer is off
|
||||
if (off) {
|
||||
styleIsOff.style.display = "block";
|
||||
setTimeout(() => styleIsOff.style.display = "none", 1500);
|
||||
}
|
||||
|
||||
// active group element
|
||||
const group = styleGroupSelect.value;
|
||||
if (sel == "ocean") el = oceanLayers.select("rect");
|
||||
else if (sel == "routes" || sel == "labels" || sel == "lakes" || sel == "anchors" || sel == "burgIcons") {
|
||||
el = d3.select("#"+sel).select("g#"+group).size()
|
||||
? d3.select("#"+sel).select("g#"+group)
|
||||
: d3.select("#"+sel).select("g");
|
||||
}
|
||||
|
||||
if (sel !== "landmass") {
|
||||
// opacity
|
||||
styleOpacity.style.display = "block";
|
||||
styleOpacityInput.value = styleOpacityOutput.value = el.attr("opacity") || 1;
|
||||
|
||||
// filter
|
||||
styleFilter.style.display = "block";
|
||||
if (sel == "ocean") el = oceanLayers;
|
||||
styleFilterInput.value = el.attr("filter") || "";
|
||||
}
|
||||
|
||||
// fill
|
||||
if (sel === "rivers" || sel === "lakes" || sel === "landmass" || sel === "prec") {
|
||||
styleFill.style.display = "block";
|
||||
styleFillInput.value = styleFillOutput.value = el.attr("fill");
|
||||
}
|
||||
|
||||
// stroke color and width
|
||||
if (sel === "routes" || sel === "lakes" || sel === "borders" || sel === "cults" || sel === "cells" || sel === "gridOverlay" || sel === "coastline" || sel === "prec" || sel === "icons" || sel === "coordinates") {
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke");
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
||||
// stroke dash
|
||||
if (sel === "routes" || sel === "borders" || sel === "gridOverlay" || sel === "temperature" || sel === "population" || sel === "coordinates") {
|
||||
styleStrokeDash.style.display = "block";
|
||||
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||
}
|
||||
|
||||
// clipping
|
||||
if (sel === "cells" || sel === "gridOverlay" || sel === "coordinates" || sel === "compass" || sel === "terrain" || sel === "temperature" || sel === "routes" || sel === "texture" || sel === "biomes") {
|
||||
styleClipping.style.display = "block";
|
||||
styleClippingInput.value = el.attr("mask") || "";
|
||||
}
|
||||
|
||||
// shift (translate)
|
||||
if (sel === "gridOverlay") {
|
||||
styleShift.style.display = "block";
|
||||
const tr = parseTransform(el.attr("transform"));
|
||||
styleShiftX.value = tr[0];
|
||||
styleShiftY.value = tr[1];
|
||||
}
|
||||
|
||||
if (sel === "compass") {
|
||||
styleCompass.style.display = "block";
|
||||
const tr = parseTransform(d3.select("#rose").attr("transform"));
|
||||
styleCompassShiftX.value = tr[0];
|
||||
styleCompassShiftY.value = tr[1];
|
||||
styleCompassSizeInput.value = styleCompassSizeOutput.value = tr[2];
|
||||
}
|
||||
|
||||
// show specific sections
|
||||
if (sel === "terrs") styleHeightmap.style.display = "block";
|
||||
if (sel === "gridOverlay") styleGrid.style.display = "block";
|
||||
if (sel === "terrain") styleRelief.style.display = "block";
|
||||
if (sel === "texture") styleTexture.style.display = "block";
|
||||
if (sel === "routes" || sel === "labels" || sel == "anchors" || sel == "burgIcons" || sel === "lakes") {styleGroup.style.display = "block";}
|
||||
|
||||
if (sel === "population") {
|
||||
stylePopulation.style.display = "block";
|
||||
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population.select("#rural").attr("stroke");
|
||||
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population.select("#urban").attr("stroke");
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
}
|
||||
|
||||
if (sel === "labels") {
|
||||
styleFill.style.display = "block";
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeWidth.style.display = "block";
|
||||
loadDefaultFonts();
|
||||
styleFont.style.display = "block";
|
||||
styleSize.style.display = "block";
|
||||
styleVisibility.style.display = "block";
|
||||
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#3e3e4b";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3a3a3a";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || 0;
|
||||
styleSelectFont.value = fonts.indexOf(el.attr("data-font"));
|
||||
styleInputFont.style.display = "none";
|
||||
styleInputFont.value = "";
|
||||
styleFontSize.value = el.attr("data-size");
|
||||
}
|
||||
|
||||
if (sel == "burgIcons") {
|
||||
styleFill.style.display = "block";
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleStrokeDash.style.display = "block";
|
||||
styleRadius.style.display = "block";
|
||||
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
|
||||
styleStrokeDasharrayInput.value = el.attr("stroke-dasharray") || "";
|
||||
styleStrokeLinecapInput.value = el.attr("stroke-linecap") || "inherit";
|
||||
styleRadiusInput.value = el.attr("size") || 1;
|
||||
}
|
||||
|
||||
if (sel == "anchors") {
|
||||
styleFill.style.display = "block";
|
||||
styleStroke.style.display = "block";
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleIconSize.style.display = "block";
|
||||
styleFillInput.value = styleFillOutput.value = el.attr("fill") || "#ffffff";
|
||||
styleStrokeInput.value = styleStrokeOutput.value = el.attr("stroke") || "#3e3e4b";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || .24;
|
||||
styleIconSizeInput.value = el.attr("size") || 2;
|
||||
}
|
||||
|
||||
if (sel === "ocean") {
|
||||
styleOcean.style.display = "block";
|
||||
styleOceanBack.value = styleOceanBackOutput.value = svg.attr("background-color");
|
||||
styleOceanFore.value = styleOceanForeOutput.value = oceanLayers.select("rect").attr("fill");
|
||||
}
|
||||
|
||||
if (sel === "coastline") {
|
||||
styleCoastline.style.display = "block";
|
||||
if (styleCoastlineAuto.checked) styleFilter.style.display = "none";
|
||||
}
|
||||
|
||||
if (sel === "temperature") {
|
||||
styleStrokeWidth.style.display = "block";
|
||||
styleTemperature.style.display = "block";
|
||||
styleStrokeWidthInput.value = styleStrokeWidthOutput.value = el.attr("stroke-width") || "";
|
||||
styleTemperatureFillOpacityInput.value = styleTemperatureFillOpacityOutput.value = el.attr("fill-opacity") || .1;
|
||||
styleTemperatureFillInput.value = styleTemperatureFillOutput.value = el.attr("fill") || "#000";
|
||||
styleTemperatureFontSizeInput.value = styleTemperatureFontSizeOutput.value = el.attr("font-size") || "8px";;
|
||||
}
|
||||
|
||||
if (sel === "coordinates") {
|
||||
styleSize.style.display = "block";
|
||||
styleFontSize.value = el.attr("data-size");
|
||||
}
|
||||
|
||||
// update group options
|
||||
styleGroupSelect.options.length = 0; // remove all options
|
||||
if (sel === "routes" || sel === "labels" || sel === "lakes" || sel === "anchors" || sel === "burgIcons") {
|
||||
document.getElementById(sel).querySelectorAll("g").forEach(el => {
|
||||
if (el.id === "burgLabels") return;
|
||||
const count = el.childElementCount;
|
||||
styleGroupSelect.options.add(new Option(`${el.id} (${count})`, el.id, false, false));
|
||||
});
|
||||
styleGroupSelect.value = el.attr("id");
|
||||
} else {
|
||||
styleGroupSelect.options.add(new Option(sel, sel, false, true));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Handle style inputs change
|
||||
styleGroupSelect.addEventListener("change", selectStyleElement);
|
||||
|
||||
function getEl() {
|
||||
const el = styleElementSelect.value, g = styleGroupSelect.value;
|
||||
if (g === el) return svg.select("#"+el); else return svg.select("#"+el).select("#"+g);
|
||||
}
|
||||
|
||||
styleFillInput.addEventListener("input", function() {
|
||||
styleFillOutput.value = this.value;
|
||||
getEl().attr('fill', this.value);
|
||||
});
|
||||
|
||||
styleStrokeInput.addEventListener("input", function() {
|
||||
styleStrokeOutput.value = this.value;
|
||||
getEl().attr('stroke', this.value);
|
||||
});
|
||||
|
||||
styleStrokeWidthInput.addEventListener("input", function() {
|
||||
styleStrokeWidthOutput.value = this.value;
|
||||
getEl().attr('stroke-width', +this.value);
|
||||
});
|
||||
|
||||
styleStrokeDasharrayInput.addEventListener("input", function() {
|
||||
getEl().attr('stroke-dasharray', this.value);
|
||||
});
|
||||
|
||||
styleStrokeLinecapInput.addEventListener("change", function() {
|
||||
getEl().attr('stroke-linecap', this.value);
|
||||
});
|
||||
|
||||
styleOpacityInput.addEventListener("input", function() {
|
||||
styleOpacityOutput.value = this.value;
|
||||
getEl().attr('opacity', this.value);
|
||||
});
|
||||
|
||||
styleFilterInput.addEventListener("change", function() {
|
||||
if (styleGroupSelect.value === "ocean") {oceanLayers.attr('filter', this.value); return;}
|
||||
getEl().attr('filter', this.value);
|
||||
});
|
||||
|
||||
styleTextureInput.addEventListener("change", function() {
|
||||
texture.select("image").attr("xlink:href", getAbsolutePath(this.value));
|
||||
});
|
||||
|
||||
styleTextureShiftX.addEventListener("input", function() {
|
||||
texture.select("image").attr("x", this.value).attr("width", svgWidth - this.valueAsNumber);
|
||||
});
|
||||
|
||||
styleTextureShiftY.addEventListener("input", function() {
|
||||
texture.select("image").attr("y", this.value).attr("height", svgHeight - this.valueAsNumber);
|
||||
});
|
||||
|
||||
styleClippingInput.addEventListener("change", function() {
|
||||
getEl().attr('mask', this.value);
|
||||
});
|
||||
|
||||
styleGridType.addEventListener("change", function() {
|
||||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
});
|
||||
|
||||
styleGridSize.addEventListener("input", function() {
|
||||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
styleGridSizeOutput.value = this.value;
|
||||
calculateFriendlyGridSize();
|
||||
});
|
||||
|
||||
function calculateFriendlyGridSize() {
|
||||
const size = styleGridSize.value * Math.cos(30 * Math.PI / 180) * 2;;
|
||||
const friendly = "(" + rn(size * distanceScale.value) + " " + distanceUnit.value + ")";
|
||||
styleGridSizeFriendly.value = friendly;
|
||||
}
|
||||
|
||||
styleShiftX.addEventListener("input", shiftElement);
|
||||
styleShiftY.addEventListener("input", shiftElement);
|
||||
|
||||
function shiftElement() {
|
||||
const x = styleShiftX.value || 0;
|
||||
const y = styleShiftY.value || 0;
|
||||
getEl().attr("transform", `translate(${x},${y})`);
|
||||
}
|
||||
|
||||
styleOceanBack.addEventListener("input", function() {
|
||||
svg.style("background-color", this.value);
|
||||
styleOceanBackOutput.value = this.value;
|
||||
});
|
||||
|
||||
styleOceanFore.addEventListener("input", function() {
|
||||
oceanLayers.select("rect").attr("fill", this.value);
|
||||
styleOceanForeOutput.value = this.value;
|
||||
});
|
||||
|
||||
styleOceanPattern.addEventListener("change", function() {
|
||||
svg.select("pattern#oceanic rect").attr("filter", this.value);
|
||||
});
|
||||
|
||||
outlineLayersInput.addEventListener("change", function() {
|
||||
oceanLayers.selectAll("path").remove();
|
||||
OceanLayers();
|
||||
});
|
||||
|
||||
styleReliefSizeInput.addEventListener("input", function() {
|
||||
styleReliefSizeOutput.value = this.value;
|
||||
const size = +this.value;
|
||||
|
||||
terrain.selectAll("use").each(function(d) {
|
||||
const newSize = this.getAttribute("data-size") * size;
|
||||
const shift = (newSize - +this.getAttribute("width")) / 2;
|
||||
this.setAttribute("width", newSize);
|
||||
this.setAttribute("height", newSize);
|
||||
const x = +this.getAttribute("x");
|
||||
const y = +this.getAttribute("y");
|
||||
this.setAttribute("x", x - shift);
|
||||
this.setAttribute("y", y - shift);
|
||||
});
|
||||
});
|
||||
|
||||
styleReliefDensityInput.addEventListener("input", function() {
|
||||
styleReliefDensityOutput.value = rn(this.value * 100) + "%";
|
||||
ReliefIcons();
|
||||
});
|
||||
|
||||
styleTemperatureFillOpacityInput.addEventListener("input", function() {
|
||||
temperature.attr("fill-opacity", this.value);
|
||||
styleTemperatureFillOpacityOutput.value = this.value;
|
||||
});
|
||||
|
||||
styleTemperatureFontSizeInput.addEventListener("input", function() {
|
||||
temperature.attr("font-size", this.value + "px");
|
||||
styleTemperatureFontSizeOutput.value = this.value + "px";
|
||||
});
|
||||
|
||||
styleTemperatureFillInput.addEventListener("input", function() {
|
||||
temperature.attr("fill", this.value);
|
||||
styleTemperatureFillOutput.value = this.value;
|
||||
});
|
||||
|
||||
stylePopulationRuralStrokeInput.addEventListener("input", function() {
|
||||
population.select("#rural").attr("stroke", this.value);
|
||||
stylePopulationRuralStrokeOutput.value = this.value;
|
||||
});
|
||||
|
||||
stylePopulationUrbanStrokeInput.addEventListener("input", function() {
|
||||
population.select("#urban").attr("stroke", this.value);
|
||||
stylePopulationUrbanStrokeOutput.value = this.value;
|
||||
});
|
||||
|
||||
styleCompassSizeInput.addEventListener("input", function() {
|
||||
styleCompassSizeOutput.value = this.value;
|
||||
shiftCompass();
|
||||
});
|
||||
|
||||
styleCompassShiftX.addEventListener("input", shiftCompass);
|
||||
styleCompassShiftY.addEventListener("input", shiftCompass);
|
||||
|
||||
function shiftCompass() {
|
||||
const tr = `translate(${styleCompassShiftX.value} ${styleCompassShiftY.value}) scale(${styleCompassSizeInput.value})`;
|
||||
d3.select("#rose").attr("transform", tr);
|
||||
}
|
||||
|
||||
styleSelectFont.addEventListener("change", changeFont);
|
||||
function changeFont() {
|
||||
const value = styleSelectFont.value;
|
||||
const font = fonts[value].split(':')[0].replace(/\+/g, " ");
|
||||
getEl().attr("font-family", font).attr("data-font", fonts[value]);
|
||||
}
|
||||
|
||||
styleFontAdd.addEventListener("click", function() {
|
||||
if (styleInputFont.style.display === "none") {
|
||||
styleInputFont.style.display = "inline-block";
|
||||
styleInputFont.focus();
|
||||
styleSelectFont.style.display = "none";
|
||||
} else {
|
||||
styleInputFont.style.display = "none";
|
||||
styleSelectFont.style.display = "inline-block";
|
||||
}
|
||||
});
|
||||
|
||||
styleInputFont.addEventListener("change", function() {
|
||||
if (!this.value) {tip("Please provide a valid Google font name or link to a @font-face declaration"); return;}
|
||||
fetchFonts(this.value).then(fetched => {
|
||||
if (!fetched) return;
|
||||
styleFontAdd.click();
|
||||
styleInputFont.value = "";
|
||||
if (fetched !== 1) return;
|
||||
styleSelectFont.value = fonts.length-1;
|
||||
changeFont(); // auto-change font if 1 font is fetched
|
||||
});
|
||||
});
|
||||
|
||||
styleFontSize.addEventListener("change", function() {
|
||||
changeFontSize(+this.value);
|
||||
});
|
||||
|
||||
styleFontPlus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("data-size") * 1.1, 2), 1);
|
||||
changeFontSize(size);
|
||||
});
|
||||
|
||||
styleFontMinus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("data-size") * .9, 2), 1);
|
||||
changeFontSize(size);
|
||||
});
|
||||
|
||||
function changeFontSize(size) {
|
||||
getEl().attr("data-size", size).attr("font-size", rn((size + (size / scale)) / 2, 2));
|
||||
styleFontSize.value = size;
|
||||
}
|
||||
|
||||
styleRadiusInput.addEventListener("change", function() {
|
||||
changeRadius(+this.value);
|
||||
});
|
||||
|
||||
styleRadiusPlus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
|
||||
changeRadius(size);
|
||||
});
|
||||
|
||||
styleRadiusMinus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
|
||||
changeRadius(size);
|
||||
});
|
||||
|
||||
function changeRadius(size) {
|
||||
getEl().attr("size", size)
|
||||
getEl().selectAll("circle").each(function() {this.setAttribute("r", size)});
|
||||
styleRadiusInput.value = size;
|
||||
const group = getEl().attr("id");
|
||||
burgLabels.select("g#"+group).selectAll("text").each(function() {this.setAttribute("dy", `${size * -1.5}px`)});
|
||||
changeIconSize(size * 2, group); // change also anchor icons
|
||||
}
|
||||
|
||||
styleIconSizeInput.addEventListener("change", function() {
|
||||
changeIconSize(+this.value);
|
||||
});
|
||||
|
||||
styleIconSizePlus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("size") * 1.1, 2), .2);
|
||||
changeIconSize(size);
|
||||
});
|
||||
|
||||
styleIconSizeMinus.addEventListener("click", function() {
|
||||
const size = Math.max(rn(getEl().attr("size") * .9, 2), .2);
|
||||
changeIconSize(size);
|
||||
});
|
||||
|
||||
function changeIconSize(size, group) {
|
||||
const el = group ? anchors.select("#"+group) : getEl();
|
||||
const oldSize = +el.attr("size");
|
||||
const shift = (size - oldSize) / 2;
|
||||
el.attr("size", size);
|
||||
el.selectAll("use").each(function() {
|
||||
const x = +this.getAttribute("x");
|
||||
const y = +this.getAttribute("y");
|
||||
this.setAttribute("x", x - shift);
|
||||
this.setAttribute("y", y - shift);
|
||||
this.setAttribute("width", size);
|
||||
this.setAttribute("height", size);
|
||||
});;
|
||||
styleIconSizeInput.value = size;
|
||||
}
|
||||
|
||||
// request to restore default style on button click
|
||||
function askToRestoreDefaultStyle() {
|
||||
alertMessage.innerHTML = "Are you sure you want to restore default style for all elements?";
|
||||
$("#alert").dialog({resizable: false, title: "Restore default style",
|
||||
buttons: {
|
||||
Restore: function() {
|
||||
applyDefaultStyle();
|
||||
selectStyleElement();
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// request a URL to image to be used as a texture
|
||||
function textureProvideURL() {
|
||||
alertMessage.innerHTML = `Provide an image URL to be used as a texture:
|
||||
<input id="textureURL" type="url" style="width: 254px" placeholder="http://www.example.com/image.jpg" oninput="fetchTextureURL(this.value)">
|
||||
<div style="border: 1px solid darkgrey; height: 144px; margin-top: 2px"><canvas id="preview" width="256px" height="144px"></canvas></div>`;
|
||||
$("#alert").dialog({resizable: false, title: "Load custom texture", width: 280,
|
||||
buttons: {
|
||||
Apply: function() {
|
||||
const name = textureURL.value.split("/").pop();
|
||||
if (!name || name === "") {tip("Please provide a valid URL", false, "error"); return;}
|
||||
const opt = document.createElement("option");
|
||||
opt.value = textureURL.value;
|
||||
opt.text = name.slice(0, 20);
|
||||
styleTextureInput.add(opt);
|
||||
styleTextureInput.value = textureURL.value;
|
||||
texture.select("image").attr('xlink:href', textureURL.value);
|
||||
zoom.scaleBy(svg, 1.00001); // enforce browser re-draw
|
||||
$(this).dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchTextureURL(url) {
|
||||
console.log("Provided URL is", url);
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
const canvas = document.getElementById("preview");
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
};
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
// Style map filters handler
|
||||
mapFilters.addEventListener("click", applyMapFilter);
|
||||
function applyMapFilter() {
|
||||
if (event.target.tagName !== "BUTTON") return;
|
||||
const button = event.target;
|
||||
svg.attr("filter", null);
|
||||
if (button.classList.contains("pressed")) {button.classList.remove("pressed"); return;}
|
||||
mapFilters.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
button.classList.add("pressed");
|
||||
svg.attr("filter", "url(#filter-" + button.id + ")");
|
||||
}
|
||||
|
||||
// Option listeners
|
||||
const optionsContent = document.getElementById("optionsContent");
|
||||
optionsContent.addEventListener("input", function(event) {
|
||||
const id = event.target.id, value = event.target.value;
|
||||
if (id === "mapWidthInput" || id === "mapHeightInput") mapSizeInputChange();
|
||||
else if (id === "densityInput" || id === "densityOutput") changeCellsDensity(value);
|
||||
else if (id === "culturesInput") culturesOutput.value = value;
|
||||
else if (id === "culturesOutput") culturesInput.value = value;
|
||||
else if (id === "regionsInput" || id === "regionsOutput") changeStatesNumber(value);
|
||||
else if (id === "powerInput") powerOutput.value = value;
|
||||
else if (id === "powerOutput") powerInput.value = value;
|
||||
else if (id === "neutralInput") neutralOutput.value = value;
|
||||
else if (id === "neutralOutput") neutralInput.value = value;
|
||||
else if (id === "manorsInput") manorsOutput.value = value;
|
||||
else if (id === "manorsOutput") manorsInput.value = value;
|
||||
else if (id === "uiSizeInput" || id === "uiSizeOutput") changeUIsize(value);
|
||||
else if (id === "tooltipSizeInput" || id === "tooltipSizeOutput") changeTooltipSize(value);
|
||||
else if (id === "transparencyInput") changeDialogsTransparency(value);
|
||||
else if (id === "pngResolutionInput") pngResolutionOutput.value = value;
|
||||
else if (id === "pngResolutionOutput") pngResolutionInput.value = value;
|
||||
});
|
||||
|
||||
optionsContent.addEventListener("change", function(event) {
|
||||
if (event.target.dataset.stored) lock(event.target.dataset.stored);
|
||||
const id = event.target.id, value = event.target.value;
|
||||
if (id === "zoomExtentMin" || id === "zoomExtentMax") changeZoomExtent(value);
|
||||
});
|
||||
|
||||
optionsContent.addEventListener("click", function(event) {
|
||||
const id = event.target.id;
|
||||
if (id === "toggleFullscreen") toggleFullscreen();
|
||||
else if (id === "optionsSeedGenerate") generateMapWithSeed();
|
||||
else if (id === "optionsMapHistory") showSeedHistoryDialog();
|
||||
else if (id === "zoomExtentDefault") restoreDefaultZoomExtent();
|
||||
});
|
||||
|
||||
function mapSizeInputChange() {
|
||||
changeMapSize();
|
||||
autoResize = false;
|
||||
localStorage.setItem("mapWidth", mapWidthInput.value);
|
||||
localStorage.setItem("mapHeight", mapHeightInput.value);
|
||||
}
|
||||
|
||||
// change svg size on manual size change or window resize, do not change graph size
|
||||
function changeMapSize() {
|
||||
svgWidth = +mapWidthInput.value;
|
||||
svgHeight = +mapHeightInput.value;
|
||||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||
const width = Math.max(svgWidth, graphWidth);
|
||||
const height = Math.max(svgHeight, graphHeight);
|
||||
zoom.translateExtent([[0, 0], [width, height]]);
|
||||
fitScaleBar();
|
||||
}
|
||||
|
||||
// just apply map size that was already set, apply graph size!
|
||||
function applyMapSize() {
|
||||
svgWidth = graphWidth = +mapWidthInput.value;
|
||||
svgHeight = graphHeight = +mapHeightInput.value;
|
||||
svg.attr("width", svgWidth).attr("height", svgHeight);
|
||||
zoom.translateExtent([[0, 0],[graphWidth, graphHeight]]).scaleExtent([1, 20]).scaleTo(svg, 1);
|
||||
viewbox.attr("transform", null);
|
||||
}
|
||||
|
||||
function toggleFullscreen() {
|
||||
if (mapWidthInput.value != window.innerWidth || mapHeightInput.value != window.innerHeight) {
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
localStorage.removeItem("mapHeight");
|
||||
localStorage.removeItem("mapWidth");
|
||||
} else {
|
||||
mapWidthInput.value = graphWidth;
|
||||
mapHeightInput.value = graphHeight;
|
||||
}
|
||||
changeMapSize();
|
||||
}
|
||||
|
||||
function generateMapWithSeed() {
|
||||
if (optionsSeed.value == seed) {
|
||||
tip("The current map already has this seed", false, "error");
|
||||
return;
|
||||
}
|
||||
regeneratePrompt();
|
||||
}
|
||||
|
||||
function showSeedHistoryDialog() {
|
||||
const alert = mapHistory.map(function(h, i) {
|
||||
const created = new Date(h.created).toLocaleTimeString();
|
||||
const button = `<i data-tip"Click to generate a map with this seed" onclick="restoreSeed(${i})" class="icon-history optionsSeedRestore"></i>`;
|
||||
return `<div>${i+1}. Seed: ${h.seed} ${button}. Size: ${h.width}x${h.height}. Template: ${h.template}. Created: ${created}</div>`;
|
||||
}).join("");
|
||||
alertMessage.innerHTML = alert;
|
||||
$("#alert").dialog({
|
||||
resizable: false, title: "Seed history",
|
||||
width: fitContent(), position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
|
||||
// generate map with historycal seed
|
||||
function restoreSeed(id) {
|
||||
if (mapHistory[id].seed == seed) {
|
||||
tip("The current map is already generated with this seed", null, "error");
|
||||
return;
|
||||
}
|
||||
optionsSeed.value = mapHistory[id].seed;
|
||||
mapWidthInput.value = mapHistory[id].width;
|
||||
mapHeightInput.value = mapHistory[id].height;
|
||||
templateInput.value = mapHistory[id].template;
|
||||
if (locked("template")) unlock("template");
|
||||
regeneratePrompt();
|
||||
}
|
||||
|
||||
function restoreDefaultZoomExtent() {
|
||||
zoomExtentMin.value = 1;
|
||||
zoomExtentMax.value = 20;
|
||||
zoom.scaleExtent([1, 20]).scaleTo(svg, 1);
|
||||
}
|
||||
|
||||
function changeCellsDensity(value) {
|
||||
densityInput.value = densityOutput.value = value;
|
||||
if (value == 3) densityOutput.style.color = "red";
|
||||
else if (value == 2) densityOutput.style.color = "yellow";
|
||||
else if (value == 1) densityOutput.style.color = "green";
|
||||
}
|
||||
|
||||
function changeStatesNumber(value) {
|
||||
regionsInput.value = regionsOutput.value = value;
|
||||
burgLabels.select("#capitals").attr("data-size", Math.max(rn(6 - value / 20), 3));
|
||||
labels.select("#countries").attr("data-size", Math.max(rn(18 - value / 6), 4));
|
||||
}
|
||||
|
||||
function changeUIsize(value) {
|
||||
uiSizeInput.value = uiSizeOutput.value = value;
|
||||
document.getElementsByTagName("body")[0].style.fontSize = value * 11 + "px";
|
||||
document.getElementById("options").style.width = (value - 1) * 300 / 2 + 300 + "px";
|
||||
}
|
||||
|
||||
function changeTooltipSize(value) {
|
||||
tooltipSizeInput.value = tooltipSizeOutput.value = value;
|
||||
tooltip.style.fontSize = `calc(${value}px + 0.5vw)`;
|
||||
}
|
||||
|
||||
// change transparency for modal windows
|
||||
function changeDialogsTransparency(value) {
|
||||
transparencyInput.value = transparencyOutput.value = value;
|
||||
const alpha = (100 - +value) / 100;
|
||||
const optionsColor = "rgba(164, 139, 149, " + alpha + ")";
|
||||
const dialogsColor = "rgba(255, 255, 255, " + alpha + ")";
|
||||
const optionButtonsColor = "rgba(145, 110, 127, " + Math.min(alpha + .3, 1) + ")";
|
||||
const optionLiColor = "rgba(153, 123, 137, " + Math.min(alpha + .3, 1) + ")";
|
||||
document.getElementById("options").style.backgroundColor = optionsColor;
|
||||
document.getElementById("dialogs").style.backgroundColor = dialogsColor;
|
||||
document.querySelectorAll(".tabcontent button").forEach(el => el.style.backgroundColor = optionButtonsColor);
|
||||
document.querySelectorAll(".tabcontent li").forEach(el => el.style.backgroundColor = optionLiColor);
|
||||
document.querySelectorAll("button.options").forEach(el => el.style.backgroundColor = optionLiColor);
|
||||
}
|
||||
|
||||
function changeZoomExtent(value) {
|
||||
zoom.scaleExtent([+zoomExtentMin.value, +zoomExtentMax.value]);
|
||||
zoom.scaleTo(svg, +value);
|
||||
}
|
||||
|
||||
// control sroted options
|
||||
function applyStoredOptions() {
|
||||
for(let i=0; i < localStorage.length; i++){
|
||||
const stored = localStorage.key(i), value = localStorage.getItem(stored);
|
||||
const input = document.getElementById(stored+"Input");
|
||||
const output = document.getElementById(stored+"Output");
|
||||
if (input) input.value = value;
|
||||
if (output) output.value = value;
|
||||
lock(stored);
|
||||
}
|
||||
|
||||
if (!localStorage.getItem("mapWidth") || !localStorage.getItem("mapHeight")) {
|
||||
mapWidthInput.value = window.innerWidth;
|
||||
mapHeightInput.value = window.innerHeight;
|
||||
}
|
||||
|
||||
if (localStorage.getItem("winds")) winds = localStorage.getItem("winds").split(",").map(w => +w);
|
||||
|
||||
changeDialogsTransparency(localStorage.getItem("transparency") || 30);
|
||||
if (localStorage.getItem("uiSize")) changeUIsize(localStorage.getItem("uiSize"));
|
||||
if (localStorage.getItem("tooltipSize")) changeTooltipSize(localStorage.getItem("tooltipSize"));
|
||||
if (localStorage.getItem("regions")) changeStatesNumber(localStorage.getItem("regions"));
|
||||
|
||||
if (localStorage.getItem("equator")) {
|
||||
const eqY = +equatorInput.value;
|
||||
equidistanceOutput.min = equidistanceInput.min = Math.max(+mapHeightInput.value - eqY, eqY);
|
||||
equidistanceOutput.max = equidistanceInput.max = equidistanceOutput.min * 10;
|
||||
}
|
||||
}
|
||||
|
||||
// randomize options if randomization is allowed in option
|
||||
function randomizeOptions() {
|
||||
Math.seedrandom(seed); // reset seed to initial one
|
||||
if (!locked("regions")) regionsInput.value = regionsOutput.value = rand(12, 17);
|
||||
if (!locked("manors")) manorsInput.value = manorsOutput.value = rn(0.5 + Math.random(), 1);
|
||||
if (!locked("power")) powerInput.value = powerOutput.value = rand(0, 4);
|
||||
if (!locked("neutral")) neutralInput.value = neutralOutput.value = rn(0.8 + Math.random(), 1);
|
||||
if (!locked("cultures")) culturesInput.value = culturesOutput.value = rand(10, 15);
|
||||
if (!locked("prec")) precInput.value = precOutput.value = gauss(100, 40, 0, 500);
|
||||
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
|
||||
if (!locked("temperatureEquator")) temperatureEquatorOutput.value = temperatureEquatorInput.value = rand(tMax-6, tMax);
|
||||
if (!locked("temperaturePole")) temperaturePoleOutput.value = temperaturePoleInput.value = rand(tMin, tMin+10);
|
||||
if (!locked("equator") && !locked("equidistance")) randomizeWorldSize();
|
||||
}
|
||||
|
||||
// define world size
|
||||
function randomizeWorldSize() {
|
||||
const eq = document.getElementById("equatorInput");
|
||||
const eqDI = document.getElementById("equidistanceInput");
|
||||
const eqDO = document.getElementById("equidistanceOutput");
|
||||
|
||||
const eqY = equatorOutput.value = eq.value = rand(+eq.min, +eq.max); // equator Y
|
||||
eqDO.min = eqDI.min = Math.max(graphHeight - eqY, eqY);
|
||||
eqDO.max = eqDI.max = eqDO.min * 10;
|
||||
eqDO.value = eqDI.value = rand(rn(eqDO.min * 1.2), rn(eqDO.min * 4)); // distance from equator to poles
|
||||
}
|
||||
|
||||
// remove all saved data from LocalStorage and reload the page
|
||||
function restoreDefaultOptions() {
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
// FONTS
|
||||
|
||||
// fetch default fonts if not done before
|
||||
function loadDefaultFonts() {
|
||||
if (!$('link[href="fonts.css"]').length) {
|
||||
$("head").append('<link rel="stylesheet" type="text/css" href="fonts.css">');
|
||||
const fontsToAdd = ["Amatic+SC:700", "IM+Fell+English", "Great+Vibes", "MedievalSharp", "Metamorphous",
|
||||
"Nova+Script", "Uncial+Antiqua", "Underdog", "Caesar+Dressing", "Bitter", "Yellowtail", "Montez",
|
||||
"Shadows+Into+Light", "Fredericka+the+Great", "Orbitron", "Dancing+Script:700",
|
||||
"Architects+Daughter", "Kaushan+Script", "Gloria+Hallelujah", "Satisfy", "Comfortaa:700", "Cinzel"];
|
||||
fontsToAdd.forEach(function(f) {if (fonts.indexOf(f) === -1) fonts.push(f);});
|
||||
updateFontOptions();
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFonts(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url === "") {
|
||||
tip("Use a direct link to any @font-face declaration or just font name to fetch from Google Fonts");
|
||||
return;
|
||||
}
|
||||
if (url.indexOf("http") === -1) {
|
||||
url = url.replace(url.charAt(0), url.charAt(0).toUpperCase()).split(" ").join("+");
|
||||
url = "https://fonts.googleapis.com/css?family=" + url;
|
||||
}
|
||||
const fetched = addFonts(url).then(fetched => {
|
||||
if (fetched === undefined) {
|
||||
tip("Cannot fetch font for this value!", false, "error");
|
||||
return;
|
||||
}
|
||||
if (fetched === 0) {
|
||||
tip("Already in the fonts list!", false, "error");
|
||||
return;
|
||||
}
|
||||
updateFontOptions();
|
||||
if (fetched === 1) {
|
||||
tip("Font " + fonts[fonts.length - 1] + " is fetched");
|
||||
} else if (fetched > 1) {
|
||||
tip(fetched + " fonts are added to the list");
|
||||
}
|
||||
resolve(fetched);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function addFonts(url) {
|
||||
$("head").append('<link rel="stylesheet" type="text/css" href="' + url + '">');
|
||||
return fetch(url)
|
||||
.then(resp => resp.text())
|
||||
.then(text => {
|
||||
let s = document.createElement('style');
|
||||
s.innerHTML = text;
|
||||
document.head.appendChild(s);
|
||||
let styleSheet = Array.prototype.filter.call(
|
||||
document.styleSheets,
|
||||
sS => sS.ownerNode === s)[0];
|
||||
let FontRule = rule => {
|
||||
let family = rule.style.getPropertyValue('font-family');
|
||||
let font = family.replace(/['"]+/g, '').replace(/ /g, "+");
|
||||
let weight = rule.style.getPropertyValue('font-weight');
|
||||
if (weight !== "400") font += ":" + weight;
|
||||
if (fonts.indexOf(font) == -1) {
|
||||
fonts.push(font);
|
||||
fetched++
|
||||
}
|
||||
};
|
||||
let fetched = 0;
|
||||
for (let r of styleSheet.cssRules) {FontRule(r);}
|
||||
document.head.removeChild(s);
|
||||
return fetched;
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
// Update font list for Label and Burg Editors
|
||||
function updateFontOptions() {
|
||||
styleSelectFont.innerHTML = "";
|
||||
for (let i=0; i < fonts.length; i++) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = i;
|
||||
const font = fonts[i].split(':')[0].replace(/\+/g, " ");
|
||||
opt.style.fontFamily = opt.innerHTML = font;
|
||||
styleSelectFont.add(opt);
|
||||
}
|
||||
}
|
||||
|
||||
// Sticked menu Options listeners
|
||||
document.getElementById("sticked").addEventListener("click", function(event) {
|
||||
const id = event.target.id;
|
||||
if (id === "newMapButton") regeneratePrompt();
|
||||
else if (id === "saveButton") toggleSavePane();
|
||||
else if (id === "loadMap") mapToLoad.click();
|
||||
else if (id === "zoomReset") resetZoom(1000);
|
||||
else if (id === "saveMap") saveMap();
|
||||
else if (id === "saveSVG") saveAsImage("svg");
|
||||
else if (id === "savePNG") saveAsImage("png");
|
||||
if (id === "saveMap" || id === "saveSVG" || id === "savePNG") toggleSavePane();
|
||||
});
|
||||
|
||||
function regeneratePrompt() {
|
||||
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
|
||||
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
|
||||
if (workingTime < 15) {regenerateMap(); return;}
|
||||
|
||||
alertMessage.innerHTML = `Are you sure you want to generate a new map?<br>
|
||||
All unsaved changes made to the current map will be lost`;
|
||||
$("#alert").dialog({resizable: false, title: "Generate new map",
|
||||
buttons: {
|
||||
Cancel: function() {$(this).dialog("close");},
|
||||
Generate: regenerateMap
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSavePane() {
|
||||
if (saveDropdown.style.display === "block") {saveDropdown.style.display = "none"; return;}
|
||||
saveDropdown.style.display = "block";
|
||||
|
||||
// ask users to allow popups
|
||||
if (!localStorage.getItem("dns_allow_popup_message")) {
|
||||
alertMessage.innerHTML = `Generator uses pop-up window to download files.
|
||||
<br>Please ensure your browser does not block popups.
|
||||
<br>Please check browser settings and turn off adBlocker if it is enabled`;
|
||||
|
||||
$("#alert").dialog({title: "File saver", resizable: false, position: {my: "center", at: "center", of: "svg"},
|
||||
buttons: {
|
||||
OK: function() {
|
||||
localStorage.setItem("dns_allow_popup_message", true);
|
||||
$(this).dialog("close");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// load map
|
||||
document.getElementById("mapToLoad").addEventListener("change", function() {
|
||||
closeDialogs();
|
||||
const fileToLoad = this.files[0];
|
||||
this.value = "";
|
||||
uploadFile(fileToLoad);
|
||||
});
|
||||
247
modules/ui/relief-editor.js
Normal file
247
modules/ui/relief-editor.js
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"use strict";
|
||||
function editReliefIcon() {
|
||||
if (customization) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleRelief")) toggleRelief();
|
||||
|
||||
terrain.selectAll("use").call(d3.drag().on("drag", dragReliefIcon)).classed("draggable", true);
|
||||
elSelected = d3.select(d3.event.target);
|
||||
|
||||
restoreEditMode();
|
||||
updateReliefIconSelected();
|
||||
updateReliefSizeInput();
|
||||
|
||||
$("#reliefEditor").dialog({
|
||||
title: "Edit Relief Icons", resizable: false,
|
||||
position: {my: "center top+40", at: "top", of: d3.event, collision: "fit"},
|
||||
close: closeReliefEditor
|
||||
});
|
||||
|
||||
if (modules.editReliefIcon) return;
|
||||
modules.editReliefIcon = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("reliefIndividual").addEventListener("click", enterIndividualMode);
|
||||
document.getElementById("reliefBulkAdd").addEventListener("click", enterBulkAddMode);
|
||||
document.getElementById("reliefBulkRemove").addEventListener("click", enterBulkRemoveMode);
|
||||
|
||||
document.getElementById("reliefSize").addEventListener("input", changeIconSize);
|
||||
document.getElementById("reliefSizeNumber").addEventListener("input", changeIconSize);
|
||||
reliefIconsDiv.querySelectorAll("button").forEach(el => el.addEventListener("click", changeIcon));
|
||||
|
||||
document.getElementById("reliefCopy").addEventListener("click", copyIcon);
|
||||
document.getElementById("reliefMoveFront").addEventListener("click", () => elSelected.raise());
|
||||
document.getElementById("reliefMoveBack").addEventListener("click", () => elSelected.lower());
|
||||
document.getElementById("reliefRemove").addEventListener("click", removeIcon);
|
||||
|
||||
function dragReliefIcon() {
|
||||
const dx = +this.getAttribute("x") - d3.event.x;
|
||||
const dy = +this.getAttribute("y") - d3.event.y;
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
this.setAttribute("x", dx+x);
|
||||
this.setAttribute("y", dy+y);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreEditMode() {
|
||||
if (!reliefTools.querySelector("button.pressed")) enterIndividualMode(); else
|
||||
if (reliefBulkAdd.classList.contains("pressed")) enterBulkAddMode(); else
|
||||
if (reliefBulkRemove.classList.contains("pressed")) enterBulkRemoveMode();
|
||||
}
|
||||
|
||||
function updateReliefIconSelected() {
|
||||
const type = elSelected.attr("data-type");
|
||||
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
reliefIconsDiv.querySelector("button[data-type='"+type+"']").classList.add("pressed");
|
||||
}
|
||||
|
||||
function updateReliefSizeInput() {
|
||||
const size = +elSelected.attr("data-size");
|
||||
reliefSize.value = reliefSizeNumber.value = rn(size);
|
||||
}
|
||||
|
||||
function enterIndividualMode() {
|
||||
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
reliefIndividual.classList.add("pressed");
|
||||
|
||||
reliefSizeDiv.style.display = "block";
|
||||
reliefRadiusDiv.style.display = "none";
|
||||
reliefSpacingDiv.style.display = "none";
|
||||
reliefIconsSeletionAny.style.display = "none";
|
||||
|
||||
updateReliefSizeInput();
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
}
|
||||
|
||||
function enterBulkAddMode() {
|
||||
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
reliefBulkAdd.classList.add("pressed");
|
||||
|
||||
reliefSizeDiv.style.display = "block";
|
||||
reliefRadiusDiv.style.display = "block";
|
||||
reliefSpacingDiv.style.display = "block";
|
||||
reliefIconsSeletionAny.style.display = "none";
|
||||
|
||||
const pressedType = reliefIconsDiv.querySelector("button.pressed");
|
||||
if (pressedType.id === "reliefIconsSeletionAny") { // in "any" is pressed, select first type
|
||||
reliefIconsSeletionAny.classList.remove("pressed");
|
||||
reliefIconsDiv.querySelector("button").classList.add("pressed");
|
||||
}
|
||||
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToAdd)).on("touchmove mousemove", moveBrush);
|
||||
tip("Drag to place relief icons within radius", true);
|
||||
}
|
||||
|
||||
function moveBrush() {
|
||||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +reliefRadius.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
}
|
||||
|
||||
function dragToAdd() {
|
||||
const pressed = reliefIconsDiv.querySelector("button.pressed");
|
||||
if (!pressed) {tip("Please select an icon", false, error); return;}
|
||||
|
||||
const type = pressed.dataset.type;
|
||||
const r = +reliefRadius.value;
|
||||
const spacing = +reliefSpacing.value;
|
||||
const size = +reliefSize.value;
|
||||
|
||||
// build a quadtree
|
||||
const tree = d3.quadtree();
|
||||
const positions = [];
|
||||
terrain.selectAll("use").each(function() {
|
||||
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
|
||||
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
|
||||
tree.add([x, y, x]);
|
||||
const box = this.getBBox();
|
||||
positions.push(box.y + box.height);
|
||||
});
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
d3.range(Math.ceil(r/10)).forEach(function() {
|
||||
const a = Math.PI * 2 * Math.random();
|
||||
const rad = r * Math.random();
|
||||
const cx = p[0] + rad * Math.cos(a);
|
||||
const cy = p[1] + rad * Math.sin(a);
|
||||
|
||||
if (tree.find(cx, cy, spacing)) return; // too close to existing icon
|
||||
if (pack.cells.h[findCell(cx, cy)] < 20) return; // on water cell
|
||||
|
||||
const h = rn(size / 2 * (Math.random() * .4 + .8), 2);
|
||||
const x = rn(cx-h, 2);
|
||||
const y = rn(cy-h, 2);
|
||||
const z = y + h * 2;
|
||||
|
||||
let nth = 1;
|
||||
while (positions[nth] && z > positions[nth]) {nth++;}
|
||||
|
||||
tree.add([cx, cy]);
|
||||
positions.push(z);
|
||||
terrain.insert("use", ":nth-child("+nth+")").attr("xlink:href", type).attr("data-type", type)
|
||||
.attr("x", x).attr("y", y).attr("data-size", h*2).attr("width", h*2).attr("height", h*2);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function enterBulkRemoveMode() {
|
||||
reliefTools.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
reliefBulkRemove.classList.add("pressed");
|
||||
|
||||
reliefSizeDiv.style.display = "none";
|
||||
reliefRadiusDiv.style.display = "block";
|
||||
reliefSpacingDiv.style.display = "none";
|
||||
reliefIconsSeletionAny.style.display = "inline-block";
|
||||
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", dragToRemove)).on("touchmove mousemove", moveBrush);;
|
||||
tip("Drag to remove relief icons in radius", true);
|
||||
}
|
||||
|
||||
function dragToRemove() {
|
||||
const pressed = reliefIconsDiv.querySelector("button.pressed");
|
||||
if (!pressed) {tip("Please select an icon", false, error); return;}
|
||||
|
||||
const r = +reliefRadius.value;
|
||||
const type = pressed.dataset.type;
|
||||
const icons = type ? terrain.selectAll("use[data-type='"+type+"']") : terrain.selectAll("use");
|
||||
const tree = d3.quadtree();
|
||||
icons.each(function() {
|
||||
const x = +this.getAttribute("x") + this.getAttribute("width") / 2;
|
||||
const y = +this.getAttribute("y") + this.getAttribute("height") / 2;
|
||||
tree.add([x, y, this]);
|
||||
});
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
const p = d3.mouse(this);
|
||||
moveCircle(p[0], p[1], r);
|
||||
tree.findAll(p[0], p[1], r).forEach(f => f[2].remove());
|
||||
});
|
||||
}
|
||||
|
||||
function changeIconSize() {
|
||||
const size = +reliefSize.value;
|
||||
reliefSize.value = reliefSizeNumber.value = size;
|
||||
if (!reliefIndividual.classList.contains("pressed")) return;
|
||||
|
||||
const shift = (size - +elSelected.attr("width")) / 2;
|
||||
elSelected.attr("width", size).attr("height", size).attr("data-size", size);
|
||||
const x = +elSelected.attr("x"), y = +elSelected.attr("y");
|
||||
elSelected.attr("x", x-shift).attr("y", y-shift);
|
||||
}
|
||||
|
||||
function changeIcon() {
|
||||
if (this.classList.contains("pressed")) return;
|
||||
|
||||
reliefIconsDiv.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"))
|
||||
this.classList.add("pressed");
|
||||
|
||||
if (reliefIndividual.classList.contains("pressed")) {
|
||||
const type = this.dataset.type;
|
||||
elSelected.attr("xlink:href", type).attr("data-type", type);
|
||||
}
|
||||
}
|
||||
|
||||
function copyIcon() {
|
||||
const parent = elSelected.node().parentNode;
|
||||
const copy = elSelected.node().cloneNode(true);
|
||||
|
||||
let x = +elSelected.attr("x") - 3, y = +elSelected.attr("y") - 3;
|
||||
while (parent.querySelector("[x='"+x+"']","[x='"+y+"']")) {
|
||||
x -= 3; y -= 3;
|
||||
}
|
||||
|
||||
copy.setAttribute("x", x);
|
||||
copy.setAttribute("y", y);
|
||||
parent.insertBefore(copy, null);
|
||||
}
|
||||
|
||||
function removeIcon() {
|
||||
alertMessage.innerHTML = `Are you sure you want to remove the icon?`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove relief icon",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#reliefEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeReliefEditor() {
|
||||
terrain.selectAll("use").call(d3.drag().on("drag", null)).classed("draggable", false);
|
||||
removeCircle();
|
||||
unselect();
|
||||
clearMainTip();
|
||||
}
|
||||
|
||||
}
|
||||
278
modules/ui/rivers-editor.js
Normal file
278
modules/ui/rivers-editor.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
"use strict";
|
||||
function editRiver() {
|
||||
if (customization) return;
|
||||
if (elSelected && d3.event.target.id === elSelected.attr("id")) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||
|
||||
const node = d3.event.target;
|
||||
elSelected = d3.select(node).on("click", addInterimControlPoint)
|
||||
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
|
||||
viewbox.on("touchmove mousemove", showEditorTips);
|
||||
debug.append("g").attr("id", "controlPoints").attr("transform", elSelected.attr("transform"));
|
||||
drawControlPoints(node);
|
||||
updateValues(node);
|
||||
|
||||
$("#riverEditor").dialog({
|
||||
title: "Edit River", resizable: false,
|
||||
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
|
||||
close: closeRiverEditor
|
||||
});
|
||||
|
||||
if (modules.editRiver) return;
|
||||
modules.editRiver = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("riverWidthShow").addEventListener("click", showRiverWidth);
|
||||
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
|
||||
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
|
||||
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
|
||||
|
||||
document.getElementById("riverResizeShow").addEventListener("click", showRiverSize);
|
||||
document.getElementById("riverResizeHide").addEventListener("click", hideRiverSize);
|
||||
document.getElementById("riverAngle").addEventListener("input", changeAngle);
|
||||
document.getElementById("riverScale").addEventListener("input", changeScale);
|
||||
document.getElementById("riverReset").addEventListener("click", resetTransformation);
|
||||
|
||||
document.getElementById("riverCopy").addEventListener("click", copyRiver);
|
||||
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
|
||||
document.getElementById("riverLegend").addEventListener("click", editRiverLegend);
|
||||
document.getElementById("riverRemove").addEventListener("click", removeRiver);
|
||||
|
||||
function showEditorTips() {
|
||||
showMainTip();
|
||||
if (d3.event.target.parentNode.id === elSelected.attr("id")) tip("Drag to move, click to add a control point"); else
|
||||
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
|
||||
}
|
||||
|
||||
function updateValues(node) {
|
||||
const tr = parseTransform(node.getAttribute("transform"));
|
||||
document.getElementById("riverAngle").value = tr[2];
|
||||
document.getElementById("riverAngleValue").innerHTML = Math.abs(+tr[2]) + "°";
|
||||
document.getElementById("riverScale").value = tr[5];
|
||||
document.getElementById("riverWidthInput").value = node.dataset.width;
|
||||
document.getElementById("riverIncrement").value = node.dataset.increment;
|
||||
}
|
||||
|
||||
function dragRiver() {
|
||||
const x = d3.event.x, y = d3.event.y;
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
|
||||
d3.event.on("drag", function() {
|
||||
let xc = d3.event.x, yc = d3.event.y;
|
||||
let transform = `translate(${(+tr[0]+xc-x)},${(+tr[1]+yc-y)}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||
elSelected.attr("transform", transform);
|
||||
debug.select("#controlPoints").attr("transform", transform);
|
||||
});
|
||||
}
|
||||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength() / 2;
|
||||
const segments = Math.ceil(l / 5);
|
||||
const increment = rn(l / segments * 1e5);
|
||||
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
||||
const p1 = node.getPointAtLength(i / 1e5);
|
||||
const p2 = node.getPointAtLength(c / 1e5);
|
||||
addControlPoint([(p1.x + p2.x) / 2, (p1.y + p2.y) / 2]);
|
||||
}
|
||||
updateRiverLength(l);
|
||||
}
|
||||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
||||
function dragControlPoint() {
|
||||
this.setAttribute("cx", d3.event.x);
|
||||
this.setAttribute("cy", d3.event.y);
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function redrawRiver() {
|
||||
const points = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
points.push([+this.getAttribute("cx"), +this.getAttribute("cy")]);
|
||||
});
|
||||
|
||||
if (points.length === 1) return;
|
||||
if (points.length === 2) {elSelected.attr("d", `M${points[0][0]},${points[0][1]} L${points[1][0]},${points[1][1]}`); return;}
|
||||
const d = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
|
||||
elSelected.attr("d", d);
|
||||
updateRiverLength();
|
||||
}
|
||||
|
||||
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
riverLength.innerHTML = rn(l * tr[5] * distanceScale.value) + " " + distanceUnit.value;
|
||||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
this.remove();
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function addInterimControlPoint() {
|
||||
const point = d3.mouse(this);
|
||||
|
||||
const dists = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
const x = +this.getAttribute("cx");
|
||||
const y = +this.getAttribute("cy");
|
||||
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||
});
|
||||
|
||||
let index = dists.length;
|
||||
if (dists.length > 1) {
|
||||
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||
const closest = dists.indexOf(sorted[0]);
|
||||
const next = dists.indexOf(sorted[1]);
|
||||
if (closest <= next) index = closest+1; else index = next+1;
|
||||
}
|
||||
|
||||
const before = ":nth-child(" + (index + 1) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function showRiverWidth() {
|
||||
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("riverWidthSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideRiverWidth() {
|
||||
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("riverWidthSection").style.display = "none";
|
||||
}
|
||||
|
||||
function changeWidth() {
|
||||
elSelected.attr("data-width", this.value);
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function changeIncrement() {
|
||||
elSelected.attr("data-increment", this.value);
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
function showRiverSize() {
|
||||
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("riverResizeSection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideRiverSize() {
|
||||
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("riverResizeSection").style.display = "none";
|
||||
}
|
||||
|
||||
function changeAngle() {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
riverAngleValue.innerHTML = Math.abs(+this.value) + "°";
|
||||
const c = elSelected.node().getBBox();
|
||||
const angle = +this.value, scale = +tr[5];
|
||||
const transform = `translate(${tr[0]},${tr[1]}) rotate(${angle} ${(c.x+c.width/2)*scale} ${(c.y+c.height/2)*scale}) scale(${scale})`;
|
||||
elSelected.attr("transform", transform);
|
||||
debug.select("#controlPoints").attr("transform", transform);
|
||||
}
|
||||
|
||||
function changeScale() {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
const scaleOld = +tr[5],scale = +this.value;
|
||||
const c = elSelected.node().getBBox();
|
||||
const cx = c.x + c.width / 2, cy = c.y + c.height / 2;
|
||||
const trX = +tr[0] + cx * (scaleOld - scale);
|
||||
const trY = +tr[1] + cy * (scaleOld - scale);
|
||||
const scX = +tr[3] * scale / scaleOld;
|
||||
const scY = +tr[4] * scale / scaleOld;
|
||||
const transform = `translate(${trX},${trY}) rotate(${tr[2]} ${scX} ${scY}) scale(${scale})`;
|
||||
elSelected.attr("transform", transform);
|
||||
debug.select("#controlPoints").attr("transform", transform);
|
||||
updateRiverLength();
|
||||
}
|
||||
|
||||
function resetTransformation() {
|
||||
elSelected.attr("transform", null);
|
||||
debug.select("#controlPoints").attr("transform", null);
|
||||
riverAngle.value = 0;
|
||||
riverAngleValue.innerHTML = "0°";
|
||||
riverScale.value = 1;
|
||||
updateRiverLength();
|
||||
}
|
||||
|
||||
function copyRiver() {
|
||||
const tr = parseTransform(elSelected.attr("transform"));
|
||||
const d = elSelected.attr("d");
|
||||
let x = 2, y = 2;
|
||||
let transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||
while (rivers.selectAll("[transform='" + transform + "'][d='" + d + "']").size() > 0) {
|
||||
x += 2; y += 2;
|
||||
transform = `translate(${tr[0]-x},${tr[1]-y}) rotate(${tr[2]} ${tr[3]} ${tr[4]}) scale(${tr[5]})`;
|
||||
}
|
||||
|
||||
rivers.append("path").attr("d", d).attr("transform", transform).attr("id", getNextId("river"))
|
||||
.attr("data-width", elSelected.attr("data-width")).attr("data-increment", elSelected.attr("data-increment"));
|
||||
}
|
||||
|
||||
function toggleRiverCreationMode() {
|
||||
document.getElementById("riverNew").classList.toggle("pressed");
|
||||
if (document.getElementById("riverNew").classList.contains("pressed")) {
|
||||
tip("Click on map to add control points", true);
|
||||
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
|
||||
elSelected.on("click", null);
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
elSelected.on("click", addInterimControlPoint).attr("data-new", null)
|
||||
.call(d3.drag().on("start", dragRiver)).classed("draggable", true);
|
||||
}
|
||||
}
|
||||
|
||||
function addPointOnClick() {
|
||||
if (!elSelected.attr("data-new")) {
|
||||
debug.select("#controlPoints").selectAll("circle").remove();
|
||||
const id = getNextId("river");
|
||||
elSelected = d3.select(elSelected.node().parentNode).append("path").attr("id", id)
|
||||
.attr("data-new", 1).attr("data-width", 2).attr("data-increment", 1);
|
||||
}
|
||||
|
||||
// add control point
|
||||
const point = d3.mouse(this);
|
||||
addControlPoint([point[0], point[1]]);
|
||||
redrawRiver();
|
||||
}
|
||||
|
||||
|
||||
function editRiverLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
}
|
||||
|
||||
function removeRiver() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the river?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove river",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#riverEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeRiverEditor() {
|
||||
elSelected.attr("data-new", null).on("click", null);
|
||||
clearMainTip();
|
||||
riverNew.classList.remove("pressed");
|
||||
debug.select("#controlPoints").remove();
|
||||
unselect();
|
||||
}
|
||||
|
||||
}
|
||||
284
modules/ui/routes-editor.js
Normal file
284
modules/ui/routes-editor.js
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
"use strict";
|
||||
function editRoute(onClick) {
|
||||
if (customization) return;
|
||||
if (!onClick && elSelected && d3.event.target.id === elSelected.attr("id")) return;
|
||||
closeDialogs(".stable");
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
|
||||
$("#routeEditor").dialog({
|
||||
title: "Edit Route", resizable: false,
|
||||
position: {my: "center top+20", at: "top", of: d3.event, collision: "fit"},
|
||||
close: closeRoutesEditor
|
||||
});
|
||||
|
||||
debug.append("g").attr("id", "controlPoints");
|
||||
const node = onClick ? elSelected.node() : d3.event.target;
|
||||
elSelected = d3.select(node).on("click", addInterimControlPoint);
|
||||
drawControlPoints(node);
|
||||
selectRouteGroup(node);
|
||||
|
||||
viewbox.on("touchmove mousemove", showEditorTips);
|
||||
if (onClick) toggleRouteCreationMode();
|
||||
|
||||
if (modules.editRoute) return;
|
||||
modules.editRoute = true;
|
||||
|
||||
// add listeners
|
||||
document.getElementById("routeGroupsShow").addEventListener("click", showGroupSection);
|
||||
document.getElementById("routeGroup").addEventListener("change", changeRouteGroup);
|
||||
document.getElementById("routeGroupAdd").addEventListener("click", toggleNewGroupInput);
|
||||
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
|
||||
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
|
||||
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
|
||||
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
|
||||
document.getElementById("routeLegend").addEventListener("click", editRouteLegend);
|
||||
document.getElementById("routeNew").addEventListener("click", toggleRouteCreationMode);
|
||||
document.getElementById("routeRemove").addEventListener("click", removeRoute);
|
||||
|
||||
function showEditorTips() {
|
||||
showMainTip();
|
||||
if (routeNew.classList.contains("pressed")) return;
|
||||
if (d3.event.target.id === elSelected.attr("id")) tip("Click to add a control point"); else
|
||||
if (d3.event.target.parentNode.id === "controlPoints") tip("Drag to move, click to delete the control point");
|
||||
}
|
||||
|
||||
function drawControlPoints(node) {
|
||||
const l = node.getTotalLength();
|
||||
const increment = l / Math.ceil(l / 5);
|
||||
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
||||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||
}
|
||||
|
||||
function addControlPoint(point) {
|
||||
debug.select("#controlPoints").append("circle")
|
||||
.attr("cx", point.x).attr("cy", point.y).attr("r", .5)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
}
|
||||
|
||||
function addInterimControlPoint() {
|
||||
const point = d3.mouse(this);
|
||||
|
||||
const dists = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
const x = +this.getAttribute("cx");
|
||||
const y = +this.getAttribute("cy");
|
||||
dists.push((point[0] - x) ** 2 + (point[1] - y) ** 2);
|
||||
});
|
||||
|
||||
let index = dists.length;
|
||||
if (dists.length > 1) {
|
||||
const sorted = dists.slice(0).sort((a, b) => a-b);
|
||||
const closest = dists.indexOf(sorted[0]);
|
||||
const next = dists.indexOf(sorted[1]);
|
||||
if (closest <= next) index = closest+1; else index = next+1;
|
||||
}
|
||||
|
||||
const before = ":nth-child(" + (index + 1) + ")";
|
||||
debug.select("#controlPoints").insert("circle", before)
|
||||
.attr("cx", point[0]).attr("cy", point[1]).attr("r", .5)
|
||||
.call(d3.drag().on("drag", dragControlPoint))
|
||||
.on("click", clickControlPoint);
|
||||
|
||||
redrawRoute();
|
||||
}
|
||||
|
||||
function dragControlPoint() {
|
||||
this.setAttribute("cx", d3.event.x);
|
||||
this.setAttribute("cy", d3.event.y);
|
||||
redrawRoute();
|
||||
}
|
||||
|
||||
function redrawRoute() {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(.1));
|
||||
const points = [];
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
});
|
||||
|
||||
elSelected.attr("d", round(lineGen(points)));
|
||||
const l = elSelected.node().getTotalLength();
|
||||
routeLength.innerHTML = rn(l * distanceScale.value) + " " + distanceUnit.value;
|
||||
}
|
||||
|
||||
function showGroupSection() {
|
||||
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("routeGroupsSelection").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function hideGroupSection() {
|
||||
document.querySelectorAll("#routeEditor > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("routeGroupsSelection").style.display = "none";
|
||||
document.getElementById("routeGroupName").style.display = "none";
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
document.getElementById("routeGroup").style.display = "inline-block";
|
||||
}
|
||||
|
||||
function selectRouteGroup(node) {
|
||||
const group = node.parentNode.id;
|
||||
const select = document.getElementById("routeGroup");
|
||||
select.options.length = 0; // remove all options
|
||||
|
||||
routes.selectAll("g").each(function() {
|
||||
select.options.add(new Option(this.id, this.id, false, this.id === group));
|
||||
});
|
||||
}
|
||||
|
||||
function changeRouteGroup() {
|
||||
document.getElementById(this.value).appendChild(elSelected.node());
|
||||
}
|
||||
|
||||
function toggleNewGroupInput() {
|
||||
if (routeGroupName.style.display === "none") {
|
||||
routeGroupName.style.display = "inline-block";
|
||||
routeGroupName.focus();
|
||||
routeGroup.style.display = "none";
|
||||
} else {
|
||||
routeGroupName.style.display = "none";
|
||||
routeGroup.style.display = "inline-block";
|
||||
}
|
||||
}
|
||||
|
||||
function createNewGroup() {
|
||||
if (!this.value) {tip("Please provide a valid group name"); return;}
|
||||
let group = this.value.toLowerCase().replace(/ /g, "_").replace(/[^\w\s]/gi, "");
|
||||
if (Number.isFinite(+group.charAt(0))) group = "g" + group;
|
||||
|
||||
if (document.getElementById(group)) {
|
||||
tip("Element with this id already exists. Please provide a unique name", false, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// just rename if only 1 element left
|
||||
const oldGroup = elSelected.node().parentNode;
|
||||
const basic = ["roads", "trails", "searoutes"].includes(oldGroup.id);
|
||||
if (!basic && oldGroup.childElementCount === 1) {
|
||||
document.getElementById("routeGroup").selectedOptions[0].remove();
|
||||
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||
oldGroup.id = group;
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroup = elSelected.node().parentNode.cloneNode(false);
|
||||
document.getElementById("routes").appendChild(newGroup);
|
||||
newGroup.id = group;
|
||||
document.getElementById("routeGroup").options.add(new Option(group, group, false, true));
|
||||
document.getElementById(group).appendChild(elSelected.node());
|
||||
|
||||
toggleNewGroupInput();
|
||||
document.getElementById("routeGroupName").value = "";
|
||||
}
|
||||
|
||||
function removeRouteGroup() {
|
||||
const group = elSelected.node().parentNode.id;
|
||||
const basic = ["roads", "trails", "searoutes"].includes(group);
|
||||
const count = elSelected.node().parentNode.childElementCount;
|
||||
alertMessage.innerHTML = `Are you sure you want to remove
|
||||
${basic ? "all elements in the group" : "the entire route group"}?
|
||||
<br><br>Routes to be removed: ${count}`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove route group",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
$("#routeEditor").dialog("close");
|
||||
hideGroupSection();
|
||||
if (basic) routes.select("#"+group).selectAll("path").remove();
|
||||
else routes.select("#"+group).remove();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRouteSplitMode() {
|
||||
document.getElementById("routeNew").classList.remove("pressed");
|
||||
this.classList.toggle("pressed");
|
||||
}
|
||||
|
||||
function clickControlPoint() {
|
||||
if (routeSplit.classList.contains("pressed")) splitRoute(this);
|
||||
else {this.remove(); redrawRoute();}
|
||||
}
|
||||
|
||||
function splitRoute(clicked) {
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(.1));
|
||||
const group = d3.select(elSelected.node().parentNode);
|
||||
routeSplit.classList.remove("pressed");
|
||||
|
||||
const points1 = [], points2 = [];
|
||||
let points = points1;
|
||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
if (this === clicked) {
|
||||
points = points2;
|
||||
points.push([this.getAttribute("cx"), this.getAttribute("cy")]);
|
||||
}
|
||||
this.remove();
|
||||
});
|
||||
|
||||
elSelected.attr("d", round(lineGen(points1)));
|
||||
const id = getNextId("route");
|
||||
group.append("path").attr("id", id).attr("d", lineGen(points2));
|
||||
debug.select("#controlPoints").selectAll("circle").remove();
|
||||
drawControlPoints(elSelected.node());
|
||||
}
|
||||
|
||||
function toggleRouteCreationMode() {
|
||||
document.getElementById("routeSplit").classList.remove("pressed");
|
||||
document.getElementById("routeNew").classList.toggle("pressed");
|
||||
if (document.getElementById("routeNew").classList.contains("pressed")) {
|
||||
tip("Click on map to add control points", true);
|
||||
viewbox.on("click", addPointOnClick).style("cursor", "crosshair");
|
||||
elSelected.on("click", null);
|
||||
} else {
|
||||
clearMainTip();
|
||||
viewbox.on("click", clicked).style("cursor", "default");
|
||||
elSelected.on("click", addInterimControlPoint).attr("data-new", null);
|
||||
}
|
||||
}
|
||||
|
||||
function addPointOnClick() {
|
||||
// create new route
|
||||
if (!elSelected.attr("data-new")) {
|
||||
debug.select("#controlPoints").selectAll("circle").remove();
|
||||
const parent = elSelected.node().parentNode;
|
||||
const id = getNextId("route");
|
||||
elSelected = d3.select(parent).append("path").attr("id", id).attr("data-new", 1);
|
||||
}
|
||||
|
||||
// add control point
|
||||
const point = d3.mouse(this);
|
||||
addControlPoint({x: point[0], y: point[1]});
|
||||
redrawRoute();
|
||||
}
|
||||
|
||||
function editRouteLegend() {
|
||||
const id = elSelected.attr("id");
|
||||
editLegends(id, id);
|
||||
}
|
||||
|
||||
function removeRoute() {
|
||||
alertMessage.innerHTML = "Are you sure you want to remove the route?";
|
||||
$("#alert").dialog({resizable: false, title: "Remove route",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
elSelected.remove();
|
||||
$("#routeEditor").dialog("close");
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeRoutesEditor() {
|
||||
elSelected.attr("data-new", null).on("click", null);
|
||||
clearMainTip();
|
||||
routeSplit.classList.remove("pressed");
|
||||
routeNew.classList.remove("pressed");
|
||||
debug.select("#controlPoints").remove();
|
||||
unselect();
|
||||
}
|
||||
}
|
||||
514
modules/ui/states-editor.js
Normal file
514
modules/ui/states-editor.js
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
"use strict";
|
||||
function editStates() {
|
||||
if (customization) return;
|
||||
closeDialogs("#statesEditor, .stable");
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
if (!layerIsOn("toggleBorders")) toggleBorders();
|
||||
if (layerIsOn("toggleCultures")) toggleCultures();
|
||||
if (layerIsOn("toggleBiomes")) toggleBiomes();
|
||||
|
||||
const body = document.getElementById("statesBodySection");
|
||||
refreshStatesEditor();
|
||||
|
||||
if (modules.editStates) return;
|
||||
modules.editStates = true;
|
||||
|
||||
$("#statesEditor").dialog({
|
||||
title: "States Editor", width: fitContent(), close: closeStatesEditor,
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("statesEditorRefresh").addEventListener("click", refreshStatesEditor);
|
||||
document.getElementById("statesPercentage").addEventListener("click", togglePercentageMode);
|
||||
document.getElementById("regenerateStateNames").addEventListener("click", regenerateNames);
|
||||
document.getElementById("statesRegenerate").addEventListener("click", openRegenerationMenu);
|
||||
document.getElementById("statesRegenerateBack").addEventListener("click", exitRegenerationMenu);
|
||||
document.getElementById("statesRecalculate").addEventListener("click", recalculateStates);
|
||||
document.getElementById("statesJustify").addEventListener("click", justifyStates);
|
||||
document.getElementById("statesRandomize").addEventListener("click", randomizeStatesExpansion);
|
||||
document.getElementById("statesNeutral").addEventListener("input", recalculateStates);
|
||||
document.getElementById("statesNeutralNumber").addEventListener("click", recalculateStates);
|
||||
document.getElementById("statesManually").addEventListener("click", enterStatesManualAssignent);
|
||||
document.getElementById("statesManuallyApply").addEventListener("click", applyStatesManualAssignent);
|
||||
document.getElementById("statesManuallyCancel").addEventListener("click", exitStatesManualAssignment);
|
||||
document.getElementById("statesAdd").addEventListener("click", enterAddStateMode);
|
||||
document.getElementById("statesExport").addEventListener("click", downloadStatesData);
|
||||
|
||||
function refreshStatesEditor() {
|
||||
statesCollectStatistics();
|
||||
statesEditorAddLines();
|
||||
}
|
||||
|
||||
function statesCollectStatistics() {
|
||||
const cells = pack.cells, states = pack.states;
|
||||
states.forEach(s => s.cells = s.area = s.burgs = s.rural = s.urban = 0);
|
||||
|
||||
for (const i of cells.i) {
|
||||
if (cells.h[i] < 20) continue;
|
||||
const s = cells.state[i];
|
||||
states[s].cells += 1;
|
||||
states[s].area += cells.area[i];
|
||||
states[s].rural += cells.pop[i];
|
||||
if (cells.burg[i]) {
|
||||
states[s].urban += pack.burgs[cells.burg[i]].population;
|
||||
states[s].burgs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add line for each state
|
||||
function statesEditorAddLines() {
|
||||
const unit = areaUnit.value === "square" ? " " + distanceUnit.value + "²" : " " + areaUnit.value;
|
||||
const hidden = statesRegenerateButtons.style.display === "block" ? "visible" : "hidden"; // show/hide regenerate columns
|
||||
let lines = "", totalArea = 0, totalPopulation = 0, totalBurgs = 0;
|
||||
|
||||
for (const s of pack.states) {
|
||||
if (s.removed) continue;
|
||||
const area = s.area * (distanceScale.value ** 2);
|
||||
const rural = s.rural * populationRate.value;
|
||||
const urban = s.urban * populationRate.value * urbanization.value;
|
||||
const population = rural + urban;
|
||||
const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si(urban)}`;
|
||||
totalArea += area;
|
||||
totalPopulation += population;
|
||||
totalBurgs += s.burgs;
|
||||
|
||||
if (!s.i) {
|
||||
// Neutral line
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-cells=${s.cells} data-area=${area}
|
||||
data-population=${population} data-burgs=${s.burgs} data-color="" data-capital="" data-culture="" data-type="" data-expansionism="">
|
||||
<input class="stateColor placeholder" type="color">
|
||||
<input data-tip="State name. Click and type to change" class="stateName italic" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||
<span class="icon-star-empty placeholder"></span>
|
||||
<input class="stateCapital placeholder">
|
||||
<select class="stateCulture placeholder">${getCultureOptions(0)}</select>
|
||||
<select class="cultureType ${hidden} placeholder">${getTypeOptions(0)}</select>
|
||||
<span class="icon-resize-full ${hidden} placeholder"></span>
|
||||
<input class="statePower ${hidden} placeholder" type="number" value=0>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
</div>`;
|
||||
continue;
|
||||
}
|
||||
const capital = pack.burgs[s.capital].name;
|
||||
lines += `<div class="states" data-id=${s.i} data-name="${s.name}" data-capital="${capital}" data-color="${s.color}" data-cells=${s.cells}
|
||||
data-area=${area} data-population=${population} data-burgs=${s.burgs} data-culture=${pack.cultures[s.culture].name} data-type=${s.type} data-expansionism=${s.expansionism}>
|
||||
<input data-tip="State color. Click to change" class="stateColor" type="color" value="${s.color}">
|
||||
<input data-tip="State name. Click and type to change" class="stateName" value="${s.name}" autocorrect="off" spellcheck="false">
|
||||
<span data-tip="State capital. Click to zoom into view" class="icon-star-empty pointer"></span>
|
||||
<input data-tip="Capital name. Click and type to rename" class="stateCapital" value="${capital}" autocorrect="off" spellcheck="false"/>
|
||||
<select data-tip="Dominant culture. Click to change" class="stateCulture">${getCultureOptions(s.culture)}</select>
|
||||
<select data-tip="State type. Click to change" class="cultureType ${hidden}">${getTypeOptions(s.type)}</select>
|
||||
<span data-tip="State expansionism" class="icon-resize-full ${hidden}"></span>
|
||||
<input data-tip="Expansionism (defines competitive size). Change to re-calculate states based on new value" class="statePower ${hidden}" type="number" min=0 max=99 step=.1 value=${s.expansionism}>
|
||||
<span data-tip="Cells count" class="icon-check-empty"></span>
|
||||
<div data-tip="Cells count" class="stateCells">${s.cells}</div>
|
||||
<span data-tip="Burgs count" style="padding-right: 1px" class="icon-dot-circled"></span>
|
||||
<div data-tip="Burgs count" class="stateBurgs">${s.burgs}</div>
|
||||
<span data-tip="State area" style="padding-right: 4px" class="icon-map-o"></span>
|
||||
<div data-tip="State area" class="biomeArea">${si(area) + unit}</div>
|
||||
<span data-tip="${populationTip}" class="icon-male"></span>
|
||||
<div data-tip="${populationTip}" class="culturePopulation">${si(population)}</div>
|
||||
<span data-tip="Remove state" class="icon-trash-empty"></span>
|
||||
</div>`;
|
||||
}
|
||||
body.innerHTML = lines;
|
||||
|
||||
// update footer
|
||||
statesFooterStates.innerHTML = pack.states.filter(s => s.i && !s.removed).length;
|
||||
statesFooterCells.innerHTML = pack.cells.h.filter(h => h >= 20).length;
|
||||
statesFooterBurgs.innerHTML = totalBurgs;
|
||||
statesFooterArea.innerHTML = si(totalArea) + unit;
|
||||
statesFooterPopulation.innerHTML = si(totalPopulation);
|
||||
statesFooterArea.dataset.area = totalArea;
|
||||
statesFooterPopulation.dataset.population = totalPopulation;
|
||||
|
||||
// add listeners
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => stateHighlightOn(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => stateHighlightOff(ev)));
|
||||
body.querySelectorAll("div.states").forEach(el => el.addEventListener("click", selectStateOnLineClick));
|
||||
body.querySelectorAll("div > input.stateColor").forEach(el => el.addEventListener("input", stateChangeColor));
|
||||
body.querySelectorAll("div > input.stateName").forEach(el => el.addEventListener("input", stateChangeName));
|
||||
body.querySelectorAll("div > input.stateCapital").forEach(el => el.addEventListener("input", stateChangeCapitalName));
|
||||
body.querySelectorAll("div > span.icon-star-empty").forEach(el => el.addEventListener("click", stateCapitalZoomIn));
|
||||
body.querySelectorAll("div > select.stateCulture").forEach(el => el.addEventListener("click", stateUpdateCulturesList));
|
||||
body.querySelectorAll("div > select.cultureType").forEach(el => el.addEventListener("input", stateChangeType));
|
||||
body.querySelectorAll("div > input.statePower").forEach(el => el.addEventListener("input", stateChangeExpansionism));
|
||||
body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", stateRemove));
|
||||
|
||||
if (body.dataset.type === "percentage") {body.dataset.type = "absolute"; togglePercentageMode();}
|
||||
applySorting(statesHeader);
|
||||
$("#statesEditor").dialog();
|
||||
}
|
||||
|
||||
function getCultureOptions(culture) {
|
||||
let options = "";
|
||||
pack.cultures.slice(1).forEach(c => options += `<option ${c.i === culture ? "selected" : ""} value="${c.i}">${c.name}</option>`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function getTypeOptions(type) {
|
||||
let options = "";
|
||||
const types = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"];
|
||||
types.forEach(t => options += `<option ${type === t ? "selected" : ""} value="${t}">${t}</option>`);
|
||||
return options;
|
||||
}
|
||||
|
||||
function stateHighlightOn(event) {
|
||||
if (!layerIsOn("toggleStates")) return;
|
||||
const state = +event.target.dataset.id;
|
||||
if (customization || !state) return;
|
||||
const path = regions.select("#state"+state).attr("d");
|
||||
debug.append("path").attr("class", "highlighted").attr("d", path)
|
||||
.attr("stroke", "red").attr("stroke-width", 1).attr("opacity", 1)
|
||||
.attr("filter", "url(#blur1)").call(transition);
|
||||
}
|
||||
|
||||
function transition(path) {
|
||||
const duration = (path.node().getTotalLength() + 5000) / 2;
|
||||
path.transition().duration(duration).attrTween("stroke-dasharray", tweenDash);
|
||||
}
|
||||
|
||||
function tweenDash() {
|
||||
const l = this.getTotalLength();
|
||||
const i = d3.interpolateString("0," + l, l + "," + l);
|
||||
return t => i(t);
|
||||
}
|
||||
|
||||
function removePath(path) {
|
||||
path.transition().duration(1000).attr("opacity", 0).remove();
|
||||
}
|
||||
|
||||
function stateHighlightOff() {
|
||||
debug.selectAll(".highlighted").each(function(el) {
|
||||
d3.select(this).call(removePath);
|
||||
});
|
||||
}
|
||||
|
||||
function stateChangeColor() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
pack.states[state].color = this.value;
|
||||
regions.select("#state"+state).attr("fill", this.value);
|
||||
regions.select("#state-gap"+state).attr("stroke", this.value);
|
||||
regions.select("#state-border"+state).attr("stroke", d3.color(this.value).darker().hex());
|
||||
}
|
||||
|
||||
function stateChangeName() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.name = this.value;
|
||||
pack.states[state].name = this.value;
|
||||
document.querySelector("#stateLabel"+state+" > textPath").textContent = this.value;
|
||||
}
|
||||
|
||||
function stateChangeCapitalName() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.capital = this.value;
|
||||
const capital = pack.states[state].capital;
|
||||
if (!capital) return;
|
||||
pack.burgs[capital].name = this.value;
|
||||
document.querySelector("#burgLabel"+capital).textContent = this.value;
|
||||
}
|
||||
|
||||
function stateCapitalZoomIn() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
const capital = pack.states[state].capital;
|
||||
const l = burgLabels.select("[data-id='" + capital + "']");
|
||||
const x = +l.attr("x"), y = +l.attr("y");
|
||||
zoomTo(x, y, 8, 2000);
|
||||
}
|
||||
|
||||
function stateUpdateCulturesList() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
const v = +this.value;
|
||||
this.parentNode.dataset.base = pack.states[state].culture = v;
|
||||
this.options.length = 0;
|
||||
pack.cultures.slice(1).forEach(c => this.options.add(new Option(c.name, c.i, false, c.i === v)));
|
||||
}
|
||||
|
||||
function stateChangeType() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.type = this.value;
|
||||
pack.states[state].type = this.value;
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function stateChangeExpansionism() {
|
||||
const state = +this.parentNode.dataset.id;
|
||||
this.parentNode.dataset.expansionism = this.value;
|
||||
pack.states[state].expansionism = +this.value;
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function stateRemove() {
|
||||
if (customization) return;
|
||||
const state = +this.parentNode.dataset.id;
|
||||
regions.select("#state"+state).remove();
|
||||
regions.select("#state-gap"+state).remove();
|
||||
regions.select("#state-border"+state).remove();
|
||||
document.querySelector("#stateLabel"+state+" > textPath").remove();
|
||||
pack.burgs.forEach(b => {if(b.state === state) b.state = 0;});
|
||||
pack.cells.state.forEach((s, i) => {if(s === state) pack.cells.state[i] = 0;});
|
||||
pack.states[state].removed = true;
|
||||
|
||||
const capital = pack.states[state].capital;
|
||||
pack.burgs[capital].capital = false;
|
||||
pack.burgs[capital].state = 0;
|
||||
moveBurgToGroup(capital, "towns");
|
||||
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function togglePercentageMode() {
|
||||
if (body.dataset.type === "absolute") {
|
||||
body.dataset.type = "percentage";
|
||||
const totalCells = +statesFooterCells.innerHTML;
|
||||
const totalBurgs = +statesFooterBurgs.innerHTML;
|
||||
const totalArea = +statesFooterArea.dataset.area;
|
||||
const totalPopulation = +statesFooterPopulation.dataset.population;
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
el.querySelector(".stateCells").innerHTML = rn(+el.dataset.cells / totalCells * 100) + "%";
|
||||
el.querySelector(".stateBurgs").innerHTML = rn(+el.dataset.burgs / totalBurgs * 100) + "%";
|
||||
el.querySelector(".biomeArea").innerHTML = rn(+el.dataset.area / totalArea * 100) + "%";
|
||||
el.querySelector(".culturePopulation").innerHTML = rn(+el.dataset.population / totalPopulation * 100) + "%";
|
||||
});
|
||||
} else {
|
||||
body.dataset.type = "absolute";
|
||||
statesEditorAddLines();
|
||||
}
|
||||
}
|
||||
|
||||
function regenerateNames() {
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
const state = +el.dataset.id;
|
||||
if (!state) return;
|
||||
const culture = pack.states[state].culture;
|
||||
const name = Names.getState(Names.getCulture(culture, 4, 7, ""), culture);
|
||||
el.querySelector(".stateName").value = name;
|
||||
pack.states[state].name = el.dataset.name = name;
|
||||
labels.select("#stateLabel"+state+" > textPath").text(name);
|
||||
});
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
}
|
||||
|
||||
function openRegenerationMenu() {
|
||||
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "none");
|
||||
statesRegenerateButtons.style.display = "block";
|
||||
statesEditor.querySelectorAll(".hidden").forEach(el => {el.classList.remove("hidden"); el.classList.add("visible");});
|
||||
$("#statesEditor").dialog({position: {my: "right top", at: "right top", of: $("#statesEditor").parent(), collision: "fit"}});
|
||||
}
|
||||
|
||||
function recalculateStates() {
|
||||
BurgsAndStates.expandStates();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function justifyStates() {
|
||||
BurgsAndStates.normalizeStates();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
refreshStatesEditor();
|
||||
}
|
||||
|
||||
function randomizeStatesExpansion() {
|
||||
pack.states.slice(1).forEach(s => {
|
||||
const expansionism = rn(Math.random() * powerInput.value / 2 + 1, 1);
|
||||
s.expansionism = expansionism;
|
||||
body.querySelector("div.states[data-id='"+s.i+"'] > input.statePower").value = expansionism;
|
||||
});
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function exitRegenerationMenu() {
|
||||
statesBottom.querySelectorAll(":scope > button").forEach(el => el.style.display = "inline-block");
|
||||
statesRegenerateButtons.style.display = "none";
|
||||
statesEditor.querySelectorAll(".visible").forEach(el => {el.classList.remove("visible"); el.classList.add("hidden");});
|
||||
}
|
||||
|
||||
function enterStatesManualAssignent() {
|
||||
if (!layerIsOn("toggleStates")) toggleStates();
|
||||
customization = 2;
|
||||
regions.append("g").attr("id", "temp");
|
||||
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "none");
|
||||
document.getElementById("statesManuallyButtons").style.display = "inline-block";
|
||||
document.getElementById("statesHalo").style.display = "none";
|
||||
|
||||
tip("Click on state to select, drag the circle to change state", true);
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag()
|
||||
.on("drag", dragStateBrush))
|
||||
.on("click", selectStateOnMapClick)
|
||||
.on("touchmove mousemove", moveStateBrush);
|
||||
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
body.querySelector("div").classList.add("selected");
|
||||
}
|
||||
|
||||
function selectStateOnLineClick(i) {
|
||||
if (customization !== 2) return;
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
this.classList.add("selected");
|
||||
}
|
||||
|
||||
function selectStateOnMapClick() {
|
||||
const point = d3.mouse(this);
|
||||
const i = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[i] < 20) return;
|
||||
|
||||
const assigned = regions.select("#temp").select("polygon[data-cell='"+i+"']");
|
||||
const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i];
|
||||
|
||||
body.querySelector("div.selected").classList.remove("selected");
|
||||
body.querySelector("div[data-id='"+state+"']").classList.add("selected");
|
||||
}
|
||||
|
||||
function dragStateBrush() {
|
||||
const p = d3.mouse(this);
|
||||
const r = +statesManuallyBrush.value;
|
||||
moveCircle(p[0], p[1], r);
|
||||
|
||||
const found = r > 5 ? findAll(p[0], p[1], r) : [findCell(p[0], p[1], r)];
|
||||
const selection = found.filter(isLand);
|
||||
if (selection) changeStateForSelection(selection);
|
||||
}
|
||||
|
||||
// change state within selection
|
||||
function changeStateForSelection(selection) {
|
||||
const temp = regions.select("#temp");
|
||||
const selected = body.querySelector("div.selected");
|
||||
|
||||
const stateNew = +selected.dataset.id;
|
||||
const color = pack.states[stateNew].color || "#ffffff";
|
||||
|
||||
selection.forEach(function(i) {
|
||||
const exists = temp.select("polygon[data-cell='"+i+"']");
|
||||
const stateOld = exists.size() ? +exists.attr("data-state") : pack.cells.state[i];
|
||||
if (stateNew === stateOld) return;
|
||||
if (i === pack.states[stateOld].center) return;
|
||||
|
||||
// change of append new element
|
||||
if (exists.size()) exists.attr("data-state", stateNew).attr("fill", color).attr("stroke", color);
|
||||
else temp.append("polygon").attr("data-cell", i).attr("data-state", stateNew).attr("points", getPackPolygon(i)).attr("fill", color).attr("stroke", color);
|
||||
});
|
||||
}
|
||||
|
||||
function moveStateBrush() {
|
||||
showMainTip();
|
||||
const point = d3.mouse(this);
|
||||
const radius = +statesManuallyBrush.value;
|
||||
moveCircle(point[0], point[1], radius);
|
||||
}
|
||||
|
||||
function applyStatesManualAssignent() {
|
||||
const cells = pack.cells;
|
||||
const changed = regions.select("#temp").selectAll("polygon");
|
||||
changed.each(function() {
|
||||
const i = +this.dataset.cell;
|
||||
const c = +this.dataset.state;
|
||||
cells.state[i] = c;
|
||||
if (cells.burg[i]) pack.burgs[cells.burg[i]].state = c;
|
||||
});
|
||||
|
||||
if (changed.size()) {
|
||||
refreshStatesEditor();
|
||||
if (!layerIsOn("toggleStates")) toggleStates(); else drawStatesWithBorders();
|
||||
if (adjustLabels.checked) BurgsAndStates.drawStateLabels();
|
||||
}
|
||||
exitStatesManualAssignment();
|
||||
}
|
||||
|
||||
function exitStatesManualAssignment() {
|
||||
customization = 0;
|
||||
regions.select("#temp").remove();
|
||||
removeCircle();
|
||||
document.querySelectorAll("#statesBottom > button").forEach(el => el.style.display = "inline-block");
|
||||
document.getElementById("statesManuallyButtons").style.display = "none";
|
||||
document.getElementById("statesHalo").style.display = "block";
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
const selected = body.querySelector("div.selected");
|
||||
if (selected) selected.classList.remove("selected");
|
||||
}
|
||||
|
||||
function enterAddStateMode() {
|
||||
if (this.classList.contains("pressed")) {exitAddStateMode(); return;};
|
||||
customization = 3;
|
||||
this.classList.add("pressed");
|
||||
tip("Click on the map to create a new capital or promote an existing burg", true);
|
||||
viewbox.style("cursor", "crosshair").on("click", addState);
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = true);
|
||||
}
|
||||
|
||||
function addState() {
|
||||
const point = d3.mouse(this);
|
||||
const center = findCell(point[0], point[1]);
|
||||
if (pack.cells.h[center] < 20) {tip("You cannot place state into the water. Please click on a land cell", false, "error"); return;}
|
||||
let burg = pack.cells.burg[center];
|
||||
if (burg && pack.burgs[burg].capital) {tip("Existing capital cannot be selected as a new state capital! Select other cell", false, "error"); return;}
|
||||
if (!burg) burg = addBurg(point); // add new burg
|
||||
|
||||
// turn burg into a capital
|
||||
pack.burgs[burg].capital = true;
|
||||
pack.burgs[burg].state = pack.states.length;
|
||||
moveBurgToGroup(burg, "cities");
|
||||
|
||||
exitAddStateMode();
|
||||
const culture = pack.cells.culture[center];
|
||||
const basename = center%5 === 0 ? pack.burgs[burg].name : Names.getCulture(culture);
|
||||
const name = Names.getState(basename, culture);
|
||||
const color = d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
|
||||
pack.states.push({i:pack.states.length, name, color, expansionism:.5, capital:burg, type:"Generic", center, culture});
|
||||
recalculateStates();
|
||||
}
|
||||
|
||||
function exitAddStateMode() {
|
||||
customization = 0;
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
body.querySelectorAll("div > *").forEach(e => e.disabled = false);
|
||||
if (statesAdd.classList.contains("pressed")) statesAdd.classList.remove("pressed");
|
||||
}
|
||||
|
||||
function downloadStatesData() {
|
||||
const unit = areaUnit.value === "square" ? distanceUnit.value + "2" : areaUnit.value;
|
||||
let data = "Id,State,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area "+unit+",Population\n"; // headers
|
||||
|
||||
body.querySelectorAll(":scope > div").forEach(function(el) {
|
||||
data += el.dataset.id + ",";
|
||||
data += el.dataset.name + ",";
|
||||
data += el.dataset.color + ",";
|
||||
data += el.dataset.capital + ",";
|
||||
data += el.dataset.culture + ",";
|
||||
data += el.dataset.type + ",";
|
||||
data += el.dataset.expansionism + ",";
|
||||
data += el.dataset.cells + ",";
|
||||
data += el.dataset.burgs + ",";
|
||||
data += el.dataset.area + ",";
|
||||
data += el.dataset.population + "\n";
|
||||
});
|
||||
|
||||
const dataBlob = new Blob([data], {type: "text/plain"});
|
||||
const url = window.URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement("a");
|
||||
document.body.appendChild(link);
|
||||
link.download = "states_data" + Date.now() + ".csv";
|
||||
link.href = url;
|
||||
link.click();
|
||||
window.setTimeout(function() {window.URL.revokeObjectURL(url);}, 2000);
|
||||
}
|
||||
|
||||
function closeStatesEditor() {
|
||||
if (customization === 2) exitStatesManualAssignment();
|
||||
if (customization === 3) exitAddStateMode();
|
||||
}
|
||||
}
|
||||
222
modules/ui/tools.js
Normal file
222
modules/ui/tools.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
// module to control the Tools options (click to edit, to re-geenerate, tp add)
|
||||
"use strict";
|
||||
|
||||
toolsContent.addEventListener("click", function() {
|
||||
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
|
||||
if (event.target.tagName !== "BUTTON") return;
|
||||
const button = event.target.id;
|
||||
|
||||
// Click to open Editor buttons
|
||||
if (button === "editHeightmapButton") editHeightmap(); else
|
||||
if (button === "editBiomesButton") editBiomes(); else
|
||||
if (button === "editStatesButton") editStates(); else
|
||||
if (button === "editCulturesButton") editCultures(); else
|
||||
if (button === "editNamesBaseButton") editNamesbase(); else
|
||||
if (button === "editBurgsButton") editBurgs(); else
|
||||
if (button === "editUnitsButton") editUnits();
|
||||
|
||||
// Click to Regenerate buttons
|
||||
if (button === "regenerateStateLabels") {BurgsAndStates.drawStateLabels(); if (!layerIsOn("toggleLabels")) toggleLabels();} else
|
||||
if (button === "regenerateReliefIcons") {ReliefIcons(); if (!layerIsOn("toggleRelief")) toggleRelief();} else
|
||||
if (button === "regenerateRoutes") {Routes.regenerate(); if (!layerIsOn("toggleRoutes")) toggleRoutes();} else
|
||||
if (button === "regenerateRivers") {Rivers.generate(); if (!layerIsOn("toggleRivers")) toggleRivers();} else
|
||||
if (button === "regeneratePopulation") recalculatePopulation();
|
||||
|
||||
// Click to Add buttons
|
||||
if (button === "addLabel") toggleAddLabel(); else
|
||||
if (button === "addBurgTool") toggleAddBurg(); else
|
||||
if (button === "addRiver") toggleAddRiver(); else
|
||||
if (button === "addRoute") toggleAddRoute(); else
|
||||
if (button === "addMarker") toggleAddMarker();
|
||||
});
|
||||
|
||||
function recalculatePopulation() {
|
||||
rankCells();
|
||||
pack.burgs.forEach(b => {
|
||||
if (!b.i || b.removed) return;
|
||||
const i = b.cell;
|
||||
|
||||
b.population = rn(Math.max((pack.cells.s[i] + pack.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 (b.port) b.population = rn(b.population * 1.3, 3); // increase port population
|
||||
});
|
||||
}
|
||||
|
||||
function unpressClickToAddButton() {
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
}
|
||||
|
||||
function toggleAddLabel() {
|
||||
const pressed = document.getElementById("addLabel").classList.contains("pressed");
|
||||
if (pressed) {unpressClickToAddButton(); return;}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addLabel.classList.add('pressed');
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addLabelOnClick);
|
||||
tip("Click on map to place label. Hold Shift to add multiple", true);
|
||||
if (!layerIsOn("toggleLabels")) toggleLabels();
|
||||
}
|
||||
|
||||
function addLabelOnClick() {
|
||||
const point = d3.mouse(this);
|
||||
|
||||
// get culture in clicked point to generate a name
|
||||
const cell = findCell(point[0], point[1]);
|
||||
const culture = pack.cells.culture[cell];
|
||||
const name = Names.getCulture(culture);
|
||||
const id = getNextId("label");
|
||||
|
||||
labels.select("#addedLabels").append("text").attr("id", id)
|
||||
.append("textPath").attr("xlink:href", "#textPath_"+id).text(name)
|
||||
.attr("startOffset", "50%").attr("font-size", "100%");
|
||||
|
||||
defs.select("#textPaths").append("path").attr("id", "textPath_"+id)
|
||||
.attr("d", `M${point[0]-60},${point[1]} h${120}`);
|
||||
|
||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||
}
|
||||
|
||||
function toggleAddBurg() {
|
||||
unpressClickToAddButton();
|
||||
document.getElementById("addBurgTool").classList.add("pressed");
|
||||
editBurgs();
|
||||
document.getElementById("addNewBurg").click();
|
||||
}
|
||||
|
||||
function toggleAddRiver() {
|
||||
const pressed = document.getElementById("addRiver").classList.contains("pressed");
|
||||
if (pressed) {unpressClickToAddButton(); return;}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addRiver.classList.add('pressed');
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
|
||||
tip("Click on map to place new river or extend an existing one. Hold Shift to place multiple rivers", true);
|
||||
if (!layerIsOn("toggleRivers")) toggleRivers();
|
||||
}
|
||||
|
||||
function addRiverOnClick() {
|
||||
const cells = pack.cells;
|
||||
const point = d3.mouse(this);
|
||||
let i = findCell(point[0], point[1]);
|
||||
if (cells.r[i] || cells.h[i] < 20 || cells.b[i]) return;
|
||||
|
||||
const dataRiver = []; // to store river points
|
||||
const river = +getNextId("river").slice(5); // river id
|
||||
cells.fl[i] = grid.cells.prec[cells.g[i]]; // initial flux
|
||||
let render = true;
|
||||
|
||||
while (i) {
|
||||
cells.r[i] = river;
|
||||
const x = cells.p[i][0], y = cells.p[i][1];
|
||||
dataRiver.push({x, y, cell:i});
|
||||
|
||||
const min = cells.c[i][d3.scan(cells.c[i], (a, b) => cells.h[a] - cells.h[b])]; // downhill cell
|
||||
|
||||
if (cells.h[i] <= cells.h[min]) {
|
||||
tip(`Clicked cell is depressed! To resolve edit the heightmap and allow system to change heights`, false, "error");
|
||||
render = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const tx = cells.p[min][0], ty = cells.p[min][1];
|
||||
|
||||
if (cells.h[min] < 20) {
|
||||
const px = (x + tx) / 2;
|
||||
const py = (y + ty) / 2;
|
||||
dataRiver.push({x: px, y: py, cell:i});
|
||||
break;
|
||||
}
|
||||
|
||||
if (!cells.r[min]) {
|
||||
cells.fl[min] += cells.fl[i];
|
||||
i = min;
|
||||
continue;
|
||||
}
|
||||
|
||||
const r = cells.r[min];
|
||||
const riverCellsUpper = cells.i.filter(i => cells.r[i] === r && cells.h[i] > cells.h[min]);
|
||||
|
||||
// new river is not perspective
|
||||
if (dataRiver.length <= riverCellsUpper.length) {
|
||||
cells.conf[min] += cells.fl[i];
|
||||
dataRiver.push({x: tx, y: ty, cell: min});
|
||||
break;
|
||||
}
|
||||
|
||||
// new river is more perspective
|
||||
rivers.select("#river"+r).remove();
|
||||
riverCellsUpper.forEach(i => cells.r[i] = 0);
|
||||
if (riverCellsUpper.length > 1) {
|
||||
// redraw upper part of the old river
|
||||
}
|
||||
|
||||
cells.conf[min] = cells.fl[min];
|
||||
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
|
||||
i = min;
|
||||
}
|
||||
|
||||
if (!render) return;
|
||||
const points = Rivers.addMeandring(dataRiver, Math.random() * .5 + .1);
|
||||
const width = Math.random() * .5 + .9;
|
||||
const increment = Math.random() * .4 + .8;
|
||||
const d = Rivers.getPath(points, width, increment);
|
||||
rivers.append("path").attr("d", d).attr("id", "river"+river).attr("data-width", width).attr("data-increment", increment);
|
||||
|
||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||
}
|
||||
|
||||
function toggleAddRoute() {
|
||||
const pressed = document.getElementById("addRoute").classList.contains("pressed");
|
||||
if (pressed) {unpressClickToAddButton(); return;}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addRoute.classList.add('pressed');
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addRouteOnClick);
|
||||
tip("Click on map to add a first control point", true);
|
||||
if (!layerIsOn("toggleRoutes")) toggleRoutes();
|
||||
}
|
||||
|
||||
function addRouteOnClick() {
|
||||
unpressClickToAddButton();
|
||||
const point = d3.mouse(this);
|
||||
const id = getNextId("route");
|
||||
elSelected = routes.select("g").append("path").attr("id", id).attr("data-new", 1).attr("d", `M${point[0]},${point[1]}`);
|
||||
editRoute(true);
|
||||
}
|
||||
|
||||
function toggleAddMarker() {
|
||||
const pressed = document.getElementById("addMarker").classList.contains("pressed");
|
||||
if (pressed) {unpressClickToAddButton(); return;}
|
||||
|
||||
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
|
||||
addMarker.classList.add('pressed');
|
||||
closeDialogs(".stable");
|
||||
viewbox.style("cursor", "crosshair").on("click", addMarkerOnClick);
|
||||
tip("Click on map to add a marker. Hold Shift to add multiple", true);
|
||||
if (!layerIsOn("toggleMarkers")) toggleMarkers();
|
||||
}
|
||||
|
||||
function addMarkerOnClick() {
|
||||
const point = d3.mouse(this);
|
||||
const x = rn(point[0], 2), y = rn(point[1], 2);
|
||||
const id = getNextId("markerElement");
|
||||
|
||||
const selected = markerSelectGroup.value;
|
||||
const valid = selected && d3.select("#defs-markers").select("#"+selected).size();
|
||||
const symbol = valid ? "#"+selected : "#marker0";
|
||||
const added = markers.select("[data-id='" + symbol + "']").size();
|
||||
let desired = valid && added ? markers.select("[data-id='" + symbol + "']").attr("data-size") : 1;
|
||||
if (isNaN(desired)) desired = 1;
|
||||
const size = desired * 5 + 25 / scale;
|
||||
|
||||
markers.append("use").attr("id", id).attr("xlink:href", symbol).attr("data-id", symbol)
|
||||
.attr("data-x", x).attr("data-y", y).attr("x", x - size / 2).attr("y", y - size)
|
||||
.attr("data-size", desired).attr("width", size).attr("height", size);
|
||||
|
||||
if (d3.event.shiftKey === false) unpressClickToAddButton();
|
||||
}
|
||||
167
modules/ui/units-editor.js
Normal file
167
modules/ui/units-editor.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
"use strict";
|
||||
function editUnits() {
|
||||
closeDialogs("#unitsEditor, .stable");
|
||||
$("#unitsEditor").dialog();
|
||||
|
||||
if (modules.editUnits) return;
|
||||
modules.editUnits = true;
|
||||
|
||||
$("#unitsEditor").dialog({
|
||||
title: "Units Editor",
|
||||
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
|
||||
});
|
||||
|
||||
// add listeners
|
||||
document.getElementById("distanceUnit").addEventListener("change", changeDistanceUnit);
|
||||
document.getElementById("distanceScaleSlider").addEventListener("input", changeDistanceScale);
|
||||
document.getElementById("distanceScale").addEventListener("change", changeDistanceScale);
|
||||
document.getElementById("distanceScale").addEventListener("mouseenter", hideDistanceUnitOutput);
|
||||
document.getElementById("distanceScale").addEventListener("mouseleave", showDistanceUnitOutput);
|
||||
document.getElementById("heightUnit").addEventListener("change", changeHeightUnit);
|
||||
document.getElementById("heightExponent").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("heightExponentSlider").addEventListener("input", changeHeightExponent);
|
||||
document.getElementById("temperatureScale").addEventListener("change", () => {if (layerIsOn("toggleTemp")) drawTemp()});
|
||||
document.getElementById("barSizeSlider").addEventListener("input", changeScaleBarSize);
|
||||
document.getElementById("barSize").addEventListener("input", changeScaleBarSize);
|
||||
document.getElementById("barLabel").addEventListener("input", drawScaleBar);
|
||||
document.getElementById("barPosX").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barPosY").addEventListener("input", fitScaleBar);
|
||||
document.getElementById("barBackOpacity").addEventListener("input", function() {scaleBar.select("rect").attr("opacity", this.value)});
|
||||
document.getElementById("barBackColor").addEventListener("input", function() {scaleBar.select("rect").attr("fill", this.value)});
|
||||
document.getElementById("populationRateSlider").addEventListener("input", changePopulationRate);
|
||||
document.getElementById("populationRate").addEventListener("change", changePopulationRate);
|
||||
document.getElementById("urbanizationSlider").addEventListener("input", changeUrbanizationRate);
|
||||
document.getElementById("urbanization").addEventListener("change", changeUrbanizationRate);
|
||||
|
||||
document.getElementById("addLinearRuler").addEventListener("click", addAdditionalRuler);
|
||||
document.getElementById("addOpisometer").addEventListener("click", toggleOpisometerMode);
|
||||
document.getElementById("addPlanimeter").addEventListener("click", togglePlanimeterMode);
|
||||
document.getElementById("removeRulers").addEventListener("click", removeAllRulers);
|
||||
|
||||
function changeDistanceUnit() {
|
||||
if (this.value === "custom_name") {
|
||||
const custom = prompt("Provide a custom name for distance unit");
|
||||
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||
else {this.value = document.getElementById("distanceUnitOutput").innerHTML; return;};
|
||||
}
|
||||
|
||||
document.getElementById("distanceUnitOutput").innerHTML = this.value;
|
||||
drawScaleBar();
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
||||
function changeDistanceScale() {
|
||||
const scale = +this.value;
|
||||
if (!scale || isNaN(scale) || scale < 0) {
|
||||
tip("Distance scale should be a positive number", false, "error");
|
||||
this.value = document.getElementById("distanceScale").dataset.value;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("distanceScaleSlider").value = scale;
|
||||
document.getElementById("distanceScale").value = scale;
|
||||
document.getElementById("distanceScale").dataset.value = scale;
|
||||
drawScaleBar();
|
||||
calculateFriendlyGridSize();
|
||||
}
|
||||
|
||||
function hideDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = .2;}
|
||||
function showDistanceUnitOutput() {document.getElementById("distanceUnitOutput").style.opacity = 1;}
|
||||
|
||||
function changeHeightUnit() {
|
||||
if (this.value !== "custom_name") return;
|
||||
const custom = prompt("Provide a custom name for height unit");
|
||||
if (custom) this.options.add(new Option(custom, custom, false, true));
|
||||
else this.value = "ft";
|
||||
}
|
||||
|
||||
function changeHeightExponent() {
|
||||
document.getElementById("heightExponent").value = this.value;
|
||||
document.getElementById("heightExponentSlider").value = this.value;
|
||||
calculateTemperatures();
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
}
|
||||
|
||||
function changeScaleBarSize() {
|
||||
document.getElementById("barSize").value = this.value;
|
||||
document.getElementById("barSizeSlider").value = this.value;
|
||||
drawScaleBar();
|
||||
}
|
||||
|
||||
function changePopulationRate() {
|
||||
const rate = +this.value;
|
||||
if (!rate || isNaN(rate) || rate <= 0) {
|
||||
tip("Population rate should be a positive number", false, "error");
|
||||
this.value = document.getElementById("populationRate").dataset.value;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("populationRateSlider").value = rate;
|
||||
document.getElementById("populationRate").value = rate;
|
||||
document.getElementById("populationRate").dataset.value = rate;
|
||||
}
|
||||
|
||||
function changeUrbanizationRate() {
|
||||
const rate = +this.value;
|
||||
if (!rate || isNaN(rate) || rate < 0) {
|
||||
tip("Urbanization rate should be a number", false, "error");
|
||||
this.value = document.getElementById("urbanization").dataset.value;
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("urbanizationSlider").value = rate;
|
||||
document.getElementById("urbanization").value = rate;
|
||||
document.getElementById("urbanization").dataset.value = rate;
|
||||
}
|
||||
|
||||
function addAdditionalRuler() {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
const y = rn(Math.random() * graphHeight * .5 + graphHeight * .25);
|
||||
addRuler(graphWidth * .2, y, graphWidth * .8, y);
|
||||
}
|
||||
|
||||
function toggleOpisometerMode() {
|
||||
if (this.classList.contains("pressed")) {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
this.classList.remove("pressed");
|
||||
} else {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
tip("Draw a curve to measure its length", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawOpisometer));
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlanimeterMode() {
|
||||
if (this.classList.contains("pressed")) {
|
||||
restoreDefaultEvents();
|
||||
clearMainTip();
|
||||
this.classList.remove("pressed");
|
||||
} else {
|
||||
if (!layerIsOn("toggleRulers")) toggleRulers();
|
||||
tip("Draw a line to measure its inner area", true);
|
||||
unitsBottom.querySelectorAll(".pressed").forEach(button => button.classList.remove("pressed"));
|
||||
this.classList.add("pressed");
|
||||
viewbox.style("cursor", "crosshair").call(d3.drag().on("start", drawPlanimeter));
|
||||
}
|
||||
}
|
||||
|
||||
function removeAllRulers() {
|
||||
if (!ruler.selectAll("g").size()) return;
|
||||
alertMessage.innerHTML = `Are you sure you want to remove all placed rulers?`;
|
||||
$("#alert").dialog({resizable: false, title: "Remove all rulers",
|
||||
buttons: {
|
||||
Remove: function() {
|
||||
$(this).dialog("close");
|
||||
ruler.selectAll("g").remove();
|
||||
},
|
||||
Cancel: function() {$(this).dialog("close");}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
114
modules/ui/world-configurator.js
Normal file
114
modules/ui/world-configurator.js
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
function editWorld() {
|
||||
if (customization) return;
|
||||
$("#worldConfigurator").dialog({title: "Configure World", width: 440});
|
||||
|
||||
const globe = d3.select("#globe");
|
||||
const clr = d3.scaleSequential(d3.interpolateSpectral);
|
||||
const tMax = +temperatureEquatorOutput.max, tMin = +temperatureEquatorOutput.min; // temperature extremes
|
||||
const projection = d3.geoOrthographic().translate([100, 100]).scale(100);
|
||||
const path = d3.geoPath(projection);
|
||||
|
||||
updateGlobeTemperature();
|
||||
updateGlobePosition();
|
||||
|
||||
if (modules.editWorld) return;
|
||||
modules.editWorld = true;
|
||||
|
||||
document.getElementById("worldControls").addEventListener("input", (e) => updateWorld(e.target));
|
||||
globe.select("#globeWindArrows").on("click", changeWind);
|
||||
globe.select("#restoreWind").on("click", restoreDefaultWinds);
|
||||
globe.select("#globeGraticule").attr("d", round(path(d3.geoGraticule()()))); // globe graticule
|
||||
updateWindDirections();
|
||||
|
||||
function updateWorld(el) {
|
||||
if (el) {
|
||||
document.getElementById(el.dataset.stored+"Input").value = el.value;
|
||||
document.getElementById(el.dataset.stored+"Output").value = el.value;
|
||||
if (el.dataset.stored) lock(el.dataset.stored);
|
||||
}
|
||||
|
||||
updateGlobeTemperature();
|
||||
updateGlobePosition();
|
||||
calculateTemperatures();
|
||||
generatePrecipitation();
|
||||
elevateLakes();
|
||||
Rivers.generate();
|
||||
defineBiomes();
|
||||
|
||||
if (layerIsOn("toggleTemp")) drawTemp();
|
||||
if (layerIsOn("togglePrec")) drawPrec();
|
||||
if (layerIsOn("toggleBiomes")) drawBiomes();
|
||||
if (layerIsOn("toggleCoordinates")) drawCoordinates();
|
||||
}
|
||||
|
||||
function updateGlobePosition() {
|
||||
const eqY = +document.getElementById("equatorOutput").value;
|
||||
const equidistance = document.getElementById("equidistanceOutput");
|
||||
equidistance.min = equidistanceInput.min = Math.max(graphHeight - eqY, eqY);
|
||||
equidistance.max = equidistanceInput.max = equidistance.min * 10;
|
||||
const eqD = +equidistance.value;
|
||||
calculateMapCoordinates();
|
||||
const mc = mapCoordinates; // shortcut
|
||||
|
||||
const scale = +distanceScale.value, unit = distanceUnit.value;
|
||||
document.getElementById("mapSize").innerHTML = `${graphWidth}x${graphHeight}`;
|
||||
document.getElementById("mapSizeFriendly").innerHTML = `${rn(graphWidth * scale)}x${rn(graphHeight * scale)} ${unit}`;
|
||||
document.getElementById("meridianLength").innerHTML = rn(eqD * 2);
|
||||
document.getElementById("meridianLengthFriendly").innerHTML = `${rn(eqD * 2 * scale)} ${unit}`;
|
||||
document.getElementById("meridianLengthEarth").innerHTML = toKilometer(eqD * 2 * scale);
|
||||
document.getElementById("mapCoordinates").innerHTML = `${lat(mc.latN)} ${Math.abs(rn(mc.lonW))}°W; ${lat(mc.latS)} ${rn(mc.lonE)}°E`;
|
||||
|
||||
function toKilometer(v) {
|
||||
let kilometers; // value converted to kilometers
|
||||
if (unit === "km") kilometers = v;
|
||||
else if (unit === "mi") kilometers = v * 1.60934;
|
||||
else if (unit === "lg") kilometers = v * 5.556;
|
||||
else if (unit === "vr") kilometers = v * 1.0668;
|
||||
else return ""; // do not show as distanceUnit is custom
|
||||
return " = " + rn(kilometers / 200) + "%🌏"; // % + Earth icon
|
||||
}
|
||||
|
||||
function lat(lat) {return lat > 0 ? Math.abs(rn(lat)) + "°N" : Math.abs(rn(lat)) + "°S";} // parse latitude value
|
||||
const area = d3.geoGraticule().extent([[mc.lonW, mc.latN], [mc.lonE, mc.latS]]);
|
||||
globe.select("#globeArea").attr("d", round(path(area.outline()))); // map area
|
||||
}
|
||||
|
||||
function updateGlobeTemperature() {
|
||||
const tEq = +document.getElementById("temperatureEquatorOutput").value;
|
||||
document.getElementById("temperatureEquatorF").innerHTML = rn(tEq * 9/5 + 32);
|
||||
const tPole = +document.getElementById("temperaturePoleOutput").value;
|
||||
document.getElementById("temperaturePoleF").innerHTML = rn(tPole * 9/5 + 32);
|
||||
globe.selectAll(".tempGradient90").attr("stop-color", clr(1 - (tPole - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient60").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 2/3 - tMin) / (tMax - tMin)));
|
||||
globe.selectAll(".tempGradient30").attr("stop-color", clr(1 - (tEq - (tEq - tPole) * 1/3 - tMin) / (tMax - tMin)));
|
||||
globe.select(".tempGradient0").attr("stop-color", clr(1 - (tEq - tMin) / (tMax - tMin)));
|
||||
}
|
||||
|
||||
function updateWindDirections() {
|
||||
globe.select("#globeWindArrows").selectAll("path").each(function(d, i) {
|
||||
const tr = parseTransform(this.getAttribute("transform"));
|
||||
this.setAttribute("transform", `rotate(${winds[i]} ${tr[1]} ${tr[2]})`);
|
||||
});
|
||||
}
|
||||
|
||||
function changeWind() {
|
||||
const arrow = d3.event.target.nextElementSibling;
|
||||
const tier = +arrow.dataset.tier;
|
||||
winds[tier] = (winds[tier] + 45) % 360;
|
||||
const tr = parseTransform(arrow.getAttribute("transform"));
|
||||
arrow.setAttribute("transform", `rotate(${winds[tier]} ${tr[1]} ${tr[2]})`);
|
||||
localStorage.setItem("winds", winds);
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||
if (mapTiers.includes(tier)) updateWorld();
|
||||
}
|
||||
|
||||
function restoreDefaultWinds() {
|
||||
const defaultWinds = [225, 45, 225, 315, 135, 315];
|
||||
const mapTiers = d3.range(mapCoordinates.latN, mapCoordinates.latS, -30).map(c => (90-c) / 30 | 0);
|
||||
const update = mapTiers.some(t => winds[t] != defaultWinds[t]);
|
||||
winds = defaultWinds;
|
||||
updateWindDirections();
|
||||
if (update) updateWorld();
|
||||
}
|
||||
|
||||
}
|
||||
436
modules/utils.js
Normal file
436
modules/utils.js
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
// FMG helper functions
|
||||
"use strict";
|
||||
|
||||
// add boundary points to pseudo-clip voronoi cells
|
||||
function getBoundaryPoints(width, height, spacing) {
|
||||
const offset = rn(-1 * spacing);
|
||||
const bSpacing = spacing * 2;
|
||||
const w = width - offset * 2;
|
||||
const h = height - offset * 2;
|
||||
const numberX = Math.ceil(w / bSpacing) - 1;
|
||||
const numberY = Math.ceil(h / bSpacing) - 1;
|
||||
let points = [];
|
||||
for (let i = 0.5; i < numberX; i++) {
|
||||
let x = Math.ceil(w * i / numberX + offset);
|
||||
points.push([x, offset], [x, h + offset]);
|
||||
}
|
||||
for (let i = 0.5; i < numberY; i++) {
|
||||
let y = Math.ceil(h * i / numberY + offset);
|
||||
points.push([offset, y], [w + offset, y]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// get points on a regular square grid and jitter them a bit
|
||||
function getJitteredGrid(width, height, spacing) {
|
||||
const radius = spacing / 2; // square radius
|
||||
const jittering = radius * 0.9; // max deviation
|
||||
const jitter = function() {return Math.random() * 2 * jittering - jittering;};
|
||||
let points = [];
|
||||
for (let y = radius; y < height; y += spacing) {
|
||||
for (let x = radius; x < width; x += spacing) {
|
||||
let xj = rn(x + jitter(), 2);
|
||||
let yj = rn(y + jitter(), 2);
|
||||
points.push([xj, yj]);
|
||||
}
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// return cell index on a regular square grid
|
||||
function findGridCell(x, y) {
|
||||
return Math.floor(Math.min(y / grid.spacing, grid.cellsY -1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX-1));
|
||||
}
|
||||
|
||||
// return array of cell indexes in radius on a regular square grid
|
||||
function findGridAll(x, y, radius) {
|
||||
const c = grid.cells.c;
|
||||
let found = [findGridCell(x, y)];
|
||||
let r = Math.floor(radius / grid.spacing);
|
||||
if (r > 0) found = found.concat(c[found[0]]);
|
||||
if (r > 1) {
|
||||
let frontier = c[found[0]];
|
||||
while (r > 1) {
|
||||
let cycle = frontier.slice();
|
||||
frontier = [];
|
||||
cycle.forEach(function(s) {
|
||||
|
||||
c[s].forEach(function(e) {
|
||||
if (found.indexOf(e) !== -1) return;
|
||||
found.push(e);
|
||||
frontier.push(e);
|
||||
});
|
||||
|
||||
});
|
||||
r--;
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
|
||||
}
|
||||
|
||||
// return closest pack points quadtree datum
|
||||
function find(x, y, radius = Infinity) {
|
||||
return pack.cells.q.find(x, y, radius);
|
||||
}
|
||||
|
||||
// return closest cell index
|
||||
function findCell(x, y, radius = Infinity) {
|
||||
const found = pack.cells.q.find(x, y, radius);
|
||||
return found ? found[2] : undefined;
|
||||
}
|
||||
|
||||
// return array of cell indexes in radius
|
||||
function findAll(x, y, radius) {
|
||||
const found = pack.cells.q.findAll(x, y, radius);
|
||||
return found.map(r => r[2]);
|
||||
}
|
||||
|
||||
// get polygon points for packed cells knowing cell id
|
||||
function getPackPolygon(i) {
|
||||
return pack.cells.v[i].map(v => pack.vertices.p[v]);
|
||||
}
|
||||
|
||||
// get polygon points for initial cells knowing cell id
|
||||
function getGridPolygon(i) {
|
||||
return grid.cells.v[i].map(v => grid.vertices.p[v]);
|
||||
}
|
||||
|
||||
// mbostock's poissonDiscSampler
|
||||
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
|
||||
if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error;
|
||||
|
||||
const width = x1 - x0;
|
||||
const height = y1 - y0;
|
||||
const r2 = r * r;
|
||||
const r2_3 = 3 * r2;
|
||||
const cellSize = r * Math.SQRT1_2;
|
||||
const gridWidth = Math.ceil(width / cellSize);
|
||||
const gridHeight = Math.ceil(height / cellSize);
|
||||
const grid = new Array(gridWidth * gridHeight);
|
||||
const queue = [];
|
||||
|
||||
function far(x, y) {
|
||||
const i = x / cellSize | 0;
|
||||
const j = y / cellSize | 0;
|
||||
const i0 = Math.max(i - 2, 0);
|
||||
const j0 = Math.max(j - 2, 0);
|
||||
const i1 = Math.min(i + 3, gridWidth);
|
||||
const j1 = Math.min(j + 3, gridHeight);
|
||||
for (let j = j0; j < j1; ++j) {
|
||||
const o = j * gridWidth;
|
||||
for (let i = i0; i < i1; ++i) {
|
||||
const s = grid[o + i];
|
||||
if (s) {
|
||||
const dx = s[0] - x;
|
||||
const dy = s[1] - y;
|
||||
if (dx * dx + dy * dy < r2) return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function sample(x, y) {
|
||||
queue.push(grid[gridWidth * (y / cellSize | 0) + (x / cellSize | 0)] = [x, y]);
|
||||
return [x + x0, y + y0];
|
||||
}
|
||||
|
||||
yield sample(width / 2, height / 2);
|
||||
|
||||
pick: while (queue.length) {
|
||||
const i = Math.random() * queue.length | 0;
|
||||
const parent = queue[i];
|
||||
|
||||
for (let j = 0; j < k; ++j) {
|
||||
const a = 2 * Math.PI * Math.random();
|
||||
const r = Math.sqrt(Math.random() * r2_3 + r2);
|
||||
const x = parent[0] + r * Math.cos(a);
|
||||
const y = parent[1] + r * Math.sin(a);
|
||||
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
|
||||
yield sample(x, y);
|
||||
continue pick;
|
||||
}
|
||||
}
|
||||
|
||||
const r = queue.pop();
|
||||
if (i < queue.length) queue[i] = r;
|
||||
}
|
||||
}
|
||||
|
||||
// filter land cells
|
||||
function isLand(i) {
|
||||
return pack.cells.h[i] >= 20;
|
||||
}
|
||||
|
||||
// filter water cells
|
||||
function isWater(i) {
|
||||
return pack.cells.h[i] < 20;
|
||||
}
|
||||
|
||||
// sort cells by height: highest go first
|
||||
function highest(a, b) {
|
||||
return pack.cells.h[b] - pack.cells.h[a];
|
||||
}
|
||||
|
||||
// convert RGB color string to HEX without #
|
||||
function toHEX(rgb){
|
||||
if (rgb.charAt(0) === "#") {return rgb;}
|
||||
rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
|
||||
return (rgb && rgb.length === 4) ? "#" +
|
||||
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
|
||||
("0" + parseInt(rgb[3],10).toString(16)).slice(-2) : '';
|
||||
}
|
||||
|
||||
// return array of standard shuffled colors
|
||||
function getColors(number) {
|
||||
const c12 = d3.scaleOrdinal(d3.schemeSet3);
|
||||
const cRB = d3.scaleSequential(d3.interpolateRainbow);
|
||||
const colors = d3.shuffle(d3.range(number).map(i => i < 12 ? c12(i) : d3.color(cRB((i-12)/(number-12))).hex()));
|
||||
//debug.selectAll("circle").data(colors).enter().append("circle").attr("r", 15).attr("cx", (d,i) => 60 + i * 40).attr("cy", 20).attr("fill", d => d);
|
||||
return colors;
|
||||
}
|
||||
|
||||
// conver temperature from °C to other scales
|
||||
function convertTemperature(c) {
|
||||
switch(temperatureScale.value) {
|
||||
case "°C": return c + "°C";
|
||||
case "°F": return rn(c * 9 / 5 + 32) + "°F";
|
||||
case "K": return rn(c + 273.15) + "K";
|
||||
case "°R": return rn((c + 273.15) * 9 / 5) + "°R";
|
||||
case "°De": return rn((100 - c) * 3 / 2) + "°De";
|
||||
case "°N": return rn(c * 33 / 100) + "°N";
|
||||
case "°Ré": return rn(c * 4 / 5) + "°Ré";
|
||||
case "°Rø": return rn(c * 21 / 40 + 7.5) + "°Rø";
|
||||
default: return c + "°C";
|
||||
}
|
||||
}
|
||||
|
||||
// random number in a range
|
||||
function rand(min, max) {
|
||||
if (min === undefined && !max === undefined) return Math.random();
|
||||
if (max === undefined) {max = min; min = 0;}
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
|
||||
return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
|
||||
}
|
||||
|
||||
// round value to d decimals
|
||||
function rn(v, d = 0) {
|
||||
const m = Math.pow(10, d);
|
||||
return Math.round(v * m) / m;
|
||||
}
|
||||
|
||||
// round string to d decimals
|
||||
function round(s, d = 1) {
|
||||
return s.replace(/[\d\.-][\d\.e-]*/g, function(n) {return rn(n, d);})
|
||||
}
|
||||
|
||||
// corvent number to short string with SI postfix
|
||||
function si(n) {
|
||||
if (n >= 1e9) {return rn(n / 1e9, 1) + "B";}
|
||||
if (n >= 1e8) {return rn(n / 1e6) + "M";}
|
||||
if (n >= 1e6) {return rn(n / 1e6, 1) + "M";}
|
||||
if (n >= 1e4) {return rn(n / 1e3) + "K";}
|
||||
if (n >= 1e3) {return rn(n / 1e3, 1) + "K";}
|
||||
return rn(n);
|
||||
}
|
||||
|
||||
// getInteger number from user input data
|
||||
function getInteger(value) {
|
||||
const metric = value.slice(-1);
|
||||
if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
|
||||
if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
|
||||
if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
|
||||
return parseInt(value);
|
||||
}
|
||||
|
||||
// remove parent element (usually if child is clicked)
|
||||
function removeParent() {
|
||||
this.parentNode.parentNode.removeChild(this.parentNode);
|
||||
}
|
||||
|
||||
// return string with 1st char capitalized
|
||||
function capitalize(string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||
}
|
||||
|
||||
// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
|
||||
function parseTransform(string) {
|
||||
if (!string) {return [0,0,0,0,0,1];}
|
||||
const a = string.replace(/[a-z()]/g, "").replace(/[ ]/g, ",").split(",");
|
||||
return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
|
||||
}
|
||||
|
||||
// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
|
||||
void function addFindAll() {
|
||||
const Quad = function(node, x0, y0, x1, y1) {
|
||||
this.node = node;
|
||||
this.x0 = x0;
|
||||
this.y0 = y0;
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
}
|
||||
|
||||
const tree_filter = function(x, y, radius) {
|
||||
var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
|
||||
if (t.node) {t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3))};
|
||||
radiusSearchInit(t, radius);
|
||||
|
||||
var i = 0;
|
||||
while (t.q = t.quads.pop()) {
|
||||
i++;
|
||||
|
||||
// Stop searching if this quadrant can’t contain a closer node.
|
||||
if (!(t.node = t.q.node)
|
||||
|| (t.x1 = t.q.x0) > t.x3
|
||||
|| (t.y1 = t.q.y0) > t.y3
|
||||
|| (t.x2 = t.q.x1) < t.x0
|
||||
|| (t.y2 = t.q.y1) < t.y0) continue;
|
||||
|
||||
// Bisect the current quadrant.
|
||||
if (t.node.length) {
|
||||
t.node.explored = true;
|
||||
var xm = (t.x1 + t.x2) / 2,
|
||||
ym = (t.y1 + t.y2) / 2;
|
||||
|
||||
t.quads.push(
|
||||
new Quad(t.node[3], xm, ym, t.x2, t.y2),
|
||||
new Quad(t.node[2], t.x1, ym, xm, t.y2),
|
||||
new Quad(t.node[1], xm, t.y1, t.x2, ym),
|
||||
new Quad(t.node[0], t.x1, t.y1, xm, ym)
|
||||
);
|
||||
|
||||
// Visit the closest quadrant first.
|
||||
if (t.i = (y >= ym) << 1 | (x >= xm)) {
|
||||
t.q = t.quads[t.quads.length - 1];
|
||||
t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
|
||||
t.quads[t.quads.length - 1 - t.i] = t.q;
|
||||
}
|
||||
}
|
||||
|
||||
// Visit this point. (Visiting coincident points isn’t necessary!)
|
||||
else {
|
||||
var dx = x - +this._x.call(null, t.node.data),
|
||||
dy = y - +this._y.call(null, t.node.data),
|
||||
d2 = dx * dx + dy * dy;
|
||||
radiusSearchVisit(t, d2);
|
||||
}
|
||||
}
|
||||
return t.result;
|
||||
}
|
||||
d3.quadtree.prototype.findAll = tree_filter;
|
||||
|
||||
var radiusSearchInit = function(t, radius) {
|
||||
t.result = [];
|
||||
t.x0 = t.x - radius, t.y0 = t.y - radius;
|
||||
t.x3 = t.x + radius, t.y3 = t.y + radius;
|
||||
t.radius = radius * radius;
|
||||
}
|
||||
|
||||
var radiusSearchVisit = function(t, d2) {
|
||||
t.node.data.scanned = true;
|
||||
if (d2 < t.radius) {
|
||||
do {t.result.push(t.node.data); t.node.data.selected = true;} while (t.node = t.node.next);
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// normalization function
|
||||
function normalize(val, min, max) {
|
||||
return Math.min(Math.max((val - min) / (max - min), 0), 1);
|
||||
}
|
||||
|
||||
// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
|
||||
// from https://gamedev.stackexchange.com/a/116875
|
||||
function biased(min, max, ex) {
|
||||
return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
|
||||
}
|
||||
|
||||
// return array of values common for both array a and array b
|
||||
function intersect(a, b) {
|
||||
const setB = new Set(b);
|
||||
return [...new Set(a)].filter(a => setB.has(a));
|
||||
}
|
||||
|
||||
// check if char is vowel
|
||||
function vowel(c) {
|
||||
return "aeiouy".includes(c);
|
||||
}
|
||||
|
||||
// return the last element of array
|
||||
function last(array) {
|
||||
return array[array.length - 1];
|
||||
}
|
||||
|
||||
// return value in range [0, 100] (height range)
|
||||
function lim(v) {
|
||||
return Math.max(Math.min(v, 100), 0);
|
||||
}
|
||||
|
||||
// get number from string in format "1-3" or "2" or "0.5"
|
||||
function getNumberInRange(r) {
|
||||
if (typeof r !== "string") {console.error("The value should be a string", r); return 0;}
|
||||
if (!isNaN(+r)) return +r;
|
||||
const sign = r[0] === "-" ? -1 : 1;
|
||||
if (isNaN(+r[0])) r = r.slice(1);
|
||||
const range = r.includes("-") ? r.split("-") : null;
|
||||
if (!range) {console.error("Cannot parse the number. Check the format", r); return 0;}
|
||||
const count = rand(range[0] * sign, +range[1]);
|
||||
if (isNaN(count) || count < 0) {console.error("Cannot parse number. Check the format", r); return 0;}
|
||||
return count;
|
||||
}
|
||||
|
||||
function analizeNamesbase() {
|
||||
const result = [];
|
||||
nameBases.forEach((b,i) => {
|
||||
const d = nameBase[i];
|
||||
const size = d.length;
|
||||
const ar = d.map(n => n.length);
|
||||
const min = d3.min(ar);
|
||||
const max = d3.max(ar);
|
||||
const mean = rn(d3.mean(ar), 1);
|
||||
const median = d3.median(ar);
|
||||
const lengths = new Uint8Array(max);
|
||||
ar.forEach(l => lengths[l]++);
|
||||
const common = d3.scan(lengths, (a,b) => b-a);
|
||||
const string = d.join("");
|
||||
const doubleArray = [];
|
||||
let double = "";
|
||||
for (let i=0; i<string.length; i++) {
|
||||
if (!doubleArray[string[i]]) doubleArray[string[i]] = 0;
|
||||
if (string[i] === string[i-1]) doubleArray[string[i]]++;
|
||||
}
|
||||
for (const l in doubleArray) {if(doubleArray[l] > size/35) double += l;}
|
||||
const multi = rn(d3.mean(d.map(n => (n.match(/ /g)||[]).length)),2);
|
||||
result.push({name:b.name, size, min, max, mean, median, common, double, multi});
|
||||
});
|
||||
console.table(result);
|
||||
}
|
||||
|
||||
// polyfill for composedPath
|
||||
function getComposedPath(node) {
|
||||
let parent;
|
||||
if (node.parentNode) parent = node.parentNode;
|
||||
else if (node.host) parent = node.host;
|
||||
else if (node.defaultView) parent = node.defaultView;
|
||||
if (parent !== undefined) return [node].concat(getComposedPath(parent));
|
||||
return [node];
|
||||
};
|
||||
|
||||
// get next unused id
|
||||
function getNextId(core, i = 1) {
|
||||
while (document.getElementById(core+i)) i++;
|
||||
return core + i;
|
||||
}
|
||||
|
||||
function getAbsolutePath(href) {
|
||||
if (!href) return "";
|
||||
var link = document.createElement("a");
|
||||
link.href = href;
|
||||
return link.href;
|
||||
}
|
||||
82
modules/voronoi.js
Normal file
82
modules/voronoi.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global.Voronoi = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
var Voronoi = function Voronoi(delaunay, points, pointsN) {
|
||||
const cells = {v: [], c: [], b: []}; // voronoi cells: v = cell vertices, c = adjacent cells, b = near-border cell
|
||||
const vertices = {p: [], v: [], c: []}; // cells vertices: p = vertex coordinates, v = neighboring vertices, c = adjacent cells
|
||||
|
||||
for (let e=0; e < delaunay.triangles.length; e++) {
|
||||
|
||||
const p = delaunay.triangles[nextHalfedge(e)];
|
||||
if (p < pointsN && !cells.c[p]) {
|
||||
const edges = edgesAroundPoint(e);
|
||||
cells.v[p] = edges.map(e => triangleOfEdge(e)); // cell: adjacent vertex
|
||||
cells.c[p] = edges.map(e => delaunay.triangles[e]).filter(c => c < pointsN); // cell: adjacent valid cells
|
||||
cells.b[p] = edges.length > cells.c[p].length ? 1 : 0; // cell: is border
|
||||
}
|
||||
|
||||
const t = triangleOfEdge(e);
|
||||
if (!vertices.p[t]) {
|
||||
vertices.p[t] = triangleCenter(t); // vertex: coordinates
|
||||
vertices.v[t] = trianglesAdjacentToTriangle(t); // vertex: adjacent vertices
|
||||
vertices.c[t] = pointsOfTriangle(t); // vertex: adjacent cells
|
||||
}
|
||||
}
|
||||
|
||||
function pointsOfTriangle(t) {
|
||||
return edgesOfTriangle(t).map(e => delaunay.triangles[e]);
|
||||
}
|
||||
|
||||
function trianglesAdjacentToTriangle(t) {
|
||||
let triangles = [];
|
||||
for (let e of edgesOfTriangle(t)) {
|
||||
let opposite = delaunay.halfedges[e];
|
||||
triangles.push(triangleOfEdge(opposite));
|
||||
}
|
||||
return triangles;
|
||||
}
|
||||
|
||||
function edgesAroundPoint(start) {
|
||||
let result = [], incoming = start;
|
||||
do {
|
||||
result.push(incoming);
|
||||
const outgoing = nextHalfedge(incoming);
|
||||
incoming = delaunay.halfedges[outgoing];
|
||||
} while (incoming !== -1 && incoming !== start && result.length < 20);
|
||||
return result;
|
||||
}
|
||||
|
||||
function triangleCenter(t) {
|
||||
let vertices = pointsOfTriangle(t).map(p => points[p]);
|
||||
return circumcenter(vertices[0], vertices[1], vertices[2]);
|
||||
}
|
||||
|
||||
return {cells, vertices}
|
||||
|
||||
}
|
||||
|
||||
function edgesOfTriangle(t) {return [3*t, 3*t+1, 3*t+2];}
|
||||
|
||||
function triangleOfEdge(e) {return Math.floor(e/3);}
|
||||
|
||||
function nextHalfedge(e) {return (e % 3 === 2) ? e-2 : e+1;}
|
||||
|
||||
function prevHalfedge(e) {return (e % 3 === 0) ? e+2 : e-1;}
|
||||
|
||||
function circumcenter(a, b, c) {
|
||||
let ad = a[0]*a[0] + a[1]*a[1],
|
||||
bd = b[0]*b[0] + b[1]*b[1],
|
||||
cd = c[0]*c[0] + c[1]*c[1];
|
||||
let D = 2 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1]));
|
||||
return [
|
||||
Math.floor(1/D * (ad * (b[1] - c[1]) + bd * (c[1] - a[1]) + cd * (a[1] - b[1]))),
|
||||
Math.floor(1/D * (ad * (c[0] - b[0]) + bd * (a[0] - c[0]) + cd * (b[0] - a[0])))
|
||||
];
|
||||
}
|
||||
|
||||
return Voronoi;
|
||||
|
||||
})));
|
||||
Loading…
Add table
Add a link
Reference in a new issue