WaBis

walter.bislins.ch

JavaScript: Creating Flight Plans for Flat Earth

This page lists the Code used on the page Creating Flight Plans for Flat Earth. All Code written by me Walter Bislin.

#INCLUDE JsGraphX3D.inc
#INCLUDE EarthMap.inc
#INCLUDE ControlPanel.inc

<style>
table#ControlPanel1 td.Label { width: 7%; }
table#ControlPanel1 td.Value { width: 15.5%; }
table#ControlPanel1 td.Value input { width: 75px; }
table#ControlPanel1 td.Col2 input { width: 100px; }
table#ControlPanel1 td.Col5 input { width: 100px; }
table#ControlPanel1 tr.Row1 { background: #dfd; }
table#ControlPanel1 tr.Row2 { background: #ddf; }

.Wiki table.FlightPlanTab { border-collapse: separate; border-left: 2px solid #aaa; border-right: 2px solid #aaa; }
.Wiki table.FlightPlanTab th { text-align: center; }
.Wiki table.FlightPlanTab td { text-align: center; white-space: nowrap; }
.Wiki table.FlightPlanTab th.bdl { border-left: 2px solid #aaa; }
.Wiki table.FlightPlanTab td.bdl { border-left: 2px solid #aaa; }
</style>

<jscript>

// some usefull functions 

var DegPrecision = 3600 * 100;

function RoundDeg( deg, prec ) {
  var s = deg < 0 ? -1 : 1;
  return s * Math.round( Math.abs(deg) * prec ) / prec;
}

function DegToDMS(x) {
  // returns { d, m, s }
  var d = Math.abs( x );
  var s = x < 0 ? -1 : 1;
  var r = { d: 0, m: 0, s: 0 };
  r.d = Math.floor( d );
  d = (d % 1) * 60;
  r.m = Math.floor( d );
  r.s = (d % 1) * 60;
  if (s < 0) {
    if (r.d > 0) {
      r.d *= -1;
    } else if (r.m > 0) {
      r.m *= -1;
    } else {
      r.s *= -1;
    }
  }
  return r;
}

function RangePM1( x ) {
  // limit x to -1..+1
  if (x > 1) return 1;
  if (x < -1) return -1;
  return x;
}

// flight plan classes

function Leg( hdg, lat, lng, dtg, tta, fob ) {
  this.Heading = hdg;
  this.Lat = lat;
  this.LatDeg = lat;
  this.LatMin = 0;
  this.LatSec = 0;
  this.Lng = lng;
  this.LngDeg = lng;
  this.LngMin = 0;
  this.LngSec = 0;
  this.DistToGo = dtg;
  this.TimeToArr = tta;
  this.FuelOnBoard = fob;
  this.DegToDMS();
}

Leg.prototype.DegToDMS = function() {
  var dms = DegToDMS( RoundDeg( this.Lat, DegPrecision ) );
  this.LatDeg = dms.d;
  this.LatMin = dms.m;
  this.LatSec = dms.s;
  dms = DegToDMS( RoundDeg( this.Lng, DegPrecision ) );
  this.LngDeg = dms.d;
  this.LngMin = dms.m;
  this.LngSec = dms.s;
}

function FlightPlan( DomContainerId, origId, destId, textAngle ) {
  this.Dom = DomContainerId;
  this.OriginId = origId;
  this.DestinationId = destId;
  this.TextAngle = textAngle;
  this.Legs = [];
}

FlightPlan.prototype.AddLeg = function( hdg, lat, lng, dtg, tta, fob ) {
  this.Legs.push( new Leg( hdg, lat, lng, dtg, tta, fob ) );
}

FlightPlan.prototype.Unrender = function() {
  xInnerHTML( this.Dom, '' );
}

FlightPlan.prototype.Render = function() {
  // creates the website table for tge flight plan
  if (this.Legs.length == 0) {
    xInnerHTML( this.Dom, '' );
    return;
  }

  function renderHeader() {
    var s = '<tr>';
    s += '<th>WP</th>';
    s += '<th>HDG</th>';
    s += '<th colspan="3" class="bdl">Latitude</th>';
    s += '<th colspan="3" class="bdl">Longitude</th>';
    s += '<th class="bdl">DTG</th>';
    s += '<th>TTA</th>';
    s += '<th>FOB</th>';
    s += '</tr>\n';
    s += '<tr>';
    s += '<th>&nbsp;</th>';
    s += '<th>deg</th>';
    s += '<th class="bdl">deg</th>';
    s += '<th>min</th>';
    s += '<th>sec</th>';
    s += '<th class="bdl">deg</th>';
    s += '<th>min</th>';
    s += '<th>sec</th>';
    s += '<th class="bdl">nm</th>';
    s += '<th>h</th>';
    s += '<th>units</th>';
    s += '</tr>\n';
    return s;
  }

  function renderLeg( wp, leg ) {
    var format0 = { Mode: 'fix0', Precision: 0, UsePrefix: false, Units: '' };
    var format1 = { Mode: 'fix0', Precision: 1, UsePrefix: false, Units: '' };
    var format2 = { Mode: 'fix0', Precision: 2, UsePrefix: false, Units: '' };
    var s = '<tr>\n';
    s += '<td>' + wp + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.Heading, format1 ) + '</th>\n';
    s += '<td class="bdl">' + NumFormatter.NumToString( leg.LatDeg, format0 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.LatMin, format0 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.LatSec, format2 ) + '</th>\n';
    s += '<td class="bdl">' + NumFormatter.NumToString( leg.LngDeg, format0 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.LngMin, format0 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.LngSec, format2 ) + '</th>\n';
    s += '<td class="bdl">' + NumFormatter.NumToString( leg.DistToGo, format1 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.TimeToArr, format2 ) + '</th>\n';
    s += '<td>' + NumFormatter.NumToString( leg.FuelOnBoard, format1 ) + '</th>\n';
    s += '</tr>\n';
    return s;
  }
  
  function renderTable( s ) {
    return '<table class="grid FlightPlanTab" style="width: 100%">\n' + s + '</table>\n';
  }
  var s = renderHeader();
  var nLegs = this.Legs.length;
  for (var i = 0; i < nLegs; i++) {
    var wp = i.toFixed(0);
    if (i == 0) wp = this.OriginId;
    if (i == nLegs-1) wp = this.DestinationId;
    if (wp == '') wp = i.toFixed(0);
    s += renderLeg( wp, this.Legs[i] );
  }
  s = renderTable( s );
  xInnerHTML( this.Dom, s );
}

FlightPlan.prototype.DrawFlatEarthPath = function( g ) {
  // g: JsGraph; graphics context for flat earth
  var nLegs = this.Legs.length;
  if (nLegs < 2) return;
  var lat1 = this.Legs[0].Lat;
  var lng1 = this.Legs[0].Lng;
  var lat2 = this.Legs[nLegs-1].Lat;
  var lng2 = this.Legs[nLegs-1].Lng;
  g.SetLineAttr( 'blue', 2 );
  EarthMap.DrawFlatEarthLine( g, lat1, lng1, lat2, lng2 );
  g.SetMarkerAttr( 'Circle', 4, 'blue', 'white', 1 );
  g.SetTextAttr( 'Arial', 11, 'black', 'normal', 'normal', 'left', 'middle', 5, 5, this.TextAngle );
  for (var i = 0; i < nLegs; i++) {
    var leg = this.Legs[i];
    EarthMap.DrawFlatEarthMarker( g, leg.Lat, leg.Lng );
    var txt = i.toFixed(0);
    if (i == 0) txt = '--- ' + this.OriginId;
    if (i == nLegs-1) txt = '--- ' + this.DestinationId;
    if (txt == '--- ') txt = i.toFixed(0);
    g.Text( txt, EarthMap.PointOnFE( leg.Lat, leg.Lng ) );
  }
}

FlightPlan.prototype.DrawGlobePath = function( g, showWP, showNav, showGC ) {
  // g: JsGraphX3D; graphics context for globe earth 
  function pointIsVisible( p ) {
    var px = g.TransPoint3D( p[0], p[1], p[2] );
    return px[0] > 0;
  }
  var nLegs = this.Legs.length;
  if (nLegs < 2) return;

  var p = JsgVect3.Null();
  var p1 = JsgVect3.Null();
  var p2 = JsgVect3.Null();
  var pOrig = JsgVect3.Null();
  var pDest = JsgVect3.Null();
  var pNavDest = null;
 
  // to clipp all graphics behind he earth horizon...
  g.AddClipPlane( [0,0,0], [0,1,0], [0,0,1] );

  // draw city markers
  g.SetMarkerAttr( 'Circle', 8, 'black', 'white', 1 );
  var legOrig = this.Legs[0];
  var legDest = this.Legs[nLegs-1];
  EarthMap.PointOnGlobe( legOrig.Lat, legOrig.Lng, pOrig );
  EarthMap.PointOnGlobe( legDest.Lat, legDest.Lng, pDest );
  g.Marker3D( pOrig, 3 );
  g.Marker3D( pDest, 3 );

  // draw great circle between departure and destination
  if (showGC) {
    g.SetLineAttr( 'green', 2 );
    if (!EarthMap.DrawGlobeGreatCircleArc( g, legOrig.Lat, legOrig.Lng, legDest.Lat, legDest.Lng )) {
      // special cases: origin is opposite dest and/or both at the pole
      // split great circle into 2 parts
      if (Math.abs(legOrig.Lat) > 89.99999) {
        // pole to pole -> use southpole longitude
        var interLat, interLng;
        if (legOrig.Lat < 0) {
          interLng = legOrig.Lng;
        } else {
          interLng = legDest.Lng;
        }
        interLat = 0;
        EarthMap.DrawGlobeGreatCircleArc( g, legOrig.Lat, legOrig.Lng, interLat, interLng );
        EarthMap.DrawGlobeGreatCircleArc( g, interLat, interLng, legDest.Lat, legDest.Lng );
      } else {
        // opposite points are not to pole -> use always path over the pole as on flat earth
        var interLat = 90;
        var interLng = 0;
        EarthMap.DrawGlobeGreatCircleArc( g, legOrig.Lat, legOrig.Lng, interLat, interLng );
        EarthMap.DrawGlobeGreatCircleArc( g, interLat, interLng, legDest.Lat, legDest.Lng );
      }
    }
  }

  // draw flat earth leg lines
  if (showWP) {
    g.SetLineAttr( 'blue', 2 );
    for (var i = 1; i < nLegs; i++) {
      var leg1 = this.Legs[i-1];
      var leg2 = this.Legs[i];
      EarthMap.DrawGlobeGreatCircleArc( g, leg1.Lat, leg1.Lng, leg2.Lat, leg2.Lng );
    }
  }

  // some usefull local functions

  function drawLocalNorthVector(p) {
    // draw vector from p to direction north
    var localEast = JsgVect3.Mult( [0,0,1], p );
    if (JsgVect3.Length(localEast) == 0) return;
    var localNorth = JsgVect3.Scale( JsgVect3.Norm( JsgVect3.Mult( p, localEast ) ), 500 );
    g.SetLineWidth( 1 );
    g.SetAlpha( 0.33 );
    g.Line3D( p, JsgVect3.Add( p, localNorth ) );
    g.SetAlpha( 1 );
  }

  function compTextAngle( p1, p2 ) {
    // compute text rotation angle to be perpendicular to line p1 to p2
    // p1, p2: JsgVect3; 3D points on the globe
    var p1screen = JsgVect3.Copy( g.VTransPoint3D( p1 ) );
    var p2screen = JsgVect3.Copy( g.VTransPoint3D( p2 ) );
    var legDir = JsgVect2.Sub( p2screen, p1screen ); // z coordinate (depth) is ignored
    var textAngle = - JsgVect2.Angle( [0,1], legDir, true ) * 180 / Math.PI;
    if (textAngle > 90) {
      textAngle -= 180;
    } else if (textAngle < -90) {
      textAngle += 180;
    }
    return textAngle;
  }

  function makeText( wp, origId, destId, prefix ) {
    if (!xStr(prefix)) prefix = '--- ';
    var txt = wp.toFixed(0);
    if (wp == 0) txt = prefix + origId;
    if (wp == nLegs-1) txt = prefix + destId;
    if (txt == prefix) txt = wp.toFixed(0);
    return txt;
  }
  
  function pointDist( p1, p2 ) {
    return JsgVect3.Length( JsgVect3.Sub( p1, p2 ) );
  }

  // draw navigation path resulting flying headings and leg distances from FE flight plan
  g.SetTextAttr( 'Arial', 11, 'red', 'normal', 'normal', 'left', 'middle', 5, 5, this.TextAngle );
  if (showNav) {
    g.SetLineAttr( 'red', 2 );
    g.SetMarkerAttr( 'Circle', 4, 'red', 'white', 1 );
    var leg1 = this.Legs[0];
    EarthMap.PointOnGlobe( leg1.Lat, leg1.Lng, p1 );
    // plane used to compute the next navigation point
    var plane = new JsgPlane( [0,0,0], [1,0,0], [0,1,0], false );
    for (var i = 1; i < nLegs; i++) {
      var leg2 = this.Legs[i];
      drawLocalNorthVector( p1 );
      // compute p2 from heading and legDist
      var localEast = JsgVect3.Mult( [0,0,1], p1 );
      if (JsgVect3.Length(localEast) == 0) {
        EarthMap.PointOnGlobe( leg2.Lat, leg2.Lng, p2 );
        var p12 = JsgVect3.Sub( p2, p1 );
        localEast = JsgVect3.Mult( p12, [0,0,1] );
      }
      JsgVect3.NormTo( localEast );
      var localNorth = JsgVect3.Norm( JsgVect3.Mult( p1, localEast ) );
      var a = leg1.Heading * Math.PI / 180;
      var flightDir = JsgVect3.Add( 
        JsgVect3.Scale( localEast,  Math.sin(a) ), 
        JsgVect3.Scale( localNorth, Math.cos(a) ) 
      );
      var localUp = JsgVect3.Norm( p1 );
      var planeNormal = JsgVect3.Norm( JsgVect3.Mult( flightDir, localUp ) );
      var planeXdir = JsgVect3.Norm( JsgVect3.Mult( localUp, planeNormal ) );
      var legDist = (leg1.DistToGo - leg2.DistToGo) * 1.852; // km
      var legAngle = legDist / EarthMap.Radius;
      // now we have all plane properties and can compute p2
      // points p1 and p2 lie on the plane, plane y dir is dir from earth center to p1
      plane.Set( [0,0,0], planeXdir, localUp, false );
      plane.PointOnPlane( EarthMap.Radius * Math.sin(legAngle), EarthMap.Radius * Math.cos(legAngle), p2 );
      // use plane to draw great circle arc from p1 to p2
      g.SetPlane( plane );
      g.SetLineWidth( 2 );
      g.ArcOnPlane( 0, 0, EarthMap.Radius, 90-(legAngle*180/Math.PI), 90, 1 );
      g.SetLineWidth( 1 );
      g.Marker3D( p1, 3 );
      if (pointIsVisible(p1) && (i > 1 || !showWP)) {
        g.SetTextRotation( compTextAngle( p1, p2 ) );
        g.Text3D( makeText( i-1, this.OriginId, '???' ), p1 );
      }
      if (i == nLegs-1) {
        g.Marker3D( p2, 3 );
        if (pointIsVisible(p2) && pointDist( pDest, p2 ) > 100 ) {
          pNavDest = JsgVect3.Copy( p2 );
          g.SetTextRotation( compTextAngle( p1, p2 ) );
          g.Text3D( makeText( i, this.OriginId, '???' ), p2 );
        }
      } else {
        JsgVect3.CopyTo( p2, p1 );
        leg1 = leg2;
      }
    }  
  }
  g.SetTextColor( 'black' );

  // draw way point markers
  if (showWP) {
    g.SetMarkerAttr( 'Circle', 4, 'blue', 'white', 1 );
    for (var i = 0; i < nLegs; i++) {
      var leg = this.Legs[i];
      EarthMap.PointOnGlobe( leg.Lat, leg.Lng, p );
      if (pointIsVisible(p)) {
        g.Marker3D( p, 3 );
        // get 2 leg points to compute text angle
        var leg1, leg2;
        if (i == nLegs-1) {
          leg1 = this.Legs[i-1];
          leg2 = this.Legs[i];
        } else {
          leg1 = this.Legs[i];
          leg2 = this.Legs[i+1];
        }
        EarthMap.PointOnGlobe( leg1.Lat, leg1.Lng, p1 );
        EarthMap.PointOnGlobe( leg2.Lat, leg2.Lng, p2 );
        g.SetTextRotation( compTextAngle( p1, p2 ) );
        g.Text3D( makeText( i, this.OriginId, this.DestinationId ), p );
      }
    }
  }

  // label airports if not already labeled
  g.SetTextAttr( 'Arial', 11, 'black', 'normal', 'normal', 'center', 'top', 8, 8, 0 );
  if (!showWP && !showNav) {
    g.Text3D( makeText( 0, this.OriginId, this.DestinationId, '' ), pOrig );
  }
  if (!showWP) {
    if (!pNavDest || pointDist( pNavDest, pDest ) > 100) {
      g.Text3D( makeText( nLegs-1, this.OriginId, this.DestinationId, '' ), pDest );
    }
  }

  g.DeleteClipPlanes();

}

// flight plan App

var FlightPlanApp = {
  gFlatEarth: null, // JsGraph; flat earth graphics context
  GlobeEarth: null, // GlobeEarthModel
  
  OrigLatDeg: 0,
  OrigLatMin: 0,
  OrigLatSec: 0,
  OrigLngDeg: 0,
  OrigLngMin: 0,
  OrigLngSec: 0,
  DestLatDeg: 0,
  DestLatMin: 0,
  DestLatSec: 0,
  DestLngDeg: 0,
  DestLngMin: 0,
  DestLngSec: 0,
  OrigLatRad: 0,
  OrigLngRad: 0,
  DestLatRad: 0,
  DestLngRad: 0,
  origValid: false,
  destValid: false,
  
  OrigFEPos: [ 0, 0 ],
  DestFEPos: [ 0, 0 ],
  R: 6371, // km
  EQ: 6371 * Math.PI / 2, // flat earth radius of equator in km
  
  legDist: 500 * 1.852,  // km
  TAS: 470 * 1.852, // km/h
  FuelOnBoard: 0,   // units
  FuelFlow: 0,      // units/h
  OriginId: '',
  DestinationId: '',
  
  dist: 0,       // flat earth distances
  dist_nm: 0,
  dist_mile: 0,
  
  distGC: 0,     // globe great circle distances
  distGC_nm: 0,
  distGC_mile: 0,
  
  vLeg: [ 0, 0 ],
  tFlight: 0,
  tLeg: 0, 
  FlightPlan: null,

  OrigToDMS: function() {
    var s = DegToDMS( this.OrigLatDeg );
    this.OrigLatDeg = s.d;
    this.OrigLatMin = s.m;
    this.OrigLatSec = s.s;
    var s = DegToDMS( this.OrigLngDeg );
    this.OrigLngDeg = s.d;
    this.OrigLngMin = s.m;
    this.OrigLngSec = s.s;
  },

  DestToDMS: function() {
    var s = DegToDMS( this.DestLatDeg );
    this.DestLatDeg = s.d;
    this.DestLatMin = s.m;
    this.DestLatSec = s.s;
    var s = DegToDMS( this.DestLngDeg );
    this.DestLngDeg = s.d;
    this.DestLngMin = s.m;
    this.DestLngSec = s.s;
  },

  ToDMS: function() {
    this.OrigToDMS();
    this.DestToDMS();
  },

  ToDeg: function() {
    var abs = Math.abs;
    function DMStoDeg( d, m, s ) {
      var sgn = d < 0 ? -1 : (m < 0 ? -1 : (s < 0 ? -1 : 1 ));
      return sgn * (abs(d) + (abs(m) + (abs(s) / 60)) / 60);
    }
    this.OrigLatDeg = DMStoDeg( this.OrigLatDeg, this.OrigLatMin, this.OrigLatSec );
    this.OrigLatMin = 0; this.OrigLatSec = 0;
    this.OrigLngDeg = DMStoDeg( this.OrigLngDeg, this.OrigLngMin, this.OrigLngSec );
    this.OrigLngMin = 0; this.OrigLngSec = 0;
    this.DestLatDeg = DMStoDeg( this.DestLatDeg, this.DestLatMin, this.DestLatSec );
    this.DestLatMin = 0; this.DestLatSec = 0;
    this.DestLngDeg = DMStoDeg( this.DestLngDeg, this.DestLngMin, this.DestLngSec );
    this.DestLngMin = 0; this.DestLngSec = 0;
  },
  
  EnterAirport: function( id, lat, lng ) {
    if (!this.origValid) {
      this.OriginId = id;
      this.OrigLatDeg = lat;
      this.OrigLngDeg = lng;
      this.OrigToDMS();
      this.origValid = true;
      if (this.destValid) {
        this.CreateFlightPlan();
      }
      return;
    }
    if (this.destValid) {
      // shift destiation to origin
      this.OriginId = this.DestinationId;
      this.OrigLatRad = this.DestLatRad;
      this.OrigLngRad = this.DestLngRad;
      this.OrigLatDeg = this.DestLatDeg;
      this.OrigLatMin = this.DestLatMin;
      this.OrigLatSec = this.DestLatSec;
      this.OrigLngDeg = this.DestLngDeg;
      this.OrigLngMin = this.DestLngMin;
      this.OrigLngSec = this.DestLngSec;
      this.origValid = this.destValid;
    }
    this.DestinationId = id;
    this.DestLatDeg = lat;
    this.DestLngDeg = lng;
    this.DestToDMS();
    this.destValid = true;
    this.CreateFlightPlan();
  },
  
  BackTrack: function() {
    var t;
    t = this.OriginId;
    this.OriginId = this.DestinationId;
    this.DestinationId = t;

    t = this.OrigLatDeg;
    this.OrigLatDeg = this.DestLatDeg;
    this.DestLatDeg = t;
    t = this.OrigLatMin;
    this.OrigLatMin = this.DestLatMin;
    this.DestLatMin = t;
    t = this.OrigLatSec;
    this.OrigLatSec = this.DestLatSec;
    this.DestLatSec = t;

    t = this.OrigLngDeg;
    this.OrigLngDeg = this.DestLngDeg;
    this.DestLngDeg = t;
    t = this.OrigLngMin;
    this.OrigLngMin = this.DestLngMin;
    this.DestLngMin = t;
    t = this.OrigLngSec;
    this.OrigLngSec = this.DestLngSec;
    this.DestLngSec = t;
  },
  
  Update: function() {
    var cos = Math.cos, sin = Math.sin, abs = Math.abs, DegToRad = Math.PI / 180;
    function DMStoRad( d, m, s ) {
      var sign = d < 0 ? -1 : 1;
      if (d == 0) sign = m < 0 ? -1 : 1;
      if (d == 0 && m == 0) sign = s < 0 ? -1 : 1;
      return DegToRad * sign * (abs(d) + abs(m) / 60 + abs(s) / 3600);
    }
    this.OrigLatRad = DMStoRad( this.OrigLatDeg, this.OrigLatMin, this.OrigLatSec );
    this.OrigLngRad = DMStoRad( this.OrigLngDeg, this.OrigLngMin, this.OrigLngSec );
    this.DestLatRad = DMStoRad( this.DestLatDeg, this.DestLatMin, this.DestLatSec );
    this.DestLngRad = DMStoRad( this.DestLngDeg, this.DestLngMin, this.DestLngSec );

    // limit some input values
    if (this.legDist < 50 * 1.852) this.legDist = 50 * 1.852;
    if (this.TAS < 50 * 1.852) this.TAS = 50 * 1.852;
    if (this.FuelOnBoard < 0) this.FuelOnBoard = 0;
    if (this.FuelFlow < 0) this.FuelFlow = 0;
    
    this.origValid = this.origValid || this.OrigLatDeg != 0 || this.OrigLatMin != 0 || this.OrigLatSec != 0;
    this.destValid = this.destValid || this.DestLatDeg != 0 || this.DestLatMin != 0 || this.DestLatSec != 0;

    // compute flat earth distances
    // see http://walter.bislins.ch/bloge/index.asp?page=Distances+on+Globe+and+Flat+Earth
    var pi2 = Math.PI / 2;
    var r1 = (1 - this.OrigLatRad/pi2) * this.EQ;
    var r2 = (1 - this.DestLatRad/pi2) * this.EQ;
    var x1 = r1 * cos( this.OrigLngRad );
    var y1 = r1 * sin( this.OrigLngRad );
    var x2 = r2 * cos( this.DestLngRad );
    var y2 = r2 * sin( this.DestLngRad );
    this.OrigFEPos = [ x1, y1 ];
    this.DestFEPos = [ x2, y2 ];
    var dx = x2 - x1;
    var dy = y2 - y1;
    this.dist = Math.sqrt( dx*dx + dy*dy );
    this.dist_nm = this.dist / 1.852;
    this.dist_mile = this.dist / 1.609344;
    
    // some leg vales
    this.vLeg = JsgVect2.Scale( JsgVect2.Norm( JsgVect2.Sub( this.DestFEPos, this.OrigFEPos ) ), this.legDist );
    this.tFlight = this.dist / this.TAS;
    this.tLeg = this.legDist / this.TAS;

    // compute globe great circle distances
    // see http://walter.bislins.ch/bloge/index.asp?page=Distances+on+Globe+and+Flat+Earth
    var r = cos( this.OrigLatRad );
    var v1x = cos( this.OrigLngRad ) * r;
    var v1y = sin( this.OrigLngRad ) * r;
    var v1z = sin( this.OrigLatRad );
    var r = cos( this.DestLatRad );
    var v2x = cos( this.DestLngRad ) * r;
    var v2y = sin( this.DestLngRad ) * r;
    var v2z = sin( this.DestLatRad );
    var alpha = Math.acos( RangePM1( v1x * v2x + v1y * v2y + v1z * v2z ) ); // 0..pi
    this.distGC = this.R * alpha;
    this.distGC_nm = this.distGC / 1.852;
    this.distGC_mile = this.distGC / 1.609344;
    
    if (this.FlightPlan || (this.origValid && this.destValid)) {
      this.UpdateFlightPlan();
    }
    this.DrawGraphs();

  },
  
  DrawGraphs: function() {
    if (this.gFlatEarth) {
      DrawFlatEarthModel( this.gFlatEarth );
    }
    if (this.GlobeEarth) {
      this.GlobeEarth.Draw();
    }
  },
  
  DeleteFlightPlan: function() {
    if (this.FlightPlan) {
      this.FlightPlan.Unrender();
      this.FlightPlan = null;
    }
    this.DrawGraphs();
    this.origValid = false;
    this.destValid = false;
  },

  CreateFlightPlan: function() {
    this.origValid = true;
    this.destValid = true;
    this.FlightPlan = this.FlightPlanFromData();
    this.FlightPlan.Render();
    this.DrawGraphs();
  },
  
  UpdateFlightPlan: function() {
    this.FlightPlan = this.FlightPlanFromData();
    this.FlightPlan.Render();
  },
  
  FlightPlanFromData: function() {
    var textAngle = - JsgVect2.Angle( [-1,0], this.vLeg, true ) * 180 / Math.PI;
    if (textAngle > 90) {
      textAngle -= 180;
    } else if (textAngle < -90) {
      textAngle += 180;
    }
    var fp = new FlightPlan( 'FlightPlan', this.OriginId, this.DestinationId, textAngle );
    var nLegs = Math.floor( this.dist / this.legDist );
    for (var i = 0; i <= nLegs; i++) {
      var p = JsgVect2.Add( this.OrigFEPos, JsgVect2.Scale( this.vLeg, i ) );
      var tta = this.tFlight - i * this.tLeg;
      var ddg = (this.dist - i * this.legDist) / 1.852;
      var pLatLng = this.LatLngOfP( p );
      var hdg = this.Heading( p, this.vLeg );
      var fob = this.FuelOnBoard - i * this.tLeg * this.FuelFlow;
      fp.AddLeg( hdg, pLatLng.Lat, pLatLng.Lng, ddg, tta, fob );
    }
    // and add last leg to destination
    var p = this.DestFEPos;
    var pLatLng = this.LatLngOfP( p );
    var hdg = this.Heading( p, this.vLeg );
    var fob = this.FuelOnBoard - this.tFlight * this.FuelFlow;
    fp.AddLeg( hdg, pLatLng.Lat, pLatLng.Lng, 0, 0, fob );
    return fp;
  },
  
  LatLngOfP: function( p ) {
    // computes latitude and longitude for FE point p: JsgVect2
    var r = JsgVect2.Length( p );
    if (r == 0) return { Lat: 90, Lng: 0 };  // north pole
    var lat = 90 * ( 1 - r / this.EQ );
    var lng = Math.acos( p[0] / r ) * 180 / Math.PI;
    if (p[1] < 0) lng = -lng;
    return { Lat: lat, Lng: lng };
  },
  
  Heading: function( p, v ) {
    // returns heading on FE of vector v at point p on flat earth 0..359.999
    var t = [ -v[1], v[0] ];
    var north = JsgVect2.Norm( JsgVect2.Scale( p, -1 ) );
    var vdir = JsgVect2.Norm( v );
    if (JsgVect2.Length(p) < 0.000001) {
      // on north pole direction to north is opposite to direction of travel v
      north = JsgVect2.Scale( vdir, -1 );
    }
    var hdg = Math.acos( RangePM1( JsgVect2.ScalarProd( vdir, north ) ) ) * 180 / Math.PI;
    if (JsgVect2.ScalarProd( north, t ) < 0) {
      hdg = 360 - hdg;
    }
    if (hdg > 359.9) hdg = 0;
    return hdg;
  },
};

function UpdateAll() {
  FlightPlanApp.Update();
  ControlPanels.Update();
}

function Reset() {
  FlightPlanApp.DeleteFlightPlan();
  ControlPanels.Reset();
  UpdateAll();
}

function ToDMS() {
  FlightPlanApp.ToDMS();
  UpdateAll();
}

function ToDeg() {
  FlightPlanApp.ToDeg();
  UpdateAll();
}

function CreateFlightPlan() {
  FlightPlanApp.CreateFlightPlan();
  UpdateAll();
}

function BackTrack() {
  FlightPlanApp.BackTrack();
  UpdateAll();
}

function EnterSydney() {
  FlightPlanApp.EnterAirport( 'YSSY', -33.86882, 151.209296 );
  UpdateAll();
}

function EnterPapeete() {
  FlightPlanApp.EnterAirport( 'NTAA', -17.5537, -149.606995 );
  UpdateAll();
}

function EnterSantiago() {
  FlightPlanApp.EnterAirport( 'SCEL', -33.3930016, -70.7857971 );
  UpdateAll();
}

function EnterPerth() {
  FlightPlanApp.EnterAirport( 'YPPH', -31.950527, 115.860457 );
  UpdateAll();
}

function EnterJohannesburg() {
  FlightPlanApp.EnterAirport( 'FAOR', -26.204103, 28.047305 );
  UpdateAll();
}

function EnterAuckland() {
  FlightPlanApp.EnterAirport( 'NZAA', -37.0080986, 174.7920074 );
  UpdateAll();
}

function EnterChristchurch() {
  FlightPlanApp.EnterAirport( 'NZCH', -43.489399, 172.5319977 );
  UpdateAll();
}

function EnterPuntaArenas() {
  FlightPlanApp.EnterAirport( 'SCCI', -53.0026016, -70.854599 );
  UpdateAll();
}

function EnterBuenosAires() {
  FlightPlanApp.EnterAirport( 'SAEZ', -34.8222, -58.5358 );
  UpdateAll();
}

function EnterLondon() {
  FlightPlanApp.EnterAirport( 'EGLL', 51.148102, -0.190278 );
  UpdateAll();
}

function EnterMoscow() {
  FlightPlanApp.EnterAirport( 'UUDD', 55.408798, 37.9063 );
  UpdateAll();
}

function EnterSPole() {
  FlightPlanApp.EnterAirport( 'NZSP', -90, -129.16130065 );
  UpdateAll();
}

xOnLoad( UpdateAll );

</jscript>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    ModelRef: 'FlightPlanApp',
    OnModelChange: UpdateAll,
    NCols: 6,
    Digits: 3,
    PanelFormat: 'LabelLeft',
  }

).AddHeader( {
    Text: '',
    ColSpan: 1
  }

).AddHeader( {
    Text: 'Latitude (N > 0, S < 0)',
    ColSpan: 5
  }

).AddHeader( {
    Text: 'Longitude (E > 0, W < 0)' + ControlPanels.ResetButtonR() + ControlPanels.SmallButtonR('&rarr;Deg','ToDeg()') + ControlPanels.SmallButtonR('&rarr;DMS','ToDMS()'),
    ColSpan: 6
  }

).AddTextField( {
    Name: 'OrigLatDeg',
    Label: 'Orig.',
    Units: '&deg;',
    Digits: 6,
  }

).AddTextField( {
    Name: 'OrigLatMin',
    Label: '-',
    Units: 'min',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'OrigLatSec',
    Label: '-',
    Units: 'sec',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'OrigLngDeg',
    Label: '',
    Units: '&deg;',
    ColSpan: 2,
    Digits: 6,
  }

).AddTextField( {
    Name: 'OrigLngMin',
    Label: '-',
    Units: 'min',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'OrigLngSec',
    Label: '-',
    Units: 'sec',
    ColSpan: 2,
  }


).AddTextField( {
    Name: 'DestLatDeg',
    Label: 'Dest.',
    Units: '&deg;',
    Digits: 6,
  }

).AddTextField( {
    Name: 'DestLatMin',
    Label: '-',
    Units: 'min',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'DestLatSec',
    Label: '-',
    Units: 'sec',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'DestLngDeg',
    Units: '&deg;',
    ColSpan: 2,
    Digits: 6,
  }

).AddTextField( {
    Name: 'DestLngMin',
    Label: '-',
    Units: 'min',
    ColSpan: 2,
  }

).AddTextField( {
    Name: 'DestLngSec',
    Label: '-',
    Units: 'sec',
    ColSpan: 2,
  }

).Render();

</jscript>

{{end scroll}}
{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    ModelRef: 'FlightPlanApp',
    OnModelChange: UpdateAll,
    NCols: 3,
    ReadOnly: true,
    Digits: 6,
  }

).AddTextField( {
    Name: 'dist_nm',
    Label: 'Distance',
    Units: 'nm'
  }

).AddTextField( {
    Name: 'dist_mile',
    Label: '-',
    Units: 'mile'
  }

).AddTextField( {
    Name: 'dist',
    Label: '-',
    Units: 'km'
  }

).AddTextField( {
    Name: 'distGC_nm',
    Label: 'GC-Dist',
    Units: 'nm'
  }

).AddTextField( {
    Name: 'distGC_mile',
    Label: '-',
    Units: 'mile'
  }

).AddTextField( {
    Name: 'distGC',
    Label: '-',
    Units: 'km'
  }

).AddTextField( {
    Name: 'OriginId',
    Label: 'Orig-Id',
    Mult: 0,
    ReadOnly: false,
  }
  
).AddTextField( {
    Name: 'legDist',
    Label: 'LegDist',
    Mult: 1.852,
    Units: 'nm',
    ReadOnly: false,
  }

).AddTextField( {
    Name: 'TAS',
    Mult: 1.852,
    Units: 'kts',
    ReadOnly: false,
  }

).AddTextField( {
    Name: 'DestinationId',
    Label: 'Dest-Id',
    Mult: 0,
    ReadOnly: false,
  }
  
).AddTextField( {
    Name: 'FuelFlow',
    Label: 'FF',
    Units: 'units/h',
    ReadOnly: false,
  }

).AddTextField( {
    Name: 'FuelOnBoard',
    Label: 'FOB',
    Units: 'units',
    ReadOnly: false,
  }

).Render();

</jscript>

{{end scroll}}

[javascript:EnterSydney()|{{ButtonText|Sydney|blue}}]
[javascript:EnterChristchurch()|{{ButtonText|Christchurch|blue}}]
[javascript:EnterAuckland()|{{ButtonText|Auckland|blue}}]
[javascript:EnterPapeete()|{{ButtonText|Papeete|blue}}]
[javascript:EnterSantiago()|{{ButtonText|Santiago|blue}}]
[javascript:EnterPuntaArenas()|{{ButtonText|Punta Arenas|blue}}]
[javascript:EnterBuenosAires()|{{ButtonText|Buenos Aires|blue}}]
[javascript:EnterJohannesburg()|{{ButtonText|Johannesburg|blue}}] 
[javascript:EnterPerth()|{{ButtonText|Perth|blue}}] 
[javascript:EnterLondon()|{{ButtonText|London|blue}}] 
[javascript:EnterMoscow()|{{ButtonText|Moscow|blue}}] 
[javascript:EnterSPole()|{{ButtonText|South Pole|blue}}] 
[javascript:BackTrack()|{{ButtonText|Back Track|red}}] 
[javascript:CreateFlightPlan()|{{ButtonText|Create Flight Plan|green}}] 

{{OnOff|Show Flight Plan|Hide Flight Plan|open|noborder}}
{{scroll}}
{{div|id=FlightPlan|$VMargin}}

To create a Flight Plan...

{{end div}}
{{end scroll}}
{{OnOffEnd}}

{{OnOff|Show Flat Earth Map|Hide Flat Earth Map|open|noborder}}

<jscript>

FlightPlanApp.gFlatEarth = NewGraph2D( {
  Id: 'JsGraph1',
  Width: '100%',
  Height: '100%',
  DrawFunc: DrawFlatEarthModel,
  AutoReset: false,
  AutoClear: false,
  AutoScalePix: true
} );

function DrawFlatEarthModel( g ) {

  g.Reset();

  var VpScaleArgs = true;
  var VpClip      = false;
  var VpLeft      = 8;
  var VpTop       = 8;
  var VpRight     = -8;
  var VpBottom    = -8;
  g.SetViewport( VpLeft, VpTop, VpRight, VpBottom, VpScaleArgs, VpClip );

  var WinXmin = -1;
  var WinXmax = 1;
  var WinYmin = -1;
  var WinYmax = 1;
  g.SetWindow( WinXmin, WinYmin, WinXmax, WinYmax );

  EarthMap.Radius = 1;
  EarthMap.SetWaterColor( '#d3e2f5' );
  EarthMap.SetLakeColor( '#d3e2f5', '#8cbe5d' );
  EarthMap.SetContinentColor( null, '#c6dfaf', '#8cbe5d' );
  EarthMap.SetLandColor( 'Antarctica', '#eee', '#ccc' );
  EarthMap.DrawFlatEarth( g );

  g.SetAlpha( 0.3 );
  g.SetLineAttr( 'gray', 1 );
  EarthMap.DrawFlatEarthGrid( g, 15, 15 );
  g.SetLineAttr( 'black', 1 );
  EarthMap.DrawFlatEarthEquator( g );
  EarthMap.DrawFlatEarthBorder( g );
  EarthMap.DrawFlatEarthMeridian( g );
  g.SetAlpha( 1 );

  if (FlightPlanApp.FlightPlan) {
    FlightPlanApp.FlightPlan.DrawFlatEarthPath( g );
  }
}

</jscript>
{{OnOffEnd}}

{{OnOff|Show Globe Earth Map|Hide Globe Earth Map|open|noborder}}

<jscript>

var GlobeEarthModel = {
  R: 6371,
  Tilt: -25,
  Rot: -225,
  Zoom: 0.95,
  ShowWpPath: true,
  ShowNavPath: true,
  ShowGCPath: true,
  
  Graph: null,   // JsGraphX3D; 3D graphics context for globe earth
  DistFact: 100, // times R; distance between camera and earth surface

  Create: function() {
    var me = this;
    this.Graph = NewGraphX3D( {
      Id: 'GlobeEarth-Graph',
      Width: '100%',
      Height: '66%',
      DrawFunc: function() { me.Draw(); },
      AutoReset: false,
      AutoClear: false,
      AutoScalePix: true,
      BorderWidth: 0,
    } );
    return this;
  },

  Draw: function() {
    var g = this.Graph;

    g.Reset3D();

    var vpWidth = g.VpHeight;
    g.SetGraphClipping( true, '', 0.01 );

    g.SetLineAttr( '#ddd', 1 );
    g.Frame();

    var dist = (this.DistFact+1) * this.R;
    var sceneSize = 2 * dist * Math.tan( Math.asin( this.R / dist ) );
    g.SetCameraScale( sceneSize );
    g.SetCameraZoom( this.Zoom );
    g.SetCameraView( [0,0,0], 0, 0, (this.DistFact+1) * this.R );
    g.SetWindowToCameraScreen();

    // set earth transformation: earth rotation and tilt
    g.TransRotateZ3D( -this.Rot );
    g.TransRotateY3D( this.Tilt );
    EarthMap.Trans = g.Trans3D;
    g.ResetTrans3D();

    // draw earth with white Antarctica
    EarthMap.Radius = this.R;
    EarthMap.SetWaterColor( '#d3e2f5' );
    EarthMap.SetLakeColor( '#d3e2f5', '#8cbe5d' );
    EarthMap.SetContinentColor( null, '#c6dfaf', '#8cbe5d' );
    EarthMap.SetLandColor( 'Antarctica', '#eee', '#ccc' );

    EarthMap.DrawGlobe( g );

    g.SetAlpha( 0.3 );
    g.SetLineAttr( 'gray', 1 );
    EarthMap.DrawGlobeGrid( g, 15, 15 );
    g.SetLineAttr( 'black', 1 );
    EarthMap.DrawGlobeEquator( g );
    EarthMap.DrawGlobeMeridian( g );
    g.SetAlpha( 1 );

    if (FlightPlanApp.FlightPlan) {
      g.TransRotateZ3D( -this.Rot );
      g.TransRotateY3D( this.Tilt );
      FlightPlanApp.FlightPlan.DrawGlobePath( g, this.ShowWpPath, this.ShowNavPath, this.ShowGCPath );
      g.ResetTrans3D();
    }
  },

};

FlightPlanApp.GlobeEarth = GlobeEarthModel.Create();

function UpdateGlobe() {
  ControlPanels.Update();
  GlobeEarthModel.Draw();
}

ControlPanels.NewSliderPanel( { 
  Name: 'GlobeEarth-Sliders',
  ModelRef: 'GlobeEarthModel',
  NCols: 1, 
  ValuePos: 'left',
  OnModelChange: UpdateGlobe, 
  Format: 'fix0', 
  Digits: 2,
  ReadOnly: false,
  PanelFormat: 'InputShortWidth'

} ).AddValueSliderField( {
  Name: 'Rot',
  Label: 'Rotation',
  Units: '&deg;',
  Color: 'blue',
  Min: -360,
  Max: 360,

} ).AddValueSliderField( {
  Name: 'Tilt',
  Units: '&deg;',
  Color: 'green',
  Min: -89.999,
  Max: 89.999,

} ).AddValueSliderField( {
  Name: 'Zoom',
  Color: 'black',
  Min: 0.9,
  Max: 5,

} ).Render();

ControlPanels.NewPanel( { 
  Name: 'GlobePath-Options',
  ModelRef: 'GlobeEarthModel',
  NCols: 1, 
  ValuePos: 'left',
  OnModelChange: UpdateAll, 

} ).AddCheckboxField( {
  Name: 'Options',
  Label: 'Show-Path',
  ColSpan: 3,
  Items: [
    {
      Name: 'ShowWpPath',
      Text: 'Way Points',
    },
    {
      Name: 'ShowNavPath',
      Text: 'Navigation',
    },
    {
      Name: 'ShowGCPath',
      Text: 'Great Circle',
    },
  ],

} ).Render();

</jscript>

{{OnOffEnd}}

More Page Infos / Sitemap
Created Tuesday, September 12, 2017
Scroll to Top of Page
Changed Wednesday, September 13, 2017