Fantasy-Map-Generator/modules/ui/elevation-profile.js
2021-12-12 23:02:38 -08:00

389 lines
13 KiB
JavaScript

'use strict';
function showEPForRoute(node) {
const points = [];
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
const routeLen = node.getTotalLength() * distanceScaleInput.value;
showElevationProfile(points, routeLen, false);
}
function showEPForRiver(node) {
const points = [];
debug
.select('#controlPoints')
.selectAll('circle')
.each(function () {
const i = findCell(this.getAttribute('cx'), this.getAttribute('cy'));
points.push(i);
});
const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
showElevationProfile(points, riverLen, true);
}
function showElevationProfile(data, routeLen, isRiver) {
// data is an array of cell indexes, routeLen is the distance (in actual metres/feet), isRiver should be true for rivers, false otherwise
document.getElementById('epScaleRange').addEventListener('change', draw);
document.getElementById('epCurve').addEventListener('change', draw);
document.getElementById('epSave').addEventListener('click', downloadCSV);
$('#elevationProfile').dialog({
title: 'Elevation profile',
resizable: false,
width: window.width,
close: closeElevationProfile,
position: {my: 'left top', at: 'left+20 bottom-500', of: window, collision: 'fit'}
});
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
let slope = 0;
if (isRiver) {
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length - 1]]) {
slope = 1; // up-hill
} else if (pack.cells.h[data[0]] > pack.cells.h[data[data.length - 1]]) {
slope = -1; // down-hill
}
}
const chartWidth = window.innerWidth - 180,
chartHeight = 300; // height of our land/sea profile, excluding the biomes data below
const xOffset = 80,
yOffset = 80; // this is our drawing starting point from top-left (y = 0) of SVG
const biomesHeight = 40;
let lastBurgIndex = 0;
let lastBurgCell = 0;
let burgCount = 0;
let chartData = {biome: [], burg: [], cell: [], height: [], mi: 1000000, ma: 0, mih: 100, mah: 0, points: []};
for (let i = 0, prevB = 0, prevH = -1; i < data.length; i++) {
let cell = data[i];
let h = pack.cells.h[cell];
if (h < 20) {
const f = pack.features[pack.cells.f[cell]];
if (f.type === 'lake') h = f.height;
else h = 20;
}
// check for river up-hill
if (prevH != -1) {
if (isRiver) {
if (slope == 1 && h < prevH) h = prevH;
else if (slope == 0 && h != prevH) h = prevH;
else if (slope == -1 && h > prevH) h = prevH;
}
}
prevH = h;
let b = pack.cells.burg[cell];
if (b == prevB) b = 0;
else prevB = b;
if (b) {
burgCount++;
lastBurgIndex = i;
lastBurgCell = cell;
}
chartData.biome[i] = pack.cells.biome[cell];
chartData.burg[i] = b;
chartData.cell[i] = cell;
let sh = getHeight(h);
chartData.height[i] = parseInt(sh.substr(0, sh.indexOf(' ')));
chartData.mih = Math.min(chartData.mih, h);
chartData.mah = Math.max(chartData.mah, h);
chartData.mi = Math.min(chartData.mi, chartData.height[i]);
chartData.ma = Math.max(chartData.ma, chartData.height[i]);
}
if (lastBurgIndex != 0 && lastBurgCell == chartData.cell[data.length - 1] && lastBurgIndex < data.length - 1) {
chartData.burg[data.length - 1] = chartData.burg[lastBurgIndex];
chartData.burg[lastBurgIndex] = 0;
}
draw();
function downloadCSV() {
let data = 'Point,X,Y,Cell,Height,Height value,Population,Burg,Burg population,Biome,Biome color,Culture,Culture color,Religion,Religion color,Province,Province color,State,State color\n'; // headers
for (let k = 0; k < chartData.points.length; k++) {
let cell = chartData.cell[k];
let burg = pack.cells.burg[cell];
let biome = pack.cells.biome[cell];
let culture = pack.cells.culture[cell];
let religion = pack.cells.religion[cell];
let province = pack.cells.province[cell];
let state = pack.cells.state[cell];
let pop = pack.cells.pop[cell];
let h = pack.cells.h[cell];
data += k + 1 + ',';
data += chartData.points[k][0] + ',';
data += chartData.points[k][1] + ',';
data += cell + ',';
data += getHeight(h) + ',';
data += h + ',';
data += rn(pop * populationRate) + ',';
if (burg) {
data += pack.burgs[burg].name + ',';
data += pack.burgs[burg].population * populationRate * urbanization + ',';
} else {
data += ',0,';
}
data += biomesData.name[biome] + ',';
data += biomesData.color[biome] + ',';
data += pack.cultures[culture].name + ',';
data += pack.cultures[culture].color + ',';
data += pack.religions[religion].name + ',';
data += pack.religions[religion].color + ',';
data += pack.provinces[province].name + ',';
data += pack.provinces[province].color + ',';
data += pack.states[state].name + ',';
data += pack.states[state].color + ',';
data = data + '\n';
}
const name = getFileName('elevation profile') + '.csv';
downloadFile(data, name);
}
function draw() {
chartData.points = [];
let heightScale = 100 / parseInt(epScaleRange.value);
heightScale *= 0.9; // curves cause the heights to go slightly higher, adjust here
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
const yscale = d3
.scaleLinear()
.domain([0, chartData.ma * heightScale])
.range([chartHeight, 0]);
for (let i = 0; i < data.length; i++) {
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
}
document.getElementById('elevationGraph').innerHTML = '';
const chart = d3
.select('#elevationGraph')
.append('svg')
.attr('width', chartWidth + 120)
.attr('height', chartHeight + yOffset + biomesHeight)
.attr('id', 'elevationSVG')
.attr('class', 'epbackground');
// arrow-head definition
chart
.append('defs')
.append('marker')
.attr('id', 'arrowhead')
.attr('orient', 'auto')
.attr('markerWidth', '2')
.attr('markerHeight', '4')
.attr('refX', '0.1')
.attr('refY', '2')
.append('path')
.attr('d', 'M0,0 V4 L2,2 Z')
.attr('fill', 'darkgray');
let colors = getColorScheme();
const landdef = chart.select('defs').append('linearGradient').attr('id', 'landdef').attr('x1', '0%').attr('y1', '0%').attr('x2', '0%').attr('y2', '100%');
if (chartData.mah == chartData.mih) {
landdef
.append('stop')
.attr('offset', '0%')
.attr('style', 'stop-color:' + getColor(chartData.mih, colors) + ';stop-opacity:1');
landdef
.append('stop')
.attr('offset', '100%')
.attr('style', 'stop-color:' + getColor(chartData.mah, colors) + ';stop-opacity:1');
} else {
for (let k = chartData.mah; k >= chartData.mih; k--) {
let perc = 1 - (k - chartData.mih) / (chartData.mah - chartData.mih);
landdef
.append('stop')
.attr('offset', perc * 100 + '%')
.attr('style', 'stop-color:' + getColor(k, colors) + ';stop-opacity:1');
}
}
// land
let curve = d3.line().curve(d3.curveBasis); // see https://github.com/d3/d3-shape#curves
let epCurveIndex = parseInt(epCurve.selectedIndex);
switch (epCurveIndex) {
case 0:
curve = d3.line().curve(d3.curveLinear);
break;
case 1:
curve = d3.line().curve(d3.curveBasis);
break;
case 2:
curve = d3.line().curve(d3.curveBundle.beta(1));
break;
case 3:
curve = d3.line().curve(d3.curveCatmullRom.alpha(0.5));
break;
case 4:
curve = d3.line().curve(d3.curveMonotoneX);
break;
case 5:
curve = d3.line().curve(d3.curveNatural);
break;
}
// copy the points so that we can add extra straight pieces, else we get curves at the ends of the chart
let extra = chartData.points.slice();
let path = curve(extra);
// this completes the right-hand side and bottom of our land "polygon"
path += ' L' + parseInt(xscale(extra.length) + +xOffset) + ',' + parseInt(extra[extra.length - 1][1]);
path += ' L' + parseInt(xscale(extra.length) + +xOffset) + ',' + parseInt(yscale(0) + +yOffset);
path += ' L' + parseInt(xscale(0) + +xOffset) + ',' + parseInt(yscale(0) + +yOffset);
path += 'Z';
chart.append('g').attr('id', 'epland').append('path').attr('d', path).attr('stroke', 'purple').attr('stroke-width', '0').attr('fill', 'url(#landdef)');
// biome / heights
let g = chart.append('g').attr('id', 'epbiomes');
const hu = heightUnit.value;
for (let k = 0; k < chartData.points.length; k++) {
const x = chartData.points[k][0];
const y = yOffset + chartHeight;
const c = biomesData.color[chartData.biome[k]];
const cell = chartData.cell[k];
const culture = pack.cells.culture[cell];
const religion = pack.cells.religion[cell];
const province = pack.cells.province[cell];
const state = pack.cells.state[cell];
let pop = pack.cells.pop[cell];
if (chartData.burg[k]) {
pop += pack.burgs[chartData.burg[k]].population * urbanization;
}
const populationDesc = rn(pop * populationRate);
const provinceDesc = province ? ', ' + pack.provinces[province].name : '';
const dataTip =
biomesData.name[chartData.biome[k]] +
provinceDesc +
', ' +
pack.states[state].name +
', ' +
pack.religions[religion].name +
', ' +
pack.cultures[culture].name +
' (height: ' +
chartData.height[k] +
' ' +
hu +
', population ' +
populationDesc +
', cell ' +
chartData.cell[k] +
')';
g.append('rect').attr('stroke', c).attr('fill', c).attr('x', x).attr('y', y).attr('width', xscale(1)).attr('height', 15).attr('data-tip', dataTip);
}
const xAxis = d3
.axisBottom(xscale)
.ticks(10)
.tickFormat(function (d) {
return rn((d / chartData.points.length) * routeLen) + ' ' + distanceUnitInput.value;
});
const yAxis = d3
.axisLeft(yscale)
.ticks(5)
.tickFormat(function (d) {
return d + ' ' + hu;
});
const xGrid = d3.axisBottom(xscale).ticks(10).tickSize(-chartHeight).tickFormat('');
const yGrid = d3.axisLeft(yscale).ticks(5).tickSize(-chartWidth).tickFormat('');
chart
.append('g')
.attr('id', 'epxaxis')
.attr('transform', 'translate(' + xOffset + ',' + parseInt(chartHeight + +yOffset + 20) + ')')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'center')
.attr('transform', function (d) {
return 'rotate(0)'; // used to rotate labels, - anti-clockwise, + clockwise
});
chart
.append('g')
.attr('id', 'epyaxis')
.attr('transform', 'translate(' + parseInt(+xOffset - 10) + ',' + parseInt(+yOffset) + ')')
.call(yAxis);
// add the X gridlines
chart
.append('g')
.attr('id', 'epxgrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + parseInt(chartHeight + +yOffset) + ')')
.call(xGrid);
// add the Y gridlines
chart
.append('g')
.attr('id', 'epygrid')
.attr('class', 'epgrid')
.attr('stroke-dasharray', '4 1')
.attr('transform', 'translate(' + xOffset + ',' + yOffset + ')')
.call(yGrid);
// draw city labels - try to avoid putting labels over one another
g = chart.append('g').attr('id', 'epburglabels');
let y1 = 0;
const add = 15;
let xwidth = chartData.points[1][0] - chartData.points[0][0];
for (let k = 0; k < chartData.points.length; k++) {
if (chartData.burg[k] > 0) {
let b = chartData.burg[k];
let x1 = chartData.points[k][0]; // left side of graph by default
if (k > 0) x1 += xwidth / 2; // center it if not first
if (k == chartData.points.length - 1) x1 = chartWidth + xOffset; // right part of graph
y1 += add;
if (y1 >= yOffset) y1 = add;
// burg name
g.append('text')
.attr('id', 'ep' + b)
.attr('class', 'epburglabel')
.attr('x', x1)
.attr('y', y1)
.attr('text-anchor', 'middle');
document.getElementById('ep' + b).innerHTML = pack.burgs[b].name;
// arrow from burg name to graph line
g.append('path')
.attr('id', 'eparrow' + b)
.attr('d', 'M' + x1.toString() + ',' + (y1 + 3).toString() + 'L' + x1.toString() + ',' + parseInt(chartData.points[k][1] - 3).toString())
.attr('stroke', 'darkgray')
.attr('fill', 'lightgray')
.attr('stroke-width', '1')
.attr('marker-end', 'url(#arrowhead)');
}
}
}
function closeElevationProfile() {
document.getElementById('epScaleRange').removeEventListener('change', draw);
document.getElementById('epCurve').removeEventListener('change', draw);
document.getElementById('epSave').removeEventListener('click', downloadCSV);
document.getElementById('elevationGraph').innerHTML = '';
modules.elevation = false;
}
}