metroStaions = {
const gridSize = 8;
const textSize = 10;
const customWidth = 800;
const customHeight = 512;
const arrWidth = customWidth / gridSize;
const arrHeight = customHeight / gridSize;
const gridVector = new Victor(gridSize, gridSize);
const paddingGrids = 32;
const lineAvoidance = 4;
const lineSelfAvoidance = -2;
const stationAvoidance = 3;
const stationRadius = 2;
const octolinearPathing = false;
const useOrderedLinks = false;
const useStringPulling = true;
const stringPullingLookahead = 16;
const stringPullingShortcutThreshold = 0;
const drawLines = true;
const drawStations = true;
const drawStationNames = false;
const container = d3.select(
html`<div style="position:relative;">${tooltipTemplate}</div>`
);
const tooltipDiv = container.select(".tooltip");
const svg = container.append("svg").attr('viewBox', [0, 0, customWidth, customHeight]);
const nodes = simulationData.stations.map(d => Object.create(d));
var linkPairs = [];
for (let lineIndex = 0; lineIndex < simulationData.lines.length; lineIndex++) {
for (let start = 0; start < simulationData.lines[lineIndex].stations.length - 1; start++) {
if (useOrderedLinks) {
// Only create links between stations in the order they are listed.
linkPairs.push({
source: simulationData.lines[lineIndex].stations[start],
target: simulationData.lines[lineIndex].stations[start + 1]
});
} else {
// Create links between every station in every line.
for (let end = start; end < simulationData.lines[lineIndex].stations.length; end++) {
linkPairs.push({
source: simulationData.lines[lineIndex].stations[start],
target: simulationData.lines[lineIndex].stations[end]
});
}
}
}
}
const links = linkPairs.map(d => Object.create(d));
// Run a simulation until it settles.
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id))
.force("charge", d3.forceManyBody());
simulation.restart();
for (let i = 0; i < 1000; i++) {
simulation.tick();
if (simulation.alpha() < 0.005) {
break;
}
}
simulation.stop();
// Create a bounding box around the resulting nodes.
var top = Number.MAX_VALUE;
var bottom = 0;
var left = Number.MAX_VALUE;
var right = 0;
for (var i = 0; i < nodes.length; i++) {
var d = nodes[i];
if (d.y > bottom) bottom = Math.round(d.y);
if (d.y < top) top = Math.round(d.y);
if (d.x > right) right = Math.round(d.x);
if (d.x < left) left = Math.round(d.x);
}
top -= paddingGrids;
bottom += paddingGrids;
left -= paddingGrids;
right += paddingGrids;
// Create a cost array.
const arr = new Array2D(arrWidth, arrHeight, 1);
arr.defaultValue = 1000;
// Place each station within the cost array.
for (var i = 0; i < nodes.length; i++) {
var d = nodes[i];
let index = simulationData.stations.findIndex(function(station) {return station.id == d.id});
let x = Math.round((d.x - left) / (right - left) * arrWidth);
let y = Math.round((d.y - top) / (bottom - top) * arrHeight);
simulationData.stations[index].position = new Victor(x, y);
arr.incrementBox(simulationData.stations[index].position, stationRadius, stationAvoidance, true);
}
// This is the expensive part. Generate curves to define each line.
for (var i = 0; i < simulationData.lines.length; i++) {
var line = simulationData.lines[i];
var stationsOnLine = [];
for (var j = 0; j < line.stations.length; j++) {
var station = simulationData.stations.find(function(station) { return station.id == line.stations[j] });
stationsOnLine.push(station);
}
var antColony = new AntColonyOptimization(stationsOnLine)
var lineSegments = antColony.simulateColony().path;
// Pathfind for each of the paths in this line.
const curve = d3.line().curve(d3.curveBundle.beta(1))
var allPointsOnLine = [];
for (var j = 0; j < lineSegments.length; j++) {
let start = simulationData.stations.find(function(station) { return station.id == lineSegments[j].source });
let end = simulationData.stations.find(function(station) { return station.id == lineSegments[j].target });
try {
let pointsOnSegment = arr.pathfind(start.position, end.position, octolinearPathing);
// Optimize the path a little bit before continuing.
var optimizedPoints = pointsOnSegment;
if (useStringPulling) {
optimizedPoints = arr.stringPull(pointsOnSegment, stringPullingLookahead, stringPullingShortcutThreshold);
}
arr.incrementList(optimizedPoints, lineSelfAvoidance, true);
for (let i = 0; i < optimizedPoints.length; i++) {
allPointsOnLine.push(optimizedPoints[i].clone());
optimizedPoints[i] = optimizedPoints[i].multiply(gridVector).toArray();
}
// Go ahead and draw the curve while we're looping through the line.
if (drawLines) {
svg
.append('path')
.attr('d', curve(optimizedPoints))
.attr('stroke', line.color)
.attr('stroke-width', 6)
.attr('fill', 'none')
.attr('stroke-linecap', 'round');
}
} catch(e) {
console.warn("Pathing failed between", start.name, "and", end.name);
}
}
arr.incrementList(allPointsOnLine, lineAvoidance, true);
}
// Draw all of the stations and station names.
for (var i = 0; i < simulationData.stations.length; i++) {
var station = simulationData.stations[i];
if (drawStations) {
svg.append('circle')
.attr('fill', '#ffffff')
.attr('stroke', '#2c2c2c')
.attr('r', 8)
.attr('stroke-width', 4)
.attr('cx', station.position.x * gridVector.x)
.attr('cy', station.position.y * gridVector.y)
.data([station])
.call(tooltip, tooltipDiv);
// svg2.append("g")
// .attr("transform", `translate(${margin.left}, ${margin.top})`)
// .selectAll("circle")
// .data(simulationData)
// .join("circle")
// .attr("station", d => d.stations)
// .attr("line", d => d.lines)
// .attr("r", 5)
// .attr("fill", "#333")
// .call(tooltip, tooltipDiv);
if (drawStationNames) {
svg.append('text')
.attr('x', station.position.x * gridVector.x)
.attr('y', station.position.y * gridVector.y + 16)
.attr('dy', '.35em')
.attr('text-anchor', 'middle')
.attr('font-family', 'helvetica')
.attr('font-weight', 'bold')
.attr('font-size', textSize)
.attr('fill', '#2c2c2c')
.text(station.name.toUpperCase());
}
}
}
return container.node();
}