make depressions resolve elevation change not that big

This commit is contained in:
Azgaar 2021-06-06 01:29:58 +03:00
parent ab065da5d2
commit 67235bc41e
8 changed files with 1765 additions and 1264 deletions

View file

@ -104,7 +104,7 @@ a {
}
#biomes {
stroke-width: .7;
stroke-width: 0.7;
}
#landmass {
@ -285,6 +285,12 @@ i.icon-lock {
animation: dash 80s linear backwards;
}
.arrow {
marker-end: url(#end-arrow-small);
stroke: #555;
stroke-width: 0.5;
}
@keyframes dash {
to {
stroke-dashoffset: 0;

View file

@ -3451,6 +3451,9 @@
<marker id="end-arrow" viewBox="0 -5 10 10" refX="6" markerWidth="7" markerHeight="7" orient="auto">
<path d="M0,-5L10,0L0,5" fill="#000"></path>
</marker>
<marker id="end-arrow-small" viewBox="0 -5 10 10" refX="6" markerWidth="2" markerHeight="2" orient="auto">
<path d="M0,-5L10,0L0,5" fill="#555"></path>
</marker>
<symbol id="icon-store" viewBox="0 0 616 512">
<path d="M602 118.6L537.1 15C531.3 5.7 521 0 510 0H106C95 0 84.7 5.7 78.9 15L14 118.6c-33.5 53.5-3.8 127.9 58.8 136.4 4.5.6 9.1.9 13.7.9 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18 20.1 44.3 33.1 73.8 33.1 29.6 0 55.8-13 73.8-33.1 18.1 20.1 44.3 33.1 73.8 33.1 4.7 0 9.2-.3 13.7-.9 62.8-8.4 92.6-82.8 59-136.4zM529.5 288c-10 0-19.9-1.5-29.5-3.8V384H116v-99.8c-9.6 2.2-19.5 3.8-29.5 3.8-6 0-12.1-.4-18-1.2-5.6-.8-11.1-2.1-16.4-3.6V480c0 17.7 14.3 32 32 32h448c17.7 0 32-14.3 32-32V283.2c-5.4 1.6-10.8 2.9-16.4 3.6-6.1.8-12.1 1.2-18.2 1.2z"></path>

816
main.js

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,9 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Lakes = factory());
}(this, (function () {'use strict';
typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) : typeof define === "function" && define.amd ? define(factory) : (global.Lakes = factory());
})(this, function () {
"use strict";
const setClimateData = function(h) {
const setClimateData = function (h) {
const cells = pack.cells;
const lakeOutCells = new Uint16Array(cells.i.length);
@ -17,24 +16,46 @@ const setClimateData = function(h) {
// temperature and evaporation to detect closed lakes
f.temp = f.cells < 6 ? grid.cells.temp[cells.g[f.firstCell]] : rn(d3.mean(f.shoreline.map(c => grid.cells.temp[cells.g[c]])), 1);
const height = (f.height - 18) ** heightExponentInput.value; // height in meters
const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp); // based on Penman formula, [1-11]
f.evaporation = rn(evaporation * f.cells);
// no outlet for lakes in depressed areas
if (f.closed) return;
// lake outlet cell
f.outCell = f.shoreline[d3.scan(f.shoreline, (a,b) => h[a] - h[b])];
f.outCell = f.shoreline[d3.scan(f.shoreline, (a, b) => h[a] - h[b])];
lakeOutCells[f.outCell] = f.i;
});
return lakeOutCells;
}
};
const cleanupLakeData = function() {
// get array of land cells aroound lake
const getShoreline = function (lake) {
const queue = [lake.firstCell];
const used = [queue[0]];
const landCellsAround = [];
while (queue.length) {
const q = queue.pop();
for (const c of pack.cells.c[q]) {
if (used[c]) continue;
used[c] = true;
if (pack.cells.f[c] === lake.i) queue.push(c);
if (pack.cells.h[c] >= 20) landCellsAround.push(c);
}
}
lake.shoreline = landCellsAround;
};
const cleanupLakeData = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
delete feature.river;
delete feature.enteringFlux;
delete feature.shoreline;
delete feature.outCell;
delete feature.closed;
feature.height = rn(feature.height);
const inlets = feature.inlets?.filter(r => pack.rivers.find(river => river.i === r));
@ -44,9 +65,9 @@ const cleanupLakeData = function() {
const outlet = feature.outlet && pack.rivers.find(river => river.i === feature.outlet);
if (!outlet) delete feature.outlet;
}
}
};
const defineGroup = function() {
const defineGroup = function () {
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
const lakeEl = lakes.select(`[data-f="${feature.i}"]`).node();
@ -55,23 +76,23 @@ const defineGroup = function() {
feature.group = getGroup(feature);
document.getElementById(feature.group).appendChild(lakeEl);
}
}
};
const generateName = function() {
const generateName = function () {
Math.random = aleaPRNG(seed);
for (const feature of pack.features) {
if (feature.type !== "lake") continue;
feature.name = getName(feature);
}
}
};
const getName = function(feature) {
const getName = function (feature) {
const landCell = pack.cells.c[feature.firstCell].find(c => pack.cells.h[c] >= 20);
const culture = pack.cells.culture[landCell];
return Names.getCulture(culture);
}
};
function getGroup(feature) {
function getGroup(feature) {
if (feature.temp < -3) return "frozen";
if (feature.height > 60 && feature.cells < 10 && feature.firstCell % 5 === 0) return "lava";
@ -83,8 +104,7 @@ function getGroup(feature) {
if (!feature.outlet && feature.evaporation > feature.flux) return "salt";
return "freshwater";
}
}
return {setClimateData, cleanupLakeData, defineGroup, generateName, getName};
})));
return {setClimateData, cleanupLakeData, defineGroup, generateName, getName, getShoreline};
});

View file

@ -1,13 +1,14 @@
(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';
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(changeHeights = true) {
TIME && console.time('generateRivers');
const generate = function (changeHeights = true) {
TIME && console.time("generateRivers");
Math.random = aleaPRNG(seed);
const cells = pack.cells, p = cells.p, features = pack.features;
const cells = pack.cells,
p = cells.p,
features = pack.features;
const riversData = []; // rivers data
cells.fl = new Uint16Array(cells.i.length); // water flux array
@ -16,44 +17,38 @@ const generate = function(changeHeights = true) {
let riverNext = 1; // first river id is 1
const h = alterHeights();
removeStoredLakeData();
resolveDepressions(h);
prepareLakeData();
resolveDepressions(h, 200);
drainWater();
defineRivers();
Lakes.cleanupLakeData();
if (changeHeights) cells.h = Uint8Array.from(h); // apply changed heights as basic one
TIME && console.timeEnd('generateRivers');
TIME && console.timeEnd("generateRivers");
// height with added t value to make map less depressed
function alterHeights() {
const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
return h;
}
function removeStoredLakeData() {
function prepareLakeData() {
features.forEach(f => {
if (f.type !== "lake") return;
delete f.flux;
delete f.inlets;
delete f.outlet;
delete f.height;
!f.shoreline && Lakes.getShoreline(f);
});
}
function drainWater() {
const MIN_FLUX_TO_FORM_RIVER = 30;
const land = cells.i.filter(i => h[i] >= 20).sort((a,b) => h[b] - h[a]);
const land = cells.i.filter(i => h[i] >= 20).sort((a, b) => h[b] - h[a]);
const lakeOutCells = Lakes.setClimateData(h);
land.forEach(function(i) {
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];
const [x, y] = p[i];
// create lake outlet if flux > evaporation
const lakes = !lakeOutCells[i] ? [] : features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation);
// create lake outlet if lake is not in deep depression and flux > evaporation
const lakes = lakeOutCells[i] ? features.filter(feature => i === feature.outCell && feature.flux > feature.evaporation) : [];
for (const lake of lakes) {
const lakeCell = cells.c[i].find(c => h[c] < 20 && cells.f[c] === lake.i);
@ -78,25 +73,23 @@ const generate = function(changeHeights = true) {
// assign all tributary rivers to outlet basin
for (let outlet = lakes[0]?.outlet, l = 0; l < lakes.length; l++) {
lakes[l].inlets?.forEach(fork => riversData.find(r => r.river === fork).parent = outlet);
lakes[l].inlets?.forEach(fork => (riversData.find(r => r.river === fork).parent = outlet));
}
// near-border cell: pour water out of the screen
if (cells.b[i] && cells.r[i]) {
const to = [];
let 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;}
if (min === y) to = [x, 0];
else if (min === graphHeight - y) to = [x, graphHeight];
else if (min === x) to = [0, y];
else if (min === graphWidth - x) to = [graphWidth, y];
riversData.push({river: cells.r[i], cell: i, x: to[0], y: to[1], flux: cells.fl[i]});
return;
}
// downhill cell (make sure it's not in the source lake)
const min = lakeOutCells[i]
? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0]
: cells.c[i].sort((a, b) => h[a] - h[b])[0];
const min = lakeOutCells[i] ? cells.c[i].filter(c => !lakes.map(lake => lake.i).includes(cells.f[c])).sort((a, b) => h[a] - h[b])[0] : cells.c[i].sort((a, b) => h[a] - h[b])[0];
if (cells.fl[i] < MIN_FLUX_TO_FORM_RIVER) {
if (h[min] >= 20) cells.fl[min] += cells.fl[i];
@ -139,7 +132,7 @@ const generate = function(changeHeights = true) {
waterBody.enteringFlux = fromFlux;
}
waterBody.flux = waterBody.flux + fromFlux;
waterBody.inlets ? waterBody.inlets.push(river) : waterBody.inlets = [river];
waterBody.inlets ? waterBody.inlets.push(river) : (waterBody.inlets = [river]);
}
} else {
// propagate flux and add next river segment
@ -165,77 +158,111 @@ const generate = function(changeHeights = true) {
}
const source = riverSegments[0].cell;
const mouth = riverSegments[riverSegments.length-2].cell;
const mouth = riverSegments[riverSegments.length - 2].cell;
const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2]
const sourceWidth = cells.h[source] >= 20 ? .1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** .4, .5), 1.7), 2);
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = cells.h[source] >= 20 ? 0.1 : rn(Math.min(Math.max((cells.fl[source] / 500) ** 0.4, 0.5), 1.7), 2);
const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, .5);
const riverMeandered = addMeandering(riverSegments, sourceWidth * 10, 0.5);
const [path, length, offset] = getPath(riverMeandered, widthFactor, sourceWidth);
riverPaths.push([path, r]);
const parent = riverSegments[0].parent || 0;
const width = rn(offset ** 2, 2); // mounth width in km
const discharge = last(riverSegments).flux; // in m3/s
pack.rivers.push({i:r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent});
pack.rivers.push({i: r, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent});
}
// draw rivers
rivers.html(riverPaths.map(d => `<path id="river${d[1]}" d="${d[0]}"/>`).join(""));
}
}
};
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function(h) {
// add distance to water value to land cells to make map less depressed
function alterHeights() {
const cells = pack.cells;
return Array.from(cells.h).map((h, i) => {
if (h < 20 || cells.t[i] < 1) return h;
return h + cells.t[i] / 100 + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000;
});
}
// depression filling algorithm (for a correct water flux modeling)
const resolveDepressions = function (h, maxIterations) {
const {cells, features} = pack;
const ITERATIONS = 150;
const height = i => features[cells.f[i]].height || h[i]; // height of lake or specific cell
const lakes = features.filter(f => f.type === "lake");
lakes.forEach(l => {
const uniqueCells = new Set();
l.vertices.forEach(v => pack.vertices.c[v].forEach(c => cells.h[c] >= 20 && uniqueCells.add(c)));
l.shoreline = [...uniqueCells];
});
const land = cells.i.filter(i => h[i] >= 20 && !cells.b[i]); // exclude near-border cells
land.sort((a,b) => h[b] - h[a]); // highest cells go first
land.sort((a, b) => h[b] - h[a]); // highest cells go first
const progress = [];
let depressions = Infinity;
for (let l = 0; depressions && l < ITERATIONS; l++) {
let prevDepressions = null;
for (let iteration = 0; depressions && iteration < maxIterations; iteration++) {
if (progress.length > 5 && d3.sum(progress) > 0) {
// bad progress, abort and set heights back
h = alterHeights();
depressions = progress[0];
break;
}
depressions = 0;
if (iteration < 180) {
for (const l of lakes) {
if (l.closed) continue;
const minHeight = d3.min(l.shoreline.map(s => h[s]));
if (minHeight >= 100 || l.height > minHeight) continue;
l.height = minHeight + 1;
if (iteration > 150) {
l.shoreline.forEach(i => (h[i] = cells.h[i]));
l.height = d3.min(l.shoreline.map(s => h[s])) - 1;
l.closed = true;
continue;
}
depressions++;
l.height = minHeight + 0.2;
}
}
for (const i of land) {
const minHeight = d3.min(cells.c[i].map(c => cells.t[c] > 0 ? h[c] : pack.features[cells.f[c]].height || h[c]));
const minHeight = d3.min(cells.c[i].map(c => height(c)));
if (minHeight >= 100 || h[i] > minHeight) continue;
h[i] = minHeight + 1;
depressions++;
}
h[i] = minHeight + 0.1;
}
depressions && ERROR && console.error("Heightmap is depressed. Issues with rivers expected. Remove depressed areas to resolve");
}
prevDepressions !== null && progress.push(depressions - prevDepressions);
prevDepressions = depressions;
}
// add more river points on 1/3 and 2/3 of length
const addMeandering = function(segments, width = 1, meandering = .5) {
if (!depressions) return;
WARN && console.warn(`Unresolved depressions: ${depressions}. Edit heightmap to fix`);
//const flow = cells.i.length < 65535 ? new Uint16Array(cells.i.length) : new Uint32Array(cells.i.length);
//flow[i] = min;
//debug.append("path").attr("class", "arrow").attr("d", `M${cells.p[i][0]},${cells.p[i][1]}L${cells.p[min][0]},${cells.p[min][1]}`);
};
// add more river points on 1/3 and 2/3 of length
const addMeandering = function (segments, width = 1, meandering = 0.5) {
const riverMeandered = []; // to store enhanced segments
for (let s = 0; s < segments.length; s++, width++) {
const sX = segments[s].x, sY = segments[s].y; // segment start coordinates
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
riverMeandered.push([sX, sY, c]);
if (s+1 === segments.length) break; // do not meander last segment
if (s + 1 === segments.length) break; // do not meander last segment
const eX = segments[s+1].x, eY = segments[s+1].y; // segment end coordinates
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 sin = Math.sin(angle),
cos = Math.cos(angle);
const meander = meandering + 1 / width + Math.random() * Math.max(meandering - width / 100, 0);
const dist2 = (eX - sX) ** 2 + (eY - sY) ** 2; // square distance between segment start and end
@ -253,53 +280,61 @@ const addMeandering = function(segments, width = 1, meandering = .5) {
const p1y = (sY + eY) / 2 + cos * meander;
riverMeandered.push([p1x, p1y]);
}
}
return riverMeandered;
}
};
const getPath = function(points, widthFactor = 1, sourceWidth = .1) {
let offset, extraOffset = sourceWidth; // 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 getPath = function (points, widthFactor = 1, sourceWidth = 0.1) {
let offset,
extraOffset = sourceWidth; // 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 = 1000 + riverLength * 30;
const riverPointsLeft = [], riverPointsRight = []; // store points on both sides to build a valid polygon
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 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;
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;
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];
(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 * widthFactor) + extraOffset;
const confOffset = Math.atan(c * 5 / widening);
(sin = Math.sin(angle)), (cos = Math.cos(angle));
offset = (Math.atan(Math.pow(p * factor, 2) / widening) / 2) * widthFactor + extraOffset;
const confOffset = Math.atan((c * 5) / widening);
extraOffset += confOffset;
xLeft = x + -sin * offset, yLeft = y + cos * (offset + confOffset);
(xLeft = x + -sin * offset), (yLeft = y + cos * (offset + confOffset));
riverPointsLeft.push([xLeft, yLeft]);
xRight = x + sin * offset, yRight = y + -cos * offset;
(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;
(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;
(xRight = x + sin * offset), (yRight = y + -cos * offset);
riverPointsRight.unshift([xRight, yRight]);
// generate polygon path and return
@ -308,33 +343,33 @@ const getPath = function(points, widthFactor = 1, sourceWidth = .1) {
let left = lineGen(riverPointsLeft);
left = left.substring(left.indexOf("C"));
return [round(right + left, 2), rn(riverLength, 2), offset];
}
};
const specify = function() {
const specify = function () {
const rivers = pack.rivers;
if (!rivers.length) return;
Math.random = aleaPRNG(seed);
const tresholdElement = Math.ceil(rivers.length * .15);
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a-b)[tresholdElement];
const smallType = {"Creek":9, "River":3, "Brook":3, "Stream":1}; // weighted small river types
const tresholdElement = Math.ceil(rivers.length * 0.15);
const smallLength = rivers.map(r => r.length || 0).sort((a, b) => a - b)[tresholdElement];
const smallType = {Creek: 9, River: 3, Brook: 3, Stream: 1}; // weighted small river types
for (const r of rivers) {
r.basin = getBasin(r.i);
r.name = getName(r.mouth);
const small = r.length < smallLength;
r.type = r.parent && !(r.i%6) ? small ? "Branch" : "Fork" : small ? rw(smallType) : "River";
r.type = r.parent && !(r.i % 6) ? (small ? "Branch" : "Fork") : small ? rw(smallType) : "River";
}
}
};
const getName = function(cell) {
const getName = function (cell) {
return Names.getCulture(pack.cells.culture[cell]);
}
};
// remove river and all its tributaries
const remove = function(id) {
// remove river and all its tributaries
const remove = function (id) {
const cells = pack.cells;
const riversToRemove = pack.rivers.filter(r => r.i === id || r.parent === id || r.basin === id).map(r => r.i);
riversToRemove.forEach(r => rivers.select("#river"+r).remove());
riversToRemove.forEach(r => rivers.select("#river" + r).remove());
cells.r.forEach((r, i) => {
if (!r || !riversToRemove.includes(r)) return;
cells.r[i] = 0;
@ -342,14 +377,13 @@ const remove = function(id) {
cells.conf[i] = 0;
});
pack.rivers = pack.rivers.filter(r => !riversToRemove.includes(r.i));
}
};
const getBasin = function(r) {
const getBasin = function (r) {
const parent = pack.rivers.find(river => river.i === r)?.parent;
if (!parent || r === parent) return r;
return getBasin(parent);
}
};
return {generate, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
})));
return {generate, resolveDepressions, addMeandering, getPath, specify, getName, getBasin, remove};
});

View file

@ -27,19 +27,19 @@ async function savePNG() {
const img = new Image();
img.src = url;
img.onload = function() {
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
link.download = getFileName() + ".png";
canvas.toBlob(function(blob) {
canvas.toBlob(function (blob) {
link.href = window.URL.createObjectURL(blob);
link.click();
window.setTimeout(function() {
window.setTimeout(function () {
canvas.remove();
window.URL.revokeObjectURL(link.href);
tip(`${link.download} is saved. Open "Downloads" screen (crtl + J) to check. You can set image scale in options`, true, "success", 5000);
}, 1000);
});
}
};
TIME && console.timeEnd("savePNG");
}
@ -55,9 +55,9 @@ async function saveJPEG() {
const img = new Image();
img.src = url;
img.onload = async function() {
img.onload = async function () {
canvas.getContext("2d").drawImage(img, 0, 0, canvas.width, canvas.height);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), .92);
const quality = Math.min(rn(1 - pngResolutionInput.value / 20, 2), 0.92);
const URL = await canvas.toDataURL("image/jpeg", quality);
const link = document.createElement("a");
link.download = getFileName() + ".jpeg";
@ -65,7 +65,7 @@ async function saveJPEG() {
link.click();
tip(`${link.download} is saved. Open "Downloads" screen (CTRL + J) to check`, true, "success", 7000);
window.setTimeout(() => window.URL.revokeObjectURL(URL), 5000);
}
};
TIME && console.timeEnd("saveJPEG");
}
@ -81,7 +81,7 @@ async function getMapURL(type, subtype) {
const cloneDefs = cloneEl.getElementsByTagName("defs")[0];
const svgDefs = document.getElementById("defElements");
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (isFirefox && type === "mesh") clone.select("#oceanPattern").remove();
if (subtype === "globe") clone.select("#scaleBar").remove();
if (subtype === "noWater") {
@ -99,32 +99,35 @@ async function getMapURL(type, subtype) {
// remove unused filters
const filters = cloneEl.querySelectorAll("filter");
for (let i=0; i < filters.length; i++) {
for (let i = 0; i < filters.length; i++) {
const id = filters[i].id;
if (cloneEl.querySelector("[filter='url(#"+id+")']")) continue;
if (cloneEl.getAttribute("filter") === "url(#"+id+")") continue;
if (cloneEl.querySelector("[filter='url(#" + id + ")']")) continue;
if (cloneEl.getAttribute("filter") === "url(#" + id + ")") continue;
filters[i].remove();
}
// remove unused patterns
const patterns = cloneEl.querySelectorAll("pattern");
for (let i=0; i < patterns.length; i++) {
for (let i = 0; i < patterns.length; i++) {
const id = patterns[i].id;
if (cloneEl.querySelector("[fill='url(#"+id+")']")) continue;
if (cloneEl.querySelector("[fill='url(#" + id + ")']")) continue;
patterns[i].remove();
}
// remove unused symbols
const symbols = cloneEl.querySelectorAll("symbol");
for (let i=0; i < symbols.length; i++) {
for (let i = 0; i < symbols.length; i++) {
const id = symbols[i].id;
if (cloneEl.querySelector("use[*|href='#"+id+"']")) continue;
if (cloneEl.querySelector("use[*|href='#" + id + "']")) continue;
symbols[i].remove();
}
// add displayed emblems
if (layerIsOn("toggleEmblems") && emblems.selectAll("use").size()) {
cloneEl.getElementById("emblems")?.querySelectorAll("use").forEach(el => {
cloneEl
.getElementById("emblems")
?.querySelectorAll("use")
.forEach(el => {
const href = el.getAttribute("href") || el.getAttribute("xlink:href");
if (!href) return;
const emblem = document.getElementById(href.slice(1));
@ -150,7 +153,7 @@ async function getMapURL(type, subtype) {
if (cloneEl.getElementById("terrain")) {
const uniqueElements = new Set();
const terrainNodes = cloneEl.getElementById("terrain").childNodes;
for (let i=0; i < terrainNodes.length; i++) {
for (let i = 0; i < terrainNodes.length; i++) {
const href = terrainNodes[i].getAttribute("href") || terrainNodes[i].getAttribute("xlink:href");
uniqueElements.add(href);
}
@ -177,7 +180,7 @@ async function getMapURL(type, subtype) {
// add grid pattern
if (cloneEl.getElementById("gridOverlay")?.hasChildNodes()) {
const type = cloneEl.getElementById("gridOverlay").getAttribute("type");
const pattern = svgDefs.getElementById("pattern_"+type);
const pattern = svgDefs.getElementById("pattern_" + type);
if (pattern) cloneDefs.appendChild(pattern.cloneNode(true));
}
@ -190,11 +193,11 @@ async function getMapURL(type, subtype) {
if (cloneEl.getElementById("armies")) cloneEl.insertAdjacentHTML("afterbegin", "<style>#armies text {stroke: none; fill: #fff; text-shadow: 0 0 4px #000; dominant-baseline: central; text-anchor: middle; font-family: Helvetica; fill-opacity: 1;}#armies text.regimentIcon {font-size: .8em;}</style>");
const fontStyle = await GFontToDataURI(getFontsToLoad(clone)); // load non-standard fonts
if (fontStyle) clone.select("defs").append("style").text(fontStyle.join('\n')); // add font to style
if (fontStyle) clone.select("defs").append("style").text(fontStyle.join("\n")); // add font to style
clone.remove();
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + (new XMLSerializer()).serializeToString(cloneEl);
const blob = new Blob([serialized], {type: 'image/svg+xml;charset=utf-8'});
const serialized = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>` + new XMLSerializer().serializeToString(cloneEl);
const blob = new Blob([serialized], {type: "image/svg+xml;charset=utf-8"});
const url = window.URL.createObjectURL(blob);
window.setTimeout(() => window.URL.revokeObjectURL(url), 5000);
return url;
@ -205,10 +208,13 @@ function removeUnusedElements(clone) {
if (!terrain.selectAll("use").size()) clone.select("#defs-relief").remove();
if (markers.style("display") === "none") clone.select("#defs-markers").remove();
for (let empty = 1; empty;) {
for (let empty = 1; empty; ) {
empty = 0;
clone.selectAll("g").each(function() {
if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) {empty++; this.remove();}
clone.selectAll("g").each(function () {
if (!this.hasChildNodes() || this.style.display === "none" || this.classList.contains("hidden")) {
empty++;
this.remove();
}
if (this.hasAttribute("display") && this.style.display === "inline") this.removeAttribute("display");
});
}
@ -218,8 +224,14 @@ function updateMeshCells(clone) {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
clone.select("#heights").attr("filter", "url(#blur1)");
clone.select("#heights").selectAll("polygon").data(data).join("polygon").attr("points", d => getGridPolygon(d))
.attr("id", d => "cell"+d).attr("stroke", d => getColor(grid.cells.h[d], scheme));
clone
.select("#heights")
.selectAll("polygon")
.data(data)
.join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + d)
.attr("stroke", d => getColor(grid.cells.h[d], scheme));
}
// for each g element get inline style
@ -227,11 +239,11 @@ function inlineStyle(clone) {
const emptyG = clone.append("g").node();
const defaultStyles = window.getComputedStyle(emptyG);
clone.selectAll("g, #ruler *, #scaleBar > text").each(function() {
clone.selectAll("g, #ruler *, #scaleBar > text").each(function () {
const compStyle = window.getComputedStyle(this);
let style = "";
for (let i=0; i < compStyle.length; i++) {
for (let i = 0; i < compStyle.length; i++) {
const key = compStyle[i];
const value = compStyle.getPropertyValue(key);
@ -244,7 +256,7 @@ function inlineStyle(clone) {
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 + ';';
style += key + ":" + value + ";";
}
for (const key in compStyle) {
@ -253,10 +265,10 @@ function inlineStyle(clone) {
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 + ';';
style += key + ":" + value + ";";
}
if (style != "") this.setAttribute('style', style);
if (style != "") this.setAttribute("style", style);
});
emptyG.remove();
@ -267,7 +279,7 @@ function getFontsToLoad(clone) {
const webSafe = ["Georgia", "Times+New+Roman", "Comic+Sans+MS", "Lucida+Sans+Unicode", "Courier+New", "Verdana", "Arial", "Impact"]; // fonts to not fetch
const fontsInUse = new Set(); // to store fonts currently in use
clone.selectAll("#labels > g").each(function() {
clone.selectAll("#labels > g").each(function () {
if (!this.hasChildNodes()) return;
const font = this.dataset.font;
if (!font || webSafe.includes(font)) return;
@ -285,16 +297,16 @@ 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');
let s = document.createElement("style");
s.innerHTML = text;
document.head.appendChild(s);
const styleSheet = Array.prototype.filter.call(document.styleSheets, sS => sS.ownerNode === s)[0];
const FontRule = rule => {
const src = rule.style.getPropertyValue('src');
const url = src ? src.split('url(')[1].split(')')[0] : "";
const src = rule.style.getPropertyValue("src");
const url = src ? src.split("url(")[1].split(")")[0] : "";
return {rule, src, url: url.substring(url.length - 1, 1)};
}
};
const fontProms = [];
for (const r of styleSheet.cssRules) {
@ -309,10 +321,10 @@ function GFontToDataURI(url) {
let f = new FileReader();
f.onload = e => resolve(f.result);
f.readAsDataURL(blob);
})
});
})
.then(dataURL => fR.rule.cssText.replace(fR.url, dataURL))
)
);
}
document.head.removeChild(s); // clean up
return Promise.all(fontProms); // wait for all this has been done
@ -328,13 +340,7 @@ function getMapData() {
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, mapId].join("|");
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value,
heightUnit.value, heightExponentInput.value, temperatureScale.value,
barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value,
barPosX.value, barPosY.value, populationRate.value, urbanization.value,
mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value,
temperaturePoleOutput.value, precOutput.value, JSON.stringify(options),
mapName.value].join("|");
const settings = [distanceUnitInput.value, distanceScaleInput.value, areaUnit.value, heightUnit.value, heightExponentInput.value, temperatureScale.value, barSize.value, barLabel.value, barBackOpacity.value, barBackColor.value, barPosX.value, barPosY.value, populationRate.value, urbanization.value, mapSizeOutput.value, latitudeOutput.value, temperatureEquatorOutput.value, temperaturePoleOutput.value, precOutput.value, JSON.stringify(options), mapName.value].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
@ -351,9 +357,9 @@ function getMapData() {
// always remove rulers
cloneEl.querySelector("#ruler").innerHTML = "";
const svg_xml = (new XMLSerializer()).serializeToString(cloneEl);
const svg_xml = new XMLSerializer().serializeToString(cloneEl);
const gridGeneral = JSON.stringify({spacing:grid.spacing, cellsX:grid.cellsX, cellsY:grid.cellsY, boundary:grid.boundary, points:grid.points, features:grid.features});
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);
@ -364,22 +370,18 @@ function getMapData() {
// store name array only if it is not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases.map((b,i) => {
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
}).join("/");
})
.join("/");
// round population to save resources
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const data = [params, settings, 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,
pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state,
pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces,
namesData, rivers, rulersString].join("\r\n");
const data = [params, settings, 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, pop, pack.cells.r, pack.cells.road, pack.cells.s, pack.cells.state, pack.cells.religion, pack.cells.province, pack.cells.crossroad, religions, provinces, namesData, rivers, rulersString].join("\r\n");
const blob = new Blob([data], {type: "text/plain"});
TIME && console.timeEnd("createMapDataBlob");
@ -389,7 +391,10 @@ function getMapData() {
// Download .map file
async function saveMap() {
if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;}
if (customization) {
tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
return;
}
closeDialogs("#alert");
const blob = await getMapData();
@ -405,8 +410,11 @@ async function saveMap() {
function saveGeoJSON_Cells() {
const json = {type: "FeatureCollection", features: []};
const cells = pack.cells;
const getPopulation = i => {const [r, u] = getCellPopulation(i); return rn(r+u)};
const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0],cells.p[i][1]]));
const getPopulation = i => {
const [r, u] = getCellPopulation(i);
return rn(r + u);
};
const getHeight = i => parseInt(getFriendlyHeight([cells.p[i][0], cells.p[i][1]]));
cells.i.forEach(i => {
const coordinates = getCellCoordinates(cells.v[i]);
@ -420,7 +428,7 @@ function saveGeoJSON_Cells() {
const religion = cells.religion[i];
const neighbors = cells.c[i];
const properties = {id:i, height, biome, type, population, state, province, culture, religion, neighbors}
const properties = {id: i, height, biome, type, population, state, province, culture, religion, neighbors};
const feature = {type: "Feature", geometry: {type: "Polygon", coordinates}, properties};
json.features.push(feature);
});
@ -432,7 +440,7 @@ function saveGeoJSON_Cells() {
function saveGeoJSON_Routes() {
const json = {type: "FeatureCollection", features: []};
routes.selectAll("g > path").each(function() {
routes.selectAll("g > path").each(function () {
const coordinates = getRoutePoints(this);
const id = this.id;
const type = this.parentElement.id;
@ -448,7 +456,7 @@ function saveGeoJSON_Routes() {
function saveGeoJSON_Rivers() {
const json = {type: "FeatureCollection", features: []};
rivers.selectAll("path").each(function() {
rivers.selectAll("path").each(function () {
const coordinates = getRiverPoints(this);
const id = this.id;
const width = +this.dataset.increment;
@ -470,10 +478,10 @@ function saveGeoJSON_Rivers() {
function saveGeoJSON_Markers() {
const json = {type: "FeatureCollection", features: []};
markers.selectAll("use").each(function() {
markers.selectAll("use").each(function () {
const coordinates = getQGIScoordinates(this.dataset.x, this.dataset.y);
const id = this.id;
const type = (this.dataset.id).substring(1);
const type = this.dataset.id.substring(1);
const icon = document.getElementById(type).textContent;
const note = notes.length ? notes.find(note => note.id === this.id) : null;
const name = note ? note.name : "";
@ -497,7 +505,7 @@ function getRoutePoints(node) {
let points = [];
const l = node.getTotalLength();
const increment = l / Math.ceil(l / 2);
for (let i=0; i <= l; i += increment) {
for (let i = 0; i <= l; i += increment) {
const p = node.getPointAtLength(i);
points.push(getQGIScoordinates(p.x, p.y));
}
@ -508,17 +516,20 @@ function getRiverPoints(node) {
let points = [];
const l = node.getTotalLength() / 2; // half-length
const increment = 0.25; // defines density of points
for (let i=l, c=i; i >= 0; i -= increment, c += increment) {
for (let i = l, c = i; i >= 0; i -= increment, c += increment) {
const p1 = node.getPointAtLength(i);
const p2 = node.getPointAtLength(c);
const [x, y] = getQGIScoordinates((p1.x + p2.x) / 2, (p1.y + p2.y) / 2);
points.push([x,y]);
points.push([x, y]);
}
return points;
}
async function quickSave() {
if (customization) {tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error"); return;}
if (customization) {
tip("Map cannot be saved when edit mode is active, please exit the mode and retry", false, "error");
return;
}
const blob = await getMapData();
if (blob) ldb.set("lastMap", blob); // auto-save map
tip("Map is saved to browser memory. Please also save as .map file to secure progress", true, "success", 2000);
@ -537,14 +548,24 @@ function quickLoad() {
function loadMapPrompt(blob) {
const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
if (workingTime < 5) {loadLastSavedMap(); return;}
if (workingTime < 5) {
loadLastSavedMap();
return;
}
alertMessage.innerHTML = `Are you sure you want to load saved map?<br>
All unsaved changes made to the current map will be lost`;
$("#alert").dialog({resizable: false, title: "Load saved map",
$("#alert").dialog({
resizable: false,
title: "Load saved map",
buttons: {
Cancel: function() {$(this).dialog("close");},
Load: function() {loadLastSavedMap(); $(this).dialog("close");}
Cancel: function () {
$(this).dialog("close");
},
Load: function () {
loadLastSavedMap();
$(this).dialog("close");
}
}
});
@ -552,31 +573,23 @@ function loadMapPrompt(blob) {
WARN && console.warn("Load last saved map");
try {
uploadMap(blob);
}
catch(error) {
} catch (error) {
ERROR && console.error(error);
tip("Cannot load last saved map", true, "error", 2000);
}
}
}
const saveReminder = function() {
const saveReminder = function () {
if (localStorage.getItem("noReminder")) return;
const message = ["Please don't forget to save your work as a .map file",
"Please remember to save work as a .map file",
"Saving in .map format will ensure your data won't be lost in case of issues",
"Safety is number one priority. Please save the map",
"Don't forget to save your map on a regular basis!",
"Just a gentle reminder for you to save the map",
"Please don't forget to save your progress (saving as .map is the best option)",
"Don't want to be reminded about need to save? Press CTRL+Q"];
const message = ["Please don't forget to save your work as a .map file", "Please remember to save work as a .map file", "Saving in .map format will ensure your data won't be lost in case of issues", "Safety is number one priority. Please save the map", "Don't forget to save your map on a regular basis!", "Just a gentle reminder for you to save the map", "Please don't forget to save your progress (saving as .map is the best option)", "Don't want to be reminded about need to save? Press CTRL+Q"];
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, "warn", 2500);
}, 1e6);
saveReminder.status = 1;
}
};
saveReminder();
@ -597,7 +610,7 @@ function uploadMap(file, callback) {
uploadMap.timeStart = performance.now();
const fileReader = new FileReader();
fileReader.onload = function(fileLoadedEvent) {
fileReader.onload = function (fileLoadedEvent) {
if (callback) callback();
document.getElementById("coas").innerHTML = ""; // remove auto-generated emblems
@ -605,11 +618,15 @@ function uploadMap(file, callback) {
const data = dataLoaded.split("\r\n");
const mapVersion = data[0].split("|")[0] || data[0];
if (mapVersion === version) {parseLoadedData(data); return;}
if (mapVersion === version) {
parseLoadedData(data);
return;
}
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
const parsed = parseFloat(mapVersion);
let message = "", load = false;
let message = "",
load = false;
if (isNaN(parsed) || data.length < 26 || !data[5]) {
message = `The file you are trying to load is outdated or not a valid .map file.
<br>Please try to open it using an ${archive}`;
@ -622,9 +639,16 @@ function uploadMap(file, callback) {
<br>Click OK to get map <b>auto-updated</b>. In case of issues please keep using an ${archive} of the Generator`;
}
alertMessage.innerHTML = message;
$("#alert").dialog({title: "Version conflict", width: "38em", buttons: {
OK: function() {$(this).dialog("close"); if (load) parseLoadedData(data);}
}});
$("#alert").dialog({
title: "Version conflict",
width: "38em",
buttons: {
OK: function () {
$(this).dialog("close");
if (load) parseLoadedData(data);
}
}
});
};
fileReader.readAsText(file, "UTF-8");
@ -640,17 +664,20 @@ function parseLoadedData(data) {
const reliefIcons = document.getElementById("defs-relief").innerHTML; // save relief icons
const hatching = document.getElementById("hatching").cloneNode(true); // save hatching
void function parseParameters() {
void (function parseParameters() {
const params = data[0].split("|");
if (params[3]) {seed = params[3]; optionsSeed.value = seed;}
if (params[3]) {
seed = params[3];
optionsSeed.value = seed;
}
if (params[4]) graphWidth = +params[4];
if (params[5]) graphHeight = +params[5];
mapId = params[6] ? +params[6] : Date.now();
}()
})();
INFO && console.group("Loaded Map " + seed);
void function parseSettings() {
void (function parseSettings() {
const settings = data[1].split("|");
if (settings[0]) applyOption(distanceUnitInput, settings[0]);
if (settings[1]) distanceScaleInput.value = distanceScaleOutput.value = settings[1];
@ -673,9 +700,9 @@ function parseLoadedData(data) {
if (settings[18]) precInput.value = precOutput.value = settings[18];
if (settings[19]) options = JSON.parse(settings[19]);
if (settings[20]) mapName.value = settings[20];
}()
})();
void function parseConfiguration() {
void (function parseConfiguration() {
if (data[2]) mapCoordinates = JSON.parse(data[2]);
if (data[4]) notes = JSON.parse(data[4]);
if (data[33]) rulers.fromString(data[33]);
@ -687,20 +714,20 @@ function parseLoadedData(data) {
biomesData.name = biomes[2].split(",");
// push custom biomes if any
for (let i=biomesData.i.length; i < biomesData.name.length; i++) {
for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
biomesData.i.push(biomesData.i.length);
biomesData.iconsDensity.push(0);
biomesData.icons.push([]);
biomesData.cost.push(50);
}
}()
})();
void function replaceSVG() {
void (function replaceSVG() {
svg.remove();
document.body.insertAdjacentHTML("afterbegin", data[5]);
}()
})();
void function redefineElements() {
void (function redefineElements() {
svg = d3.select("#map");
defs = svg.select("#deftemp");
viewbox = svg.select("#viewbox");
@ -750,9 +777,9 @@ function parseLoadedData(data) {
fogging = viewbox.select("#fogging");
debug = viewbox.select("#debug");
burgLabels = labels.select("#burgLabels");
}()
})();
void function parseGridData() {
void (function parseGridData() {
grid = JSON.parse(data[6]);
calculateVoronoi(grid, grid.points);
grid.cells.h = Uint8Array.from(data[7].split(","));
@ -760,9 +787,9 @@ function parseLoadedData(data) {
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() {
void (function parsePackData() {
pack = {};
reGraph();
reMarkFeatures();
@ -795,19 +822,22 @@ function parseLoadedData(data) {
const e = d.split("|");
if (!e.length) return;
const b = e[5].split(",").length > 2 || !nameBases[i] ? e[5] : nameBases[i].b;
nameBases[i] = {name:e[0], min:e[1], max:e[2], d:e[3], m:e[4], b};
nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
});
}
}()
})();
const notHidden = selection => selection.node() && selection.style("display") !== "none";
const hasChildren = selection => selection.node()?.hasChildNodes();
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
const turnOn = el => document.getElementById(el).classList.remove("buttonoff");
void function restoreLayersState() {
void (function restoreLayersState() {
// turn all layers off
document.getElementById("mapLayers").querySelectorAll("li").forEach(el => el.classList.add("buttonoff"));
document
.getElementById("mapLayers")
.querySelectorAll("li")
.forEach(el => el.classList.add("buttonoff"));
// turn on active layers
if (notHidden(texture) && hasChild(texture, "image")) turnOn("toggleTexture");
@ -839,14 +869,14 @@ function parseLoadedData(data) {
if (notHidden(scaleBar)) turnOn("toggleScaleBar");
getCurrentPreset();
}()
})();
void function restoreEvents() {
void (function restoreEvents() {
scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
legend.on("mousemove", () => tip("Drag to change the position. Click to hide the legend")).on("click", () => clearLegend());
}()
})();
void function resolveVersionConflicts() {
void (function resolveVersionConflicts() {
const version = parseFloat(data[0].split("|")[0]);
if (version < 0.9) {
// 0.9 has additional relief icons to be included into older maps
@ -860,19 +890,17 @@ function parseLoadedData(data) {
// 1.0 adds a legend box
legend = svg.append("g").attr("id", "legend");
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93)
.attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
legend.attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 13).attr("data-size", 13).attr("data-x", 99).attr("data-y", 93).attr("stroke-width", 2.5).attr("stroke", "#812929").attr("stroke-dasharray", "0 4 10 4").attr("stroke-linecap", "round");
// 1.0 separated drawBorders fron drawStates()
stateBorders = borders.append("g").attr("id", "stateBorders");
provinceBorders = borders.append("g").attr("id", "provinceBorders");
borders.attr("opacity", null).attr("stroke", null).attr("stroke-width", null).attr("stroke-dasharray", null).attr("stroke-linecap", null).attr("filter", null);
stateBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", .8).attr("stroke", "#56566d").attr("stroke-width", .5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
stateBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 1).attr("stroke-dasharray", "2").attr("stroke-linecap", "butt");
provinceBorders.attr("opacity", 0.8).attr("stroke", "#56566d").attr("stroke-width", 0.5).attr("stroke-dasharray", "1").attr("stroke-linecap", "butt");
// 1.0 adds state relations, provinces, forms and full names
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", .6);
provs = viewbox.insert("g", "#borders").attr("id", "provs").attr("opacity", 0.6);
BurgsAndStates.collectStatistics();
BurgsAndStates.generateCampaigns();
BurgsAndStates.generateDiplomacy();
@ -880,7 +908,7 @@ function parseLoadedData(data) {
drawStates();
BurgsAndStates.generateProvinces();
drawBorders();
if (!layerIsOn("toggleBorders")) $('#borders').fadeOut();
if (!layerIsOn("toggleBorders")) $("#borders").fadeOut();
if (!layerIsOn("toggleStates")) regions.attr("display", "none").selectAll("path").remove();
// 1.0 adds hatching
@ -888,9 +916,12 @@ function parseLoadedData(data) {
// 1.0 adds zones layer
zones = viewbox.insert("g", "#borders").attr("id", "zones").attr("display", "none");
zones.attr("opacity", .6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
zones.attr("opacity", 0.6).attr("stroke", null).attr("stroke-width", 0).attr("stroke-dasharray", null).attr("stroke-linecap", "butt");
addZones();
if (!markers.selectAll("*").size()) {addMarkers(); turnButtonOn("toggleMarkers");}
if (!markers.selectAll("*").size()) {
addMarkers();
turnButtonOn("toggleMarkers");
}
// 1.0 add fogging layer (state focus)
fogging = viewbox.insert("g", "#ruler").attr("id", "fogging-cont").attr("mask", "url(#fog)").append("g").attr("id", "fogging").style("display", "none");
@ -904,7 +935,7 @@ function parseLoadedData(data) {
}
// 1.0 changed labels to multi-lined
labels.selectAll("textPath").each(function() {
labels.selectAll("textPath").each(function () {
const text = this.textContent;
const shift = this.getComputedTextLength() / -1.5;
this.innerHTML = `<tspan x="${shift}">${text}</tspan>`;
@ -923,7 +954,7 @@ function parseLoadedData(data) {
// v 1.0 initially has Sympathy status then relaced with Friendly
for (const s of pack.states) {
if (!s.diplomacy) continue;
s.diplomacy = s.diplomacy.map(r => r === "Sympathy" ? "Friendly" : r);
s.diplomacy = s.diplomacy.map(r => (r === "Sympathy" ? "Friendly" : r));
}
// labels should be toggled via style attribute, so remove display attribute
@ -931,7 +962,9 @@ function parseLoadedData(data) {
// v 1.0 added religions heirarchy tree
if (pack.religions[1] && !pack.religions[1].code) {
pack.religions.filter(r => r.i).forEach(r => {
pack.religions
.filter(r => r.i)
.forEach(r => {
r.origin = 0;
r.code = r.name.slice(0, 2);
});
@ -939,12 +972,12 @@ function parseLoadedData(data) {
if (!document.getElementById("freshwater")) {
lakes.append("g").attr("id", "freshwater");
lakes.select("#freshwater").attr("opacity", .5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", .7).attr("filter", null);
lakes.select("#freshwater").attr("opacity", 0.5).attr("fill", "#a6c1fd").attr("stroke", "#5f799d").attr("stroke-width", 0.7).attr("filter", null);
}
if (!document.getElementById("salt")) {
lakes.append("g").attr("id", "salt");
lakes.select("#salt").attr("opacity", .5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", .7).attr("filter", null);
lakes.select("#salt").attr("opacity", 0.5).attr("fill", "#409b8a").attr("stroke", "#388985").attr("stroke-width", 0.7).attr("filter", null);
}
// v 1.1 added new lake and coast groups
@ -952,14 +985,14 @@ function parseLoadedData(data) {
lakes.append("g").attr("id", "sinkhole");
lakes.append("g").attr("id", "frozen");
lakes.append("g").attr("id", "lava");
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", .7).attr("filter", null);
lakes.select("#frozen").attr("opacity", .95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
lakes.select("#lava").attr("opacity", .7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
lakes.select("#sinkhole").attr("opacity", 1).attr("fill", "#5bc9fd").attr("stroke", "#53a3b0").attr("stroke-width", 0.7).attr("filter", null);
lakes.select("#frozen").attr("opacity", 0.95).attr("fill", "#cdd4e7").attr("stroke", "#cfe0eb").attr("stroke-width", 0).attr("filter", null);
lakes.select("#lava").attr("opacity", 0.7).attr("fill", "#90270d").attr("stroke", "#f93e0c").attr("stroke-width", 2).attr("filter", "url(#crumpled)");
coastline.append("g").attr("id", "sea_island");
coastline.append("g").attr("id", "lake_island");
coastline.select("#sea_island").attr("opacity", .5).attr("stroke", "#1f3846").attr("stroke-width", .7).attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", .35).attr("filter", null);
coastline.select("#sea_island").attr("opacity", 0.5).attr("stroke", "#1f3846").attr("stroke-width", 0.7).attr("filter", "url(#dropShadow)");
coastline.select("#lake_island").attr("opacity", 1).attr("stroke", "#7c8eaf").attr("stroke-width", 0.35).attr("filter", null);
}
// v 1.1 features stores more data
@ -979,7 +1012,9 @@ function parseLoadedData(data) {
// v 1.11 added cultures heirarchy tree
if (pack.cultures[1] && !pack.cultures[1].code) {
pack.cultures.filter(c => c.i).forEach(c => {
pack.cultures
.filter(c => c.i)
.forEach(c => {
c.origin = 0;
c.code = c.name.slice(0, 2);
});
@ -991,12 +1026,12 @@ function parseLoadedData(data) {
// v 1.2 added new terrain attributes
if (!terrain.attr("set")) terrain.attr("set", "simple");
if (!terrain.attr("size")) terrain.attr("size", 1);
if (!terrain.attr("density")) terrain.attr("density", .4);
if (!terrain.attr("density")) terrain.attr("density", 0.4);
}
if (version < 1.21) {
// v 1.11 replaced "display" attribute by "display" style
viewbox.selectAll("g").each(function() {
viewbox.selectAll("g").each(function () {
if (this.hasAttribute("display")) {
this.removeAttribute("display");
this.style.display = "none";
@ -1005,16 +1040,17 @@ function parseLoadedData(data) {
// v 1.21 added rivers data to pack
pack.rivers = []; // rivers data
rivers.selectAll("path").each(function() {
rivers.selectAll("path").each(function () {
const i = +this.id.slice(5);
const length = this.getTotalLength() / 2;
const s = this.getPointAtLength(length), e = this.getPointAtLength(0);
const source = findCell(s.x, s.y), mouth = findCell(e.x, e.y);
const s = this.getPointAtLength(length),
e = this.getPointAtLength(0);
const source = findCell(s.x, s.y),
mouth = findCell(e.x, e.y);
const name = Rivers.getName(mouth);
const type = length < 25 ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
pack.rivers.push({i, parent:0, length, source, mouth, basin:i, name, type});
const type = length < 25 ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
pack.rivers.push({i, parent: 0, length, source, mouth, basin: i, name, type});
});
}
if (version < 1.22) {
@ -1026,7 +1062,7 @@ function parseLoadedData(data) {
// v 1.3 added global options object
const winds = options.slice(); // previostly wind was saved in settings[19]
const year = rand(100, 2000);
const era = Names.getBaseShort(P(.7) ? 1 : rand(nameBases.length)) + " Era";
const era = Names.getBaseShort(P(0.7) ? 1 : rand(nameBases.length)) + " Era";
const eraShort = era[0] + "E";
const military = Military.getDefaultOptions();
options = {winds, year, era, eraShort, military};
@ -1036,7 +1072,7 @@ function parseLoadedData(data) {
// v 1.3 added militry layer
armies = viewbox.insert("g", "#icons").attr("id", "armies");
armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", .3);
armies.attr("opacity", 1).attr("fill-opacity", 1).attr("font-size", 6).attr("box-size", 3).attr("stroke", "#000").attr("stroke-width", 0.3);
turnButtonOn("toggleMilitary");
Military.generate();
}
@ -1045,7 +1081,7 @@ function parseLoadedData(data) {
// v 1.35 added dry lakes
if (!lakes.select("#dry").size()) {
lakes.append("g").attr("id", "dry");
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", .7).attr("filter", null);
lakes.select("#dry").attr("opacity", 1).attr("fill", "#c9bfa7").attr("stroke", "#8e816f").attr("stroke-width", 0.7).attr("filter", null);
}
// v 1.4 added ice layer
@ -1071,7 +1107,7 @@ function parseLoadedData(data) {
}
// 1.4 added state reference for regiments
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => r.state = s.i));
pack.states.filter(s => s.military).forEach(s => s.military.forEach(r => (r.state = s.i)));
}
if (version < 1.5) {
@ -1103,7 +1139,7 @@ function parseLoadedData(data) {
toggleEmblems();
// v 1.5 changed releif icons data
terrain.selectAll("use").each(function() {
terrain.selectAll("use").each(function () {
const type = this.getAttribute("data-type") || this.getAttribute("xlink:href");
this.removeAttribute("xlink:href");
this.removeAttribute("data-type");
@ -1115,14 +1151,14 @@ function parseLoadedData(data) {
if (version < 1.6) {
// v 1.6 changed rivers data
for (const river of pack.rivers) {
const el = document.getElementById("river"+river.i);
const el = document.getElementById("river" + river.i);
if (el) {
river.widthFactor = +el.getAttribute("data-width");
el.removeAttribute("data-width");
el.removeAttribute("data-increment");
river.discharge = pack.cells.fl[river.mouth] || 1;
river.width = rn(river.length / 100, 2);
river.sourceWidth = .1;
river.sourceWidth = 0.1;
} else {
Rivers.remove(river.i);
}
@ -1137,7 +1173,7 @@ function parseLoadedData(data) {
f.temp = grid.cells.temp[pack.cells.g[f.firstCell]];
f.height = f.height || d3.min(pack.cells.c[f.firstCell].map(c => pack.cells.h[c]).filter(h => h >= 20));
const height = (f.height - 18) ** heightExponentInput.value;
const evaporation = (700 * (f.temp + .006 * height) / 50 + 75) / (80 - f.temp);
const evaporation = ((700 * (f.temp + 0.006 * height)) / 50 + 75) / (80 - f.temp);
f.evaporation = rn(evaporation * f.cells);
f.name = f.name || Lakes.getName(f);
delete f.river;
@ -1149,31 +1185,34 @@ function parseLoadedData(data) {
ruler.style("display", null);
rulers = new Rulers();
ruler.selectAll(".ruler > .white").each(function() {
ruler.selectAll(".ruler > .white").each(function () {
const x1 = +this.getAttribute("x1");
const y1 = +this.getAttribute("y1");
const x2 = +this.getAttribute("x2");
const y2 = +this.getAttribute("y2");
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) return;
const points = [[x1, y1], [x2, y2]];
const points = [
[x1, y1],
[x2, y2]
];
rulers.create(Ruler, points);
});
ruler.selectAll("g.opisometer").each(function() {
ruler.selectAll("g.opisometer").each(function () {
const pointsString = this.dataset.points;
if (!pointsString) return;
const points = JSON.parse(pointsString);
rulers.create(Opisometer, points);
});
ruler.selectAll("path.planimeter").each(function() {
ruler.selectAll("path.planimeter").each(function () {
const length = this.getTotalLength();
if (length < 30) return;
const step = length > 1000 ? 40 : length > 400 ? 20 : 10;
const increment = length / Math.ceil(length / step);
const points = [];
for (let i=0; i <= length; i += increment) {
for (let i = 0; i <= length; i += increment) {
const point = this.getPointAtLength(i);
points.push([point.x | 0, point.y | 0]);
}
@ -1193,16 +1232,16 @@ function parseLoadedData(data) {
const filter = pattern.firstElementChild.getAttribute("filter");
const href = filter ? "./images/" + filter.replace("url(#", "").replace(")", "") + ".png" : "";
pattern.innerHTML = `<image id="oceanicPattern" href=${href} width="100" height="100"></image>`;
document.getElementById("oceanPattern").setAttribute("opacity", .2);
document.getElementById("oceanPattern").setAttribute("opacity", 0.2);
}
}()
})();
if (version < 1.62) {
// v 1.62 changed grid data
gridOverlay.attr("size", null);
}
void function checkDataIntegrity() {
void (function checkDataIntegrity() {
const cells = pack.cells;
if (pack.cells.i.length !== pack.cells.state.length) {
@ -1212,28 +1251,28 @@ function parseLoadedData(data) {
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
invalidStates.forEach(s => {
const invalidCells = cells.i.filter(i => cells.state[i] === s);
invalidCells.forEach(i => cells.state[i] = 0);
invalidCells.forEach(i => (cells.state[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid state", s, "is assigned to cells", invalidCells);
});
const invalidProvinces = [...new Set(cells.province)].filter(p => p && (!pack.provinces[p] || pack.provinces[p].removed));
invalidProvinces.forEach(p => {
const invalidCells = cells.i.filter(i => cells.province[i] === p);
invalidCells.forEach(i => cells.province[i] = 0);
invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid province", p, "is assigned to cells", invalidCells);
});
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
invalidCultures.forEach(c => {
const invalidCells = cells.i.filter(i => cells.culture[i] === c);
invalidCells.forEach(i => cells.province[i] = 0);
invalidCells.forEach(i => (cells.province[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid culture", c, "is assigned to cells", invalidCells);
});
const invalidReligions = [...new Set(cells.religion)].filter(r => !pack.religions[r] || pack.religions[r].removed);
invalidReligions.forEach(r => {
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
invalidCells.forEach(i => cells.religion[i] = 0);
invalidCells.forEach(i => (cells.religion[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid religion", c, "is assigned to cells", invalidCells);
});
@ -1247,26 +1286,29 @@ function parseLoadedData(data) {
const invalidBurgs = [...new Set(cells.burg)].filter(b => b && (!pack.burgs[b] || pack.burgs[b].removed));
invalidBurgs.forEach(b => {
const invalidCells = cells.i.filter(i => cells.burg[i] === b);
invalidCells.forEach(i => cells.burg[i] = 0);
invalidCells.forEach(i => (cells.burg[i] = 0));
ERROR && console.error("Data Integrity Check. Invalid burg", b, "is assigned to cells", invalidCells);
});
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
invalidRivers.forEach(r => {
const invalidCells = cells.i.filter(i => cells.r[i] === r);
invalidCells.forEach(i => cells.r[i] = 0);
rivers.select("river"+r).remove();
invalidCells.forEach(i => (cells.r[i] = 0));
rivers.select("river" + r).remove();
ERROR && console.error("Data Integrity Check. Invalid river", r, "is assigned to cells", invalidCells);
});
pack.burgs.forEach(b => {
if (!b.i || b.removed) return;
if (b.port < 0) {ERROR && console.error("Data Integrity Check. Burg", b.i, "has invalid port value", b.port); b.port = 0;}
if (b.port < 0) {
ERROR && console.error("Data Integrity Check. Burg", b.i, "has invalid port value", b.port);
b.port = 0;
}
if (b.cell >= cells.i.length) {
ERROR && console.error("Data Integrity Check. Burg", b.i, "is linked to invalid cell", b.cell);
b.cell = findCell(b.x, b.y);
cells.i.filter(i => cells.burg[i] === b.i).forEach(i => cells.burg[i] = 0);
cells.i.filter(i => cells.burg[i] === b.i).forEach(i => (cells.burg[i] = 0));
cells.burg[b.cell] = b.i;
}
@ -1282,7 +1324,7 @@ function parseLoadedData(data) {
ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state);
p.removed = true; // remove incorrect province
});
}()
})();
changeMapSize();
@ -1301,12 +1343,11 @@ function parseLoadedData(data) {
focusOn(); // based on searchParams focus on point, cell or burg
invokeActiveZooming();
WARN && console.warn(`TOTAL: ${rn((performance.now()-uploadMap.timeStart)/1000,2)}s`);
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
showStatistics();
INFO && console.groupEnd("Loaded Map " + seed);
tip("Map is successfully loaded", true, "success", 7000);
}
catch(error) {
} catch (error) {
ERROR && console.error(error);
clearMainTip();
@ -1314,12 +1355,23 @@ function parseLoadedData(data) {
<br>generate a new random map or cancel the loading
<p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false, title: "Loading error", maxWidth:"50em", buttons: {
"Select file": function() {$(this).dialog("close"); mapToLoad.click();},
"New map": function() {$(this).dialog("close"); regenerateMap();},
Cancel: function() {$(this).dialog("close")}
}, position: {my: "center", at: "center", of: "svg"}
resizable: false,
title: "Loading error",
maxWidth: "50em",
buttons: {
"Select file": function () {
$(this).dialog("close");
mapToLoad.click();
},
"New map": function () {
$(this).dialog("close");
regenerateMap();
},
Cancel: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
}
}

View file

@ -2,7 +2,7 @@
"use strict";
function editHeightmap() {
void function selectEditMode() {
void (function selectEditMode() {
alertMessage.innerHTML = `Heightmap is a core element on which all other data (rivers, burgs, states etc) is based.
So the best edit approach is to <i>erase</i> the secondary data and let the system automatically regenerate it on edit completion.
<p><i>Erase</i> mode also allows you Convert an Image into a heightmap or use Template Editor.</p>
@ -11,15 +11,26 @@ function editHeightmap() {
<p>Please <span class="pseudoLink" onclick=saveMap(); editHeightmap();>save the map</span> before editing the heightmap!</p>
<p>Check out ${link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Heightmap-customization", "wiki")} for guidance.</p>`;
$("#alert").dialog({resizable: false, title: "Edit Heightmap", width: "28em",
$("#alert").dialog({
resizable: false,
title: "Edit Heightmap",
width: "28em",
buttons: {
Erase: function() {enterHeightmapEditMode("erase");},
Keep: function() {enterHeightmapEditMode("keep");},
Risk: function() {enterHeightmapEditMode("risk");},
Cancel: function() {$(this).dialog("close");}
Erase: function () {
enterHeightmapEditMode("erase");
},
Keep: function () {
enterHeightmapEditMode("keep");
},
Risk: function () {
enterHeightmapEditMode("risk");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}()
})();
restartHistory();
viewbox.insert("g", "#terrs").attr("id", "heights");
@ -35,8 +46,8 @@ function editHeightmap() {
document.getElementById("heightmap3DView").addEventListener("click", changeViewMode);
document.getElementById("finalizeHeightmap").addEventListener("click", finalizeHeightmap);
document.getElementById("renderOcean").addEventListener("click", mockHeightmap);
document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n+1));
document.getElementById("templateUndo").addEventListener("click", () => restoreHistory(edits.n - 1));
document.getElementById("templateRedo").addEventListener("click", () => restoreHistory(edits.n + 1));
function enterHeightmapEditMode(type) {
editHeightmap.layers = Array.from(mapLayers.querySelectorAll("li:not(.buttonoff)")).map(node => node.id); // store layers preset
@ -77,9 +88,7 @@ function editHeightmap() {
exitCustomization.style.bottom = svgHeight / 2 + "px";
exitCustomization.style.transform = "scale(2)";
exitCustomization.style.display = "block";
d3.select("#exitCustomization")
.transition().duration(1000).style("opacity", 1)
.transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)");
d3.select("#exitCustomization").transition().duration(1000).style("opacity", 1).transition().duration(2000).ease(d3.easeSinInOut).style("right", "10px").style("bottom", "10px").style("transform", "scale(1)");
} else exitCustomization.style.display = "block";
openBrushesPanel();
@ -91,7 +100,8 @@ function editHeightmap() {
}
function moveCursor() {
const p = d3.mouse(this), cell = findGridCell(p[0], p[1]);
const p = d3.mouse(this),
cell = findGridCell(p[0], p[1]);
heightmapInfoX.innerHTML = rn(p[0]);
heightmapInfoY.innerHTML = rn(p[1]);
heightmapInfoCell.innerHTML = cell;
@ -108,12 +118,13 @@ function editHeightmap() {
function getHeight(h) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
if (unit === "m") unitRatio = 1; // if meter
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, +heightExponentInput.value);
else if (h < 20 && h > 0) height = (h - 20) / h * 50;
else if (h < 20 && h > 0) height = ((h - 20) / h) * 50;
return rn(height * unitRatio) + " " + unit;
}
@ -156,8 +167,12 @@ function editHeightmap() {
//viewbox.select("#heights").remove();
document.getElementById("heights").remove();
turnButtonOff("toggleHeight");
document.getElementById("mapLayers").querySelectorAll("li").forEach(function(e) {
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click(); // turn on
document
.getElementById("mapLayers")
.querySelectorAll("li")
.forEach(function (e) {
if (editHeightmap.layers.includes(e.id) && !layerIsOn(e.id)) e.click();
// turn on
else if (!editHeightmap.layers.includes(e.id) && layerIsOn(e.id)) e.click(); // turn off
});
getCurrentPreset();
@ -169,7 +184,7 @@ function editHeightmap() {
const change = changeHeights.checked;
markFeatures();
getSignedDistanceField();
markupGridOcean();
if (change) openNearSeaLakes();
OceanLayers();
calculateTemperatures();
@ -275,7 +290,7 @@ function editHeightmap() {
}
// recalculate zones to grid
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const dataCells = zone.attr("data-cells");
const cells = dataCells ? dataCells.split(",").map(i => +i) : [];
@ -285,7 +300,7 @@ function editHeightmap() {
});
markFeatures();
getSignedDistanceField();
markupGridOcean();
OceanLayers();
calculateTemperatures();
generatePrecipitation();
@ -339,14 +354,12 @@ function editHeightmap() {
}
// find closest land cell to burg
const findBurgCell = function(x, y) {
const findBurgCell = function (x, y) {
let i = findCell(x, y);
if (pack.cells.h[i] >= 20) return i;
const dist = pack.cells.c[i].map(c =>
pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2
);
const dist = pack.cells.c[i].map(c => (pack.cells.h[c] < 20 ? Infinity : (pack.cells.p[c][0] - x) ** 2 + (pack.cells.p[c][1] - y) ** 2));
return pack.cells.c[i][d3.scan(dist)];
}
};
// find best cell for burgs
for (const b of pack.burgs) {
@ -372,7 +385,10 @@ function editHeightmap() {
}
if (p.burg && !pack.burgs[p.burg].removed) p.center = pack.burgs[p.burg].cell;
else {p.center = provCells[0]; p.burg = pack.cells.burg[p.center];}
else {
p.center = provCells[0];
p.burg = pack.cells.burg[p.center];
}
}
for (const c of pack.cultures) {
@ -390,7 +406,7 @@ function editHeightmap() {
}
// restore zones from grid
zones.selectAll("g").each(function() {
zones.selectAll("g").each(function () {
const zone = d3.select(this);
const g = zone.attr("data-cells");
const gCells = g ? g.split(",").map(i => +i) : [];
@ -399,8 +415,13 @@ function editHeightmap() {
zone.attr("data-cells", cells);
zone.selectAll("*").remove();
const base = zone.attr("id") + "_"; // id generic part
zone.selectAll("*").data(cells).enter().append("polygon")
.attr("points", d => getPackPolygon(d)).attr("id", d => base + d);
zone
.selectAll("*")
.data(cells)
.enter()
.append("polygon")
.attr("points", d => getPackPolygon(d))
.attr("id", d => base + d);
});
TIME && console.timeEnd("restoreRiskedData");
@ -410,7 +431,7 @@ function editHeightmap() {
// trigger heightmap redraw and history update if at least 1 cell is changed
function updateHeightmap() {
const prev = last(edits);
const changed = grid.cells.h.reduce((s, h, i) => h !== prev[i] ? s+1 : s, 0);
const changed = grid.cells.h.reduce((s, h, i) => (h !== prev[i] ? s + 1 : s), 0);
tip("Cells changed: " + changed);
if (!changed) return;
@ -429,8 +450,13 @@ function editHeightmap() {
function mockHeightmap() {
const data = renderOcean.checked ? grid.cells.i : grid.cells.i.filter(i => grid.cells.h[i] >= 20);
const scheme = getColorScheme();
viewbox.select("#heights").selectAll("polygon").data(data).join("polygon")
.attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d)
viewbox
.select("#heights")
.selectAll("polygon")
.data(data)
.join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + d)
.attr("fill", d => getColor(grid.cells.h[d], scheme));
}
@ -439,17 +465,25 @@ function editHeightmap() {
const ocean = renderOcean.checked;
const scheme = getColorScheme();
selection.forEach(function(i) {
let cell = viewbox.select("#heights").select("#cell"+i);
if (!ocean && grid.cells.h[i] < 20) {cell.remove(); return;}
if (!cell.size()) cell = viewbox.select("#heights").append("polygon").attr("points", getGridPolygon(i)).attr("id", "cell"+i);
selection.forEach(function (i) {
let cell = viewbox.select("#heights").select("#cell" + i);
if (!ocean && grid.cells.h[i] < 20) {
cell.remove();
return;
}
if (!cell.size())
cell = viewbox
.select("#heights")
.append("polygon")
.attr("points", getGridPolygon(i))
.attr("id", "cell" + i);
cell.attr("fill", getColor(grid.cells.h[i], scheme));
});
}
function updateStatistics() {
const landCells = grid.cells.h.reduce((s, h) => h >= 20 ? s+1 : s);
landmassCounter.innerHTML = `${landCells} (${rn(landCells/grid.cells.i.length*100)}%)`;
const landCells = grid.cells.h.reduce((s, h) => (h >= 20 ? s + 1 : s));
landmassCounter.innerHTML = `${landCells} (${rn((landCells / grid.cells.i.length) * 100)}%)`;
landmassAverage.innerHTML = rn(d3.mean(grid.cells.h));
}
@ -493,10 +527,13 @@ function editHeightmap() {
function openBrushesPanel() {
if ($("#brushesPanel").is(":visible")) return;
$("#brushesPanel").dialog({
title: "Paint Brushes", resizable: false,
$("#brushesPanel")
.dialog({
title: "Paint Brushes",
resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
}).on('dialogclose', exitBrushMode);
})
.on("dialogclose", exitBrushMode);
if (modules.openBrushesPanel) return;
modules.openBrushesPanel = true;
@ -504,8 +541,8 @@ function editHeightmap() {
// add listeners
document.getElementById("brushesButtons").addEventListener("click", e => toggleBrushMode(e));
document.getElementById("changeOnlyLand").addEventListener("click", e => changeOnlyLandClick(e));
document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n-1));
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n+1));
document.getElementById("undo").addEventListener("click", () => restoreHistory(edits.n - 1));
document.getElementById("redo").addEventListener("click", () => restoreHistory(edits.n + 1));
document.getElementById("rescaleShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleSection").style.display = "block";
@ -514,7 +551,7 @@ function editHeightmap() {
document.getElementById("modifyButtons").style.display = "block";
document.getElementById("rescaleSection").style.display = "none";
});
document.getElementById("rescaler").addEventListener("change", (e) => rescale(e.target.valueAsNumber));
document.getElementById("rescaler").addEventListener("change", e => rescale(e.target.valueAsNumber));
document.getElementById("rescaleCondShow").addEventListener("click", () => {
document.getElementById("modifyButtons").style.display = "none";
document.getElementById("rescaleCondSection").style.display = "block";
@ -539,7 +576,10 @@ function editHeightmap() {
}
function toggleBrushMode(e) {
if (e.target.classList.contains("pressed")) {exitBrushMode(); return;}
if (e.target.classList.contains("pressed")) {
exitBrushMode();
return;
}
exitBrushMode();
document.getElementById("brushesSliders").style.display = "block";
e.target.classList.add("pressed");
@ -568,17 +608,19 @@ function editHeightmap() {
const power = brushPower.valueAsNumber;
const interpolate = d3.interpolateRound(power, 1);
const land = changeOnlyLand.checked;
function lim(v) {return Math.max(Math.min(v, 100), land ? 20 : 0);}
function lim(v) {
return Math.max(Math.min(v, 100), land ? 20 : 0);
}
const h = grid.cells.h;
const brush = document.querySelector("#brushesButtons > button.pressed").id;
if (brush === "brushRaise") s.forEach(i => h[i] = h[i] < 20 ? 20 : lim(h[i] + power)); else
if (brush === "brushElevate") s.forEach((i,d) => h[i] = lim(h[i] + interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushLower") s.forEach(i => h[i] = lim(h[i] - power)); else
if (brush === "brushDepress") s.forEach((i,d) => h[i] = lim(h[i] - interpolate(d/Math.max(s.length-1, 1)))); else
if (brush === "brushAlign") s.forEach(i => h[i] = lim(h[start])); else
if (brush === "brushSmooth") s.forEach(i => h[i] = rn((d3.mean(grid.cells.c[i].filter(i => land ? h[i] >= 20 : 1).map(c => h[c])) + h[i]*(10-power) + .6) / (11-power),1)); else
if (brush === "brushDisrupt") s.forEach(i => h[i] = h[i] < 15 ? h[i] : lim(h[i] + power/1.6 - Math.random()*power));
if (brush === "brushRaise") s.forEach(i => (h[i] = h[i] < 20 ? 20 : lim(h[i] + power)));
else if (brush === "brushElevate") s.forEach((i, d) => (h[i] = lim(h[i] + interpolate(d / Math.max(s.length - 1, 1)))));
else if (brush === "brushLower") s.forEach(i => (h[i] = lim(h[i] - power)));
else if (brush === "brushDepress") s.forEach((i, d) => (h[i] = lim(h[i] - interpolate(d / Math.max(s.length - 1, 1)))));
else if (brush === "brushAlign") s.forEach(i => (h[i] = lim(h[start])));
else if (brush === "brushSmooth") s.forEach(i => (h[i] = rn((d3.mean(grid.cells.c[i].filter(i => (land ? h[i] >= 20 : 1)).map(c => h[c])) + h[i] * (10 - power) + 0.6) / (11 - power), 1)));
else if (brush === "brushDisrupt") s.forEach(i => (h[i] = h[i] < 15 ? h[i] : lim(h[i] + power / 1.6 - Math.random() * power)));
mockHeightmapSelection(s);
// updateHistory(); uncomment to update history every step
@ -592,7 +634,7 @@ function editHeightmap() {
function rescale(v) {
const land = changeOnlyLand.checked;
grid.cells.h = grid.cells.h.map(h => land && (h < 20 || h+v < 20) ? h : lim(h+v));
grid.cells.h = grid.cells.h.map(h => (land && (h < 20 || h + v < 20) ? h : lim(h + v)));
updateHeightmap();
document.getElementById("rescaler").value = 0;
}
@ -601,14 +643,20 @@ function editHeightmap() {
const range = rescaleLower.value + "-" + rescaleHigher.value;
const operator = conditionSign.value;
const operand = rescaleModifier.valueAsNumber;
if (Number.isNaN(operand)) {tip("Operand should be a number", false, "error"); return;}
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) {tip("Operand should be an integer", false, "error"); return;}
if (Number.isNaN(operand)) {
tip("Operand should be a number", false, "error");
return;
}
if ((operator === "add" || operator === "subtract") && !Number.isInteger(operand)) {
tip("Operand should be an integer", false, "error");
return;
}
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0); else
if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0); else
if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0); else
if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0); else
if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
if (operator === "multiply") HeightmapGenerator.modify(range, 0, operand, 0);
else if (operator === "divide") HeightmapGenerator.modify(range, 0, 1 / operand, 0);
else if (operator === "add") HeightmapGenerator.modify(range, operand, 1, 0);
else if (operator === "subtract") HeightmapGenerator.modify(range, -1 * operand, 1, 0);
else if (operator === "exponent") HeightmapGenerator.modify(range, 0, 1, operand);
updateHeightmap();
}
@ -619,19 +667,24 @@ function editHeightmap() {
}
function disruptAllHeights() {
grid.cells.h = grid.cells.h.map(h => h < 15 ? h : lim(h + 2.5 - Math.random() * 4));
grid.cells.h = grid.cells.h.map(h => (h < 15 ? h : lim(h + 2.5 - Math.random() * 4)));
updateHeightmap();
}
function startFromScratch() {
if (changeOnlyLand.checked) {tip("Not allowed when 'Change only land cells' mode is set", false, "error"); return;}
if (changeOnlyLand.checked) {
tip("Not allowed when 'Change only land cells' mode is set", false, "error");
return;
}
const someHeights = grid.cells.h.some(h => h);
if (!someHeights) {tip("Heightmap is already cleared, please do not click twice if not required", false, "error"); return;}
if (!someHeights) {
tip("Heightmap is already cleared, please do not click twice if not required", false, "error");
return;
}
grid.cells.h = new Uint8Array(grid.cells.i.length);
viewbox.select("#heights").selectAll("*").remove();
updateHistory();
}
}
function openTemplateEditor() {
@ -639,7 +692,10 @@ function editHeightmap() {
const body = document.getElementById("templateBody");
$("#templateEditor").dialog({
title: "Template Editor", minHeight: "auto", width: "fit-content", resizable: false,
title: "Template Editor",
minHeight: "auto",
width: "fit-content",
resizable: false,
position: {my: "right top", at: "right-10 top+10", of: "svg"}
});
@ -649,12 +705,12 @@ function editHeightmap() {
$("#templateBody").sortable({items: "> div", handle: ".icon-resize-vertical", containment: "#templateBody", axis: "y"});
// add listeners
body.addEventListener("click", function(ev) {
body.addEventListener("click", function (ev) {
const el = ev.target;
if (el.classList.contains("icon-check")) {
el.classList.remove("icon-check");
el.classList.add("icon-check-empty");
el.parentElement.style.opacity = .5;
el.parentElement.style.opacity = 0.5;
body.dataset.changed = 1;
return;
}
@ -665,7 +721,8 @@ function editHeightmap() {
return;
}
if (el.classList.contains("icon-trash-empty")) {
el.parentElement.remove(); return;
el.parentElement.remove();
return;
}
});
@ -674,7 +731,9 @@ function editHeightmap() {
document.getElementById("templateRun").addEventListener("click", executeTemplate);
document.getElementById("templateSave").addEventListener("click", downloadTemplate);
document.getElementById("templateLoad").addEventListener("click", () => templateToLoad.click());
document.getElementById("templateToLoad").addEventListener("change", function() {uploadFile(this, uploadTemplate)});
document.getElementById("templateToLoad").addEventListener("change", function () {
uploadFile(this, uploadTemplate);
});
function addStepOnClick(e) {
if (e.target.tagName !== "BUTTON") return;
@ -689,7 +748,9 @@ function editHeightmap() {
const elDist = body.querySelector("div:last-child").querySelector(".templateDist");
if (elDist) elDist.addEventListener("change", setRange);
if (dist && elDist && elDist.tagName === "SELECT") {
for (const o of elDist.options) {if (o.value === dist) elDist.value = dist;}
for (const o of elDist.options) {
if (o.value === dist) elDist.value = dist;
}
if (elDist.value !== dist) {
const opt = document.createElement("option");
opt.value = opt.innerHTML = dist;
@ -705,23 +766,23 @@ function editHeightmap() {
const Reorder = `<i class="icon-resize-vertical" data-tip="Drag to reorder"></i>`;
const common = `<div data-type="${type}">${Hide}<div style="width:4em">${type}</div>${Trash}${Reorder}`;
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5||"20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4||"15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3||"40-50"}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count||"1-2"}></span>`;
const TempY = `<span>y:<input class="templateY" data-tip="Placement range percentage along Y axis (minY-maxY)" value=${arg5 || "20-80"}></span>`;
const TempX = `<span>x:<input class="templateX" data-tip="Placement range percentage along X axis (minX-maxX)" value=${arg4 || "15-85"}></span>`;
const Height = `<span>h:<input class="templateHeight" data-tip="Blob maximum height, use hyphen to get a random number in range" value=${arg3 || "40-50"}></span>`;
const Count = `<span>n:<input class="templateCount" data-tip="Blobs to add, use hyphen to get a random number in range" value=${count || "1-2"}></span>`;
const blob = `${common}${TempY}${TempX}${Height}${Count}</div>`;
if (type === "Hill" || type === "Pit" || type === "Range" || type === "Trough") return blob;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count||"2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count||-10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count||1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count||2}></span></div>`;
if (type === "Strait") return `${common}<span>d:<select class="templateDist" data-tip="Strait direction"><option value="vertical" selected>vertical</option><option value="horizontal">horizontal</option></select></span><span>w:<input class="templateCount" data-tip="Strait width, use hyphen to get a random number in range" value=${count || "2-7"}></span></div>`;
if (type === "Add") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Add value to height of all cells (negative values are allowed)" type="number" value=${count || -10} min=-100 max=100 step=1></span></div>`;
if (type === "Multiply") return `${common}<span>to:<select class="templateDist" data-tip="Change only land or all cells"><option value="all" selected>all cells</option><option value="land">land only</option><option value="interval">interval</option></select></span><span>v:<input class="templateCount" data-tip="Multiply all cells Height by the value" type="number" value=${count || 1.1} min=0 max=10 step=.1></span></div>`;
if (type === "Smooth") return `${common}<span>f:<input class="templateCount" data-tip="Set smooth fraction. 1 - full smooth, 2 - half-smooth, etc." type="number" min=1 max=10 value=${count || 2}></span></div>`;
}
function setRange(event) {
if (event.target.value !== "interval") return;
prompt("Set a height interval. Avoid space, use hyphen as a separator", {default:"17-20"}, v => {
prompt("Set a height interval. Avoid space, use hyphen as a separator", {default: "17-20"}, v => {
const opt = document.createElement("option");
opt.value = opt.innerHTML = v;
event.target.add(opt);
@ -734,13 +795,24 @@ function editHeightmap() {
const steps = body.querySelectorAll("div").length;
const changed = +body.getAttribute("data-changed");
const template = e.target.value;
if (!steps || !changed) {changeTemplate(template); return;}
if (!steps || !changed) {
changeTemplate(template);
return;
}
alertMessage.innerHTML = "Are you sure you want to select a different template? All changes will be lost.";
$("#alert").dialog({resizable: false, title: "Change Template",
$("#alert").dialog({
resizable: false,
title: "Change Template",
buttons: {
Change: function() {changeTemplate(template); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}}
Change: function () {
changeTemplate(template);
$(this).dialog("close");
},
Cancel: function () {
$(this).dialog("close");
}
}
});
}
@ -751,15 +823,13 @@ function editHeightmap() {
if (template === "templateVolcano") {
addStep("Hill", "1", "90-100", "44-56", "40-60");
addStep("Multiply", .8, "50-100");
addStep("Multiply", 0.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");
}
else if (template === "templateHighIsland") {
} else if (template === "templateHighIsland") {
addStep("Hill", "1", "90-100", "65-75", "47-53");
addStep("Add", 5, "all");
addStep("Hill", "6", "20-23", "25-55", "45-55");
@ -769,13 +839,11 @@ function editHeightmap() {
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("Multiply", 0.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");
}
else if (template === "templateLowIsland") {
} else if (template === "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");
@ -785,13 +853,11 @@ function editHeightmap() {
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");
}
else if (template === "templateContinents") {
addStep("Multiply", 0.4, "20-100");
} else if (template === "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("Multiply", 0.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");
@ -802,9 +868,7 @@ function editHeightmap() {
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");
}
else if (template === "templateArchipelago") {
} else if (template === "templateArchipelago") {
addStep("Add", 11, "all");
addStep("Range", "2-3", "40-60", "20-80", "20-80");
addStep("Hill", "5", "15-20", "10-90", "30-70");
@ -814,18 +878,14 @@ function editHeightmap() {
addStep("Trough", "10", "20-30", "5-95", "5-95");
addStep("Strait", "2", "vertical");
addStep("Strait", "2", "horizontal");
}
else if (template === "templateAtoll") {
} else if (template === "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("Multiply", 0.2, "25-100");
addStep("Hill", ".5", "10-20", "50-55", "48-52");
}
else if (template === "templateMediterranean") {
} else if (template === "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");
@ -833,12 +893,10 @@ function editHeightmap() {
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("Multiply", 0.8, "land");
addStep("Trough", "3-5", "40-50", "0-100", "0-10");
addStep("Trough", "3-5", "40-50", "0-100", "90-100");
}
else if (template === "templatePeninsula") {
} else if (template === "templatePeninsula") {
addStep("Range", "2-3", "20-35", "40-50", "0-15");
addStep("Add", 5, "all");
addStep("Hill", "1", "90-100", "10-90", "0-5");
@ -847,22 +905,18 @@ function editHeightmap() {
addStep("Hill", "1-2", "3-5", "5-95", "40-60");
addStep("Trough", "5-6", "10-25", "5-95", "5-95");
addStep("Smooth", 3);
}
else if (template === "templatePangea") {
} else if (template === "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("Multiply", 0.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");
}
else if (template === "templateIsthmus") {
} else if (template === "templateIsthmus") {
addStep("Hill", "5-10", "15-30", "0-30", "0-20");
addStep("Hill", "5-10", "15-30", "10-50", "20-40");
addStep("Hill", "5-10", "15-30", "30-70", "40-60");
@ -874,15 +928,12 @@ function editHeightmap() {
addStep("Trough", "4-8", "15-30", "30-70", "40-60");
addStep("Trough", "4-8", "15-30", "50-90", "60-80");
addStep("Trough", "4-8", "15-30", "70-100", "80-100");
}
else if (template === "templateShattered") {
} else if (template === "templateShattered") {
addStep("Hill", "8", "35-40", "15-85", "30-70");
addStep("Trough", "10-20", "40-50", "5-95", "5-95");
addStep("Range", "5-7", "30-40", "10-90", "20-80");
addStep("Pit", "12-20", "30-40", "15-85", "20-80");
}
}
function executeTemplate() {
@ -893,7 +944,7 @@ function editHeightmap() {
grid.cells.h = new Uint8Array(grid.cells.i.length); // clean all heights
for (const s of steps) {
if (s.style.opacity == .5) continue;
if (s.style.opacity == 0.5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount") || "";
const elHeight = s.querySelector(".templateHeight") || "";
@ -906,14 +957,14 @@ function editHeightmap() {
const templateY = s.querySelector(".templateY");
const y = templateY ? templateY.value : null;
if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y); else
if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y); else
if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y); else
if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y); else
if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist); else
if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1); else
if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value); else
if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value);
if (type === "Hill") HeightmapGenerator.addHill(elCount.value, elHeight.value, x, y);
else if (type === "Pit") HeightmapGenerator.addPit(elCount.value, elHeight.value, x, y);
else if (type === "Range") HeightmapGenerator.addRange(elCount.value, elHeight.value, x, y);
else if (type === "Trough") HeightmapGenerator.addTrough(elCount.value, elHeight.value, x, y);
else if (type === "Strait") HeightmapGenerator.addStrait(elCount.value, dist);
else if (type === "Add") HeightmapGenerator.modify(dist, +elCount.value, 1);
else if (type === "Multiply") HeightmapGenerator.modify(dist, 0, +elCount.value);
else if (type === "Smooth") HeightmapGenerator.smooth(+elCount.value);
updateHistory("noStat"); // update history every step
}
@ -932,7 +983,7 @@ function editHeightmap() {
let data = "";
for (const s of steps) {
if (s.style.opacity == .5) continue;
if (s.style.opacity == 0.5) continue;
const type = s.getAttribute("data-type");
const elCount = s.querySelector(".templateCount");
const count = elCount ? elCount.value : "0";
@ -952,14 +1003,19 @@ function editHeightmap() {
function uploadTemplate(dataLoaded) {
const steps = dataLoaded.split("\r\n");
if (!steps.length) {tip("Cannot parse the template, please check the file", false, "error"); return;}
if (!steps.length) {
tip("Cannot parse the template, please check the file", false, "error");
return;
}
templateBody.innerHTML = "";
for (const s of steps) {
const step = s.split(" ");
if (step.length !== 5) {ERROR && console.error("Cannot parse step, wrong arguments count", s); continue;}
if (step.length !== 5) {
ERROR && console.error("Cannot parse step, wrong arguments count", s);
continue;
}
addStep(step[0], step[1], step[2], step[3], step[4]);
}
}
}
@ -969,7 +1025,10 @@ function editHeightmap() {
closeDialogs("#imageConverter");
$("#imageConverter").dialog({
title: "Image Converter", maxHeight: svgHeight*.8, minHeight: "auto", width: "20em",
title: "Image Converter",
maxHeight: svgHeight * 0.8,
minHeight: "auto",
width: "20em",
position: {my: "right top", at: "right-10 top+10", of: "svg"},
beforeClose: closeImageConverter
});
@ -983,7 +1042,7 @@ function editHeightmap() {
setOverlayOpacity(0);
clearMainTip();
tip('Image Converter is opened. Upload image and assign height value for each color', false, "warn"); // main tip
tip("Image Converter is opened. Upload image and assign height value for each color", false, "warn"); // main tip
// remove all heights
grid.cells.h = new Uint8Array(grid.cells.i.length);
@ -994,13 +1053,18 @@ function editHeightmap() {
modules.openImageConverter = true;
// add color pallete
void function createColorPallete() {
d3.select("#imageConverterPalette").selectAll("div").data(d3.range(101))
.enter().append("div").attr("data-color", i => i)
.style("background-color", i => color(1-(i < 20 ? i-5 : i) / 100))
.style("width", i => i < 40 || i > 68 ? ".2em" : ".1em")
.on("touchmove mousemove", showPalleteHeight).on("click", assignHeight);
}()
void (function createColorPallete() {
d3.select("#imageConverterPalette")
.selectAll("div")
.data(d3.range(101))
.enter()
.append("div")
.attr("data-color", i => i)
.style("background-color", i => color(1 - (i < 20 ? i - 5 : i) / 100))
.style("width", i => (i < 40 || i > 68 ? ".2em" : ".1em"))
.on("touchmove mousemove", showPalleteHeight)
.on("click", assignHeight);
})();
// add listeners
document.getElementById("convertImageLoad").addEventListener("click", () => imageToLoad.click());
@ -1011,14 +1075,18 @@ function editHeightmap() {
document.getElementById("convertColorsButton").addEventListener("click", setConvertColorsNumber);
document.getElementById("convertComplete").addEventListener("click", applyConversion);
document.getElementById("convertCancel").addEventListener("click", cancelConversion);
document.getElementById("convertOverlay").addEventListener("input", function() {setOverlayOpacity(this.value)});
document.getElementById("convertOverlayNumber").addEventListener("input", function() {setOverlayOpacity(this.value)});
document.getElementById("convertOverlay").addEventListener("input", function () {
setOverlayOpacity(this.value);
});
document.getElementById("convertOverlayNumber").addEventListener("input", function () {
setOverlayOpacity(this.value);
});
function showPalleteHeight() {
const height = +this.getAttribute("data-color");
colorsSelectValue.innerHTML = height;
colorsSelectFriendly.innerHTML = getHeight(height);
const former = imageConverterPalette.querySelector(".hoveredColor")
const former = imageConverterPalette.querySelector(".hoveredColor");
if (former) former.className = "";
this.className = "hoveredColor";
}
@ -1028,15 +1096,15 @@ function editHeightmap() {
this.value = ""; // reset input value to get triggered if the file is re-uploaded
const reader = new FileReader();
const img = new Image;
img.onload = function() {
const img = new Image();
img.onload = function () {
const ctx = document.getElementById("canvas").getContext("2d");
ctx.drawImage(img, 0, 0, graphWidth, graphHeight);
heightsFromImage(+convertColors.value);
resetZoom();
};
reader.onloadend = () => img.src = reader.result;
reader.onloadend = () => (img.src = reader.result);
reader.readAsDataURL(file);
}
@ -1045,9 +1113,9 @@ function editHeightmap() {
const sampleCanvas = document.createElement("canvas");
sampleCanvas.width = grid.cellsX;
sampleCanvas.height = grid.cellsY;
sampleCanvas.getContext('2d').drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY);
sampleCanvas.getContext("2d").drawImage(sourceImage, 0, 0, grid.cellsX, grid.cellsY);
const q = new RgbQuant({colors:count});
const q = new RgbQuant({colors: count});
q.sample(sampleCanvas);
const data = q.reduce(sampleCanvas);
const pallete = q.palette(true);
@ -1059,15 +1127,26 @@ function editHeightmap() {
colorsAssigned.style.display = "none";
sampleCanvas.remove(); // no need to keep
viewbox.select("#heights").selectAll("polygon").data(grid.cells.i).join("polygon")
.attr("points", d => getGridPolygon(d)).attr("id", d => "cell"+d)
.attr("fill", d => `rgb(${data[d*4]}, ${data[d*4+1]}, ${data[d*4+2]})`)
viewbox
.select("#heights")
.selectAll("polygon")
.data(grid.cells.i)
.join("polygon")
.attr("points", d => getGridPolygon(d))
.attr("id", d => "cell" + d)
.attr("fill", d => `rgb(${data[d * 4]}, ${data[d * 4 + 1]}, ${data[d * 4 + 2]})`)
.on("click", mapClicked);
const colors = pallete.map(p => `rgb(${p[0]}, ${p[1]}, ${p[2]})`);
d3.select("#colorsUnassigned").selectAll("div").data(colors).enter().append("div")
.attr("data-color", i => i).style("background-color", i => i)
.attr("class", "color-div").on("click", colorClicked);
d3.select("#colorsUnassigned")
.selectAll("div")
.data(colors)
.enter()
.append("div")
.attr("data-color", i => i)
.style("background-color", i => i)
.attr("class", "color-div")
.on("click", colorClicked);
document.getElementById("colorsUnassignedNumber").innerHTML = colors.length;
}
@ -1100,18 +1179,24 @@ function editHeightmap() {
const color = this.getAttribute("data-color");
viewbox.select("#heights").selectAll("polygon.selectedCell").classed("selectedCell", 0);
viewbox.select("#heights").selectAll("polygon[fill='" + color + "']").classed("selectedCell", 1);
viewbox
.select("#heights")
.selectAll("polygon[fill='" + color + "']")
.classed("selectedCell", 1);
}
function assignHeight() {
const height = +this.dataset.color;
const rgb = color(1 - (height < 20 ? height-5 : height) / 100);
const rgb = color(1 - (height < 20 ? height - 5 : height) / 100);
const selectedColor = imageConverter.querySelector("div.selectedColor");
selectedColor.style.backgroundColor = rgb;
selectedColor.setAttribute("data-color", rgb);
selectedColor.setAttribute("data-height", height);
viewbox.select("#heights").selectAll(".selectedCell").each(function() {
viewbox
.select("#heights")
.selectAll(".selectedCell")
.each(function () {
this.setAttribute("fill", rgb);
this.setAttribute("data-height", height);
});
@ -1123,7 +1208,6 @@ function editHeightmap() {
document.getElementById("colorsUnassignedNumber").innerHTML = colorsUnassigned.childElementCount - 2;
document.getElementById("colorsAssignedNumber").innerHTML = colorsAssigned.childElementCount - 2;
}
}
// auto assign color based on luminosity or hue
@ -1138,37 +1222,44 @@ function editHeightmap() {
}
}
const getHeightByHue = function(color) {
const getHeightByHue = function (color) {
let hue = d3.hsl(color).h;
if (hue > 300) hue -= 360;
if (hue > 170) return Math.abs(hue-250) / 3 |0; // water
return Math.abs(hue-250+20) / 3 |0; // land
}
if (hue > 170) return (Math.abs(hue - 250) / 3) | 0; // water
return (Math.abs(hue - 250 + 20) / 3) | 0; // land
};
const getHeightByLum = function(color) {
const getHeightByLum = function (color) {
let lum = d3.lab(color).l;
if (lum < 13) return lum / 13 * 20 |0; // water
return lum|0; // land
}
if (lum < 13) return ((lum / 13) * 20) | 0; // water
return lum | 0; // land
};
const scheme = d3.range(101).map(i => getColor(i, color()));
const hues = scheme.map(rgb => d3.hsl(rgb).h|0);
const getHeightByScheme = function(color) {
const hues = scheme.map(rgb => d3.hsl(rgb).h | 0);
const getHeightByScheme = function (color) {
let height = scheme.indexOf(color);
if (height !== -1) return height; // exact match
const hue = d3.hsl(color).h;
const closest = hues.reduce((prev, curr) => (Math.abs(curr - hue) < Math.abs(prev - hue) ? curr : prev));
return hues.indexOf(closest);
}
};
const assinged = []; // store assigned heights
unassigned.forEach(el => {
const clr = el.dataset.color;
const height = type === "hue" ? getHeightByHue(clr) : type === "lum" ? getHeightByLum(clr) : getHeightByScheme(clr);
const colorTo = color(1 - (height < 20 ? (height-5) / 100 : height / 100));
viewbox.select("#heights").selectAll("polygon[fill='" + clr + "']").attr("fill", colorTo).attr("data-height", height);
const colorTo = color(1 - (height < 20 ? (height - 5) / 100 : height / 100));
viewbox
.select("#heights")
.selectAll("polygon[fill='" + clr + "']")
.attr("fill", colorTo)
.attr("data-height", height);
if (assinged[height]) {el.remove(); return;} // if color is already added, remove it
if (assinged[height]) {
el.remove();
return;
} // if color is already added, remove it
el.style.backgroundColor = el.dataset.color = colorTo;
el.dataset.height = height;
colorsAssigned.appendChild(el);
@ -1186,8 +1277,7 @@ function editHeightmap() {
}
function setConvertColorsNumber() {
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`,
{default:+convertColors.value, step:1, min:3, max:255}, number => {
prompt(`Please set maximum number of colors. <br>An actual number is usually lower and depends on color scheme`, {default: +convertColors.value, step: 1, min: 3, max: 255}, number => {
convertColors.value = number;
heightsFromImage(number);
});
@ -1204,7 +1294,10 @@ function editHeightmap() {
return;
}
viewbox.select("#heights").selectAll("polygon").each(function() {
viewbox
.select("#heights")
.selectAll("polygon")
.each(function () {
const height = +this.dataset.height || 0;
const i = +this.id.slice(4);
grid.cells.h[i] = height;
@ -1218,7 +1311,7 @@ function editHeightmap() {
function cancelConversion() {
restoreImageConverterState();
viewbox.select("#heights").selectAll("polygon").remove();
restoreHistory(edits.n-1);
restoreHistory(edits.n - 1);
}
function restoreImageConverterState() {
@ -1244,20 +1337,22 @@ function editHeightmap() {
Click "Complete" to apply the conversion.
Click "Close" to exit conversion mode and restore previous heightmap`;
$("#alert").dialog({resizable: false, title: "Close Image Converter",
$("#alert").dialog({
resizable: false,
title: "Close Image Converter",
buttons: {
Cancel: function() {
Cancel: function () {
$(this).dialog("close");
},
Complete: function() {
Complete: function () {
$(this).dialog("close");
applyConversion();
},
Close: function() {
Close: function () {
$(this).dialog("close");
restoreImageConverterState();
viewbox.select("#heights").selectAll("polygon").remove();
restoreHistory(edits.n-1);
restoreHistory(edits.n - 1);
}
}
});
@ -1285,11 +1380,11 @@ function editHeightmap() {
grid.cells.h.forEach((height, i) => {
let h = height < 20 ? Math.max(height / 1.5, 0) : height;
const v = h / 100 * 255;
imageData.data[i*4] = v;
imageData.data[i*4 + 1] = v;
imageData.data[i*4 + 2] = v;
imageData.data[i*4 + 3] = 255;
const v = (h / 100) * 255;
imageData.data[i * 4] = v;
imageData.data[i * 4 + 1] = v;
imageData.data[i * 4 + 2] = v;
imageData.data[i * 4 + 3] = 255;
});
ctx.putImageData(imageData, 0, 0);
@ -1302,7 +1397,7 @@ function editHeightmap() {
const img = new Image();
img.src = dataURL;
img.onload = function() {
img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = svgWidth;
@ -1315,7 +1410,6 @@ function editHeightmap() {
link.href = imgBig;
link.click();
canvas.remove();
};
}
}
}

View file

@ -1,44 +1,57 @@
// module to control the Tools options (click to edit, to re-geenerate, tp add)
"use strict";
toolsContent.addEventListener("click", function(event) {
if (customization) {tip("Please exit the customization mode first", false, "warning"); return;}
toolsContent.addEventListener("click", function (event) {
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 === "editProvincesButton") editProvinces(); else
if (button === "editDiplomacyButton") editDiplomacy(); else
if (button === "editCulturesButton") editCultures(); else
if (button === "editReligions") editReligions(); else
if (button === "editEmblemButton") openEmblemEditor(); else
if (button === "editNamesBaseButton") editNamesbase(); else
if (button === "editUnitsButton") editUnits(); else
if (button === "editNotesButton") editNotes(); else
if (button === "editZonesButton") editZones(); else
if (button === "overviewBurgsButton") overviewBurgs(); else
if (button === "overviewRiversButton") overviewRivers(); else
if (button === "overviewMilitaryButton") overviewMilitary(); else
if (button === "overviewCellsButton") viewCellDetails();
if (button === "editHeightmapButton") editHeightmap();
else if (button === "editBiomesButton") editBiomes();
else if (button === "editStatesButton") editStates();
else if (button === "editProvincesButton") editProvinces();
else if (button === "editDiplomacyButton") editDiplomacy();
else if (button === "editCulturesButton") editCultures();
else if (button === "editReligions") editReligions();
else if (button === "editEmblemButton") openEmblemEditor();
else if (button === "editNamesBaseButton") editNamesbase();
else if (button === "editUnitsButton") editUnits();
else if (button === "editNotesButton") editNotes();
else if (button === "editZonesButton") editZones();
else if (button === "overviewBurgsButton") overviewBurgs();
else if (button === "overviewRiversButton") overviewRivers();
else if (button === "overviewMilitaryButton") overviewMilitary();
else if (button === "overviewCellsButton") viewCellDetails();
// Click to Regenerate buttons
if (event.target.parentNode.id === "regenerateFeature") {
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {processFeatureRegeneration(event, button); return;}
if (sessionStorage.getItem("regenerateFeatureDontAsk")) {
processFeatureRegeneration(event, button);
return;
}
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`
$("#alert").dialog({resizable: false, title: "Regenerate element",
alertMessage.innerHTML = `Regeneration will remove all the custom changes for the element.<br><br>Are you sure you want to proceed?`;
$("#alert").dialog({
resizable: false,
title: "Regenerate element",
buttons: {
Proceed: function() {processFeatureRegeneration(event, button); $(this).dialog("close");},
Cancel: function() {$(this).dialog("close");}
Proceed: function () {
processFeatureRegeneration(event, button);
$(this).dialog("close");
},
open: function() {
Cancel: function () {
$(this).dialog("close");
}
},
open: function () {
const pane = $(this).dialog("widget").find(".ui-dialog-buttonpane");
$('<span><input id="dontAsk" class="checkbox" type="checkbox"><label for="dontAsk" class="checkbox-label dontAsk"><i>do not ask again</i></label><span>').prependTo(pane);
},
close: function() {
close: function () {
const box = $(this).dialog("widget").find(".checkbox")[0];
if (!box) return;
if (box.checked) sessionStorage.setItem("regenerateFeatureDontAsk", true);
@ -48,29 +61,35 @@ toolsContent.addEventListener("click", function(event) {
}
// 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();
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 processFeatureRegeneration(event, button) {
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") regenerateRivers(); else
if (button === "regeneratePopulation") recalculatePopulation(); else
if (button === "regenerateStates") regenerateStates(); else
if (button === "regenerateProvinces") regenerateProvinces(); else
if (button === "regenerateBurgs") regenerateBurgs(); else
if (button === "regenerateEmblems") regenerateEmblems(); else
if (button === "regenerateReligions") regenerateReligions(); else
if (button === "regenerateCultures") regenerateCultures(); else
if (button === "regenerateMilitary") regenerateMilitary(); else
if (button === "regenerateIce") regenerateIce(); else
if (button === "regenerateMarkers") regenerateMarkers(event); else
if (button === "regenerateZones") regenerateZones(event);
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") regenerateRivers();
else if (button === "regeneratePopulation") recalculatePopulation();
else if (button === "regenerateStates") regenerateStates();
else if (button === "regenerateProvinces") regenerateProvinces();
else if (button === "regenerateBurgs") regenerateBurgs();
else if (button === "regenerateEmblems") regenerateEmblems();
else if (button === "regenerateReligions") regenerateReligions();
else if (button === "regenerateCultures") regenerateCultures();
else if (button === "regenerateMilitary") regenerateMilitary();
else if (button === "regenerateIce") regenerateIce();
else if (button === "regenerateMarkers") regenerateMarkers(event);
else if (button === "regenerateZones") regenerateZones(event);
}
async function openEmblemEditor() {
@ -106,10 +125,10 @@ function recalculatePopulation() {
if (!b.i || b.removed || b.lock) return;
const i = b.cell;
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + i % 100 / 1000, .1), 3);
b.population = rn(Math.max((pack.cells.s[i] + pack.cells.road[i] / 2) / 8 + b.i / 1000 + (i % 100) / 1000, 0.1), 3);
if (b.capital) b.population = b.population * 1.3; // increase capital population
if (b.port) b.population = b.population * 1.3; // increase port population
b.population = rn(b.population * gauss(2,3,.6,20,3), 3);
b.population = rn(b.population * gauss(2, 3, 0.6, 20, 3), 3);
});
}
@ -126,11 +145,16 @@ function regenerateStates() {
}
// burg local ids sorted by a bit randomized population:
const sorted = burgs.map((b, i) => [i, b.population * Math.random()]).sort((a, b) => b[1] - a[1]).map(b => b[0]);
const sorted = burgs
.map((b, i) => [i, b.population * Math.random()])
.sort((a, b) => b[1] - a[1])
.map(b => b[0]);
const capitalsTree = d3.quadtree();
// turn all old capitals into towns
burgs.filter(b => b.capital).forEach(b => {
burgs
.filter(b => b.capital)
.forEach(b => {
moveBurgToGroup(b.i, "towns");
b.capital = 0;
});
@ -145,7 +169,7 @@ function regenerateStates() {
// if desired states number is 0
if (regionsInput.value == 0) {
tip(`Cannot generate zero states. Please check the <i>States Number</i> option`, false, "warn");
pack.states = pack.states.slice(0,1); // remove all except of neutrals
pack.states = pack.states.slice(0, 1); // remove all except of neutrals
pack.states[0].diplomacy = []; // clear diplomacy
pack.provinces = [0]; // remove all provinces
pack.cells.state = new Uint16Array(pack.cells.i.length); // reset cells data
@ -165,10 +189,12 @@ function regenerateStates() {
pack.states = d3.range(count).map(i => {
if (!i) return {i, name: neutral};
let capital = null, x = 0, y = 0;
let capital = null,
x = 0,
y = 0;
for (const i of sorted) {
capital = burgs[i];
x = capital.x, y = capital.y;
(x = capital.x), (y = capital.y);
if (capitalsTree.find(x, y, spacing) === undefined) break;
spacing = Math.max(spacing - 1, 1);
}
@ -178,17 +204,17 @@ function regenerateStates() {
moveBurgToGroup(capital.i, "cities");
const culture = capital.culture;
const basename = capital.name.length < 9 && capital.cell%5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const basename = capital.name.length < 9 && capital.cell % 5 === 0 ? capital.name : Names.getCulture(culture, 3, 6, "", 0);
const name = Names.getState(basename, culture);
const nomadic = [1, 2, 3, 4].includes(pack.cells.biome[capital.cell]);
const type = nomadic ? "Nomadic" : pack.cultures[culture].type === "Nomadic" ? "Generic" : pack.cultures[culture].type;
const expansionism = rn(Math.random() * powerInput.value + 1, 1);
const cultureType = pack.cultures[culture].type;
const coa = COA.generate(capital.coa, .3, null, cultureType);
const coa = COA.generate(capital.coa, 0.3, null, cultureType);
coa.shield = capital.coa.shield;
return {i, name, type, capital:capital.i, center:capital.cell, culture, expansionism, coa};
return {i, name, type, capital: capital.i, center: capital.cell, culture, expansionism, coa};
});
BurgsAndStates.expandStates();
@ -199,8 +225,10 @@ function regenerateStates() {
BurgsAndStates.generateDiplomacy();
BurgsAndStates.defineStateForms();
BurgsAndStates.generateProvinces(true);
if (!layerIsOn("toggleStates")) toggleStates(); else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders(); else drawBorders();
if (!layerIsOn("toggleStates")) toggleStates();
else drawStates();
if (!layerIsOn("toggleBorders")) toggleBorders();
else drawBorders();
BurgsAndStates.drawStateLabels();
Military.generate();
if (layerIsOn("toggleEmblems")) drawEmblems(); // redrawEmblems
@ -224,43 +252,52 @@ function regenerateProvinces() {
}
function regenerateBurgs() {
const cells = pack.cells, states = pack.states, Lockedburgs = pack.burgs.filter(b =>b.lock);
const cells = pack.cells,
states = pack.states,
Lockedburgs = pack.burgs.filter(b => b.lock);
rankCells();
cells.burg = new Uint16Array(cells.i.length);
const burgs = pack.burgs = [0]; // clear burgs array
states.filter(s => s.i).forEach(s => s.capital = 0); // clear state capitals
pack.provinces.filter(p => p.i).forEach(p => p.burg = 0); // clear province capitals
const burgs = (pack.burgs = [0]); // clear burgs array
states.filter(s => s.i).forEach(s => (s.capital = 0)); // clear state capitals
pack.provinces.filter(p => p.i).forEach(p => (p.burg = 0)); // clear province capitals
const burgsTree = d3.quadtree();
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
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** .8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** .7 / 66); // base min distance between towns
const burgsCount = manorsInput.value == 1000 ? rn(sorted.length / 5 / (grid.points.length / 10000) ** 0.8) + states.length : +manorsInput.value + states.length;
const spacing = (graphWidth + graphHeight) / 150 / (burgsCount ** 0.7 / 66); // base min distance between towns
//clear locked list since ids will change
//burglock.selectAll("text").remove();
for (let j=0; j < Lockedburgs.length; j++) {
for (let j = 0; j < Lockedburgs.length; j++) {
const id = burgs.length;
const oldBurg = Lockedburgs[j];
oldBurg.i = id;
burgs.push(oldBurg);
burgsTree.add([oldBurg.x, oldBurg.y]);
cells.burg[oldBurg.cell] = id;
if (oldBurg.capital) {states[oldBurg.state].capital = id; states[oldBurg.state].center = oldBurg.cell;}
if (oldBurg.capital) {
states[oldBurg.state].capital = id;
states[oldBurg.state].center = oldBurg.cell;
}
//burglock.append("text").attr("data-id", id);
}
for (let i=0; i < sorted.length && burgs.length < burgsCount; i++) {
for (let i = 0; i < sorted.length && burgs.length < burgsCount; i++) {
const id = burgs.length;
const cell = sorted[i];
const x = cells.p[cell][0], y = cells.p[cell][1];
const x = cells.p[cell][0],
y = cells.p[cell][1];
const s = spacing * gauss(1, .3, .2, 2, 2); // randomize to make the placement not uniform
const s = spacing * gauss(1, 0.3, 0.2, 2, 2); // randomize to make the placement not uniform
if (burgsTree.find(x, y, s) !== undefined) continue; // to close to existing burg
const state = cells.state[cell];
const capital = state && !states[state].capital; // if state doesn't have capital, make this burg a capital, no capital for neutral lands
if (capital) {states[state].capital = id; states[state].center = cell;}
if (capital) {
states[state].capital = id;
states[state].center = cell;
}
const culture = cells.culture[cell];
const name = Names.getCulture(culture);
@ -270,7 +307,9 @@ function regenerateBurgs() {
}
// add a capital at former place for states without added capitals
states.filter(s => s.i && !s.removed && !s.capital).forEach(s => {
states
.filter(s => s.i && !s.removed && !s.capital)
.forEach(s => {
const burg = addBurg([cells.p[s.center][0], cells.p[s.center][1]]); // add new burg
s.capital = burg;
s.center = pack.burgs[burg].cell;
@ -279,7 +318,9 @@ function regenerateBurgs() {
moveBurgToGroup(burg, "cities");
});
pack.features.forEach(f => {if (f.port) f.port = 0}); // reset features ports counter
pack.features.forEach(f => {
if (f.port) f.port = 0;
}); // reset features ports counter
BurgsAndStates.specifyBurgs();
BurgsAndStates.defineBurgFeatures();
BurgsAndStates.drawBurgs();
@ -313,10 +354,10 @@ function regenerateEmblems() {
if (!burg.i || burg.removed) return;
const state = pack.states[burg.state];
let kinship = state ? .25 : 0;
if (burg.capital) kinship += .1;
else if (burg.port) kinship -= .1;
if (state && burg.culture !== state.culture) kinship -= .25;
let kinship = state ? 0.25 : 0;
if (burg.capital) kinship += 0.1;
else if (burg.port) kinship -= 0.1;
if (state && burg.culture !== state.culture) kinship -= 0.25;
burg.coa = COA.generate(state ? state.coa : null, kinship, null, burg.type);
burg.coa.shield = COA.getShield(burg.culture, state ? burg.state : 0);
});
@ -327,16 +368,16 @@ function regenerateEmblems() {
let dominion = false;
if (!province.burg) {
dominion = P(.2);
if (province.formName === "Colony") dominion = P(.95); else
if (province.formName === "Island") dominion = P(.6); else
if (province.formName === "Islands") dominion = P(.5); else
if (province.formName === "Territory") dominion = P(.4); else
if (province.formName === "Land") dominion = P(.3);
dominion = P(0.2);
if (province.formName === "Colony") dominion = P(0.95);
else if (province.formName === "Island") dominion = P(0.6);
else if (province.formName === "Islands") dominion = P(0.5);
else if (province.formName === "Territory") dominion = P(0.4);
else if (province.formName === "Land") dominion = P(0.3);
}
const nameByBurg = province.burg && province.name.slice(0, 3) === parent.name.slice(0, 3);
const kinship = dominion ? 0 : nameByBurg ? .8 : .4;
const kinship = dominion ? 0 : nameByBurg ? 0.8 : 0.4;
const culture = pack.cells.culture[province.center];
const type = BurgsAndStates.getType(province.center, parent.port);
province.coa = COA.generate(parent.coa, kinship, dominion, type);
@ -348,7 +389,8 @@ function regenerateEmblems() {
function regenerateReligions() {
Religions.generate();
if (!layerIsOn("toggleReligions")) toggleReligions(); else drawReligions();
if (!layerIsOn("toggleReligions")) toggleReligions();
else drawReligions();
}
function regenerateCultures() {
@ -356,7 +398,8 @@ function regenerateCultures() {
Cultures.expand();
BurgsAndStates.updateCultures();
Religions.updateCultures();
if (!layerIsOn("toggleCultures")) toggleCultures(); else drawCultures();
if (!layerIsOn("toggleCultures")) toggleCultures();
else drawCultures();
refreshAllEditors();
}
@ -373,15 +416,18 @@ function regenerateIce() {
}
function regenerateMarkers(event) {
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, .5, .3, 5, 2));
if (isCtrlClick(event)) prompt("Please provide markers number multiplier", {default: 1, step: 0.01, min: 0, max: 100}, v => addNumberOfMarkers(v));
else addNumberOfMarkers(gauss(1, 0.5, 0.3, 5, 2));
function addNumberOfMarkers(number) {
// remove existing markers and assigned notes
markers.selectAll("use").each(function() {
markers
.selectAll("use")
.each(function () {
const index = notes.findIndex(n => n.id === this.id);
if (index != -1) notes.splice(index, 1);
}).remove();
})
.remove();
addMarkers(number);
if (!layerIsOn("toggleMarkers")) toggleMarkers();
@ -389,8 +435,8 @@ function regenerateMarkers(event) {
}
function regenerateZones(event) {
if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default:1, step:.01, min:0, max:100}, v => addNumberOfZones(v));
else addNumberOfZones(gauss(1, .5, .6, 5, 2));
if (isCtrlClick(event)) prompt("Please provide zones number multiplier", {default: 1, step: 0.01, min: 0, max: 100}, v => addNumberOfZones(v));
else addNumberOfZones(gauss(1, 0.5, 0.6, 5, 2));
function addNumberOfZones(number) {
zones.selectAll("g").remove(); // remove existing zones
@ -408,10 +454,13 @@ function unpressClickToAddButton() {
function toggleAddLabel() {
const pressed = document.getElementById("addLabel").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addLabel.classList.add('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);
@ -428,10 +477,7 @@ function addLabelOnClick() {
const id = getNextId("label");
let group = labels.select("#addedLabels");
if (!group.size()) group = labels.append("g").attr("id", "addedLabels")
.attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a")
.attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC")
.attr("font-size", 18).attr("data-size", 18).attr("filter", null);
if (!group.size()) group = labels.append("g").attr("id", "addedLabels").attr("fill", "#3e3e4b").attr("opacity", 1).attr("stroke", "#3a3a3a").attr("stroke-width", 0).attr("font-family", "Almendra SC").attr("data-font", "Almendra+SC").attr("font-size", 18).attr("data-size", 18).attr("filter", null);
const example = group.append("text").attr("x", 0).attr("x", 0).text(name);
const width = example.node().getBBox().width;
@ -439,13 +485,22 @@ function addLabelOnClick() {
example.remove();
group.classed("hidden", false);
group.append("text").attr("id", id)
.append("textPath").attr("xlink:href", "#textPath_"+id).attr("startOffset", "50%").attr("font-size", "100%")
.append("tspan").attr("x", x).text(name);
group
.append("text")
.attr("id", id)
.append("textPath")
.attr("xlink:href", "#textPath_" + id)
.attr("startOffset", "50%")
.attr("font-size", "100%")
.append("tspan")
.attr("x", x)
.text(name);
defs.select("#textPaths")
.append("path").attr("id", "textPath_"+id)
.attr("d", `M${point[0]-width},${point[1]} h${width*2}`);
defs
.select("#textPaths")
.append("path")
.attr("id", "textPath_" + id)
.attr("d", `M${point[0] - width},${point[1]} h${width * 2}`);
if (d3.event.shiftKey === false) unpressClickToAddButton();
}
@ -466,7 +521,7 @@ function toggleAddRiver() {
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRiver.classList.add('pressed');
addRiver.classList.add("pressed");
document.getElementById("addNewRiver").classList.add("pressed");
closeDialogs(".stable");
viewbox.style("cursor", "crosshair").on("click", addRiverOnClick);
@ -486,22 +541,27 @@ function addRiverOnClick() {
// height with added t value to make map less depressed
const h = Array.from(cells.h)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100)
.map((h, i) => h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000);
Rivers.resolveDepressions(h);
.map((h, i) => (h < 20 || cells.t[i] < 1 ? h : h + cells.t[i] / 100))
.map((h, i) => (h < 20 || cells.t[i] < 1 ? h : h + d3.mean(cells.c[i].map(c => cells.t[c])) / 10000));
Rivers.resolveDepressions(h, 200);
while (i) {
cells.r[i] = river;
const x = cells.p[i][0], y = cells.p[i][1];
dataRiver.push({x, y, cell:i});
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) => h[a] - h[b])]; // downhill cell
if (h[i] <= h[min]) {tip(`Cell ${i} is depressed, river cannot flow further`, false, "error"); return;}
const tx = cells.p[min][0], ty = cells.p[min][1];
if (h[i] <= h[min]) {
tip(`Cell ${i} is depressed, river cannot flow further`, false, "error");
return;
}
const tx = cells.p[min][0],
ty = cells.p[min][1];
if (h[min] < 20) {
// pour to water body
dataRiver.push({x: tx, y: ty, cell:i});
dataRiver.push({x: tx, y: ty, cell: i});
break;
}
@ -526,19 +586,22 @@ function addRiverOnClick() {
}
// extend old river
rivers.select("#river"+r).remove();
cells.i.filter(i => cells.r[i] === river).forEach(i => cells.r[i] = r);
riverCells.forEach(i => cells.r[i] = 0);
rivers.select("#river" + r).remove();
cells.i.filter(i => cells.r[i] === river).forEach(i => (cells.r[i] = r));
riverCells.forEach(i => (cells.r[i] = 0));
river = r;
cells.fl[min] = cells.fl[i] + grid.cells.prec[cells.g[min]];
i = min;
}
const points = Rivers.addMeandering(dataRiver, 1, .5);
const widthFactor = rn(.8 + Math.random() * .4, 1); // river width modifier [.8, 1.2]
const sourceWidth = .1;
const points = Rivers.addMeandering(dataRiver, 1, 0.5);
const widthFactor = rn(0.8 + Math.random() * 0.4, 1); // river width modifier [.8, 1.2]
const sourceWidth = 0.1;
const [path, length, offset] = Rivers.getPath(points, widthFactor, sourceWidth);
rivers.append("path").attr("d", path).attr("id", "river"+river);
rivers
.append("path")
.attr("d", path)
.attr("id", "river" + river);
// add new river to data or change extended river attributes
const r = pack.rivers.find(r => r.i === river);
@ -555,10 +618,10 @@ function addRiverOnClick() {
const source = dataRiver[0].cell;
const width = rn(offset ** 2, 2); // mounth width in km
const name = Rivers.getName(mouth);
const smallLength = pack.rivers.map(r => r.length||0).sort((a,b) => a-b)[Math.ceil(pack.rivers.length * .15)];
const type = length < smallLength ? rw({"Creek":9, "River":3, "Brook":3, "Stream":1}) : "River";
const smallLength = pack.rivers.map(r => r.length || 0).sort((a, b) => a - b)[Math.ceil(pack.rivers.length * 0.15)];
const type = length < smallLength ? rw({Creek: 9, River: 3, Brook: 3, Stream: 1}) : "River";
pack.rivers.push({i:river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
pack.rivers.push({i: river, source, mouth, discharge, length, width, widthFactor, sourceWidth, parent, basin, name, type});
}
if (d3.event.shiftKey === false) {
@ -570,10 +633,13 @@ function addRiverOnClick() {
function toggleAddRoute() {
const pressed = document.getElementById("addRoute").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addRoute.classList.add('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);
@ -590,10 +656,13 @@ function addRouteOnClick() {
function toggleAddMarker() {
const pressed = document.getElementById("addMarker").classList.contains("pressed");
if (pressed) {unpressClickToAddButton(); return;}
if (pressed) {
unpressClickToAddButton();
return;
}
addFeature.querySelectorAll("button.pressed").forEach(b => b.classList.remove("pressed"));
addMarker.classList.add('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);
@ -602,27 +671,44 @@ function toggleAddMarker() {
function addMarkerOnClick() {
const point = d3.mouse(this);
const x = rn(point[0], 2), y = rn(point[1], 2);
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 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);
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();
}
function viewCellDetails() {
$("#cellInfo").dialog({
resizable: false, width: "22em", title: "Cell Details",
resizable: false,
width: "22em",
title: "Cell Details",
position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"}
});
}