mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
Added colour, biomes, adjustable height, and changed graph code to use d3 more
This commit is contained in:
parent
a21e7f5ea4
commit
9f54b1e3f0
5 changed files with 174 additions and 83 deletions
10
index.css
10
index.css
|
|
@ -2101,6 +2101,16 @@ svg.button {
|
||||||
text-shadow: 0px 1px 4px #4c3a35;
|
text-shadow: 0px 1px 4px #4c3a35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.epgrid line {
|
||||||
|
stroke: lightgrey;
|
||||||
|
stroke-opacity: 0.7;
|
||||||
|
shape-rendering: crispEdges;
|
||||||
|
}
|
||||||
|
|
||||||
|
.epgrid path {
|
||||||
|
stroke-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#debug {
|
#debug {
|
||||||
font-size: 1px;
|
font-size: 1px;
|
||||||
opacity: .8;
|
opacity: .8;
|
||||||
|
|
|
||||||
16
index.html
16
index.html
|
|
@ -2134,6 +2134,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="elevationProfile" class="dialog" style="display: none" width="100%">
|
<div id="elevationProfile" class="dialog" style="display: none" width="100%">
|
||||||
|
<div data-tip="Set height scale">
|
||||||
|
<div>Height scale:</div>
|
||||||
|
<div><input id="epScaleRange" type="range" min=1 max=100 value=50></div>
|
||||||
|
|
||||||
|
<div>Curve:</div>
|
||||||
|
<div>
|
||||||
|
<select id="epCurve">
|
||||||
|
<option>Linear</option>
|
||||||
|
<option selected>Basis spline</option>
|
||||||
|
<option>Bundle</option>
|
||||||
|
<option>Cubic Catmull-Rom</option>
|
||||||
|
<option>Monotone X</option>
|
||||||
|
<option>Natural</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="elevationGraph" data-tip="Elevation profile"></div>
|
<div id="elevationGraph" data-tip="Elevation profile"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function showEPForRoute(node) {
|
function showEPForRoute(node) {
|
||||||
const points = [];
|
const points = [];
|
||||||
debug.select("#controlPoints").selectAll("circle").each(function() {
|
debug.select("#controlPoints").selectAll("circle").each(function() {
|
||||||
|
|
@ -21,19 +22,23 @@ function showEPForRiver(node) {
|
||||||
showElevationProfile(points, riverLen, true);
|
showElevationProfile(points, riverLen, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resizeElevationProfile() {
|
function closeElevationProfile() {
|
||||||
|
modules.elevation = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showElevationProfile(data, routeLen, isRiver) {
|
function showElevationProfile(data, routeLen, isRiver) {
|
||||||
// data is an array of cell indexes, routeLen is the distance, isRiver should be true for rivers, false otherwise
|
// 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("elevationGraph").innerHTML = "";
|
|
||||||
|
document.getElementById("epScaleRange").addEventListener("change", draw);
|
||||||
|
document.getElementById("epCurve").addEventListener("change", draw);
|
||||||
|
|
||||||
$("#elevationProfile").dialog({
|
$("#elevationProfile").dialog({
|
||||||
title: "Elevation profile", resizable: false, width: window.width,
|
title: "Elevation profile", resizable: false, width: window.width,
|
||||||
position: {my: "left top", at: "left+20 bottom-240", of: window, collision: "fit"}
|
close: closeElevationProfile,
|
||||||
|
position: {my: "left top", at: "left+20 bottom-500", of: window, collision: "fit"}
|
||||||
});
|
});
|
||||||
|
|
||||||
// prevent river graphs from showing rivers as flowing uphill
|
// prevent river graphs from showing rivers as flowing uphill - remember the general slope
|
||||||
var slope = 0;
|
var slope = 0;
|
||||||
if (isRiver) {
|
if (isRiver) {
|
||||||
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length-1]]) {
|
if (pack.cells.h[data[0]] < pack.cells.h[data[data.length-1]]) {
|
||||||
|
|
@ -43,12 +48,19 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = [];
|
const chartWidth = window.innerWidth-280;
|
||||||
var prevB=0, prevH=-1, i=0, j=0, cell=0, b=0, ma=0, mi=100, h=0;
|
const chartHeight = 200; // height of our land/sea profile, excluding the biomes data below
|
||||||
for (var i=0; i<data.length; i++) {
|
|
||||||
cell = data[i];
|
|
||||||
|
|
||||||
h = pack.cells.h[cell];
|
const xOffset = 160;
|
||||||
|
const yOffset = 140; // this is our drawing starting point from top-left (y = 0) of SVG
|
||||||
|
|
||||||
|
const biomesHeight = 40;
|
||||||
|
|
||||||
|
let chartData = {biome:[], burg:[], cell:[], height:[], mi:1000000, ma: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) h = 20;
|
if (h < 20) h = 20;
|
||||||
|
|
||||||
// check for river up-hill
|
// check for river up-hill
|
||||||
|
|
@ -62,83 +74,136 @@ function showElevationProfile(data, routeLen, isRiver) {
|
||||||
prevH = h;
|
prevH = h;
|
||||||
// river up-hill checks stop here
|
// river up-hill checks stop here
|
||||||
|
|
||||||
mi = Math.min(mi, h);
|
let b = pack.cells.burg[cell];
|
||||||
ma = Math.max(ma, h);
|
|
||||||
|
|
||||||
b = pack.cells.burg[cell];
|
|
||||||
if (b == prevB) b = 0;
|
if (b == prevB) b = 0;
|
||||||
else prevB = b;
|
else prevB = b;
|
||||||
points.push({x:j, y:h, b:b});
|
|
||||||
j++;
|
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.mi = Math.min(chartData.mi, chartData.height[i]);
|
||||||
|
chartData.ma = Math.max(chartData.ma, chartData.height[i]);
|
||||||
}
|
}
|
||||||
|
draw();
|
||||||
|
|
||||||
const w = window.innerWidth-280;
|
function draw() {
|
||||||
h = 100;
|
chartData.points = [];
|
||||||
|
let heightScale = 100 / parseInt(epScaleRange.value);
|
||||||
|
|
||||||
const xOffset = 100;
|
heightScale *= 0.90; // curves cause the heights to go slightly higher, adjust here
|
||||||
const yOffset = 80;
|
|
||||||
|
|
||||||
var chart = d3.select("#elevationGraph").append("svg").attr("width", w+200).attr("height", h+yOffset).attr("id", "elevationGraph");
|
const xscale = d3.scaleLinear().domain([0, data.length]).range([0, chartWidth]);
|
||||||
// arrow-head definition
|
const yscale = d3.scaleLinear().domain([0, (chartData.ma-chartData.mi) * heightScale]).range([chartHeight, 0]);
|
||||||
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");
|
|
||||||
|
|
||||||
// main graph line
|
for (let i=0; i<data.length; i++) {
|
||||||
var lineFunc = d3.line().x(d => d.x * w / points.length + xOffset).y(d => h-d.y + yOffset);
|
chartData.points.push([xscale(i) + xOffset, yscale(chartData.height[i]) + yOffset]);
|
||||||
chart.append("path").attr("d", lineFunc(points)).attr("stroke", "purple").attr("fill", "none").attr("id", "elevationLine");
|
|
||||||
|
|
||||||
// y-axis labels for starting and ending heights
|
|
||||||
chart.append("text").attr("id", "epy0").attr("x", xOffset-10).attr("y", h-points[0].y + yOffset).attr("text-anchor", "end");
|
|
||||||
document.getElementById("epy0").innerHTML = getHeight(points[0].y);
|
|
||||||
chart.append("text").attr("id", "epy1").attr("x", w+100).attr("y", h-points[points.length-1].y + yOffset).attr("text-anchor", "start");
|
|
||||||
document.getElementById("epy1").innerHTML = getHeight(points[points.length-1].y);
|
|
||||||
|
|
||||||
// y-axis labels for minimum and maximum heights (if not too close to start/end heights)
|
|
||||||
if (Math.abs(ma - points[0].y) > 3 && Math.abs(ma - points[points.length-1].y) > 3) {
|
|
||||||
chart.append("text").attr("id", "epy2").attr("x", xOffset-10).attr("y", h-ma + yOffset).attr("text-anchor", "end");
|
|
||||||
document.getElementById("epy2").innerHTML = getHeight(ma);
|
|
||||||
}
|
|
||||||
if (Math.abs(mi - points[0].y) > 3 && Math.abs(mi - points[points.length-1].y) > 3) {
|
|
||||||
chart.append("text").attr("id", "epy3").attr("x", xOffset-10).attr("y", h-mi + yOffset).attr("text-anchor", "end");
|
|
||||||
document.getElementById("epy3").innerHTML = getHeight(mi);
|
|
||||||
}
|
|
||||||
|
|
||||||
// x-axis label for start, quarter, halfway and three-quarter, and end
|
|
||||||
chart.append("text").attr("id", "epx1").attr("x", xOffset).attr("y", h+yOffset).attr("text-anchor", "middle");
|
|
||||||
chart.append("text").attr("id", "epx2").attr("x", w / 4 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle");
|
|
||||||
chart.append("text").attr("id", "epx3").attr("x", w / 2 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle");
|
|
||||||
chart.append("text").attr("id", "epx4").attr("x", w / 4*3 + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle");
|
|
||||||
chart.append("text").attr("id", "epx5").attr("x", w + xOffset).attr("y", h+yOffset).attr("text-anchor", "middle");
|
|
||||||
document.getElementById("epx1").innerHTML = "0 " + distanceUnitInput.value;
|
|
||||||
document.getElementById("epx2").innerHTML = rn(routeLen / 4) + " " + distanceUnitInput.value;
|
|
||||||
document.getElementById("epx3").innerHTML = rn(routeLen / 2) + " " + distanceUnitInput.value;
|
|
||||||
document.getElementById("epx4").innerHTML = rn(routeLen / 4*3) + " " + distanceUnitInput.value;
|
|
||||||
document.getElementById("epx5").innerHTML = rn(routeLen) + " " + distanceUnitInput.value;
|
|
||||||
|
|
||||||
chart.append("path").attr("id", "epx11").attr("d", "M" + (xOffset).toString() + ",0L" + (xOffset).toString() +"," + (h+yOffset-15).toString()).attr("stroke", "lightgray").attr("stroke-width", "1");
|
|
||||||
chart.append("path").attr("id", "epx12").attr("d", "M" + (w / 4 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 4 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1");
|
|
||||||
chart.append("path").attr("id", "epx13").attr("d", "M" + (w / 2 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 2 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1");
|
|
||||||
chart.append("path").attr("id", "epx14").attr("d", "M" + (w / 4*3 + xOffset).toString() + "," + (h+yOffset-15).toString() + "L" + (w / 4*3 + xOffset).toString() + ",0").attr("stroke", "lightgray").attr("stroke-width", "1");
|
|
||||||
chart.append("path").attr("id", "epx15").attr("d", "M" + (w + xOffset).toString() + ",0L" + (w + xOffset).toString() +"," + (h+yOffset-15).toString()).attr("stroke", "lightgray").attr("stroke-width", "1");
|
|
||||||
|
|
||||||
// draw city labels - try to avoid putting labels over one another
|
|
||||||
var y1 = 0;
|
|
||||||
var add = 15;
|
|
||||||
points.forEach(function(p) {
|
|
||||||
if (p.b > 0) {
|
|
||||||
var x1 = p.x * w / points.length + xOffset;
|
|
||||||
y1+=add;
|
|
||||||
if (y1 >= yOffset) { y1 = add; }
|
|
||||||
var d1 = 0;
|
|
||||||
|
|
||||||
// burg name
|
|
||||||
chart.append("text").attr("id", "ep" + p.b).attr("x", x1).attr("y", y1).attr("text-anchor", "middle");
|
|
||||||
document.getElementById("ep" + p.b).innerHTML = pack.burgs[p.b].name;
|
|
||||||
|
|
||||||
// arrow from burg name to graph line
|
|
||||||
chart.append("path").attr("id", "eparrow" + p.b).attr("d", "M" + x1.toString() + "," + (y1).toString() + "L" + x1.toString() + "," + parseInt(h-p.y-3+yOffset).toString()).attr("stroke", "black").attr("stroke-width", "1").attr("marker-end", "url(#arrowhead)");
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
document.getElementById("elevationGraph").innerHTML = "";
|
||||||
|
|
||||||
|
const chart = d3.select("#elevationGraph").append("svg").attr("width", chartWidth+200).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");
|
||||||
|
|
||||||
|
var landdef = chart.select("defs").append("linearGradient").attr("id", "landdef").attr("x1", "0%").attr("y1", "0%").attr("x2", "0%").attr("y2", "100%");
|
||||||
|
landdef.append("stop").attr("offset", "0%").attr("style", "stop-color:rgb(64,255,128);stop-opacity:1");
|
||||||
|
landdef.append("stop").attr("offset", "100%").attr("style", "stop-color:rgb(0,192,64);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();
|
||||||
|
var 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(var 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 dataTip = biomesData.name[chartData.biome[k]]+" (" + chartData.height[k] + " " + hu + ")";
|
||||||
|
|
||||||
|
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");
|
||||||
|
var y1 = 0;
|
||||||
|
var add = 15;
|
||||||
|
for (let k=0; k<chartData.points.length; k++)
|
||||||
|
{
|
||||||
|
if (chartData.burg[k] > 0) {
|
||||||
|
let b = chartData.burg[k];
|
||||||
|
|
||||||
|
var x1 = chartData.points[k][0];
|
||||||
|
y1+=add;
|
||||||
|
if (y1 >= yOffset) { y1 = add; }
|
||||||
|
var d1 = 0;
|
||||||
|
|
||||||
|
// 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)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ function editRiver(id) {
|
||||||
|
|
||||||
function drawControlPoints(node) {
|
function drawControlPoints(node) {
|
||||||
const l = node.getTotalLength() / 2;
|
const l = node.getTotalLength() / 2;
|
||||||
const segments = Math.ceil(l / 8);
|
const segments = Math.ceil(l / 4);
|
||||||
const increment = rn(l / segments * 1e5);
|
const increment = rn(l / segments * 1e5);
|
||||||
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
for (let i=increment*segments, c=i; i >= 0; i -= increment, c += increment) {
|
||||||
const p1 = node.getPointAtLength(i / 1e5);
|
const p1 = node.getPointAtLength(i / 1e5);
|
||||||
|
|
@ -183,7 +183,7 @@ function editRiver(id) {
|
||||||
|
|
||||||
function showElevationProfile() {
|
function showElevationProfile() {
|
||||||
modules.elevation = true;
|
modules.elevation = true;
|
||||||
showEPForRiver(node);
|
showEPForRiver(elSelected.node());
|
||||||
}
|
}
|
||||||
|
|
||||||
function showRiverWidth() {
|
function showRiverWidth() {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function editRoute(onClick) {
|
||||||
|
|
||||||
function drawControlPoints(node) {
|
function drawControlPoints(node) {
|
||||||
const l = node.getTotalLength();
|
const l = node.getTotalLength();
|
||||||
const increment = l / Math.ceil(l / 8);
|
const increment = l / Math.ceil(l / 4);
|
||||||
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
for (let i=0; i <= l; i += increment) {addControlPoint(node.getPointAtLength(i));}
|
||||||
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
|
||||||
}
|
}
|
||||||
|
|
@ -110,7 +110,7 @@ function editRoute(onClick) {
|
||||||
|
|
||||||
function showElevationProfile() {
|
function showElevationProfile() {
|
||||||
modules.elevation = true;
|
modules.elevation = true;
|
||||||
showEPForRoute(node);
|
showEPForRoute(elSelected.node());
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGroupSection() {
|
function showGroupSection() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue