diff --git a/index.html b/index.html
index 7f80d5cc..94a12ef2 100644
--- a/index.html
+++ b/index.html
@@ -612,6 +612,7 @@
id="toggleRoutes"
data-tip="Trade routes: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="U"
+ class="buttonoff"
onclick="toggleRoutes(event)"
>
Routes
diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js
index 85f623f9..ab156f18 100644
--- a/modules/dynamic/auto-update.js
+++ b/modules/dynamic/auto-update.js
@@ -843,4 +843,12 @@ export function resolveVersionConflicts(version) {
}
});
}
+
+ if (version < 1.98) {
+ // v1.98.00 changed routes generation algorithm and data format
+ // 1. cells.road => cells.route; 1 = MAIN; 2 = TRAIL; 3 = SEA;
+ // 2. cells.crossroad is removed
+ // 3. pack.routes is added
+ // 4. rendering is changed
+ }
}
diff --git a/modules/routes-generator.js b/modules/routes-generator.js
index de43511a..84c88eb4 100644
--- a/modules/routes-generator.js
+++ b/modules/routes-generator.js
@@ -134,15 +134,15 @@ window.Routes = (function () {
const routes = [];
for (const {feature, cells} of mainRoads) {
- routes.push({i: routes.length, type: "road", feature, cells});
+ routes.push({i: routes.length, group: "roads", feature, cells});
}
for (const {feature, cells} of trails) {
- routes.push({i: routes.length, type: "trail", feature, cells});
+ routes.push({i: routes.length, group: "trails", feature, cells});
}
for (const {feature, cells} of seaRoutes) {
- routes.push({i: routes.length, type: "sea", feature, cells});
+ routes.push({i: routes.length, group: "searoutes", feature, cells});
}
return routes;
diff --git a/modules/ui/layers.js b/modules/ui/layers.js
index 9db58faa..e48afffa 100644
--- a/modules/ui/layers.js
+++ b/modules/ui/layers.js
@@ -169,6 +169,7 @@ function restoreLayers() {
if (layerIsOn("toggleGrid")) drawGrid();
if (layerIsOn("toggleCoordinates")) drawCoordinates();
if (layerIsOn("toggleCompass")) compass.style("display", "block");
+ if (layerIsOn("toggleRoutes")) drawRoutes();
if (layerIsOn("toggleTemp")) drawTemp();
if (layerIsOn("togglePrec")) drawPrec();
if (layerIsOn("togglePopulation")) drawPopulation();
@@ -1624,18 +1625,137 @@ function drawRivers() {
function toggleRoutes(event) {
if (!layerIsOn("toggleRoutes")) {
turnButtonOn("toggleRoutes");
- $("#routes").fadeIn();
+ drawRoutes();
if (event && isCtrlClick(event)) editStyle("routes");
} else {
- if (event && isCtrlClick(event)) {
- editStyle("routes");
- return;
- }
- $("#routes").fadeOut();
+ if (event && isCtrlClick(event)) return editStyle("routes");
+ routes.selectAll("path").remove();
turnButtonOff("toggleRoutes");
}
}
+function drawRoutes() {
+ TIME && console.time("drawRoutes");
+ const {cells, burgs} = pack;
+ const lineGen = d3.line();
+
+ const SHARP_ANGLE = 135;
+ const VERY_SHARP_ANGLE = 115;
+
+ const points = adjustBurgPoints(); // mutable array of points
+ const routePaths = {};
+
+ const lineGenMap = {
+ roads: d3.curveCatmullRom.alpha(0.1),
+ trails: d3.curveCatmullRom.alpha(0.1),
+ searoutes: d3.curveBasis,
+ default: d3.curveCatmullRom.alpha(0.1)
+ };
+
+ for (const {i, group, cells} of pack.routes) {
+ if (group !== "searoutes") straightenPathAngles(cells); // mutates points
+ const pathPoints = getPathPoints(cells);
+
+ lineGen.curve(lineGenMap[group] || lineGenMap.default);
+ const path = round(lineGen(pathPoints), 1);
+
+ if (!routePaths[group]) routePaths[group] = [];
+ routePaths[group].push(``);
+ }
+
+ routes.selectAll("path").remove();
+ for (const group in routePaths) {
+ routes.select("#" + group).html(routePaths[group].join(""));
+ }
+
+ TIME && console.timeEnd("drawRoutes");
+
+ function adjustBurgPoints() {
+ const points = Array.from(cells.p);
+
+ for (const burg of burgs) {
+ if (burg.i === 0) continue;
+ const {cell, x, y} = burg;
+ points[cell] = [x, y];
+ }
+
+ return points;
+ }
+
+ function straightenPathAngles(cellIds) {
+ for (let i = 1; i < cellIds.length - 1; i++) {
+ const cellId = cellIds[i];
+ if (cells.burg[cellId]) continue;
+
+ const prev = points[cellIds[i - 1]];
+ const that = points[cellId];
+ const next = points[cellIds[i + 1]];
+
+ const dAx = prev[0] - that[0];
+ const dAy = prev[1] - that[1];
+ const dBx = next[0] - that[0];
+ const dBy = next[1] - that[1];
+ const angle = (Math.atan2(dAx * dBy - dAy * dBx, dAx * dBx + dAy * dBy) * 180) / Math.PI;
+
+ if (Math.abs(angle) < SHARP_ANGLE) {
+ const middleX = (prev[0] + next[0]) / 2;
+ const middleY = (prev[1] + next[1]) / 2;
+
+ if (Math.abs(angle) < VERY_SHARP_ANGLE) {
+ const newX = (that[0] + middleX * 2) / 3;
+ const newY = (that[1] + middleY * 2) / 3;
+ points[cellId] = [newX, newY];
+ continue;
+ }
+
+ const newX = (that[0] + middleX) / 2;
+ const newY = (that[1] + middleY) / 2;
+ points[cellId] = [newX, newY];
+ }
+ }
+ }
+
+ function getPathPoints(cellIds) {
+ const pathPoints = cellIds.map(cellId => points[cellId]);
+
+ if (pathPoints.length === 2) {
+ // curve and shorten 2-points line
+ const [[x1, y1], [x2, y2]] = pathPoints;
+
+ const middleX = (x1 + x2) / 2;
+ const middleY = (y1 + y2) / 2;
+
+ // add shifted point at the middle to curve the line a bit
+ const NORMAL_LENGTH = 0.3;
+ const normal = getNormal([x1, y1], [x2, y2]);
+ const sign = cellIds[0] % 2 ? 1 : -1;
+ const normalX = middleX + NORMAL_LENGTH * Math.cos(normal) * sign;
+ const normalY = middleY + NORMAL_LENGTH * Math.sin(normal) * sign;
+
+ // make line shorter to avoid overlapping with other lines
+ const SHORT_LINE_LENGTH_MODIFIER = 0.8;
+ const distX = x2 - x1;
+ const distY = y2 - y1;
+ const nx1 = x1 + distX * SHORT_LINE_LENGTH_MODIFIER;
+ const ny1 = y1 + distY * SHORT_LINE_LENGTH_MODIFIER;
+ const nx2 = x2 - distX * SHORT_LINE_LENGTH_MODIFIER;
+ const ny2 = y2 - distY * SHORT_LINE_LENGTH_MODIFIER;
+
+ return [
+ [nx1, ny1],
+ [normalX, normalY],
+ [nx2, ny2]
+ ];
+ }
+
+ return pathPoints;
+ }
+
+ function getNormal([x1, y1], [x2, y2]) {
+ return Math.atan2(y1 - y2, x1 - x2) + Math.PI / 2;
+ }
+}
+
function toggleMilitary() {
if (!layerIsOn("toggleMilitary")) {
turnButtonOn("toggleMilitary");