diff --git a/modules/resample.js b/modules/resample.js index 8174a349..86da378e 100644 --- a/modules/resample.js +++ b/modules/resample.js @@ -10,6 +10,7 @@ window.Resample = (function () { */ function process({projection, inverse, scale}) { const parentMap = {grid: deepCopy(grid), pack: deepCopy(pack), notes: deepCopy(notes)}; + const riversData = saveRiversData(pack.rivers); grid = generateGrid(); pack = {}; @@ -30,7 +31,7 @@ window.Resample = (function () { createDefaultRuler(); restoreCellData(parentMap, inverse, scale); - restoreRivers(parentMap, projection, scale); + restoreRivers(riversData, projection, scale); restoreCultures(parentMap, projection); restoreBurgs(parentMap, projection, scale); restoreStates(parentMap, projection); @@ -104,28 +105,30 @@ window.Resample = (function () { } } - function restoreRivers(parentMap, projection, scale) { + function saveRiversData(parentRivers) { + return parentRivers.map(river => { + const meanderedPoints = Rivers.addMeandering(river.cells, river.points); + return {...river, meanderedPoints}; + }); + } + + function restoreRivers(riversData, projection, scale) { pack.cells.r = new Uint16Array(pack.cells.i.length); pack.cells.conf = new Uint8Array(pack.cells.i.length); - const offset = grid.spacing * 2; - const getCellCost = cellId => { - if (pack.cells.h[cellId] < 20) return Infinity; - return pack.cells.h[cellId]; - }; - - pack.rivers = parentMap.pack.rivers + pack.rivers = riversData .map(river => { - const parentPoints = river.points || river.cells.map(cellId => parentMap.pack.cells.p[cellId]); - const newPoints = parentPoints - .map(([parentX, parentY]) => { - const [x, y] = projection(parentX, parentY); - return isInMap(x, y, offset) ? [rn(x, 2), rn(y, 2)] : null; - }) - .filter(Boolean); - if (newPoints.length < 2) return null; + let wasInMap = true; + const points = []; + + river.meanderedPoints.forEach(([parentX, parentY]) => { + const [x, y] = projection(parentX, parentY); + const inMap = isInMap(x, y); + if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]); + wasInMap = inMap; + }); + if (points.length < 2) return null; - const points = addIntermidiatePoints(newPoints, getCellCost); const cells = points.map(point => findCell(...point)); cells.forEach(cellId => { if (pack.cells.r[cellId]) pack.cells.conf[cellId] = 1; @@ -218,20 +221,17 @@ window.Resample = (function () { } function restoreRoutes(parentMap, projection) { - const offset = grid.spacing * 2; - pack.routes = parentMap.pack.routes .map(route => { - const points = route.points - .map(([parentX, parentY]) => { - const [x, y] = projection(parentX, parentY); - if (!isInMap(x, y, offset)) return null; - - const cell = findCell(x, y); - return [rn(x, 2), rn(y, 2), cell]; - }) - .filter(Boolean); + let wasInMap = true; + const points = []; + route.points.forEach(([parentX, parentY]) => { + const [x, y] = projection(parentX, parentY); + const inMap = isInMap(x, y); + if (inMap || wasInMap) points.push([rn(x, 2), rn(y, 2)]); + wasInMap = inMap; + }); if (points.length < 2) return null; const firstCell = points[0][2]; @@ -333,29 +333,12 @@ window.Resample = (function () { ); } - // fill gaps in points array with intermidiate points - function addIntermidiatePoints(points, getCellCost) { - const newPoints = []; - - for (let i = 0; i < points.length; i++) { - newPoints.push(points[i]); - if (points[i + 1]) { - const start = findCell(...points[i]); - const exit = findCell(...points[i + 1]); - const pathCells = findPath(start, exit, getCellCost); - if (pathCells) newPoints.push(...pathCells.map(cellId => pack.cells.p[cellId])); - } - } - - return newPoints; - } - function isWater(graph, cellId) { return graph.cells.h[cellId] < 20; } - function isInMap(x, y, offset = 0) { - return x + offset >= 0 && x - offset <= graphWidth && y + offset >= 0 && y - offset <= graphHeight; + function isInMap(x, y) { + return x >= 0 && x <= graphWidth && y >= 0 && y <= graphHeight; } return {process}; diff --git a/modules/river-generator.js b/modules/river-generator.js index 8ce926fc..9e1f15eb 100644 --- a/modules/river-generator.js +++ b/modules/river-generator.js @@ -401,6 +401,7 @@ window.Rivers = (function () { // build polygon from a list of points and calculated offset (width) const getRiverPath = (points, widthFactor, startingWidth) => { + lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const riverPointsLeft = []; const riverPointsRight = []; let flux = 0; diff --git a/modules/routes-generator.js b/modules/routes-generator.js index 318ea7c6..1f378310 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -1,6 +1,15 @@ const ROUTES_SHARP_ANGLE = 135; const ROUTES_VERY_SHARP_ANGLE = 115; +const MIN_PASSABLE_SEA_TEMP = -4; +const ROUTE_TYPE_MODIFIERS = { + "-1": 1, // coastline + "-2": 1.8, // sea + "-3": 4, // open sea + "-4": 6, // ocean + default: 8 // far ocean +}; + window.Routes = (function () { function generate(lockedRoutes = []) { const {capitalsByFeature, burgsByFeature, portsByFeature} = sortBurgsByFeature(pack.burgs); @@ -118,10 +127,8 @@ window.Routes = (function () { } function findPathSegments({isWater, connections, start, exit}) { - const from = findPath(isWater, start, exit, connections); - if (!from) return []; - - const pathCells = restorePath(start, exit, from); + const getCost = createCostEvaluator({isWater, connections}); + const pathCells = findPath(start, current => current === exit, getCost); const segments = getRouteSegments(pathCells, connections); return segments; } @@ -174,6 +181,38 @@ window.Routes = (function () { } } + function createCostEvaluator({isWater, connections}) { + return isWater ? getWaterPathCost : getLandPathCost; + + function getLandPathCost(current, next) { + if (pack.cells.h[next] < 20) return Infinity; // ignore water cells + + const habitability = biomesData.habitability[pack.cells.biome[next]]; + if (!habitability) return Infinity; // inhabitable cells are not passable (e.g. glacier) + + const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]); + const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1]; + const heightModifier = 1 + Math.max(pack.cells.h[next] - 25, 25) / 25; // [1, 3]; + const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1; + const burgModifier = pack.cells.burg[next] ? 1 : 3; + + const pathCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier; + return pathCost; + } + + function getWaterPathCost(current, next) { + if (pack.cells.h[next] >= 20) return Infinity; // ignore land cells + if (grid.cells.temp[pack.cells.g[next]] < MIN_PASSABLE_SEA_TEMP) return Infinity; // ignore too cold cells + + const distanceCost = dist2(pack.cells.p[current], pack.cells.p[next]); + const typeModifier = ROUTE_TYPE_MODIFIERS[pack.cells.t[next]] || ROUTE_TYPE_MODIFIERS.default; + const connectionModifier = connections.has(`${current}-${next}`) ? 0.5 : 1; + + const pathCost = distanceCost * typeModifier * connectionModifier; + return pathCost; + } + } + function buildLinks(routes) { const links = {}; @@ -249,109 +288,6 @@ window.Routes = (function () { return data; // [[x, y, cell], [x, y, cell]]; } - const MIN_PASSABLE_SEA_TEMP = -4; - const TYPE_MODIFIERS = { - "-1": 1, // coastline - "-2": 1.8, // sea - "-3": 4, // open sea - "-4": 6, // ocean - default: 8 // far ocean - }; - - function findPath(isWater, start, exit, connections) { - const {temp} = grid.cells; - const {cells} = pack; - - const from = []; - const cost = []; - const queue = new FlatQueue(); - queue.push(start, 0); - - return isWater ? findWaterPath() : findLandPath(); - - function findLandPath() { - while (queue.length) { - const priority = queue.peekValue(); - const next = queue.pop(); - - for (const neibCellId of cells.c[next]) { - if (neibCellId === exit) { - from[neibCellId] = next; - return from; - } - - if (cells.h[neibCellId] < 20) continue; // ignore water cells - const habitability = biomesData.habitability[cells.biome[neibCellId]]; - if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier) - - const distanceCost = dist2(cells.p[next], cells.p[neibCellId]); - const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1]; - const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3]; - const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2; - const burgModifier = cells.burg[neibCellId] ? 1 : 3; - - const cellCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier; - const totalCost = priority + cellCost; - - if (totalCost >= cost[neibCellId]) continue; - from[neibCellId] = next; - cost[neibCellId] = totalCost; - queue.push(neibCellId, totalCost); - } - } - - return null; // path is not found - } - - function findWaterPath() { - while (queue.length) { - const priority = queue.peekValue(); - const next = queue.pop(); - - for (const neibCellId of cells.c[next]) { - if (neibCellId === exit) { - from[neibCellId] = next; - return from; - } - - if (cells.h[neibCellId] >= 20) continue; // ignore land cells - if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells - - const distanceCost = dist2(cells.p[next], cells.p[neibCellId]); - const typeModifier = TYPE_MODIFIERS[cells.t[neibCellId]] || TYPE_MODIFIERS.default; - const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 2; - - const cellsCost = distanceCost * typeModifier * connectionModifier; - const totalCost = priority + cellsCost; - - if (totalCost >= cost[neibCellId]) continue; - from[neibCellId] = next; - cost[neibCellId] = totalCost; - queue.push(neibCellId, totalCost); - } - } - - return null; // path is not found - } - } - - function restorePath(start, end, from) { - const cells = []; - - let current = end; - let prev = end; - - while (current !== start) { - cells.push(current); - prev = from[current]; - current = prev; - } - - cells.push(current); - - return cells; - } - function getRouteSegments(pathCells, connections) { const segments = []; let segment = []; @@ -422,21 +358,16 @@ window.Routes = (function () { // connect cell with routes system by land function connect(cellId) { - if (isConnected(cellId)) return; + const getCost = createCostEvaluator({isWater: false, connections: new Map()}); + const pathCells = findPath(cellId, isConnected, getCost); + if (!pathCells) return; - const {cells, routes} = pack; - - const path = findConnectionPath(cellId); - if (!path) return; - - const pathCells = restorePath(...path); const pointsArray = preparePointsArray(); const points = getPoints("trails", pathCells, pointsArray); - const feature = cells.f[cellId]; - + const feature = pack.cells.f[cellId]; const routeId = getNextId(); const newRoute = {i: routeId, group: "trails", feature, points}; - routes.push(newRoute); + pack.routes.push(newRoute); for (let i = 0; i < pathCells.length; i++) { const cellId = pathCells[i]; @@ -446,43 +377,6 @@ window.Routes = (function () { return newRoute; - function findConnectionPath(start) { - const from = []; - const cost = []; - const queue = new FlatQueue(); - queue.push(start, 0); - - while (queue.length) { - const priority = queue.peekValue(); - const next = queue.pop(); - - for (const neibCellId of cells.c[next]) { - if (isConnected(neibCellId)) { - from[neibCellId] = next; - return [start, neibCellId, from]; - } - - if (cells.h[neibCellId] < 20) continue; // ignore water cells - const habitability = biomesData.habitability[cells.biome[neibCellId]]; - if (!habitability) continue; // inhabitable cells are not passable (eg. lava, glacier) - - const distanceCost = dist2(cells.p[next], cells.p[neibCellId]); - const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1]; - const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3]; - - const cellsCost = distanceCost * habitabilityModifier * heightModifier; - const totalCost = priority + cellsCost; - - if (totalCost >= cost[neibCellId]) continue; - from[neibCellId] = next; - cost[neibCellId] = totalCost; - queue.push(neibCellId, totalCost); - } - } - - return null; // path is not found - } - function addConnection(from, to, routeId) { const routes = pack.cells.routes; diff --git a/modules/ui/layers.js b/modules/ui/layers.js index 86691478..1591a778 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -796,14 +796,12 @@ function drawRivers() { TIME && console.time("drawRivers"); rivers.selectAll("*").remove(); - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => { if (!cells || cells.length < 2) return; if (points && points.length !== cells.length) { console.error( - `River ${i} has ${cells.length} cells, but only ${points.length} points defined.`, - "Resetting points data" + `River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data` ); points = undefined; } diff --git a/modules/ui/rivers-creator.js b/modules/ui/rivers-creator.js index 8c01565a..a88581a5 100644 --- a/modules/ui/rivers-creator.js +++ b/modules/ui/rivers-creator.js @@ -122,8 +122,6 @@ function createRiver() { }); const id = "river" + riverId; - // render river - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); viewbox .select("#rivers") .append("path") diff --git a/modules/ui/rivers-editor.js b/modules/ui/rivers-editor.js index 3944fdb6..a00d488b 100644 --- a/modules/ui/rivers-editor.js +++ b/modules/ui/rivers-editor.js @@ -164,11 +164,9 @@ function editRiver(id) { river.points = debug.selectAll("#controlPoints > *").data(); river.cells = river.points.map(([x, y]) => findCell(x, y)); - const {widthFactor, sourceWidth} = river; - const meanderedPoints = Rivers.addMeandering(river.cells, river.points); - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth); + const meanderedPoints = Rivers.addMeandering(river.cells, river.points); + const path = Rivers.getRiverPath(meanderedPoints, river.widthFactor, river.sourceWidth); elSelected.attr("d", path); updateRiverLength(river); diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 67c4ae4e..5c67ca0f 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -793,7 +793,6 @@ function addRiverOnClick() { } // render river - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); const path = Rivers.getRiverPath(meanderedPoints, widthFactor, sourceWidth); const id = "river" + riverId; const riversG = viewbox.select("#rivers"); diff --git a/utils/pathUtils.js b/utils/pathUtils.js index 8c717314..deafd678 100644 --- a/utils/pathUtils.js +++ b/utils/pathUtils.js @@ -180,42 +180,36 @@ function connectVertices({vertices, startingVertex, ofSameType, addToChecked, cl /** * Finds the shortest path between two cells using a cost-based pathfinding algorithm. * @param {number} start - The ID of the starting cell. - * @param {number} exit - The ID of the destination cell. - * @param {function} getCellCost - A function that returns the cost of a cell. Should return Infinity for impassable cells. - * @returns {number[]|null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same. + * @param {(id: number) => boolean} isExit - A function that returns true if the cell is the exit cell. + * @param {(current: number, next: number) => number} getCost - A function that returns the path cost from current cell to the next cell. Must return `Infinity` for impassable connections. + * @returns {number[] | null} An array of cell IDs of the path from start to exit, or null if no path is found or start and exit are the same. */ -function findPath(start, exit, getCellCost) { - if (start === exit) return null; +function findPath(start, isExit, getCost) { + if (isExit(start)) return null; const from = []; const cost = []; const queue = new FlatQueue(); queue.push(start, 0); - let iteration = 0; - while (queue.length) { - const priority = queue.peekValue(); - const next = queue.pop(); - iteration++; - console.log(iteration, next); + const currentCost = queue.peekValue(); + const current = queue.pop(); - for (const neibCellId of pack.cells.c[next]) { - if (neibCellId === exit) { - from[neibCellId] = next; - return restorePath(start, exit, from); + for (const next of pack.cells.c[current]) { + if (isExit(next)) { + from[next] = current; + return restorePath(next, start, from); } - const cellCost = getCellCost(neibCellId); - if (cellCost === Infinity) continue; // impassable cell + const nextCost = getCost(current, next); + if (nextCost === Infinity) continue; // impassable cell + const totalCost = currentCost + nextCost; - const distanceCost = dist2(pack.cells.p[next], pack.cells.p[neibCellId]); - const totalCost = priority + distanceCost + getCellCost(neibCellId); - - if (totalCost >= cost[neibCellId]) continue; - from[neibCellId] = next; - cost[neibCellId] = totalCost; - queue.push(neibCellId, totalCost); + if (totalCost >= cost[next]) continue; // has cheaper path + from[next] = current; + cost[next] = totalCost; + queue.push(next, totalCost); } } @@ -223,7 +217,7 @@ function findPath(start, exit, getCellCost) { } // supplementary function for findPath -function restorePath(start, exit, from) { +function restorePath(exit, start, from) { const pathCells = []; let current = exit; @@ -237,5 +231,5 @@ function restorePath(start, exit, from) { pathCells.push(current); - return pathCells; + return pathCells.reverse(); }