diff --git a/index.html b/index.html
index c3b0d294..c0618b86 100644
--- a/index.html
+++ b/index.html
@@ -2124,11 +2124,16 @@
+
+
@@ -2141,6 +2146,7 @@
+
@@ -3579,6 +3585,7 @@
+
diff --git a/modules/ui/elevation-profile.js b/modules/ui/elevation-profile.js
new file mode 100644
index 00000000..de19378f
--- /dev/null
+++ b/modules/ui/elevation-profile.js
@@ -0,0 +1,162 @@
+"use strict";
+
+function showEPForRoute(node) {
+ const points = [];
+ var prevB=0, i=0, j=0, b=0, ma=0, mi=100, h=0;
+ debug.select("#controlPoints").selectAll("circle").each(function() {
+ 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 = [];
+ var prevB=0, i=0, j=0, b=0, ma=0, mi=100, h=0;
+ debug.select("#controlPoints").selectAll("circle").each(function() {
+ i = findCell(this.getAttribute("cx"), this.getAttribute("cy"));
+ points.push(i);
+ });
+
+ const riverLen = (node.getTotalLength() / 2) * distanceScaleInput.value;
+
+ showElevationProfile(points, riverLen, true);
+}
+
+function resizeElevationProfile() {
+}
+
+function closeElevationProfile() {
+ modules.elevation = false;
+}
+
+function showElevationProfile(data, routeLen, isRiver) {
+// data is an array of cell indexes, routeLen is the distance, isRiver should be true for rivers, false otherwise
+ document.getElementById("elevationGraph").innerHTML = "";
+
+ $("#elevationProfile").dialog({
+ title: "Elevation profile", resizable: false,
+ width: window.width,
+ close: closeElevationProfile,
+ position: {my: "left top", at: "left+20 bottom-240", of: window, collision: "fit"}
+ });
+
+ // Azgaar asked if we can prevent river graphs from showing rivers as flowing uphill
+ var 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 points = [];
+ var prevB=0, prevH=-1, i=0, j=0, cell=0, b=0, ma=0, mi=100, h=0;
+ for (var i=0; i prevH) h = prevH;
+ }
+ }
+ prevH = h;
+ // river up-hill checks stop here
+
+ mi = Math.min(mi, h);
+ ma = Math.max(ma, h);
+
+ b = pack.cells.burg[cell];
+ if (b == prevB) b = 0;
+ else prevB = b;
+ points.push({x:j, y:h, b:b});
+ j++;
+ }
+
+ const heightDiff = ma-mi+1;
+
+
+// const w = document.getElementById("elevationProfile").clientWidth - 32;
+ const w = window.innerWidth-280;
+ h = 100;
+
+ const xOffset = 100;
+ const yOffset = 80;
+
+ var chart = d3.select("#elevationGraph").append("svg").attr("width", w+200).attr("height", h+yOffset).attr("id", "elevationGraph");
+ // 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");
+
+ // main graph line
+ var lineFunc = d3.line()
+ .x(function(d) { return d.x * w / points.length + xOffset})
+ .y(function(d) { return h-d.y + 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)");
+ }
+ });
+
+
+}
+
diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js
index 157c0dcd..85551976 100644
--- a/modules/ui/rivers-editor.js
+++ b/modules/ui/rivers-editor.js
@@ -33,6 +33,7 @@ function editRiver(id) {
document.getElementById("riverWidthHide").addEventListener("click", hideRiverWidth);
document.getElementById("riverWidthInput").addEventListener("input", changeWidth);
document.getElementById("riverIncrement").addEventListener("input", changeIncrement);
+ document.getElementById("riverElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("riverEditStyle").addEventListener("click", () => editStyle("rivers"));
document.getElementById("riverNew").addEventListener("click", toggleRiverCreationMode);
@@ -94,6 +95,10 @@ function editRiver(id) {
const [d, length] = Rivers.getPath(points, +riverWidthInput.value, +riverIncrement.value);
elSelected.attr("d", d);
updateRiverLength(length);
+
+ if (modules.elevation) {
+ showEPForRiver(elSelected.node());
+ }
}
function updateRiverLength(l = elSelected.node().getTotalLength() / 2) {
@@ -176,6 +181,11 @@ function editRiver(id) {
if (r) r.name = riverName.value = Names.getBase(rand(nameBases.length-1));
}
+ function showElevationProfile() {
+ modules.elevation = true;
+ showEPForRiver(node);
+ }
+
function showRiverWidth() {
document.querySelectorAll("#riverEditor > button").forEach(el => el.style.display = "none");
document.getElementById("riverWidthSection").style.display = "inline-block";
diff --git a/modules/ui/routes-editor.js b/modules/ui/routes-editor.js
index b1f30c67..98974f29 100644
--- a/modules/ui/routes-editor.js
+++ b/modules/ui/routes-editor.js
@@ -30,6 +30,7 @@ function editRoute(onClick) {
document.getElementById("routeGroupName").addEventListener("change", createNewGroup);
document.getElementById("routeGroupRemove").addEventListener("click", removeRouteGroup);
document.getElementById("routeGroupsHide").addEventListener("click", hideGroupSection);
+ document.getElementById("routeElevationProfile").addEventListener("click", showElevationProfile);
document.getElementById("routeEditStyle").addEventListener("click", editGroupStyle);
document.getElementById("routeSplit").addEventListener("click", toggleRouteSplitMode);
@@ -101,6 +102,15 @@ function editRoute(onClick) {
elSelected.attr("d", round(lineGen(points)));
const l = elSelected.node().getTotalLength();
routeLength.innerHTML = rn(l * distanceScaleInput.value) + " " + distanceUnitInput.value;
+
+ if (modules.elevation) {
+ showEPForRoute(elSelected.node());
+ }
+ }
+
+ function showElevationProfile() {
+ modules.elevation = true;
+ showEPForRoute(node);
}
function showGroupSection() {