# 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. L Distance travelled along the curve at any given point. L = 0 at the beginning. A 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
   Résultat .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; } } Lenght : StartRadius : isCCW: TrueFalse isEntry: TrueFalse ClothoideConstant: /* 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();   
 
 Discover our success stories. A software development project? Contact us! 
 
 
 This entry was posted in Desktop application development by Jan D’Orgeville. Share this article Facebook Linkedin Twitter Copy linkLink copied Twitter Twitter 
 Subscribe to our weekly newsletter Subscribe to our weekly newsletter Please complete this required field. An error has occurred. Please try again later Thank you!   You’ll receive weekly the blog posts written by our geeks. Recent articles How do I plan my software development project to have the quickest ROI? May 14 2020 Launch of Sentinel service for the construction industry May 07 2020 Five important things I’ve learned as a junior developer May 07 2020 Golfing with Dict Apr 23 2020 Design UI: 10 good reasons to choose Figma Apr 16 2020 Optimizing Shared Data Apr 09 2020 Articles by the same author How to Keep your Train from going off the Rails Oct 03 2016 Creating a Flash Kiosk: Team Architecture and Organisation Apr 18 2016 Construire une radio Web avec un Raspberry-PIfrench only Dec 07 2015 Compiler votre propre Android pour vos applications embarquéesfrench only Jul 15 2015 Google IO 2015, nos commentairesfrench only Jun 03 2015 Creating a Multi-platform Application for Augmented Reality Using Unity and Vuforia Jan 22 2015 RSS