 • Strategy
• Customer Experience
• Design
• Development October 3, 2016.

# How to Keep your Train from going off the Rails

Thirty years ago, I received a beautiful electric train set.It came with three different kinds of rails: Thirty years ago, I received a beautiful electric train set.

It came with three different kinds of rails: 1. straight rails,
2. curved rails,
3. and… curved rails… what ? Again?!

Years later, this third type of rail still puzzled me. The second kind of rail was definitely arc-shaped, but the third had a funny curve to it.

One thing was sure: when I would build an oval run, if I didn’t put the right rails in the right place, my train was guaranteed to derail at high speed. ## All aboard the clothoid!

Enough with the teaser. The mystery curve was called a clothoid, or Euler spiral, or just spiral among friends.

A clothoid is the result of a specific mathematical formula that defines a curve whose curvature changes linearly with its curve length.

By definition, the radius of a curve is:

• the radius of a circle (in our case, the second type of rail),
• infinity for straight rails (i.e. the same rails with an infinite radius).

Following the sequence below (called “S-C-S” for Spiral-Curve-Spiral):

• rail-straight, R = +∞,
• rail-spiral, R : +∞ → 60 cm,
• rail-curve, R = 60 cm,
• rail-spiral, R : 60 cm → +∞,
• rail-straight, R = +∞,

the train trip becomes much smoother, without the risk of derailments!

To better understand the effect, let’s take another example from railroads: The blue curve is exclusively made up of arcs from a circle with a 60-cm radius, while the green and red curve is made up of two different spirals.

If we now represent the curve (1/R) along the road, this is what we get: The second itinerary is longer, but free of curvature breaks. This type of curve has many applications in civil engineering: for roads, bridges, railroads, mechanics.

Remember this next time you take a highway exit safely at 100 km/h!

## Why are clothoids also called spirals?

Looking at the math behind the curve, if you draw such a curve on an infinite trajectory, this is what you get:

Two symmetrical spirals.

R Radius of curvature at any given point in a trajectory. Given that R = +∞ at the beginning of the trajectory. Angle between the tangent to the trajectory at any given point and the axis of the abscissa. φ = 0 at the beginning. Distance travelled along the curve at any given point. L = 0 at the beginning. Constant describing the change in the radius of the curve along the trajectory, by definition: 1/R = L x 1/A2, i.e. A2 = RL Note that in this figure, the two φ angles are identical:

• φ = internal angle between (d1) and (d2).
• (d3) is perpendicular to (d1) by construction.
• (d4) is perpendicular to (d2), since it is the tangent of circle CM at point M.
• Therefore the angle between (d3) and (d4) is also φ.

## Let’s draw a clothoid!

Now let’s draw our own spiral.

If we imagine the trajectory of point M(x,y) along the following itinerary:

• from the curved abscissa L1 (=0),
• to the curved abscissa L2 (=L length of trajectory),

we get: Note that φ is indeed a function of l: So, given: we get: which gives us the following JavaScript code:

` `
``````function drawSpiral(A, L1, L2)
{
var a  = A*Math.sqrt(2);
var S1 = L1/a;
var S2 = L2/a;

var s=S1;
var ds = Math.abs(S2-S1)/10000;

var M = new Point(0,0);

while (s
``````

## Result

``` .wrapper_result { position:relative; } .canvas_img { position: absolute; float:left; background-color: #e8e8e8; border: 2px solid #777; border-radius: 10px; padding:10px ; margin:10px; box-shadow: 0 12px 16px 0 rgba(0,0,0,0.24), 0 17px 50px 0 rgba(0,0,0,0.19); } @media all and (max-width: 940px) { .canvas_img { position: initial; float:none; } .wrapper_canvas canvas { width:100%; height:auto; } } ```

• Length :
``` /* DRAWING PRIMITIVES */ /* https://github.com/chtimi59/sm2d.js */ var canvas = document.getElementById("myCanvas"); var ctx = canvas.getContext("2d"); function f2str(f,digit) { if (digit===undefined) digit = 1; var p = Math.pow(10,digit); return Math.round(f*p)/p; } function clear() { ctx.clearRect(0, 0, canvas.width, canvas.height); xaxis(0); yaxis(0); xmark(1); ymark(1); } var MINX = -10; var MAXX = +10; var MINY = -10; var WIDTH = (MAXX-MINX); var HEIGHT = WIDTH; var MAXY = MINY+WIDTH; var SCALE = canvas.width/WIDTH; function coord(pt) { return new Point( SCALE*(pt.x - MINX), canvas.height - SCALE*(pt.y - MINY)); } function Point(vx, vy) { this.x = vx; this.y = vy; this.add = function(p) { return new Point(this.x+p.x, this.y+p.y); } this.norm = function() { return Math.sqrt(this.x*this.x + this.y*this.y); } } function Vector(vstart, vangle, vlenght) { this.start = new Point(vstart.x,vstart.y); this.angle = vangle; this.lenght = vlenght; this.end = function() { return new Point(this.start.x + this.lenght*Math.cos(this.angle), this.start.y + this.lenght*Math.sin(this.angle)) } this.middle = function() { var e = this.end(); return new Point(0.5*this.start.x+0.5*e.x, 0.5*this.start.y+0.5*e.y); } } function Vector2(p1,p2) { var diff = new Point(p2.x-p1.x, p2.y-p1.y); var angle = Math.atan2(diff.y,diff.x); var lenght = diff.norm(); return new Vector(p1, angle, lenght); } function lineTo(pt) { var pt2 = coord(pt); ctx.lineTo(pt2.x,pt2.y); } function moveTo(pt) { var pt2 = coord(pt); ctx.moveTo(pt2.x,pt2.y); } function line(a,b) { moveTo(a); lineTo(b); } function yaxis(y,w,l) { if (!w) w=1; if(!l) l=WIDTH; ctx.beginPath(); line(new Point(-l,y),new Point(+l,y)); ctx.lineWidth = w; ctx.strokeStyle = '#555'; ctx.stroke(); } function xaxis(x,w,l) { if (!w) w=1; if(!l) l=HEIGHT; ctx.beginPath(); line(new Point(x,-l),new Point(x,+l)); ctx.lineWidth = w; ctx.strokeStyle = '#555'; ctx.stroke(); } function xmark(s) { for(x=0;x<MAXX;x+=s) xaxis(x,1,HEIGHT/100); for(x=0;x>MINX;x-=s) xaxis(x,1,HEIGHT/100); } function ymark(s) { for(y=0;y<MAXY;y+=s) yaxis(y,1,WIDTH/100); for(y=0;y>MINY;y-=s) yaxis(y,1,WIDTH/100); } function ray(v,name,color) { var p1 = v.start; var p2 = v.end(); var p3 = new Vector(p2,v.angle+9*Math.PI/10, WIDTH/50).end(); var p4 = new Vector(p2,v.angle-9*Math.PI/10, WIDTH/50).end(); var p5 = v.middle(); if (!name) name=""+f2str(p2.x-p1.x)+", "+f2str(p2.y-p1.y)+")"; if(!color) color='#FCC'; ctx.beginPath(); ctx.lineWidth = 0.5; line(p1,p2); ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.lineWidth = 0.5; moveTo(p3); lineTo(p4); lineTo(p2); lineTo(p3); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = "#333"; ctx.stroke(); ctx.fillStyle = color; ctx.font = "12px Arial"; var pt2 =coord(p5); ctx.fillText(name,pt2.x+7,pt2.y-7); dot(v.start," ",color); } function dot(p,name,color) { ctx.beginPath(); var pt2 =coord(p); if (!name) name="("+f2str(p.x)+", "+f2str(p.y)+")"; if(!color) color='#CCC'; line(new Point(p.x-WIDTH/100,p.y),new Point(p.x+HEIGHT/100,p.y)); line(new Point(p.x,p.y-HEIGHT/100),new Point(p.x,p.y+HEIGHT/100)); ctx.lineWidth = 1; ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.arc(pt2.x,pt2.y,5,0,2*Math.PI); ctx.fillStyle = color; ctx.fill(); ctx.stroke(); ctx.fillStyle = color; ctx.font = "12px Arial"; ctx.fillText(name,pt2.x+7,pt2.y-7); } function circle(p,radius,color) { ctx.beginPath(); var pt2 =coord(p); if(!color) color='#CCC'; line(new Point(p.x-WIDTH/100,p.y),new Point(p.x+HEIGHT/100,p.y)); line(new Point(p.x,p.y-HEIGHT/100),new Point(p.x,p.y+HEIGHT/100)); ctx.lineWidth = 1; ctx.strokeStyle = color; ctx.stroke(); ctx.beginPath(); ctx.arc(pt2.x,pt2.y,SCALE*radius,0,2*Math.PI); ctx.strokeStyle = color; ctx.stroke(); } clear(); function drawEulerSpiral(A, L1, L2, isCCW) { var a = A*Math.sqrt(2); var l1 = L1/a; var l2 = L2/a; var dx, dy; var s=l1; var ds = Math.abs(l2-l1)/10000; // 10000 points var M = new Point(0,0); var signe = ((isCCW)?1:-1); // for test and UI var firstDelta = null; var lastDelta = null; var testLenght = 0; // just for double check (total path lenght) ctx.beginPath(); ctx.lineWidth = 2; ctx.strokeStyle = "#F00"; while (s<l2) { dx = a * Math.cos(s*s) * ds; dy = signe * a * Math.sin(s*s) * ds; s += ds; var delta = new Point(dx,dy); var M2 = M.add(delta); // for test and UI testLenght += delta.norm(); if (!firstDelta) firstDelta = new Vector2(M, M2); lastDelta = new Vector2(M, M2); M=M2; lineTo(M2); } ctx.stroke(); var inRadius = null; if (L1!=0) inRadius = A*A/L1; var inStrRadius = (!inRadius)?"+inf":""+f2str(inRadius); var outRadius = null; if (L2!=0) outRadius = A*A/L2; var outStrRadius = (!outRadius)?"+inf":""+f2str(outRadius); firstDelta.lenght = 3; ray(firstDelta, "input (R="+inStrRadius+")", "#F88"); lastDelta.lenght = 3; ray(lastDelta, "output (R="+outStrRadius+")", "#88E"); var inAngle = (L1*L1)/(a*a); var outAngle = (L2*L2)/(a*a); if (inRadius) { firstDelta.lenght = Math.abs(inRadius); firstDelta.angle += ((inRadius<0)?-1:1) * signe * Math.PI/2; circle(firstDelta.end(),firstDelta.lenght,"#755"); } if (outRadius) { lastDelta.lenght = Math.abs(outRadius); lastDelta.angle += ((outRadius<0)?-1:1) * signe * Math.PI/2; circle(lastDelta.end(),lastDelta.lenght,"#557"); } } function ifcclothoidalarcsegment2d(Lenght, StartRadius, isCCW, isEntry, ClothoideConstant) { var A = ClothoideConstant; var L = Lenght; var startL; if (isEntry) { startL = 0; if (StartRadius!=null) startL = A*A / StartRadius; } else { startL = -L; if (StartRadius!=null) startL = -A*A / StartRadius; isCCW = !isCCW; } stopL = startL+L; drawEulerSpiral(A,startL,stopL,isCCW) } function onClear() { clear(); } function onClick() { var lenght = Number(document.getElementById("lenght").value); var StartRadius = Number(document.getElementById("StartRadius").value); if (isNaN(StartRadius) || StartRadius<=0 ) StartRadius = null; var isCCW = Number(document.getElementById("isCCW").value); var isEntry = Number( document.getElementById("isEntry").value); var ClothoideConstant = Number(document.getElementById("ClothoideConstant").value); ifcclothoidalarcsegment2d(lenght,StartRadius,isCCW,isEntry,ClothoideConstant); } onClick(); ```