The goal of this bl.ock is a basic proof of concept in using force-directed layouts to set paths.
The force does not always work on this layout, but one of the two should each time at a minimum.
All land has been covered in a regular grid of points, which are incorporated into the force simulation. The force runs for 100 iterations before drawing a final line. Each route is drawn along the shortest path between the end points, however, a number of points are added along the route (and a number over the start and end points). The points are linked together with force links. The new points are added to the force simulation and the simulation is started.
After 100 iterations, a splined line is drawn connecting the points that were added and then forced into a usually sea-only routing.
This simulation wouldn't be able to handle Lisbon to Colombo directly very well (the middle port is Luanda) as it's just a bit to indirect to achieve routinely with this approach.
xxxxxxxxxx
<meta charset="utf-8">
<style>
.links line {
stroke: #999;
stroke-opacity: 0.5;
stroke-width:5px;
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var width = 900,
height = 450;
var vertices = [];
var projection = d3.geoMercator().scale(250);//.scale(1000).center([175,65]);//.scale(1000).center([25,40]);
var path = d3.geoPath(projection);
var ports = {
Lisbon: [-9,38.7],
Luanda:[13,-8],
Colombo: [79.9,6.9]
};
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(2) )
.force("charge", d3.forceManyBody().strength(function(d) { if(d.type == "land") { return -250; } else { return -30; } }).distanceMax(30) )
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style('background','#43a2ca');
// add two layers:
var g1 = svg.append('g');
var g2 = svg.append('g');
d3.json("world.json", function(error, world) {
//append the world:
g1.selectAll(".world")
.data(topojson.feature(world, world.objects.countries).features)
.enter()
.append('path')
.attr("d", function(d) { return path(d); })
.attr('fill','#a8ddb5')
.attr('stroke','#999');
// Append the three ports:
g2.selectAll('.port')
.data(d3.entries(ports))
.enter()
.append('g')
.attr('transform',function(d) { return 'translate('+projection(d.value)+')'; })
.append('circle')
.attr('r',6)
.attr('stroke','darkblue')
.attr('stroke-width',2)
.attr('fill','white')
// Load up the force points covering land masses:
d3.json("points.json", function(error, p) {
var points = [];
var links = [];
var i = 0;
p.forEach(function(d) {
i++
var x = projection(d)[0];
var y = projection(d)[1];
points.push({"x":x,"y":y,"fx":x,"fy":y,"type":"land" });
});
// Draw routes:
var routes = svg.selectAll('.route')
.data([{source:"Colombo",target:"Luanda"},{source:"Luanda",target:"Lisbon"}])
.enter()
.append('path')
.attr('d', function(d) {
return path ({
type:"LineString",
coordinates: [ ports[d.source],ports[d.target] ]
});
})
.attr('fill','none')
.attr('stroke','yellow')
.attr('stroke-width',0)
// Add points along a route:
routes.each(function(d,j) {
var route = d3.select(this).node();
var totalLength = route.getTotalLength();
var spacing = 40;
var n = totalLength/spacing;
var i = 0;
d.points = [];
points.push({"x":projection(ports[d.source])[0],"fx":projection(ports[d.source])[0],"y":projection(ports[d.source])[1],"fy":projection(ports[d.source])[1],"type":"route","routeID":j,"fid":(j+"."+0)})
while ( i < 5 ) {
points.push({"x":projection(ports[d.source])[0],"y":projection(ports[d.source])[1],"type":"route","routeID":j,"fid":(j+"."+i)})
links.push({"source":points.length-1,"target":points.length-2});
i++;
}
while ( i < n) {
var xy = route.getPointAtLength( spacing * i + spacing /2 )
points.push({"x":xy.x,"y":xy.y,"type":"route","routeID":j,"routeID":j,"fid":(j+"."+(i+1))})
links.push({"source":points.length-1,"target":points.length-2});
i++;
}
while ( i < (n + 5) ) {
points.push({"x":projection(ports[d.target])[0],"y":projection(ports[d.target])[1],"type":"route","routeID":j,"fid":(j+"."+i)})
links.push({"source":points.length-1,"target":points.length-2});
i++;
}
points.push({"x":projection(ports[d.target])[0],"fx":projection(ports[d.target])[0],"y":projection(ports[d.target])[1],"fy":projection(ports[d.target])[1],"type":"route","routeID":j,"fid":(j+"."+i)})
links.push({"source":points.length-1,"target":points.length-2});
i= 0;
});
// draw the temporary lines to show the force at work:
var link = g1.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line");
// do the simulation:
simulation
.nodes(points)
.on("tick", ticked);
simulation.force("link")
.links(links);
var j = 0;
function ticked() {
j++;
if (j > 100) {simulation.stop();
// draw the final route:
var splineLine = d3.line().curve(d3.curveCardinal).x(function(d) { return d.x}).y(function(d) { return d.y; });
var spliner = d3.selectAll('.spline')
.data(points)
.enter()
.filter(function(d) { return d.type == "route"});
var spline = g1.append('path').attr('d',splineLine(spliner.data())).attr('fill',"none").attr('stroke-width',4).attr('stroke','black');
var totalLength = spline.node().getTotalLength();
spline.attr('stroke-dasharray',(totalLength + 4) + ", " + (totalLength + 4))
.attr('stroke-dashoffset',totalLength + 4 )
spline.transition()
.attr('stroke-dashoffset',0)
.duration(3000);
}
else {
link
.attr("x1", function(d) { return d.source.x })
.attr("y1", function(d) { return d.source.y})
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
}
});
});
</script>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v2.min.js