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");