Even I’ve already wrote two posts about rivers and got a positive feedback, rivers are still a weak point and need to be re-worked. First thing you notice when you start to edit maps in vector graphics editor is that rivers are just a bunch of separate curved segments with different width. I had to use this trick as currently svg does not allow lines to have variable width. But there is a better way to do it — consider rivers not as strokes, but as polygons filled with color. Unlike lines, polygons can have any shape and it’s not a problem to make polygons looking like narrowed rivers.

Polygonal rivers

Sounds like a complex change and I did not dare to try it until the recent time even having a perfectly understandable description by Scott Turner. But in reality it’s pretty easy and took just about an hour to implement a new algorithm. The general idea is to loop through the the river points, calculate a normal for each point to place two new points with a desired offset along the river course, add these points into two separate arrays, merge the arrays and draw a polygon. Sounds confusing, so I will add some explanations below.

First steps remain almost the same — we have to get a set of points for each river and then amend it in order to add more points and make rivers more meandering. Then we interpolate the points into a curve using D3 curve interpolation. Now we can get a river length using getTotalLength() method.

Having the total length and basic river path we can loop through the length to get point along the river, calculate a normal for this point and place new point on each side of the river with the same offset. The offset value depends on the current length and hereby river is getting wider over its length. To make it looks natural I advise to use a hyperbolic tangent function.

var riverLength = river.node().getTotalLength(); var riverPointsLeft = [], riverPointsRight = []; var widening = 200; // default value for (var l=0; l < riverLength; l++) { var point = river.node().getPointAtLength(l); var from = river.node().getPointAtLength(l - 0.1); var to = river.node().getPointAtLength(l + 0.1); var angle = Math.atan2(from.y - to.y, from.x - to.x); var offset = Math.atan(l / widening); var xLeft = point.x + -Math.sin(angle) * offset; var yLeft = point.y + Math.cos(angle) * offset; riverPointsLeft.push([xLeft, yLeft]); var xRight = point.x + Math.sin(angle) * offset; var yRight = point.y + -Math.cos(angle) * offset; riverPointsRight.unshift([xRight, yRight]); }

At the end of the loop we should get 2 new arrays: riverPointsLeft containing points for the left side of the river from its source to mouth and riverPointsRight containing points for the right side in opposite order (from river mouth to its source). This will simplify polygon creation.

One more thing to notice. Let’s say the river TotalLength is 90.9. In this case the last loop iteration will at the point at length 90, so the rest of the river course will be lost. It’s not good as the last river segment is usually pretty important one. To fix this we always need to add this last point into both arrays. To do this we can just add an extra loop iteration for the point at river.node().getPointAtLength(riverLength).

The next step is to interpolate each array into a curve separately and then unite two curves into a single path. And that is all. A result is a good-looking single-segmented river:

As you may remember I had a separate routine to deal with river confluences. It was not ideal and effect was really subtle, so I’m not going to add it to a polygonal rivers implementation. The only thing I have added is a confluence check. For each river point we check whether the point is a river confluence. If so, we add an extra-width to the river depending on the confluence “volume”.

River Editor

To make my maps interactive I’m implementing tools that allow user to edit map elements. On any river click River Editor is getting opened and red dots showing the river core points are getting appended. You can either drag a river entirely or move a selected point. River’s path will be recalculated automatically.

There are some tools available:

Resize — ability to rotate and re-scale the river

Regenerate — regenerate river based on core points, the river width will be randomized so the button could be used to change the river width

Add point — click on the map to add new river point and re-draw a river.

Remove point — click on a river point to remove it and re-draw a river

Copy — copy river and place near the selected one

Remove — remove the river (the action cannot be canceled)

New River — click on the map to create a new river and define a river course. Click on added point or river course to finish the river creation

So user can edit existing or create new rivers. I does not affect graph structure, just a visual change and hence there are no any restrictions here.

Thank you for your attention, that’s all for today. I hope I will manage to deploy some new tools soon.