WaBis

walter.bislins.ch

Source Code: Refraction Simulator

This page shows the source code of the Simulation of Atmospheric Refraction. This code itself is stored in Special: Refraction Simulator Code.

The simulation is entirely written in JavaScript and uses the HTML canvas element to display the rendered graphics. The source code of the simulation App alone, excluding the code of the modules for the graphic output and control panels, consists of about 5800 lines.

The description of the graphic module JsGraph and the ControlPanels module can be found here:

#INCLUDE Async.inc
#INCLUDE JsgVectMat3.inc
#INCLUDE JsGraph.inc
#INCLUDE ControlPanel.inc
#INCLUDE Tabs.inc
#INCLUDE DataX.inc
#INCLUDE TextControl.inc



<style>
#SceneDescription { margin-top: 0em; margin-bottom: 0.5em; font-size: smaller; }
#ScenePresetButtons { margin-top: 1em; margin-bottom: 0em; }
#ScenePresetButtons p { margin: 0; }
#SceneDescription { background-color: #ff8; }
.Wiki div.GraphicPanels { margin-top: 0.5em; }

span.lbtn { border-radius: 4px; }
table.ControlPanel th { background-color: #ddd; }
table.ControlPanel tr, table.ControlPanel td { vertical-align: middle; padding-bottom: 0px; padding-top: 3px; }

#LeftDisplaySelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#LeftDisplayModelSelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#LeftDisplayCurveSelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#LeftDisplayModelSelectorPanel .FieldGrid { padding-bottom: 0px; }
#LeftDisplayCurveSelectorPanel .FieldGrid { padding-bottom: 0px; }

#RightDisplaySelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#RightDisplayModelSelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#RightDisplayCurveSelectorPanel td { padding-top: 0px; padding-bottom: 0px; line-height: 1em; }
#RightDisplayModelSelectorPanel .FieldGrid { padding-bottom: 0px; }
#RightDisplayCurveSelectorPanel .FieldGrid { padding-bottom: 0px; }

#TargetPlanePanel th.HdCol1 { text-align: right; padding-right: 0; }
#TargetPlanePanel-Name { text-align: left; width: 90% !important; }
#TargetPlanePanel tr.Row2 { background-color: #ddd; border-top: 1px solid #bbb; }
#TargetPlanePanel table.FieldGrid { background: #ddd; }
#TargetPlanePanel td.FieldCell { border: 1px solid #ddd; }
#TargetPlanePanel td.FieldCell:hover { background: #eee; border: 1px solid #ccc; }

#BaroSettingsObserverPanel tr.Row1, #BaroSettingsObserverPanel tr.Row2 { background-color: #ddd; }
#BaroSettingsTargetPanel tr.Row1, #BaroSettingsTargetPanel tr.Row2 { background-color: #ddd; }

#CameraPanel { margin-bottom: 0; }

#TargetPanel-ImageSrc { text-align: left; width: 90% !important; }
#TargetPanel tr.Row1, #TargetPanel tr.Row2 { background-color: #ddd; }

#SaveRestorePanel { font-family: Courier; margin-bottom: 0.5em; }

#LeftDisplaySelectorPanel, #LeftDisplaySelectorPanel table.FieldGrid { background: #ddd; }
#LeftDisplaySelectorPanel td.FieldCell { border: 1px solid #ddd; }
#LeftDisplaySelectorPanel td.FieldCell:hover { background: #eee; border: 1px solid #ccc; }

#RightDisplaySelectorPanel, #RightDisplaySelectorPanel table.FieldGrid { background: #ddd; }
#RightDisplaySelectorPanel td.FieldCell { border: 1px solid #ddd; }
#RightDisplaySelectorPanel td.FieldCell:hover { background: #eee; border: 1px solid #ccc; }

#LeftDisplaySelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#LeftDisplaySelectorPanel td.Label div.FieldText { width: 75px !important; }
#LeftDisplayModelSelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#LeftDisplayModelSelectorPanel td.Label div.FieldText { width: 75px !important; }
#LeftDisplayCurveSelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#LeftDisplayCurveSelectorPanel td.Label div.FieldText { width: 75px !important; }

#RightDisplaySelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#RightDisplaySelectorPanel td.Label div.FieldText { width: 75px !important; }
#RightDisplayModelSelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#RightDisplayModelSelectorPanel td.Label div.FieldText { width: 75px !important; }
#RightDisplayCurveSelectorPanel table.FieldGrid td.FieldCell { width: 85px !important; }
#RightDisplayCurveSelectorPanel td.Label div.FieldText { width: 75px !important; }

#LeftDisplaySelectorPanel, #RightDisplaySelectorPanel { margin-bottom: 0; }
#LeftDisplayModelSelectorPanel, #RightDisplayModelSelectorPanel { margin-bottom: 0; }
#LeftDisplayCurveSelectorPanel, #RightDisplayCurveSelectorPanel { margin-bottom: 0; }

#BaroSettingsObserverPanel tr.Row2, #BaroSettingsTargetPanel tr.Row2 { border-bottom: 2px solid #ddd; }
#TargetPlanePanel { margin-bottom: 0; }
#TargetPanel tr.Row2, #TargetPanel tr.Row5 { border-bottom: 2px solid #ddd; }

input[type="text"]:disabled { background: #ddd; }
#BaroSettingsObserverPanel-ObserverDistance { background: #ff8; }
#BaroSettingsTargetPanel-TargetDistance { background: #ff8; }
#RayTracerPanel-RayDistanceLimit { background: #ff8; }
#TargetPanel-Width, #TargetPanel-Height { background: #ff8; }

#TargetPlanePanel-TargetPresets { padding-bottom: 7px; }
</style>

<jscript>

/*
// debug
xDebug = true;
xDebugOutId = 'DebugLogPanel';
IC.EnableStatusDisplay = true;
*/

var DefaultDigits = 6;

//NumFormatter.SetLang( 'en' );

var V2 = JsgVect2;
var M2 = JsgMat2;
var V3 = JsgVect3;
var M3 = JsgMat3;
var abs = Math.abs;
var sin = Math.sin;
var cos = Math.cos;
var tan = Math.tan;
var acos = Math.acos;
var sqrt = Math.sqrt;
var floor = Math.floor;
var round = Math.round;
var log10 = Math.log10;
var log = Math.log;
var pow = Math.pow;

function maxOf( a, b ) {
  return a > b ? a : b;
}

function sqr( x ) { return x * x; }

function rad( x ) { return x * Math.PI / 180; }

function wrapModulo( x, m ) {
  if (x >= 0) return x % m;
  return (m-1) - ((-x-1) % m);
}

function ContainsStr( s, find ) {
  var sList = s.split(',');
  for (var i = 0; i < sList.length; i++) {
    if (sList[i] == find) {
      return true;
    }
  }
  return false;
}

function Help( header ) {
  // creates a wiki link to a help page header
  var txt = '[[Refraction Simulation Help';
  if (header) {
    txt += '#' + header;
  }
  txt += ']]';
  return txt;
}

function CompTicParams( valRange, nTicsMax ) {
  // returns { TicSize: num, NTics: int, Digits: int }

  var result = {
    TicSize: 0,
    NTics: 0,
    Digits: 0,
  };
  var e = floor( log10( valRange ) );
  var r = pow( 10, e );
  var n0 = floor( valRange / r / 2 );
  var n1 = floor( valRange / r );
  var n2 = floor( 2 * valRange / r );
  var n3 = floor( 5 * valRange / r );
  var n4 = floor( 10 * valRange / r );
  if (n4 <= nTicsMax) {
    result.TicSize = r / 10;
    result.NTics = n4;
  } else if (n3 <= nTicsMax) {
    result.TicSize = r / 5;
    result.NTics = n3;
  } else if (n2 <= nTicsMax) {
    result.TicSize = r / 2;
    result.NTics = n2;
  } else if (n1 <= nTicsMax) {
    result.TicSize = r / 2;
    result.NTics = n1;
  } else {
    result.TicSize = r;
    result.NTics = n0;
    e++;
  }
  var digits = 1 - e;
  if (digits < 0) digits = 0;
  result.Digits = digits;
  return result;
}

//////////////////////////////////////////////////////////////
// meta data for Json generation and loading

var PlaneMetaData = [
  { Name: 'Pos', Type: 'arr', Size: 3, ArrayType: 'num', Default: [ 0, 0, 0 ] },
  { Name: 'RotX', Type: 'num', Default: 0 },
  { Name: 'RotY', Type: 'num', Default: 0 },
  { Name: 'RotZ', Type: 'num', Default: 0 }
];

var TargetMetaData = [
  { Name: 'Name', Type: 'str', Default: 'unnamed' },
  { Name: 'Hidden', Type: 'bool', Default: false },
  { Name: 'Plane', Type: 'obj', ObjType: PlaneMetaData },
  { Name: 'ImageSrc', Type: 'str', Default: '' },
  { Name: 'Width', Type: 'num', Default: 0 },
  { Name: 'Height', Type: 'num', Default: 0 },
  { Name: 'LimitType', Type: 'int', Default: 0 },
  { Name: 'LimitLeft', Type: 'num,null' },
  { Name: 'LimitRight', Type: 'num,null' },
  { Name: 'LimitTop', Type: 'num,null' },
  { Name: 'LimitBottom', Type: 'num,null' },
  { Name: 'Pattern', Type: 'int', Default: -1 },
  { Name: 'Param1', Type: 'num', Default: 0.5 },
  { Name: 'Param2', Type: 'num', Default: 0.5 },
  { Name: 'Color1', Type: 'arr', Size: 4, ArrayType: 'num', Default: [ 0, 0, 0, 1 ] },
  { Name: 'Color2', Type: 'arr', Size: 4, ArrayType: 'num', Default: [ 0, 0, 0, 1 ] },
  { Name: 'TransparentColor', Type: 'arr', Size: 4, ArrayType: 'num', Default: [ 0, 0, 0, 0 ] },
  { Name: 'Alpha', Type: 'num', Default: 1 },
  { Name: 'ForceOversampling', Type: 'bool', Default: true },
];

var RayTracerMetaData = [
  { Name: 'Visibility', Type: 'num', Default: 0 },
  { Name: 'RayDelta', Type: 'num', Default: 0 },
  { Name: 'NRaySegments', Type: 'int', Default: 25 },
  { Name: 'MaxSegments', Type: 'int', Default: 5000 },
  { Name: 'RayDistanceLimit', Type: 'num', Default: 0 },
  { Name: 'DomeHorizonColor', Type: 'arr', Size: 4, ArrayType: 'num', Default: [ 0.94, 0.98, 1, 1] },
  { Name: 'DomeGradientHeight', Type: 'num', Default: 0 },
  { Name: 'DomeZenithColor', Type: 'arr', Size: 4, ArrayType: 'num', Default: [ 0.2, 0.4, 1, 1] },
  { Name: 'RefrEqFact1', Type: 'num', Default: 503 },
  { Name: 'RefrEqFact2', Type: 'num', Default: 0.0343 },
  { Name: 'Targets', Type: 'arr', ArrayType:
    { Type: 'obj', ObjType: TargetMetaData,
      Create: function CreateTarget() { return RefrSimApp.CreateTarget(); },
      Init: function InitTarget( target ) { target.Update(); }
    }
  },
];

var BaroDataMetaData = [
  { Name: 'ObserverPressureAlt', Type: 'num', Default: 0 },
  { Name: 'ObserverPressure', Type: 'num', Default: 1013.25 },
  { Name: 'ObserverDistance', Type: 'num', Default: 0 },
  { Name: 'TargetPressureAlt', Type: 'num', Default: 0 },
  { Name: 'TargetPressure', Type: 'num', Default: 1013.25 },
  { Name: 'TargetDistance', Type: 'num', Default: 10000 },
  { Name: 'ObserverAlt', Type: 'arr', Size: 5, ArrayType: 'num', Default: [ -1, -1, -1, -1, -1 ] },
  { Name: 'ObserverTemp', Type: 'arr', Size: 5, ArrayType: 'num', Default: [ 15, 15, 15, 15, 15 ] },
  { Name: 'TargetAlt', Type: 'arr', Size: 5, ArrayType: 'num', Default: [ -1, -1, -1, -1, -1 ] },
  { Name: 'TargetTemp', Type: 'arr', Size: 5, ArrayType: 'num', Default: [ 15, 15, 15, 15, 15 ] },
  { Name: 'NSplineSegments', Type: 'int', Default: 20 },
  { Name: 'SplineSmoothness', Type: 'num', Default: 0.75 },
];

var GraphDisplayParamsMetaData = [
  { Name: 'AltitudeRange', Type: 'num', Default: 0 },
  { Name: 'DistanceRange', Type: 'num', Default: 0 },
  { Name: 'RayAngleRange', Type: 'num', Default: 2 },
  { Name: 'NRays', Type: 'int', Default: 100 },
  { Name: 'RaysSpreading', Type: 'int', Default: 0 },
  { Name: 'RaysDspAspectRatio', Type: 'int', Default: 0 },
  { Name: 'ShowHorizon', Type: 'bool', Default: true },
  { Name: 'ShowEyeLevel', Type: 'bool', Default: false },
];

var RayTracerCameraMetaData = [
  { Name: 'Height', Type: 'num', Default: 2 },
  { Name: 'FocalLength', Type: 'num', Default: 1000 },
  { Name: 'Tilt', Type: 'num', Default: 0 },
  { Name: 'PixelSize', Type: 'int', Default: 1 },
  { Name: 'Oversampling', Type: 'int', Default: 1 },
  { Name: 'BoostOversampling', Type: 'bool', Default: false },
  { Name: 'EnableSmoothing', Type: 'bool', Default: false },
  { Name: 'Smoothing', Type: 'int', Default: 2 },
  { Name: 'ShowEyeLevel', Type: 'bool', Default: false },
  { Name: 'ShowHorizon', Type: 'bool', Default: false },
  { Name: 'StartPixelSize', Type: 'int', Default: 64 },
  { Name: 'SensorSize', Type: 'num', Default: 43.2 },
  { Name: 'ProgressBarWidth', Type: 'int', Default: 4 },
];

var RefrSimAppMetaData = {
  Compact: false,
  DefaultPrec: DefaultDigits,
  Properties: [
    { Name: 'LeftDspViewType', Type: 'int', Default: 0 },
    { Name: 'LeftDspModel', Type: 'int', Default: 0 },
    { Name: 'LeftDspRefrType', Type: 'int', Default: 0 },
    { Name: 'LeftDspLeftCurveType', Type: 'int', Default: 0 },
    { Name: 'LeftDspRightCurveType', Type: 'int', Default: 2 },
    { Name: 'RightDspViewType', Type: 'int', Default: 0 },
    { Name: 'RightDspModel', Type: 'int', Default: 0 },
    { Name: 'RightDspRefrType', Type: 'int', Default: 0 },
    { Name: 'RightDspLeftCurveType', Type: 'int', Default: 0 },
    { Name: 'RightDspRightCurveType', Type: 'int', Default: 2 },
    { Name: 'Camera', Type: 'obj', ObjType: RayTracerCameraMetaData },
    { Name: 'RayTracer', Type: 'obj', ObjType: RayTracerMetaData },
    { Name: 'CurrTargetIndex', Type: 'int', Default: -1 },
    { Name: 'BaroData', Type: 'obj', ObjType: BaroDataMetaData },
    { Name: 'GraphDisplayParams', Type: 'obj', ObjType: GraphDisplayParamsMetaData },
  ],
};

//////////////////////////////////////////
// some controller helper functions

function MatchesControllerField( controller, names ) {
  // controller: undefined or CpField or string
  // names: comma separated string of names to check against

  if (!xDef(controller)) return true;
  if (xStr(controller)) return ContainsStr( names, controller );
  return ContainsStr( names, controller.Name );
}

function MatchesControllerPanel( controller, names ) {
  // controller: undefined or CpField or string
  // names: comma separated string of names to check against

  if (!xDef(controller)) return true;
  if (xStr(controller)) return ContainsStr( names, controller );
  return ContainsStr( names, controller.Panel.Name );
}

function MatchesControllerName( controller, names ) {
  // controller: undefined or string
  // names: comma separated string of names to check against

  if (!xDef(controller)) return true;
  if (!xStr(controller)) return false;
  return ContainsStr( names, controller );
}

function MatchesAnyController( controller ) {
  // controller: undefined or CpField

  return !xDef( controller );
}

function GetControllerPanelNames( controller ) {

  if (xArray(controller)) {
    return controller;
  }
  if (xStr(controller)) {
    var sList = controller.split(',');
    return sList;
  }
  if (xObj(controller)) {
    // assert controller is a CpField
    return controller.Panel.Name;
  }
  return null;
}

////////////////////////////////////////
// ImageData

function ImageData( img ) {
  // creates a canvas, draws the image on it and provides a function to access the pixels of the image
  // note: img must be already loaded!
  this.img = img;
  this.width = img.naturalWidth;
  this.height = img.naturalHeight;
  this.canvas = document.createElement("canvas");
  this.canvas.width  = this.width;
  this.canvas.height = this.height;
  this.context = this.canvas.getContext("2d");
  this.context.drawImage( img, 0, 0 );
  this.imgData = this.context.getImageData( 0, 0, this.width, this.height );
}

ImageData.prototype.GetDataIndex = function( x, y ) {
  // or GetDataIndex( [ x, y ] )
  if (xArray(x)) return this.GetDataIndex( x[0], x[1] );
  return 4 * ( y * this.width + x );
}

ImageData.prototype.GetRGBA = function( x, y ) {
  // or GetRGBA( [ x, y ] )
  var data = this.imgData.data;
  var i = this.GetDataIndex( x, y );
  return [ data[i], data[i+1], data[i+2], data[i+3] ];
}

ImageData.prototype.GetAlpha = function( x, y ) {
  // or GetAlpha( [ x, y ] )
  return this.imgData.data[ this.GetDataIndex( x, y ) + 3 ];
}

////////////////////////////////////////
// Plane

function Plane( pos, xdir, ydir, rotx, roty, rotz ) {
  // pos, xdir, ydir: V3; these are not copied
  // plane xy values of intersection line-plane can be accessed with this.Intersect
  // If rot's are defined, the directions xdir and ydir are rotated accordingly.
  // Call Update() to recalculate XDir, YDir and Normal from xdir and ydir after Rot's are changes.

  // some props to hold temp data
  this.WorkPoint  = V3.Null();
  this.Result = V3.Null();
  this.Intersect = [ 0, 0 ];

  this.Pos = pos;
  this.RotX = xDefNum( rotx, 0 );
  this.RotY = xDefNum( roty, 0 );
  this.RotZ = xDefNum( rotz, 0 );
  this.XDir = xdir;
  this.YDir = ydir;
  this.XDirInitial = null;
  this.YDirInitial = null;
  if (xNum(rotx)) {
    this.XDirInitial = V3.Copy( this.XDir );
    this.YDirInitial = V3.Copy( this.YDir );
  }
  this.Normal = V3.Null();
  this.Update();
}

Plane.prototype.Update = function( controller ) {

  if (MatchesControllerField( controller, 'RotX,RotY,RotZ' )) {
    if (this.XDirInitial) {
      // rotate initial directions
      var rotMat = M3.RotatingZ( this.RotZ*Math.PI/180, M3.RotatingY( this.RotY*Math.PI/180, M3.RotatingX( this.RotX*Math.PI/180 ) ) );
      M3.TransTo( rotMat, this.XDirInitial, this.XDir );
      M3.TransTo( rotMat, this.YDirInitial, this.YDir );
    }
    this.Normalize();
  }
}

Plane.prototype.Normalize = function() {
  // makes plane vectors unit length and perpendicular
  var XdirNorm = V3.NormTo( this.XDir );
  var ZDir = V3.MultTo( this.WorkPoint, XdirNorm, this.YDir );
  V3.NormTo( V3.MultTo( this.YDir, ZDir, XdirNorm ) );
  V3.MultTo( this.Normal, this.XDir, this.YDir );
  return this;
}

Plane.prototype.IntersectLine = function( p1, p2 ) {
  // p1, p2: V3; two points of line
  // returns this.Result: V3 or null; null -> no intersection
  // The plane coordinates in terms of XDir and YDir of the intersection point is stored in this.Intersect
  //
  // http://walter.bislins.ch/blog/index.asp?page=Schnittpunkt+Ebene+mit+Gerade+einfach+berechnen+%28JavaScript%29
  //
  // function is optimized to not use temporary objects by using static this.WorkPoint instead

  var r = V3.SubFrom( V3.CopyTo( p2, this.Result ), p1 );
  var r3 = V3.ScalarProd( r, this.Normal );
  if (r3 == 0) {
    // rounding error? try with swaped points
    var tmp = p1;
    p1 = p2;
    p2 = tmp;
    r = V3.SubFrom( V3.CopyTo( p2, this.Result ), p1 );
    r3 = V3.ScalarProd( r, this.Normal );
    if (r3 == 0) {
      // definitely no intersection
      return null;
    }
  }
  var pos_p1 = V3.SubFrom( V3.CopyTo( p1, this.WorkPoint ), this.Pos );
  var r1 = V3.ScalarProd( r, this.XDir );
  var r2 = V3.ScalarProd( r, this.YDir );
  var P1 = V3.ScalarProd( pos_p1, this.XDir );
  var P2 = V3.ScalarProd( pos_p1, this.YDir );
  var P3 = V3.ScalarProd( pos_p1, this.Normal );
  var a = -P3 / r3;
  this.Intersect = [ P1 + a * r1, P2 + a * r2 ];
  return V3.AddTo( V3.ScaleTo( r, a ), p1 );
}

Plane.prototype.IsPointOnTop = function( p ) {
  // returns true if p is at the side of the plane where the normal is pointing to or on the plane
  // p: V3
  //
  // function is optimized to not use temporary objects by using static this.WorkPoint instead

  var pos_p = V3.SubFrom( V3.CopyTo( p, this.WorkPoint ), this.Pos );
  return V3.ScalarProd( pos_p, this.Normal ) >= 0;
}

Plane.prototype.PointOnPlane = function( x, y, v ) {
  // or PointOnPlane( V2, v )
  // v: V3 or undefined
  // returns this.Result or v
  //
  // transforms x, y coordinates on this plane to 3D coordinates and stores the result in v or this.Result

  if (V2.Ok(x)) {
    return this.PointOnPlane( x[0], x[1], y );
  }

  v = v || this.Result;
  V3.CopyTo( this.Pos, v );
  V3.AddTo( v, V3.ScaleTo( V3.CopyTo( this.XDir, this.WorkPoint ), x ) );
  V3.AddTo( v, V3.ScaleTo( V3.CopyTo( this.YDir, this.WorkPoint ), y ) );
  return v;
}

///////////////////////////////////////////////////
// Target Patterns

var TargetPatterns = {

  // array of {
  //  Name: string,
  //  UsedParameters: string,
  //  Func: function( pos, width, height, color1, color2, param1, param2 )
  // }
  // returning JsgColor

  Patterns: [

    { Name: 'Waves',
      UsedParameters: 'Param1,Param2,Color1,Color2',
      Func: function WavePattern( pos, width, height, color1, color2, param1, param2 ) {
        // height = wavelength, param1 = amlitude, param2 = offset
        height = abs(height);
        if (height == 0) height = 100;
        if (param1 < 0) param1 = 0;
        if (param1 > 1) param1 = 1;
        if (param2 < 0) param2 = 0;
        if (param2 > 1) param2 = 1;
        var val = pos[1] / height + 0.5 + (1-param2);
        if (val >= 0) {
          val = val % 1;
        } else {
          val = 1 - (-val % 1);
        }
        var level = 0.5 - param1 / 2 * cos( 2 * Math.PI * val );  // 0..1
        color1 = JsgColor.Copy( color1 );
        return JsgColor.Blend( color1, color1, color2, level );
      }
    },

    { Name: 'Stripes',
      UsedParameters: 'Param1,Param2,Color1,Color2',
      Func: function StripesPattern( pos, width, height, color1, color2, param1, param2 ) {
        // height = stripe pattern height, param1 = ratio color1:color2, param2 = offset
        height = abs(height);
        if (height == 0) height = 100;
        if (param1 <= 0.01) param1 = 0.01;
        if (param1 > 0.99) param1 = 0.99;
        if (param2 < 0) param2 = 0;
        if (param2 > 1) param2 = 1;
        var val = pos[1] / height + 0.5 + (1-param2);
        if (val >= 0) {
          val = val % 1;
        } else {
          val = 1 - (-val % 1);
        }
        if (val < param1) {
          return color1;
        } else {
          return color2;
        }
      }
    },

    { Name: 'Light',
      UsedParameters: 'Param1,Param2,Color1,Color2',
      Func: function LightPattern( pos, width, height, color1, color2, param1, param2 ) {
        // color1 = light color, color2 = halo color, param1 = relative light size, param2 = halo start transparency
        width = abs(width);
        if (width == 0) width = 10;
        height = abs(height);
        if (height == 0) height = width;
        if (param1 < 0) param1 = 0;
        if (param1 > 1) param1 = 1;
        if (param2 < 0) param2 = 0;
        if (param2 > 1) param2 = 1;
        var r = sqrt( sqr( pos[0] / (width/2) ) + sqr( pos[1] / (height/2) ) );
        if (r > 1) {
          return JsgColor.RGBA( 0, 0, 0, 0 );
        }
        if (r <= param1 || param1 == 1) {
          return color1;
        }
        var v = (r - param1) / (1 - param1); // v = 0..1
        v = v * param2 + (1 - param2);       // v = param2..1
        var color = JsgColor.Copy( color1 );
        JsgColor.Blend( color, color, color2, v );
        color[3] = 1 - v;
        return color;
      }
    },
  ],

  IsParameterUsed: function( patternIx, parameterName ) {

    return this.Patterns[patternIx].UsedParameters.indexOf( parameterName ) >= 0;
  },

  GetColor: function( patternIx, pos, width, height, color1, color2, param1, param2 ) {
    // patternIx: int; which pattern of this.PatternFuncs to use
    // pos: V2; calculate the pattern color for this position
    // width, height: number; scaling of the pattern; native patter size is 1x1
    // color1, color2: JsgColor; colors the pattern function may use as a parameter
    // param1, param2: number; some parameters the pattern function may use

    if (patternIx < 0 || patternIx >= this.Patterns.length) return color1;
    return this.Patterns[patternIx].Func( pos, width, height, color1, color2, param1, param2 );
  },

  GetName: function( patternIx ) {
    if (patternIx < 0 || patternIx >= this.Patterns.length) return 'undefined';
    return this.Patterns[patternIx].Name;
  },

  GetRadioButtonItems: function() {
    var items = [];
    items.push( { Name: 'none', Value: -1 } );
    var patterns = this.Patterns;
    for (var i = 0; i < patterns.length; i++) {
      items.push( { Name: patterns[i].Name, Value: i } );
    }
    return items;
  },

};


///////////////////////////////////////////////////
// Target

function Target( params, onImageLoad ) {
  // Creates a ray tracing target defined by a plane pos: V3, rotx..rotz.
  // pos is the bottom center point of the image
  // Properties of the target can be defined by params.
  // If an image source is given, onImageLoad(this) is called as soon as the image is loaded.
  //
  // params = {
  //   Name: string; Name is only for display for easier destinguishing of different targets
  //   Plane: object = { Pos: [x,y,z], RotX, RotY, RotZ };
  //   ImageSrc: string (optional)
  //   Width, Height: number (optional); defines the image world dimension or a pattern size
  //     If image and not defined, the image pixel dimensions are used as a default
  //   LimitType: int (optional); default 0; 0 -> Custom, 1 -> Width/Height, 2 -> no Limits
  //   LimitLeft: number (optional); default null, null or undefined -> infinite
  //   LimitRight: number (optional); default null, null or undefined -> infinite
  //   LimitTop: number (optional); default null, null or undefined -> infinite
  //   LimitBottom: number (optional); default null, null or undefined -> infinite
  //   Color1: JsgColor (optional); color to use if no image is defined
  //   Color2: JsgColor (optional); second color for color functions
  //   Pattern: int (optional) Default -1; Index into a pattern function of TargetPatterns
  //     function( ix, pos, width, height, color1, color2, param1, param2 ) returns JsgColor
  //   TransparentColor: JsgColor (optional); image pixels of this color are treated as transparent; set alpha = 0 to ignore this color
  //   Alpha: number (optional); default 1; overall transparency for an image. Is multiplied with pixel alpha
  //   ForceOversampling: bool (optional); default true;
  //     force oversampling even if BoostOversampling is enabled and no image is assigned. Usefull for pattern targets
  // }
  //

  this.Debug = false;

  this.OnImageLoadFunc = xDefFuncOrNull( onImageLoad, null );

  // actual used size of target from user set Width and Height
  this.ActualWidth = 0;
  this.ActualHeight = 0;

  this.PrevImageSrc = '';
  this.SetParams( params );
  this.Hidden = false;
  this.NoImageColor = [ 1, 1, 1, 1 ];

}

Target.prototype.SetParams = function( params ) {

  params = xDefObj( params, {} );
  this.Image = null;
  this.ImageData = null;
  this.ImageState = '';  // '' -> no image, 'ok' -> loaded, 'loading', 'reloading', 'error', 'denied', 'failed'
  this.ImageReloadTimer = null;
  this.Plane = new Plane( [1000,0,0], [0,0,1], [0,1,0], 0, 0, 0 );
  if (xObj(params.Plane)) {
    // clone params.Plane
    var plane = params.Plane;
    if (xArray(plane.Pos)) V3.CopyTo( plane.Pos, this.Plane.Pos );
    if (xNum(plane.RotX)) this.Plane.RotX = plane.RotX;
    if (xNum(plane.RotY)) this.Plane.RotY = plane.RotY;
    if (xNum(plane.RotZ)) this.Plane.RotZ = plane.RotZ;
  }
  this.Name = xDefStr( params.Name, 'unnamed' );
  this.ImageSrc = xDefStr( params.ImageSrc, '' );
  this.Width = xDefNum( params.Width, 0 );
  this.Height = xDefNum( params.Height, 0 );
  this.LimitType = xDefNum( params.LimitType, 0 );
  this.LimitLeft = xDefNum( params.LimitLeft, null );
  this.LimitRight = xDefNum( params.LimitRight, null );
  this.LimitTop = xDefNum( params.LimitTop, null );
  this.LimitBottom = xDefNum( params.LimitBottom, null );
  this.Pattern = xDefNum( params.Pattern, -1 );
  this.Color1 = xDefArray( params.Color1, JsgColor.Black() );
  this.Color2 = xDefArray( params.Color2, JsgColor.White() );
  this.Param1 = xDefNum( params.Param1, 0.5 );
  this.Param2 = xDefNum( params.Param2, 0.5 );
  this.TransparentColor = xDefArray( params.TransparentColor, [0,0,0,0] );
  this.Alpha = xDefNum( params.Alpha, 1 );
  this.ForceOversampling = xDefBool( params.ForceOversampling, true );
  this.Ready = true;

  this.Update();
}

Target.prototype.CreateImageData = function() {

  try {

    this.ImageData = new ImageData( this.Image );
    this.ImageState = 'ok';

  } catch (e) {

    this.ImageState = 'denied';
    this.Ready = false;

  }
  if (this.Debug) xLog( 'Target.CreateImageData: State = #', this.ImageState );
}

Target.prototype.OnImageLoad = function( imgID ) {

  if (this.Debug) xLog( 'Target.OnImageLoad start: id = #, State = #', imgID, this.ImageState );

  this.Ready = IC.IsLoaded( imgID );
  if (this.Ready) {

    if (this.Debug) xLog( 'Target.OnImageLoad: load complete, creating ImageData' );
    // CreateImageData sets ImageState to 'ok' or 'denied'
    this.CreateImageData();

  } else {

    if (this.Debug) xLog( 'Target.OnImageLoad: load failed' );

    if (this.ImageState == 'reloading') {

      // reload failed, give up
      if (this.Debug) xLog( 'Target.OnImageLoad: give up' );
      this.ImageState = 'failed';

    } else if (this.ImageState == 'tryreload') {

      // try to reload image once with a delay of 1 s
      if (this.Debug) xLog( 'Target.OnImageLoad: setup reload timer' );
      if (this.ImageReloadTimer) {
        clearTimeout( this.ImageReloadTimer );
        this.ImageReloadTimer = null;
      }

      var me = this;
      this.ImageReloadTimer = setTimeout(
        function(){
          if (me.Debug) xLog( 'Target.OnImageLoad.ReloadTimer: reload timer fired: State = #', me.ImageState );
          clearTimeout( me.ImageReloadTimer );
          me.ImageReloadTimer = null;
          me.ImageState = 'reloading';
          var imgID = IC.LoadImage( me.ImageSrc,
            function( id ){
              if (me.Debug) xLog( 'Target.OnImageLoad.Reloaded: id = #', id );
              me.OnImageLoad( id );
            }
          );
          me.Image = IC.Image( imgID );
          if (me.Debug) xLog( 'Target.OnImageLoad.ReloadTimer: reload initiated: id = #, State = #', imgID, me.ImageState );
        }, 1000
      );
      this.ImageState = 'reloading';

    } else if (this.ImageState == 'loading') {

      // try reloading once
      this.ImageState = 'tryreload';
      this.OnImageLoad( imgID );  // recursive
      if (this.Debug) xLog( 'Target.OnImageLoad end: id = #, State = #', imgID, this.ImageState );
      return;

    } else if (ContainsStr( 'denied,failed', this.ImageState )) {

      if (this.Debug) xLog( 'Target.OnImageLoad: keep error State = #', this.ImageState );

    } else {

      // unknown error
      if (this.Debug) xLog( 'Target.OnImageLoad: unknown error, State = #, (now set to error)', this.ImageState );
      this.ImageState = 'error';

    }
  }

  this.CompActualSize();
  if (this.OnImageLoadFunc) {
    this.OnImageLoadFunc( this );
  }
  if (this.Debug) xLog( 'Target.OnImageLoad end: id = #, State = #', imgID, this.ImageState );
}

Target.prototype.Update = function( controller ) {
  // Call this function after changes are made to this Target.
  // Checks whether ImageSource has changed and loads/unloads the image correspondingly.
  // Calles OnImageLoadFunc as soon as the load/unload has happend.

  if (MatchesControllerPanel( controller, 'TargetPlanePanel' )) {
    this.Plane.Update( controller );
  }

  if (MatchesControllerField( controller, 'Alpha' )) {
    if (this.Alpha < 0) this.Alpha = 0;
    if (this.Alpha > 1) this.Alpha = 1;
  }

  if (MatchesControllerField( controller, 'LimitLeft,LimitRight' )) {
    if (this.LimitLeft > this.LimitRight) {
      var tmp = this.LimitLeft;
      this.LimitLeft = this.LimitRight;
      this.LimitRight = tmp;
    }
  }

  if (MatchesControllerField( controller, 'LimitBottom,LimitTop' )) {
    if (this.LimitBottom > this.LimitTop) {
      var tmp = this.LimitBottom;
      this.LimitBottom = this.LimitTop;
      this.LimitTop = tmp;
    }
  }

  if (MatchesControllerField( controller, 'ImageSrc' ) && this.PrevImageSrc != this.ImageSrc) {

    // release old image
    if (this.Image) {
      if (this.ImageReloadTimer) {
        clearTimeout( this.ImageReloadTimer );
        this.ImageReloadTimer = null;
      }
      this.ImageData = null;
      this.Image = null;
      this.ImageState = '';
      if (this.Debug) xLog( 'Target.Update: image released' );
    }
    this.Ready = true;

    if (this.ImageSrc != '') {

      // load image
      this.Ready = false;
      var imgID = IC.FindImage( this.ImageSrc );

      if (imgID == -1) {
        var me = this;
        this.ImageState = 'loading';
        if (this.Debug) xLog( 'Target.Update: image initiating load, State = loading' );
        imgID = IC.LoadImage( this.ImageSrc,
          function( id ) {
            me.OnImageLoad( id );
          }
        );
      }

      this.Image = IC.Image( imgID );
      this.Ready = IC.IsLoaded( imgID );

      if (this.Ready) {

        if (this.Debug) xLog( 'Target.Update: image already loaded' );
        // CreateImageData sets ImageState to 'ok' or 'denied'
        this.CreateImageData();

      } else if (IC.IsErrorOrAbort(imgID)) {

        // try reload once
        if (this.Debug) xLog( 'Target.Update: error on this image, State = #, try reload', this.ImageState );
        this.ImageState = 'tryreload';
        this.OnImageLoad( imgID );
      }
    }
    this.PrevImageSrc = this.ImageSrc;
  }

  // must be executed after this.Image is loaded to have access to its size
  if (MatchesControllerField( controller, 'Width,Height' )) {
    this.CompActualSize();
  }

}

Target.prototype.CompActualSize = function() {
  // Width and Height can both be 0 = auto. This function calculates the size taking this into account.
  // Sets ActualWidth, ActualHeight to calculated size of target

  if (this.Image == null) {

    // no image
    if (this.Width == 0 && this.Height == 0) {

      // use defaults
      this.ActualWidth = 10;
      this.ActualHeight = 10;

    } else {

      if (this.Width == 0) {

        this.ActualWidth = this.Height;
        this.ActualHeight = this.Height;

      } else if (this.Height == 0) {

        this.ActualWidth = this.Width;
        this.ActualHeight = this.Width;

      } else {

        this.ActualWidth = this.Width;
        this.ActualHeight = this.Height;

      }

    }

  } else {

    // image is defined
    if (this.Width == 0 && this.Height == 0) {

      // use image dimensions
      this.ActualWidth = this.Image.naturalWidth;
      this.ActualHeight = this.Image.naturalHeight;

    } else {

      var imgRatio = this.Image.naturalWidth / this.Image.naturalHeight;

      if (this.Width == 0) {

        this.ActualWidth = this.Height * imgRatio;
        this.ActualHeight = this.Height;

      } else if (this.Height == 0) {

        this.ActualWidth = this.Width;
        this.ActualHeight = this.Width / imgRatio;

      } else {

        this.ActualWidth = this.Width;
        this.ActualHeight = this.Height;

      }

    }

  }

}

Target.prototype.IsInsideBoundary = function( planePos ) {

  //   LimitType: 0 -> Custom, 1 -> Width/Height, 2 -> no Limits
  if (this.LimitType == 0) {

    // Custom Limits
    if (this.LimitLeft   != null && planePos[0] < this.LimitLeft  ) return false;
    if (this.LimitRight  != null && planePos[0] > this.LimitRight ) return false;
    if (this.LimitBottom != null && planePos[1] < this.LimitBottom) return false;
    if (this.LimitTop    != null && planePos[1] > this.LimitTop   ) return false;

  } else if (this.LimitType == 1) {

    // Width/Height as Limits
    if (this.ImageSrc == '') {

      // symmetric limits
      if (planePos[0] < -abs(this.ActualWidth/2) ) return false;
      if (planePos[0] >  abs(this.ActualWidth/2) ) return false;
      if (planePos[1] < -abs(this.ActualHeight/2)) return false;
      if (planePos[1] >  abs(this.ActualHeight/2)) return false;

    } else {

      // vertical limit is from 0 to Height
      if (planePos[0] < -abs(this.ActualWidth/2) ) return false;
      if (planePos[0] >  abs(this.ActualWidth/2) ) return false;
      if (planePos[1] <  0                       ) return false;
      if (planePos[1] >  abs(this.ActualHeight)  ) return false;

    }

  } else {

    // no Limits
    return true;

  }
  return true;
}

Target.prototype.IntersectRay = function( p1, p2 ) {
  // checks whether ray segment p1 to p2 intersects with this target and is inside the boundaries this.LimitXxx.
  // returns the intersection point (V3) or null if no intersection occours or is outside boundaries.
  // You can access the plane intersection coordinates in this.Plane.Intersect

  var plane = this.Plane;
  var p1Top = plane.IsPointOnTop( p1 ) ? 1 : -1;
  var p2Top = plane.IsPointOnTop( p2 ) ? 1 : -1;
  if (p1Top * p2Top > 0) return null;

  var pIntersect = plane.IntersectLine( p1, p2 );
  if (pIntersect != null) {
    if (!this.IsInsideBoundary( plane.Intersect )) {
      pIntersect = null;
    }
  }
  return pIntersect;
}

Target.prototype.IsTransparentPixel = function( imgPos ) {
  // returns true if the pixel at imgPos is transparent
  // Transparent pixels are either pixel width transparent alpha channel,
  // this.Alpha < 1 or if this.TransparentColor matches a pixel color.

  if (!this.Image || !this.ImageData) return false;
  if (this.Alpha < 1) return true;
  var imgData = this.ImageData;
  if (imgData.GetAlpha( imgPos ) < 255) return true;
  if (this.TransparentColor[3] < 1) return false;
  var pixelColor = JsgColor.FromBitmap( imgData.GetRGBA( imgPos ) );
  return JsgColor.IsSameColor( pixelColor, this.TransparentColor );
}

Target.prototype.IsTransparent = function( planePos ) {
  // returns true if target at planePos (V2) is transparent.
  // Transparent pixels are either image pixels width transparent alpha channel,
  // this.Alpha < 1 or if this.TransparentColor matches a image pixel color.
  // If no target image is defined, transparent pixels are either this.Color1,Color2 alpha channel
  // or this.Pattern function alpha channel < 1 or if the color matches this.TransparentColor.

  if (this.Image) {
    return this.IsTransparentPixel( this.PlaneToImagePos( planePos, true ) );
  }

  if (this.Alpha < 1) return true;

  var color = this.Color1;
  if (this.Pattern >= 0) {
    color = TargetPatterns.GetColor(
      this.Pattern, planePos, this.ActualWidth, this.ActualHeight,
      this.Color1, this.Color2, this.Param1, this.Param2
    );
  }
  if (color[3] < 1) return true;

  if (this.TransparentColor[3] < 1) return false;
  return JsgColor.IsSameColor( color, this.TransparentColor );
}

Target.prototype.GetColor = function( planePos ) {
  // returns JsgColor for pixel at planePos (V2).
  // Pixel color can be defined by this.Image, this.Color1 or this.Pattern

  var color = this.Color1;

  if (this.Image) {
    if (this.ImageData) {

      color = JsgColor.FromBitmap( this.ImageData.GetRGBA( this.PlaneToImagePos( planePos ) ) );

    } else {

      color = this.NoImageColor;

    }

  } else if (this.Pattern >= 0) {

    color = TargetPatterns.GetColor(
      this.Pattern, planePos, this.ActualWidth, this.ActualHeight,
      this.Color1, this.Color2, this.Param1, this.Param2
    );

  }

  if (this.TransparentColor[3] == 1 && JsgColor.IsSameColor( color, this.TransparentColor ) ) {

    color = JsgColor.Copy( color );
    color[3] = 0;

  } else if (this.Alpha < 1) {

    color = JsgColor.Copy( color );
    color[3] *= this.Alpha;

  }

  return color;
}

Target.prototype.PlaneToImagePos = function( planePos, restrictedToImage ) {
  // planePos: V2 point in plane coordinate system
  // restrictedToImage: bool, true -> coordinates are wraped via modulo function to image size range
  // returns [ x, y ] in pixel coordinates. [0,0] is top left

  if (!this.Image || !this.Ready) return planePos;
  restrictedToImage = xDefBool( restrictedToImage, true );

  var imgWidth = this.Image.naturalWidth;
  var imgHeight = this.Image.naturalHeight;

  var imgX = Math.floor( planePos[0] * imgWidth / this.ActualWidth + imgWidth / 2 );
  var imgY = (imgHeight-1) - Math.floor( planePos[1] * imgHeight / this.ActualHeight );

  if (restrictedToImage) {
    imgX = wrapModulo( imgX, imgWidth );
    imgY = wrapModulo( imgY, imgHeight );
  }
  return [ imgX, imgY ];
}

Target.prototype.IsParameterUsed = function( parameterName ) {

  if (this.ImageSrc != '') {
    return false;
  } else {
    if (parameterName == 'Pattern') return true;
    if (this.Pattern == -1) {
      return 'Color1' == parameterName;
    } else {
      return TargetPatterns.IsParameterUsed( this.Pattern, parameterName );
    }
  }
}

////////////////////////////////////////
// RefractionRayTracer

var RefractionRayTracer = {
  // The RefractionRayTracer can be used to calculate a ray through a scene (flat earth or globe)
  // whose curvature is defined by arbitrary refraction functions. If the ray hits a target or multiple
  // transparent targets, the combined color of all targets hit and the ray can be determined.
  //
  // Coordinate system is: x = away, y = up, z = to the right
  // The ray is calculated in the x/y plane and then transformed
  // according to the start vector RayStartDir into a rotated plane
  // and then transformed to a surface coordinate system
  // in which all model objects are defined.
  //
  // used coordinate systems
  // Ec2D: earth centered 2D [ x, y ]; [0,0] is at surface at observer height 0; ray is calculated in this coord system
  // Ec3D: earth centered 3D [ x, y, z ]; [0,0,0] is at surface at observer height 0; ray start and end positions and directions
  // Surf2D: straightened 2D surface coordinates; x direction is along surface, y is along radius of earth
  // Surf3D: straightened 3D surface coordinates; x,z direction is along surface in RayStartDir direction, y is along radius of earth
  // Target2D: target plane coordinate system;
  // Image2D: image coordinate system in pixels derived from target coordinates

  // input parameters

  RayStartPos: [ 0, 2, 0 ],  // [ x, y, z ] as Ec3D coords; note y is always perpendicular to surface and z = 0
  RayStartDir: [ 1, 0, 0 ],  // as Ec3D; can be calculated from focal point to screen pixel, length alway 1
  RayDelta: 0,               // ray is calculated with this segment lengths; if 0 RayDelta is computed from NRaySegments
  NRaySegments: 25,          // if RayDelta = 0 then RayDelta is calculated by farthest Target distance / NRaySegement plus...
  SaveRay: false,            // true -> stores all ray points in this.Ray
  EnableTransparency: true,  // false -> stop rays at transparent targets
  IgnoreTargets: false,      // true -> uses only a surface target

  RefractionType: 2,         // 0 -> no refraction, 1 -> standard refraction, 2 -> custom refraction using CustomRefrFunc()
  EarthModel: 0,             // 0 -> globe model, 1 -> flat earth model

  DomeZenithColor: JsgColor.RGB( 0.2, 0.4, 1 ),
  DomeHorizonColor: JsgColor.RGB( 0.94, 0.98, 1 ),
  DomeGradientHeight: 0,
  FieldOfView: Math.PI / 2,  // used for auto DomeGradientHeight

  MistColor: JsgColor.RGB( 0.9, 0.9, 0.9 ),
  Visibility: 0,             // 0 -> infinite, else if RayLength > Visibility use full MistColor, else blend in MistColor

  Targets: [],               // array of Target that define the boundaries, ordered last element is the nearest one

  // input ambient function that gets refraction depending on ray position

  CustomRefrFunc: null,      // function( rayPos_Surf3D ) returns refraction coefficient

  // calculated outputs

  RayEndPos: [ 0, 0, 0 ],    // as Ec3D coords
  RayEndDir: [ 0, 0, 0 ],    // as Ec3D coords
  RayLength: 0,
  TargetColor: JsgColor.Black(), // pixel color of Target hit
  Ray: [],                       // array of ray points [ x, y, z ] of Ec3D coords
  TargetIndex: 0,                // -1 if ray does not reach a target, else the index of the target in the this.Targets list

  // constraints

  MinCurvature: 1 / (1e6 * 6371000),
  MaxSegments: 5000,
  RayDistanceLimit: 0,  // auto
  AutoDistanceLimitFactor: 1.5,  // actual ray distance limit in auto mode is extended by this factor

  LimitColor: JsgColor.RGB(0.5,0,0),   // color used if MaxSegments is reached to indicate limit reached

  // constants

  RadiusEarth: 6371000,
  RefrEqFact1: 503,
  RefrEqFact2: 0.0343,

  // private

  OnUpdateFunc: null,      // func()
  rayPos1_Ec2D: [ 0, 0 ],
  rayPos2_Ec2D: [ 0, 0 ],
  rayDir1_Ec2D: [ 0, 0 ],
  rayDir2_Ec2D: [ 0, 0 ],
  actualRayDelta: 0,
  actualDistLimit: 0,
  cosRot: 0, // cosRot and sinRot are used to rotate around y axes ray points from 2D ray plane to plane of RayStartDir
  sinRot: 0,
  surfaceTarget: [],
  savedTargets: null,

  savedState: {
    SaveRay: false,
    RefractionType: 2,
    EarthModel: 0,
    EnableTransparency: true,
    IgnoreTargets: false,
    RayDistanceLimit: 0,
    RayDelta: 0,
    NRaySegments: 25,
  },

  // functions

  Update: function( controller ) {
    // check input values
    if (this.RayDelta < 0) this.RayDelta = 0;
    this.NRaySegments = round( this.NRaySegments );
    if (this.NRaySegments < 2) this.NRaySegments = 2;
    if (this.NRaySegments > 10000) this.NRaySegments = 10000;
    this.MaxSegments = round( this.MaxSegments );
    if (this.MaxSegments < 10) this.MaxSegments = 10;
    if (this.MaxSegments > 10000) this.MaxSegments = 10000;
    if (this.Visibility < 0) this.Visibility = 0;
  },

  IsReady: function() {
    // returns true if all targets are ready, eg. all images are loaded

    for (var i = 0; i < this.Targets.length; i++) {
      if (!this.Targets[i].Ready) {
        return false;
      }
    }
    return true;
  },

  SaveState: function() {
    var s = this.savedState;
    s.SaveRay = this.SaveRay;
    s.RefractionType = this.RefractionType;
    s.EarthModel = this.EarthModel;
    s.EnableTransparency = this.EnableTransparency;
    s.IgnoreTargets = this.IgnoreTargets;
    s.RayDistanceLimit = this.RayDistanceLimit;
    s.RayDelta = this.RayDelta;
    s.NRaySegments = this.NRaySegments;
  },

  RestoreState: function() {
    var s = this.savedState;
    this.SaveRay = s.SaveRay;
    this.RefractionType = s.RefractionType;
    this.EarthModel = s.EarthModel;
    this.EnableTransparency = s.EnableTransparency;
    this.IgnoreTargets = s.IgnoreTargets;
    this.RayDistanceLimit = s.RayDistanceLimit;
    this.RayDelta = s.RayDelta;
    this.NRaySegments = s.NRaySegments;
  },

  Clear: function() {
    this.Targets.length = 0;
  },

  InsertTarget: function( target, posIx ) {
    // adds target at posIx in this.Targets and returns the index to the target in this.Targets
    // if posIx is not defined or out of bound, target is appendet to the end

    posIx = xDefNum( posIx, -1 );
    if (posIx < 0 || posIx >= this.Targets.length) posIx = -1;
    if (posIx == -1) {
      this.Targets.push( target );
      posIx = this.Targets.length - 1;
    } else {
      this.Targets.splice( posIx, 0, target );
    }
    return posIx;
  },

  RemoveTarget: function( posIx ) {
    // removes target posIx from this.Targets.

    if (posIx < 0 || posIx >= this.Targets.length) return null;
    return this.Targets.splice( posIx, 1 )[0];
  },

  ComputeRay: function() {
    // Set the input parameters and then call ComputeRay()
    // to calculate the corresponding outputs (refracted light ray, target and target color).

    this.InitRayTracing();
    var nSegments = 0;

    while (this.CompNextRayPoint() && nSegments <= this.MaxSegments) {
      nSegments++;
    }

    if (nSegments > this.MaxSegments) {

      // this.TargetIndex = -1; keep last hit target, that may be transparent
      this.RayEndPos = this.Any2Dto3D( this.rayPos2_Ec2D );
      this.RayEndDir = V3.Norm( this.Any2Dto3D( this.rayDir2_Ec2D ) );
      JsgColor.Combine( this.TargetColor, this.LimitColor );

    } else if (this.Visibility > 0) {

      // mix in mist
      // var val = this.RayLength / this.Visibility;
      // if (val > 1) val = 1;
      var val = 1 - Math.pow( 10, -this.RayLength / this.Visibility );
      JsgColor.Blend( this.TargetColor, this.TargetColor, this.MistColor, val );

    }

    // restore this.Targets
    if (this.IgnoreTargets) {
      this.Targets = this.savedTargets;
      this.savedTargets = null;
    }
    return this.TargetIndex;
  },

  GetDomeColor: function( pos_3D ) {
    var gradHeight = this.DomeGradientHeight;
    if (gradHeight <= 0) {
      gradHeight = this.actualDistLimit * this.FieldOfView / 2
    }
    var posY = abs( pos_3D[1] );
    var angle = Math.PI / 2;
    if (posY < this.actualDistLimit) {
      angle = Math.asin( posY / this.actualDistLimit );
    }
    posHeight = angle * this.actualDistLimit;
    if (posHeight >= gradHeight) {
      return this.DomeZenithColor;
    } else {
      var val = posHeight / gradHeight;
      return JsgColor.Blend( JsgColor.Black(), this.DomeHorizonColor, this.DomeZenithColor, val );
    }
  },

  Ec2DtoSurf2D: function( p_Ec2D ) {
    // transforms p_Ec2D as earth (centered - [0,R]) coord into flat earth surface coord

    if (this.EarthModel == 1) return p_Ec2D; // flat earth
    var py = p_Ec2D[1] + this.RadiusEarth;
    var pr = sqrt( sqr( p_Ec2D[0] ) + sqr( py ) );
    var a = acos( py / pr );
    var x = a * this.RadiusEarth;
    var y = pr - this.RadiusEarth;
    return [ x, y ];
  },

  Surf2DtoEc2D: function( p_Surf2D ) {
    // transforms p_Surf2D as flat earth surface coord into earth (centered - [0,R]) coord

    if (this.EarthModel == 1) return p_Surf2D; // flat earth
    var pr = this.RadiusEarth + p_Surf2D[1];
    var a = p_Surf2D[0] / this.RadiusEarth;
    var x = pr * sin( a );
    var y = pr * cos( a ) - this.RadiusEarth;
    return [ x, y ];
  },

  Surf3DtoEc3D: function( p_Surf3D ) {
    // transforms p_Surf3D as flat earth surface 3D coord into earth (centered - [0,0,R]) coord

    var pXZ = [ p_Surf3D[0], p_Surf3D[2] ];
    var r = V2.Length( pXZ );
    var v = V2.Norm( pXZ );
    var p_2D = [ r, p_Surf3D[1] ];
    var p_Ec2D = this.Surf2DtoEc2D( p_2D );
    return [ v[0] * p_Ec2D[0], p_Ec2D[1], v[1] * p_Ec2D[0] ];
  },

  Any2Dto3D: function( p_2D ) {
    // rotates the ray position around the vertical y axes into the direction of RayStartDir

    return [ p_2D[0] * this.cosRot, p_2D[1], p_2D[0] * this.sinRot ];
  },

  Ec2DtoSurf3D: function( p_Ec2D ) {

    return this.Any2Dto3D( this.Ec2DtoSurf2D( p_Ec2D ) );
  },

  RefractionCoefficient: function( p, t, tg ) {
    // p in mBar, t in K, tg in °C/m

    return this.RefrEqFact1 * p / sqr(t) * ( this.RefrEqFact2 + tg );
  },

  CompStdRefraction: function( alt ) {
    if (alt >= BaroConst.hMax) {
      return 0;
    }
    if (alt < 0) {
      alt = 0;
    }
    return this.RefractionCoefficient( BaroModel.Pressure(alt)/100, BaroModel.TemperatureK(alt), BaroModel.alpha(alt) );
  },

  RayCurvature: function( rayPos_Surf3D ) {
    // returns the inverse of the RayRadius. 0 -> straight line, + -> down bending
    // note k is defined for R = 6371 km even on a flat earth

    var k;
    if (this.RefractionType == 0) {
      return 0;
    }

    if (this.RefractionType == 1 || this.CustomRefrFunc == null) {

      k = this.CompStdRefraction( rayPos_Surf3D[1] );

    } else {

      k = this.CustomRefrFunc( rayPos_Surf3D );

    }

    // reduce refraction according to angle a of ray direction wrt surface up vector
    // k' = k * sin( a ); sin( a ) = | dir x up |
    // This is neccessary because rays perpendicular to the surface are not bent!

    var posAngle = V2.Length( [ rayPos_Surf3D[0], rayPos_Surf3D[2] ] ) / this.RadiusEarth;
    var sina = this.rayDir1_Ec2D[0] * cos(posAngle) - this.rayDir1_Ec2D[1] * sin(posAngle);
    var kCorrected = k * sina;

    return kCorrected / 6371000;
  },

  RayRadiusForPos: function( rayPos_Surf3D ) {
    // returns 0 for a straight ray, + is bending down

    var curve = this.RayCurvature( rayPos_Surf3D );
    if (abs(curve) < this.MinCurvature) {
      return 0;
    }
    return 1 / curve;
  },

  InitRayTracing: function() {

    // create target that serves as the surface of the earth if this.IgnoreTargets = true
    if (this.surfaceTarget.length == 0) {
      this.surfaceTarget[0] = new Target( {
          Plane: { Pos: [0,0,0], RotX: 0, RotY: 0, RotZ: -90 },
          LimitLeft: null,
          LimitRight: null,
          LimitBottom: null,
          LimitTop: null,
        }
      );
    }

    // install surface target if this.IgnoreTargets = true
    if (this.IgnoreTargets) {
      this.savedTargets = this.Targets;
      this.Targets = this.surfaceTarget;
    }

    // calculate actual distance limit if set to auto from farthest target size and distance
    this.actualDistLimit = this.RayDistanceLimit;
    if (this.RayDistanceLimit <= 0) {
      var maxTargetDist = 25000;  // default
      var targetSize = 500;       // default
      var targetIndex = this.GetFarthestTarget();
      if (targetIndex >= 0) {
        var target = this.Targets[targetIndex];
        maxTargetDist = target.Plane.Pos[0];
        targetSize = maxOf( abs(target.ActualWidth), abs(target.ActualHeight) );
      }
      this.actualDistLimit = this.AutoDistanceLimitFactor * sqrt( sqr( targetSize ) + sqr( maxTargetDist ) );
      
      // calculate the horizon distance as a function of observer height RayStartPos[1]
      if (this.EarthModel == 0) {
      
        var horizonDist = sqrt( sqr( this.RadiusEarth+this.RayStartPos[1]) - sqr(this.RadiusEarth) ) * this.AutoDistanceLimitFactor;
        if (horizonDist > this.actualDistLimit) this.actualDistLimit = horizonDist;
        
      } else {

        var horizonDist = this.RayStartPos[1] * 200 / this.FieldOfView;      
        if (horizonDist > this.actualDistLimit) this.actualDistLimit = horizonDist;
      
      }
    }

    // calculate actualRayDelta
    if (this.RayDelta <= 0) {
      // auto RayDelta
      var nearDist = this.GetNearestTargetDistance();
      var farDist = this.GetFarthestTargetDistance();
      if (farDist == 0) farDist = this.actualDistLimit;
      this.actualRayDelta = farDist / this.NRaySegments;
      if (this.actualRayDelta > nearDist/2.5) this.actualRayDelta = nearDist / 2.5;
    } else {
      this.actualRayDelta = this.RayDelta;
    }

    this.Ray.length = 0;
    this.RayLength = 0;
    this.TargetIndex = -1;
    // prepare target color for combining with transparent colors
    JsgColor.SetRGBA( this.TargetColor, 0, 0, 0, 0 );
    this.rayPos2_Ec2D = V2.Copy( this.RayStartPos ); // note converts 3D to 2D

    // compute 3D ray direction from RayStartDir
    // Note: the angle between up and the 3D RayStartDir must be used for the 2D ray angle to up
    // requires this.RayStartDir length is 1
    var sp = V3.ScalarProd( [0,1,0], this.RayStartDir );
    if (sp < -1) sp = -1; if (sp > 1) sp = 1;
    var angle = acos( sp );
    this.rayDir2_Ec2D = [ sin(angle), cos(angle) ];

    if (this.SaveRay) {
      this.Ray.push( this.RayStartPos );
    }
    // cosRot and sinRot are used to rotate around y axes ray points from 2D ray plane to plane of RayStartDir
    var lengthRot = sqrt( sqr( this.RayStartDir[0] ) + sqr( this.RayStartDir[2] ) );
    this.cosRot = this.RayStartDir[0] / lengthRot;
    this.sinRot = this.RayStartDir[2] / lengthRot;
  },

  GetNearestTargetDistance: function() {

    // find nearest upright target
    var d = this.actualDistLimit;
    for (var i = 0; i < this.Targets.length; i++) {
      var target = this.Targets[i];
      if (!target.Hidden) {
        var targetPlane = this.Targets[i].Plane;
        if (abs(targetPlane.RotZ) <= 45) {
          if (targetPlane.Pos[0] < d) d = targetPlane.Pos[0];
        }
      }
    }
    return d;
  },

  GetFarthestTarget: function() {

    // find farthest upright target
    var d = 0;
    var targetIndex = -1;
    for (var i = 0; i < this.Targets.length; i++) {
      var target = this.Targets[i];
      if (!target.Hidden) {
        var targetPlane = this.Targets[i].Plane;
        if (abs(targetPlane.RotZ) <= 45) {
          if (targetPlane.Pos[0] > d) {
            d = targetPlane.Pos[0];
            targetIndex = i;
          }
        }
      }
    }
    return targetIndex;
  },

  GetFarthestTargetDistance: function() {

    var targetIndex = this.GetFarthestTarget();
    if (targetIndex >= 0) {
      return this.Targets[targetIndex].Plane.Pos[0];
    }
    return this.actualDistLimit;
  },

  CompNextRaySegment: function() {
    // Takes rayPos1_Ec2D, rayDir1_Ec2D and calculates next ray segment pos rayPos2_Ec2D and dir rayDir2_Ec2D

    this.rayPos1_Ec2D = this.rayPos2_Ec2D;
    this.rayDir1_Ec2D = this.rayDir2_Ec2D;

    var rayPos1_Surf3D = this.Ec2DtoSurf3D( this.rayPos1_Ec2D );
    var rayRad = this.RayRadiusForPos( rayPos1_Surf3D );
    this.RayLength += this.actualRayDelta;
    if (rayRad == 0) {

      // straight segment
      this.rayPos2_Ec2D = V2.Add( this.rayPos1_Ec2D, V2.Scale( this.rayDir1_Ec2D, this.actualRayDelta ) );
      this.rayDir2_Ec2D = this.rayDir1_Ec2D;
      return;
    }

    // curved segment
    var angle = this.actualRayDelta / rayRad;
    var dirCenter = [ this.rayDir1_Ec2D[1], -this.rayDir1_Ec2D[0] ];
    var rotMat = M2.Rotating( -angle );
    var center = V2.Add( this.rayPos1_Ec2D, V2.Scale( dirCenter, rayRad ) );
    M2.Trans( rotMat, dirCenter );
    this.rayPos2_Ec2D = V2.Add( center, V2.Scale( dirCenter, -rayRad ) );
    this.rayDir2_Ec2D = V2.Copy( this.rayDir1_Ec2D );
    M2.Trans( rotMat, this.rayDir2_Ec2D );
    this.rayDir2_Ec2D = V2.Norm( this.rayDir2_Ec2D );
  },

  CompNextRayPoint: function() {
    // Computes next ray point from rayPos1_Ec2D, rayDir1_Ec2D to rayPos2_Ec2D, rayDir2_Ec2D
    // and checks whether any target get hit.
    // returns false if any target is reached else true
    // See TargetIndex for which last target is hit
    // Note: Intersection is computed in Surf3D coordinates, because targets are in this system.
    // Note: if ray intersects a target, this straigs line segment between p1 and p2 is
    // used for the intersection calculation, not the ray arc. This is an acceptable approximation.

    this.CompNextRaySegment();

    // transform to surface coordinates
    var p1_surf3D = this.Ec2DtoSurf3D( this.rayPos1_Ec2D );
    var p2_surf3D = this.Ec2DtoSurf3D( this.rayPos2_Ec2D );

    // check actualDistLimit (dome hit)
    var p2_dist = V3.Length( p2_surf3D );
    if (p2_dist > this.actualDistLimit) {
      // calculate intersection point to dome
      var toLimit = p2_dist - this.actualDistLimit;
      this.RayEndDir = V3.Norm( this.Any2Dto3D( this.rayDir1_Ec2D ) );
      this.RayEndPos = V3.Add( this.Surf3DtoEc3D( p1_surf3D ), V3.Scale( this.RayEndDir, toLimit ) );
      if (this.SaveRay) {
        this.Ray.push( this.RayEndPos );
      }
      JsgColor.Combine( this.TargetColor, this.GetDomeColor( this.RayEndPos ) );
      return false;
    }

    for (var i = this.Targets.length-1; i >= 0; i--) {

      // check whether target is hit
      var target = this.Targets[i];
      if (!target.Hidden) {
        var hitPoint_surf3D = target.IntersectRay( p1_surf3D, p2_surf3D );

        if (hitPoint_surf3D != null) {

          // get intersection point in target coordinates from target.Plane
          // which is calculated in target.IntersectRay() above
          var hitPoint_target2D = target.Plane.Intersect;

          // Combine transparent colors
          JsgColor.Combine( this.TargetColor, target.GetColor( hitPoint_target2D ) );

          // store hit target index, even on transparent targets
          this.TargetIndex = i;

          // if hitPoint is transparent pixel then we have no intersection
          if (!this.EnableTransparency || !target.IsTransparent( hitPoint_target2D )) {

            // back transform suface coord to Ec coord for endpoint and enddir calculations
            var hitPoint_Ec3D = this.Surf3DtoEc3D( hitPoint_surf3D );
            var p1_Ec3D = this.Surf3DtoEc3D( p1_surf3D );
            var toHitPoint_Ec3D = V3.Sub( hitPoint_Ec3D, p1_Ec3D );

            // correct RayLength to end at hitPoint instead ray end
            this.RayLength += V3.Length( toHitPoint_Ec3D ) - this.actualRayDelta;
            this.RayEndPos = hitPoint_Ec3D;
            this.RayEndDir = V3.Norm( toHitPoint_Ec3D );
            if (this.SaveRay) {
              this.Ray.push( hitPoint_Ec3D );
            }
            return false;

          }
        }
      }
    }

    if (this.SaveRay) {
      this.Ray.push( this.Surf3DtoEc3D( p2_surf3D ) );
    }

    return true;
  },
};

//////////////////////////////////////////////////////////
// RayTracerCamera

function RayTracerCamera( rayTracer, params ) {
  // Creates a camera object that generates ray start directions for the rayTracer,
  // calls the ray tracer to find the pixel color of a display pixel and draws
  // the pixels on the jsGraph canvas when function DrawImage() is called.
  //
  // This function is executed asynchronous (using Async class) so the browser does
  // not block until the image is drawn.
  //
  // rayTracer: RefractionRayTracer
  // params = {
  //   PixelSize: int > 0; minimal display pixel size; use powers of 2
  //   Oversampling: int >= 1
  //   BoostOversampling: boolean; true -> dont oversample targets without image
  //   Smoothing: int >= 2; to blend in neighboring pixels; requires EnableSmoothing = true
  //   ShowEyeLevel: bool
  //   ShowHorizon: bool
  //   StartPixelSize: int > PixelSize; use powers of 2 times PixelSize
  //   Tilt: num; camera tilt angle in degrees; + -> tilt camera up = scene moves down
  // }

  this.Async = new CAsync();
  this.Async.RunTime = 100;  // collect all draw commands during this time interval

  this.RayTracer = rayTracer;
  this.SetParams( params );
}

RayTracerCamera.prototype.SetParams = function( params ) {
  params = xDefObj( params, {} );

  this.Height = xDefNum( params.Height, 2 );
  this.FocalLength = xDefNum( params.FocalLength, 1000 ); // mm 35 mm equivalent
  this.Tilt = xDefNum( params.Tilt, 0 );

  this.PixelSize = xDefNum( params.PixelSize, 1 );
  this.Oversampling = xDefNum( params.Oversampling, 1 );
  this.BoostOversampling = xDefBool( params.BoostOversampling, false );
  this.EnableSmoothing = xDefBool( params.EnableSmoothing, false );
  this.Smoothing = xDefNum( params.Smoothing, 2 );
  this.ShowEyeLevel = xDefBool( params.ShowEyeLevel, false );
  this.ShowHorizon = xDefBool( params.ShowHorizon, false );
  this.StartPixelSize = xDefNum( params.StartPixelSize, 64 );
  this.SensorSize = xDefNum( params.SensorSize, 43.2 ); // 43.2 = 35 mm equivalent
  this.ProgressBarWidth = xDefNum( params.ProgressBarWidth, 4 );

  this.SetTilt( this.Tilt );
}

RayTracerCamera.prototype.Update = function( controller ) {

  if (MatchesControllerField( controller, 'Height' )) {
    if (this.Height < 0) this.Height = 0;
  }

  if (MatchesControllerField( controller, 'FocalLength' )) {
    if (this.FocalLength < 5) this.FocalLength = 5;
  }

  if (MatchesControllerField( controller, 'Tilt' )) {
    if (this.Tilt < -90) this.Tilt = -90;
    if (this.Tilt > 90) this.Tilt = 90;
    this.SetTilt( this.Tilt );
  }

}

RayTracerCamera.prototype.SetTilt = function( angle_deg ) {
  this.Tilt = angle_deg;
  this.TiltMat = M3.RotatingZ( angle_deg * Math.PI / 180 );
}

RayTracerCamera.prototype.DrawImage = function( graph, aRefractionType, aEarthModel ) {
  // graph: JsGraph
  // Initiates the redraw of the canvas. This function returns immediately
  // and the draw process takes place asynchronously in steps managed by Async class.

  this.Async.Stop();

  var barWidth = this.ProgressBarWidth > 0 ? this.ProgressBarWidth + 2 : 0; // correct for canvas border
  var vpWidth = round( graph.CanvasWidth );
  var vpHeight = round( graph.CanvasHeight ) - barWidth;
  var vpDiagonal = sqrt( sqr( vpWidth ) + sqr( vpHeight ) );
  var sensorScale = this.SensorSize / vpDiagonal;
  var yHalf = vpHeight / 2;
  var eyeLevelDeviation = this.FocalLength * tan( rad( this.Tilt ) ) / sensorScale;
  var yEyeLevel = round(yHalf) - round(eyeLevelDeviation);
  var yHorizon = yEyeLevel;
  if (aEarthModel == 0) {
    // earth model horizon is not at eye level
    var dropAngle = acos( this.RayTracer.RadiusEarth / (this.RayTracer.RadiusEarth + this.Height) );
    var horizonDeviation = this.FocalLength * tan( rad( this.Tilt ) + dropAngle ) / sensorScale;
    var yHorizon = round(yHalf) - round(horizonDeviation);
  }

  var loopState = {
    graph: graph,
    refractionType: aRefractionType,
    earthModel: aEarthModel,
    x: 0,
    y: 0,
    yRun: 0,
    vpWidth: vpWidth,
    vpHeight: vpHeight,
    sensorScale: sensorScale,
    xMax: vpWidth,
    xHalf: vpWidth / 2,
    yMax: vpHeight,
    yHalf: vpHeight / 2,
    yEyeLevel: yEyeLevel,
    yHorizon: yHorizon,
    pixSize: this.StartPixelSize,
    numPixels: (Math.floor((vpWidth-1)/this.PixelSize)+1) * (Math.floor((vpHeight-1)/this.PixelSize)+1),
    pixelCount: 0,
    barWidth: barWidth,
  };

  var me = this;
  this.Async.CallObjDefered( 100, me, this.InitDrawImage, loopState );
  this.Async.CallObj( me, this.DoDrawImage, loopState );
}

RayTracerCamera.prototype.StopDrawing = function() {
  this.Async.Stop();
}

RayTracerCamera.prototype.InitDrawImage = function( loopState ) {
  var g = loopState.graph;
  g.Reset();
  g.SelectTrans( 'canvas' );
  this.RayTracer.FieldOfView = 0.78 * this.SensorSize / this.FocalLength;
}

RayTracerCamera.prototype.Sample = function( loopState, oversampling ) {

  var rayTracer = this.RayTracer;

  if (this.EnableSmoothing && oversampling <= 1) oversampling = this.Smoothing;

  if (oversampling <= 1) {
    var ySensor = (loopState.y - loopState.yHalf) * loopState.sensorScale;
    var xSensor = (loopState.x - loopState.xHalf) * loopState.sensorScale;
    var rayDir = V3.Norm( M3.Trans( this.TiltMat, [ this.FocalLength, ySensor, xSensor ] ) );

    rayTracer.RayStartPos = [ 0, this.Height, 0 ];
    rayTracer.RayStartDir = rayDir;

    rayTracer.ComputeRay();

    return rayTracer.TargetColor;
  }

  var smoothing = this.EnableSmoothing ? this.Smoothing : 1;
  var ps = this.PixelSize * smoothing / oversampling;
  var colorMean = JsgColor.Black();
  var nSamples = 0;
  var breakSampling = false;
  for (var dx = 0; dx < oversampling && !breakSampling; dx++) {
    for (var dy = 0; dy < oversampling; dy++) {
      var ySensor = (loopState.y + dy*ps - loopState.yHalf) * loopState.sensorScale;
      var xSensor = (loopState.x + dx*ps - loopState.xHalf) * loopState.sensorScale;
      var rayDir = V3.Norm( M3.Trans( this.TiltMat, [ this.FocalLength, ySensor, xSensor ] ) );

      rayTracer.RayStartPos = [ 0, this.Height, 0 ];
      rayTracer.RayStartDir = rayDir;

      rayTracer.ComputeRay();

      JsgColor.Add( colorMean, rayTracer.TargetColor );
      nSamples++;

      // optimization: dome and targets without images and not forced oversampling are not oversampled
      if (this.BoostOversampling) {
        var targetIndex = rayTracer.TargetIndex;
        if (targetIndex < 0 || (rayTracer.Targets[targetIndex].Image == null && !rayTracer.Targets[targetIndex].ForceOversampling)) {
          breakSampling = true;
          break;
        }
      }
    }
  }
  JsgColor.Scale( colorMean, 1/nSamples );
  return colorMean;

}

RayTracerCamera.prototype.DoDrawImage = function( loopState ) {
  // uses an algorythm that refines the image with each pass
  //
  // ps = StartPixelSize
  // run = 0
  // while ps >= PixelSize
  //   if run = 1
  //     y = ps
  //   else
  //     y = 0
  //   while y <= ymax
  //     if run = 2
  //       x = ps
  //     else
  //       x = 0
  //     while x <= xmax
  //       if run = 1
  //         Draw x, y, 2*ps, ps
  //       else
  //         Draw x, y, ps, ps
  //       if run = 1
  //         x += ps
  //       else
  //         x += 2*ps
  //     end while
  //     if run = 1
  //       y += 2*ps
  //     else
  //       y += ps
  //   end while
  //   if run != 1
  //     ps /= 2
  //   run++
  //   if run > 2
  //     run = 1
  // end while
  //
  // The algorithmus is decomposed in small steps which are called from Async piece by piece
  // and the state of the loop is hold in loopState.

  var g = loopState.graph;

  if (loopState.pixSize >= this.PixelSize) {

    if (loopState.y <= loopState.yMax) {

      if (loopState.x <= loopState.xMax) {

        var rayTracer = this.RayTracer;
        rayTracer.RefractionType = loopState.refractionType;
        rayTracer.EarthModel = loopState.earthModel;

        this.Async.StartTimer();
        while (loopState.x <= loopState.xMax) {

          var color = this.Sample( loopState, this.Oversampling );
          g.SetBgColor( color );
          if (loopState.yRun == 1) {
            g.Context2D.fillRect( loopState.x, loopState.vpHeight-loopState.y-loopState.pixSize, 2*loopState.pixSize, loopState.pixSize );
          } else {
            g.Context2D.fillRect( loopState.x, loopState.vpHeight-loopState.y-loopState.pixSize, loopState.pixSize, loopState.pixSize );
          }

          if (loopState.yRun == 0) {
            loopState.x += loopState.pixSize;
          } else {
            loopState.x += 2 * loopState.pixSize;
          }
          loopState.pixelCount++;

          if (this.Async.IsTimerExpired()) break;
        }

        // draw progress bar
        if (loopState.barWidth > 0) {
          var progress = loopState.pixelCount / loopState.numPixels;
          this.DrawProgressBar( g, progress, loopState );
        }

        return true;

      } else {

        // draw geometric horizon line
        if (this.ShowHorizon && loopState.yHorizon >= loopState.y && loopState.yHorizon <= loopState.y+loopState.pixSize) {
          g.SetBgColor( 'cyan' );
          this.DrawLevelLine( g, loopState.yHorizon, loopState );
        }

        // draw eye level line
        if (this.ShowEyeLevel && loopState.yEyeLevel >= loopState.y && loopState.yEyeLevel <= loopState.y+loopState.pixSize) {
          g.SetBgColor( 'magenta' );
          this.DrawLevelLine( g, loopState.yEyeLevel, loopState );
        }

        if (loopState.yRun == 1) {
          loopState.y += 2 * loopState.pixSize;
        } else {
          loopState.y += loopState.pixSize;
        }

        if (loopState.yRun == 2) {
          loopState.x = loopState.pixSize;
        } else {
          loopState.x = 0;
        }
        return true;
      }

    } else {

      if (loopState.yRun != 1) {
        loopState.pixSize /= 2;
      }

      loopState.yRun++;
      if (loopState.yRun > 2) {
        loopState.yRun = 1;
      }

      if (loopState.yRun == 1) {
        loopState.y = loopState.pixSize;
      } else {
        loopState.y = 0;
      }

      if (loopState.yRun == 2) {
        loopState.x = loopState.pixSize;
      } else {
        loopState.x = 0;
      }
      return true;

    }

  } else {

    // draw final progress bar
    if (loopState.barWidth > 0) {
      this.DrawProgressBar( g, 1, loopState );
    }
    return false;

  }

}

RayTracerCamera.prototype.DrawProgressBar = function( g, progress, loopState ) {
  var barSize = round( progress * loopState.vpWidth );
  g.SetBgColor( 'black' );
  g.Context2D.fillRect( 0, loopState.vpHeight, barSize, loopState.barWidth );
  g.SetBgColor( 'white' );
  g.Context2D.fillRect( barSize+1, loopState.vpHeight, loopState.vpWidth-barSize, loopState.barWidth );
}

RayTracerCamera.prototype.DrawLevelLine = function( g, y, loopState ) {
  var yPixel = loopState.vpHeight - y;
  g.Context2D.fillRect( 0.01*loopState.vpWidth, yPixel, 0.98*loopState.vpWidth, 1 );
}

////////////////////////////////////////////////
// BaroDisplay

function BaroDisplay( baroData ) {
  // baroData: BaroSettings

  this.Graph = null;
  this.BaroData = baroData;
  this.Async = new CAsync();
  this.LeftCurveType = 0;
  this.RightCurveType = 2;
}

BaroDisplay.prototype.StopDrawing = function() {
  this.Async.Stop();
}

BaroDisplay.prototype.Draw = function( graph, leftCurveType, rightCurveType ) {
  // g: jsGraph

  this.Async.Stop();
  this.Graph = graph;
  this.LeftCurveType = leftCurveType;
  this.RightCurveType = rightCurveType;

  var me = this;
  this.Async.CallObjDefered( 100, me, this.DoDraw );
}

BaroDisplay.prototype.DoDraw = function() {
  var g = this.Graph;

  g.Reset();

  var obsData = this.BaroData.ObserverBaroData;
  var trgData = this.BaroData.TargetBaroData;

  if (obsData.length < 3) {

    // draw red cross
    g.SelectTrans( 'canvas' );
    var xmin = 0;
    var xmax = g.CanvasWidth;
    var ymin = 0;
    var ymax = g.CanvasHeight;
    g.SetLineAttr( 'red', 2 );
    g.Line( xmin, ymin, xmax, ymax );
    g.Line( xmin, ymax, xmax, ymin );
    return;

  }

  var altRangeExp = 1.1;
  if (GraphDisplayParams.AltitudeRange == 0) altRangeExp = 2;
  var altRange = altRangeExp * this.BaroData.GetMaxInputAlt( GraphDisplayParams.AltitudeRange );
  var obsLimits = this.BaroData.GetLimits( altRange, obsData );
  var trgLimits;
  if (trgData.length > 2) {
    trgLimits = this.BaroData.GetLimits( altRange, trgData );
    if (trgLimits.TempMin < obsLimits.TempMin) obsLimits.TempMin = trgLimits.TempMin;
    if (trgLimits.TempMax > obsLimits.TempMax) obsLimits.TempMax = trgLimits.TempMax;
    if (trgLimits.GradMin < obsLimits.GradMin) obsLimits.GradMin = trgLimits.GradMin;
    if (trgLimits.GradMax > obsLimits.GradMax) obsLimits.GradMax = trgLimits.GradMax;
    if (trgLimits.RefrMin < obsLimits.RefrMin) obsLimits.RefrMin = trgLimits.RefrMin;
    if (trgLimits.RefrMax > obsLimits.RefrMax) obsLimits.RefrMax = trgLimits.RefrMax;
  }

  var leftMargin = 10;
  var rightMargin = 10;
  var bottomMargin = 26;
  var topMargin = 10;

  // draw left viewport
  g.SetViewport( 0, 0, g.CanvasWidth/2, g.CanvasHeight );
  g.SetViewportRel( leftMargin+40, topMargin, rightMargin/2, bottomMargin, true, false );

  if (this.LeftCurveType == 0) {

    this.DrawTemperatureGraph( altRange, obsLimits, trgLimits, true );

  } else if (this.LeftCurveType == 1) {

    this.DrawGradientGraph( altRange, obsLimits, trgLimits, true );

  } else {

    this.DrawRefractionGraph( altRange, obsLimits, trgLimits, true );

  }

  // draw right viewport

  g.SetViewport( g.CanvasWidth/2, 0, g.CanvasWidth/2, g.CanvasHeight );
  g.SetViewportRel( leftMargin/2, topMargin, rightMargin, bottomMargin, true, false );

  if (this.RightCurveType == 0) {

    this.DrawTemperatureGraph( altRange, obsLimits, trgLimits, false );

  } else if (this.RightCurveType == 1) {

    this.DrawGradientGraph( altRange, obsLimits, trgLimits, false );

  } else {

    this.DrawRefractionGraph( altRange, obsLimits, trgLimits, false );

  }

}

BaroDisplay.prototype.DrawAltScale = function( g, winXmin, altTicParams ) {

  g.TicsY( winXmin, altTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsY( winXmin, altTicParams.TicSize, g.ScalePix(-3), 1, altTicParams.Digits, false, false, '' ) ;

}

BaroDisplay.prototype.DrawTemperatureGraph = function( altRange, obsLimits, trgLimits, isLeftVp ) {
  // viewport is defined and selected

  var g = this.Graph;

  var obsData = this.BaroData.ObserverBaroData;
  var trgData = this.BaroData.TargetBaroData;

  // compute display range and tic size, horizontal temperature
  var winXmin = obsLimits.TempMin;
  var winXmax = obsLimits.TempMax;
  if (winXmin > -5) winXmin = -5;
  if (winXmax < 5) winXmax = 5;
  var winRange = winXmax - winXmin;
  winXmin -= 0.05 * winRange;
  winXmax += 0.05 * winRange;
  var tempTicParams = CompTicParams( winXmax - winXmin, 7 );

  // compute display range and tic size, vertical altitude
  var winYmin = 0;
  var winYmax = altRange;
  var altTicParams = CompTicParams( winYmax-winYmin, 16 );

  g.SetWindow( winXmin, winYmin, winXmax, winYmax );

  g.SetLineAttr( 'black', 1 );

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'center', 'top', g.ScalePix(2) ) ;

  g.TicsX( 0, tempTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsX( 0, tempTicParams.TicSize, g.ScalePix(-3), 1, tempTicParams.Digits, false, false, '' ) ;

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'right', 'middle', g.ScalePix(2) ) ;

  if (isLeftVp) {
    this.DrawAltScale( g, winXmin, altTicParams );
  }

  g.SetLineAttr( '#ddd', 1 );
  g.Grid( tempTicParams.TicSize, altTicParams.TicSize, true, false );

  g.SetLineAttr( 'gray', 1 );
  g.Axes();
  g.SetLineAttr( 'black', 1 );
  g.Frame();

  g.SetClipping( 'viewport' );

  // draw target temp

  //g.SetMarkerSymbol( 'Cross' );
  //g.SetMarkerSize( 8 );
  if (trgData.length > 2) {
    g.SetLineAttr( '#08f', 1.5 );
    g.NewPoly();
    var maxData = trgLimits.maxIx;
    for (var i = 0; i < maxData; i++) {
      var item = trgData[i];
      g.AddPointToPoly( item.Temp, item.Alt );
      //g.Marker( item.Temp, item.Alt, 1 );
    }
    g.DrawPoly( 1 );

    // draw provided temperature points
    g.SetMarkerAttr( 'Circle', g.ScalePix(10), '#08f', 'white', 1 );
    var trgAlt = this.BaroData.TargetAlt;
    var trgTemp = this.BaroData.TargetTemp;
    for (var i = 0; i < 5 && trgAlt[i] != -1; i++) {
      g.Marker( trgTemp[i], trgAlt[i] );
    }
  }

  // draw observer temp
  g.SetLineAttr( 'blue', 2 );
  g.NewPoly();
  var maxData = obsLimits.maxIx;
  for (var i = 0; i < maxData; i++) {
    var item = obsData[i];
    g.AddPointToPoly( item.Temp, item.Alt );
    //g.Marker( item.Temp, item.Alt, 1 );
  }
  g.DrawPoly( 1 );

  // draw provided temperature points
  g.SetMarkerAttr( 'Circle', g.ScalePix(10), '#blue', 'white', 1 );
  var obsAlt = this.BaroData.ObserverAlt;
  var obsTemp = this.BaroData.ObserverTemp;
  for (var i = 0; i < 5 && obsAlt[i] != -1; i++) {
    g.Marker( obsTemp[i], obsAlt[i] );
  }

  // label temperature graph

  g.SetAreaAttr( 'white', 'black', 1 );
  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'left', 'top', g.ScalePix(6) ) ;
  g.TextBox( 'Alt [m]', winXmin, winYmax, 3 );
  g.Text( 'Alt [m]', winXmin, winYmax );

  g.SetTextAttr( 'Arial', g.ScalePix(20), 'black', 'bold', 'normal', 'center', 'top', g.ScalePix(10) ) ;
  g.Text( 'Temperature [°C]', (winXmax+winXmin)/2, winYmax );

}

BaroDisplay.prototype.DrawGradientGraph = function( altRange, obsLimits, trgLimits, isLeftVp ) {
  // viewport is defined and selected

  var g = this.Graph;

  var obsData = this.BaroData.ObserverBaroData;
  var trgData = this.BaroData.TargetBaroData;

  // compute display range and tic size, horizontal temperature
  var winXmin = obsLimits.GradMin * 100;
  var winXmax = obsLimits.GradMax * 100;
  if (winXmin > -0.2) winXmin = -0.2;
  if (winXmax < 0.2) winXmax = 0.2;
  var winRange = winXmax - winXmin;
  winXmin -= 0.05 * winRange;
  winXmax += 0.05 * winRange;
  var gradTicParams = CompTicParams( winXmax - winXmin, 7 );

  // compute display range and tic size, vertical altitude
  var winYmin = 0;
  var winYmax = altRange;
  var altTicParams = CompTicParams( winYmax-winYmin, 16 );

  g.SetWindow( winXmin, winYmin, winXmax, winYmax );

  g.SetLineAttr( 'black', 1 );

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'center', 'top', g.ScalePix(2) ) ;

  g.TicsX( 0, gradTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsX( 0, gradTicParams.TicSize, g.ScalePix(-3), 1, gradTicParams.Digits, false, false, '' ) ;

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'right', 'middle', g.ScalePix(2) ) ;

  if (isLeftVp) {
    this.DrawAltScale( g, winXmin, altTicParams );
  }

  g.SetLineAttr( '#ddd', 1 );
  g.Grid( gradTicParams.TicSize, altTicParams.TicSize, true, false );

  g.SetLineAttr( 'gray', 1 );
  g.Axes();
  g.SetLineAttr( 'black', 1 );
  g.Frame();

  g.SetClipping( 'viewport' );

  // draw target gradient

  //g.SetMarkerSymbol( 'Cross' );
  //g.SetMarkerSize( 8 );
  if (trgData.length > 2) {
    g.SetLineAttr( '#0a4', 1.5 );
    g.NewPoly();
    var maxData = trgLimits.maxIx;
    for (var i = 0; i < maxData; i++) {
      var item = trgData[i];
      g.AddPointToPoly( item.Grad*100, item.Alt );
      //g.Marker( item.Grad*100, item.Alt, 1 );
    }
    g.DrawPoly( 1 );
  }

  // draw observer gradient
  g.SetLineAttr( '#080', 2 );
  g.NewPoly();
  var maxData = obsLimits.maxIx;
  for (var i = 0; i < maxData; i++) {
    var item = obsData[i];
    g.AddPointToPoly( item.Grad*100, item.Alt );
    //g.Marker( item.Grad*100, item.Alt, 1 );
  }
  g.DrawPoly( 1 );

  // label gradient graph

  g.SetAreaAttr( 'white', 'black', 1 );
  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'left', 'top', g.ScalePix(6) ) ;
  g.TextBox( 'Alt [m]', winXmin, winYmax, 3 );
  g.Text( 'Alt [m]', winXmin, winYmax );

  g.SetTextAttr( 'Arial', g.ScalePix(20), 'black', 'bold', 'normal', 'center', 'top', g.ScalePix(10) ) ;
  g.Text( 'Gradient [°C / 100 m]', (winXmax+winXmin)/2, winYmax );

}

BaroDisplay.prototype.DrawRefractionGraph = function( altRange, obsLimits, trgLimits, isLeftVp ) {
  // viewport is defined and selected

  var g = this.Graph;

  var obsData = this.BaroData.ObserverBaroData;
  var trgData = this.BaroData.TargetBaroData;

  // compute display range and tic size
  var winXmin = obsLimits.RefrMin;
  var winXmax = obsLimits.RefrMax;
  if (winXmin > -0.05) winXmin = -0.05;
  if (winXmax < 0.05) winXmax = 0.05;
  var winRange = winXmax - winXmin;
  winXmin -= 0.05 * winRange;
  winXmax += 0.05 * winRange;
  var refrTicParams = CompTicParams( winXmax - winXmin, 6 );

  // compute display range and tic size, vertical altitude
  var winYmin = 0;
  var winYmax = altRange;
  var altTicParams = CompTicParams( winYmax-winYmin, 16 );

  g.SetWindow( winXmin, winYmin, winXmax, winYmax );

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'center', 'top', g.ScalePix(2) ) ;
  g.SetLineAttr( 'black', 1 );
  g.TicsX( 0, refrTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsX( 0, refrTicParams.TicSize, g.ScalePix(-3), 1, refrTicParams.Digits, false, false, '' ) ;

  if (isLeftVp) {
    this.DrawAltScale( g, winXmin, altTicParams );
  }

  g.SetLineAttr( '#ddd', 1 );
  g.Grid( refrTicParams.TicSize, altTicParams.TicSize, true, false );

  g.SetLineAttr( 'gray', 1 );
  g.Axes();
  g.SetLineAttr( 'black', 1 );
  g.Frame();

  g.SetClipping( 'viewport' );

  // draw target refraction
  if (trgData.length > 2) {
    g.SetLineAttr( 'orange', 1.5 );
    g.NewPoly();
    var maxData = trgLimits.maxIx;
    for (var i = 0; i < maxData; i++) {
      var item = trgData[i];
      g.AddPointToPoly( item.Refr, item.Alt );
      //g.Marker( item.Refr, item.Alt, 1 );
    }
    g.DrawPoly( 1 );
  }

  // draw observer refraction
  g.SetLineAttr( 'red', 2 );
  g.NewPoly();
  var maxData = obsLimits.maxIx;
  for (var i = 0; i < maxData; i++) {
    var item = obsData[i];
    g.AddPointToPoly( item.Refr, item.Alt );
    //g.Marker( item.Refr, item.Alt, 1 );
  }
  g.DrawPoly( 1 );

  // label refraction graph

  g.SetAreaAttr( 'white', 'black', 1 );
  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'left', 'top', g.ScalePix(6) ) ;
  g.TextBox( 'Alt [m]', winXmin, winYmax, 3 );
  g.Text( 'Alt [m]', winXmin, winYmax );

  g.SetAreaAttr( 'white', 'black', 1 );
  g.SetTextAttr( 'Arial', g.ScalePix(20), 'black', 'bold', 'normal', 'center', 'top', g.ScalePix(10) ) ;
  g.Text( 'Refraction Coefficient k', (winXmax+winXmin)/2, winYmax );

}

////////////////////////////////////////////////
// RaysDisplay

function RaysDisplay( rayTracer, camera, baroData ) {
  // rayTracer: RefractionRayTracer
  // baroData: BaroSettings

  this.RayTracer = rayTracer;
  this.Camera = camera;
  this.BaroData = baroData;
  this.Async = new CAsync();
}

RaysDisplay.prototype.StopDrawing = function() {
  this.Async.Stop();
}

RaysDisplay.prototype.Draw = function( graph, aRefractionType, aEarthModel ) {
  // g: jsGraph

  this.Async.Stop();

  var params = {
    graph: graph,
    refrType: aRefractionType,
    model: aEarthModel,
  };

  var me = this;
  this.Async.CallObjDefered( 100, me, this.DoDraw, params );
}

RaysDisplay.prototype.DoDraw = function( params ) {

  var g = params.graph;
  var rayTracer = this.RayTracer;
  var camera = this.Camera;
  var obsDist = this.BaroData.ObserverDistance;
  var trgDist = this.BaroData.TargetDistance;
  if (this.BaroData.TargetBaroData.length == 0) trgDist = 0;

  g.Reset();
  g.SetAngleMeasure( 'rad' );
  g.MaxCurveSegments *= 16;

  var leftMargin = 10;
  var rightMargin = 10;
  var bottomMargin = 26;
  var topMargin = 10;

  g.SetViewportRel( leftMargin+40, topMargin, rightMargin, bottomMargin, true, false );

  var winXmin = 0;
  var winXmax = GraphDisplayParams.DistanceRange;
  if (winXmax == 0) {
    // auto setting
    winXmax = this.RayTracer.GetFarthestTargetDistance();
  }
  var winYmin = 0;
  var winYmax = this.BaroData.GetMaxInputAlt( GraphDisplayParams.AltitudeRange );
  if (winYmax < this.Camera.Height) winYmax = this.Camera.Height;
  var distAngle = 0;

  // correct winYmin for curved earth surface
  winYmin -= rayTracer.RadiusEarth - sqrt( sqr(rayTracer.RadiusEarth) - sqr(winXmax) );
  distAngle = winXmax / rayTracer.RadiusEarth;
  winXmax = rayTracer.RadiusEarth * sin( distAngle );

  if (GraphDisplayParams.RaysDspAspectRatio == 0) {

    // auto mapping
    winYmax = 2 * this.Camera.Height - winYmin;
    if (winYmax - winYmin > winXmax) {
      var displayRatio = g.VpWidth / g.VpHeight;
      winXmax = (winYmax - winYmin) * displayRatio;
    }

  } else if (GraphDisplayParams.RaysDspAspectRatio == 1) {

    // streched: make 10% room above
    winYmax += 0.1 * (winYmax - winYmin);

  } else if (GraphDisplayParams.RaysDspAspectRatio == 2) {

    // 1:1 mapping
    var displayRatio = g.VpWidth / g.VpHeight;
    var windowWidth = winXmax - winXmin;
    var windowHeight = winYmax - winYmin;
    var windowRatio = windowWidth / windowHeight;
    if (windowRatio > displayRatio) {
      windowHeight = windowWidth / displayRatio;
      winYmax = winYmin + windowHeight;
    } else {
      windowWidth = windowHeight * displayRatio;
      winXmax = winXmin + windowWidth;
    }
  }

  // make some room
  var winMarginX = 0.02 * (winXmax - winXmin);
  var winMarginY = 0.02 * (winYmax - winYmin);
  winXmin -= winMarginX;
  winXmax += winMarginX;
  winYmin -= winMarginY;
  winYmax += winMarginY;

  g.SetWindow( winXmin, winYmin, winXmax, winYmax );

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'center', 'top', g.ScalePix(2) ) ;
  g.SetLineAttr( 'black', 1 );

  var distTicParams = CompTicParams( (winXmax-winXmin)/1000, 17 );
  g.TicsX( winYmin, 1000*distTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsX( winYmin, 1000*distTicParams.TicSize, g.ScalePix(-3), 0.001, distTicParams.Digits, false, false, '' ) ;

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'right', 'middle', g.ScalePix(2) ) ;

  var altScale = 1;
  if (winYmax > 10000) altScale = 1000;
  var altTicParams = CompTicParams( (winYmax-winYmin)/altScale, 16 );
  g.TicsY( winXmin, altScale*altTicParams.TicSize, g.ScalePix(3), 0, false, false );
  g.TicLabelsY( winXmin, altScale*altTicParams.TicSize, g.ScalePix(-3), 1/altScale, altTicParams.Digits, false, false, '' ) ;

  g.SetLineAttr( 'black', 1 );
  g.Frame();

  g.SetClipping( 'viewport' );

  // draw observer and target measuring distances
  g.SetLineAttr( 'red', 1 );
  g.Line( obsDist, winYmin, obsDist, winYmax );
  if (trgDist > 0) {
    g.Line( trgDist, winYmin, trgDist, winYmax );
  }

  // draw earth surface
  g.SetLineAttr( 'lightgray', 3 );
  if (params.model == 0) {

    g.Arc( 0, -rayTracer.RadiusEarth, -rayTracer.RadiusEarth, 0, distAngle, 1 );

  } else {

    g.Line( winXmin, 0, winXmax, 0 );

  }

  // draw observer
  g.SetLineAttr( 'black', 2 );
  g.Line( 0, 0, 0, camera.Height );
  g.SetMarkerAttr( 'Circle', 6, 'red', 'white', 2 );
  g.Marker( 0, camera.Height, 3 );

  // draw rays
  g.SetLineAttr( 'blue', 1 );

  rayTracer.SaveState();

  rayTracer.SaveRay = true;
  rayTracer.RefractionType = params.refrType;
  rayTracer.EarthModel = params.model;
  rayTracer.EnableTransparency = false;
  rayTracer.IgnoreTargets = true;
  rayTracer.RayDistanceLimit = winXmax;
  rayTracer.RayDelta = winXmax / 50;

  if (GraphDisplayParams.RaysSpreading == 0) {

    var dAngle = GraphDisplayParams.RayAngleRange / GraphDisplayParams.NRays;
    var startAngle = -GraphDisplayParams.RayAngleRange / 2;
    var endAngle = GraphDisplayParams.RayAngleRange / 2 + dAngle / 2;

    for (var angle = startAngle; angle <= endAngle; angle += dAngle) {

      var angleRad = rad( angle );
      rayTracer.RayStartPos = [ 0, camera.Height, 0 ];
      rayTracer.RayStartDir = [ cos( angleRad ), sin( angleRad ), 0 ];

      rayTracer.ComputeRay();

      var ray = rayTracer.Ray;
      g.NewPoly();
      for (var i = 0; i < ray.length; i++) {
        g.AddPointToPoly( ray[i][0], ray[i][1] );
      }
      g.DrawPoly( 1 );
    }

  } else { // RaysSpreading = 1 -> parallel

    var dAlt = winYmax / GraphDisplayParams.NRays;
    var startAlt = 0;
    var endAlt = winYmax + dAlt / 2;

    for (var alt = startAlt; alt <= endAlt; alt += dAlt) {

      rayTracer.RayStartPos = [ 0, alt, 0 ];
      rayTracer.RayStartDir = [ 1, 0, 0 ];

      rayTracer.ComputeRay();

      var ray = rayTracer.Ray;
      g.NewPoly();
      for (var i = 0; i < ray.length; i++) {
        g.AddPointToPoly( ray[i][0], ray[i][1] );
      }
      g.DrawPoly( 1 );
    }

  }

  rayTracer.RestoreState();

  // draw horizon distance, but only on globe earth model
  if (params.model == 0) {

    // calculate position on earth
    var color1 = 'cyan';
    var color2 = 'darkcyan';
    var q = rayTracer.RadiusEarth / (rayTracer.RadiusEarth + camera.Height );
    var x = rayTracer.RadiusEarth * sqrt( 1 - sqr(q) );
    var y = camera.Height * q;
    g.SetMarkerAttr( 'Arrow1', 8, color2, color2, 1 );
    if (GraphDisplayParams.RaysDspAspectRatio == 2) {

      // draw arrow from above
      g.Arrow( x, winYmax/2, x, -y );

    } else {

      // draw arrow from below
      g.Arrow( x, winYmin, x, -y );

      // draw tangent to horizon
      if (GraphDisplayParams.ShowHorizon) {
        var z = -( winXmax * (camera.Height + y) / x - camera.Height );
        g.SetColor( color1 );
        g.Line( 0, camera.Height, winXmax, z );
      }

    }

  }

  // show eye level
  if (GraphDisplayParams.ShowEyeLevel) {
    g.SetLineAttr( 'magenta', 1 );
    g.Line( 0, camera.Height, winXmax, camera.Height );
  }

  g.SetAreaAttr( 'white', 'black', 1 );
  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'left', 'top', g.ScalePix(6) ) ;
  txt = 'Alt [m]';
  if (altScale > 1) txt = 'Alt [km]';
  g.TextBox( txt, winXmin, winYmax, 3 );
  g.Text( txt, winXmin, winYmax );

  g.SetTextAttr( 'Arial', g.ScalePix(16), 'black', 'normal', 'normal', 'right', 'bottom', g.ScalePix(6) ) ;
  g.TextBox( 'Dist [km]', winXmax, winYmin, 3 );
  g.Text( 'Dist [km]', winXmax, winYmin );

  g.SelectTrans( 'viewport' );
  g.SetTextAttr( 'Arial', g.ScalePix(24), 'black', 'bold', 'normal', 'right', 'top', g.ScalePix(4) ) ;
  g.SetAreaAttr( 'yellow', 'red', 1 );
  var refrTxt = [ 'no', 'Standard', 'Custom' ];
  var txt = refrTxt[params.refrType] + ' Refraction';
  g.TextBox( txt, g.VpWidth-g.ScalePix(8), g.ScalePix(8), 3 );
  g.Text( txt, g.VpWidth-g.ScalePix(8), g.ScalePix(8) );
}

///////////////////////////////////////////////
// Barometric Model of Standard Atmosphere

var BaroConst = {
  hIx: 0,
  hMax: 84852,

  // air level data of standard atmosphere model
  hLimitTab: [
    11000,
    20000,
    32000,
    47000,
    51000,
    71000,
    84852,
        0
  ],
  hRefTab: [
        0,
    11000,
    20000,
    32000,
    47000,
    51000,
    71000,
    NaN
  ],
  alphaTab: [
    -0.0065,
     0,
     0.001,
     0.0028,
     0,
    -0.0028,
    -0.002,
    NaN
  ],
  TRefTab: [
    288.15,
    216.65,
    216.65,
    228.65,
    270.65,
    270.65,
    214.65,
    NaN
  ],
  rhoRefTab: [
    1.225,
    0.363918,
    0.0880348,
    0.013225,
    0.00142753,
    0.000861605,
    0.000064211,
    NaN
  ],
  pRefTab: [
    101325,
    22632.1,
    5474.89,
    868.019,
    110.906,
    66.9389,
    3.95642,
    NaN
  ],

  // some general constants
  g:     9.80665,
  R:     8.31432,
  RS:    287.053,
  kappa: 1.4,
  M:     28.9644,

  // private function
  defIx:  function(i) { return xDefNum( i, this.hIx ); },

  // set altitude range for following functions
  SetAltRange: function( h ) {
    for (var i = 0; i < this.hLimitTab.length; i++) {
      if (h <= this.hLimitTab[i]) {
        this.hIx = i;
        return;
      }
    }
    this.hIx = this.hLimitTab.length - 2;
  },

  // query level dependent constants
  // default for i is this.hIx, see SetAltRange(h)
  hRef:   function(i) { return this.hRefTab[this.defIx(i)]; },
  alpha:  function(i) { return this.alphaTab[this.defIx(i)]; },
  TRef:   function(i) { return this.TRefTab[this.defIx(i)]; },
  rhoRef: function(i) { return this.rhoRefTab[this.defIx(i)]; },
  pRef:   function(i) { return this.pRefTab[this.defIx(i)]; }

};

var BaroModel = {

  h: -1,  // used for optimisation

  alpha: function( h ) {
    this.Update( h );
    return BaroConst.alpha();
  },

  TemperatureK: function( h ) {
    this.Update( h );
    return BaroConst.TRef() +
      BaroConst.alpha() * (h - BaroConst.hRef());
  },

  TemperatureC: function( h ) {
    return this.TemperatureK( h ) - 273.15;
  },

  Pressure: function( h ) {
    // return pressure in Pa
    this.Update( h );
    var alpha = BaroConst.alpha();
    if (alpha == 0) {
      // isoterm
      var hs = BaroConst.RS * BaroConst.TRef() / BaroConst.g;
      var p = BaroConst.pRef() * Math.exp( -(h - BaroConst.hRef()) / hs );
      return p;
    } else {
      var beta = BaroConst.g / BaroConst.RS / alpha;
      var p = BaroConst.pRef() * Math.pow( 1 + alpha * (h - BaroConst.hRef()) / BaroConst.TRef(), -beta );
      return p;
    }
  },

  Density: function( h ) {
    this.Update( h );
    var alpha = BaroConst.alpha();
    if (alpha == 0) {
      // isoterm
      var hs = BaroConst.RS * BaroConst.TRef() / BaroConst.g;
      var r = BaroConst.rhoRef() * Math.exp( -(h - BaroConst.hRef()) / hs );
      return r;
    } else {
      var beta = BaroConst.g / BaroConst.RS / alpha;
      var r = BaroConst.rhoRef() * Math.pow( 1 + alpha * (h - BaroConst.hRef()) / BaroConst.TRef(), -beta-1 );
      return r;
    }
  },

  Update: function( h ) {
    if (this.h == h) return;
    BaroConst.SetAltRange( h );
    this.h = h;
  }

};


///////////////////////////////////////////////////////////
// BaroDataFinder

function BaroDataFinder( data ) {
  this.Data = data;
  this.LastFoundIx = -1;
}

BaroDataFinder.prototype.Find = function( alt ) {

  var i = this.LastFoundIx;
  var data = this.Data;
  var last = data.length - 1;

  if (data.length == 0) {
    this.LastFoundIx = -1;
    return -1;
  }

  // check bounds
  if (alt < data[0].Alt) {
    this.LastFoundIx = 0;
    return 0;
  }
  if (alt >= data[last].Alt) {
    this.LastFoundIx = last;
    return last;
  }

  // check near LastFoundIx
  if (i != -1 && i <= last) {

    // check if in last found item
    if (i == last) {
      if (alt >= data[i].Alt) return i;
    } else {
      if (alt >= data[i].Alt && alt < data[i+1].Alt) return i;
    }

    // check neighbor up
    if (i < last && alt >= data[i+1].Alt) {
      var j = i + 1;
      if (j == last || alt < data[j+1].Alt) {
        this.LastFoundIx = j;
        return j;
      }
    }

    // check neighbor down
    if (i > 0 && alt < data[i].Alt) {
      j = i - 1;
      if (alt >= data[j].Alt) {
        this.LastFoundIx = j;
        return j;
      }
    }

  }

  // binary search recursively
  this.LastFoundIx = this.FindRange( 0, last, alt );
  return this.LastFoundIx;

}

BaroDataFinder.prototype.FindRange = function( minIx, maxIx, alt ) {

  if (maxIx - minIx <= 1) return minIx;

  var midIx = floor((maxIx - minIx) / 2) + minIx;
  if (alt >= this.Data[midIx].Alt) {

    return this.FindRange( midIx, maxIx, alt );

  } else {

    return this.FindRange( minIx, midIx, alt );

  }
}

/////////////////////////////////////////////
// BaroDataItem

function BaroDataItem( alt, T ) {
  this.Alt = alt;  // m
  this.Temp = T;   // °C
  this.Grad = 0;   // °C/m
  this.Pres = 0;   // pressure mBar
  this.Refr = 0;   // R_earth / R_refr
}

//////////////////////////////////////////////
// BaroSettings

function BaroSettings( jsGraph, rayTracer, baroModel ) {
  // jsGraph is used to calculate spline curves
  // baroModel is used to calculate standard atmosphere pressure and temperature

  // interface to ControlPanels with user gui

  this.ObserverPressureAlt = 0;
  this.ObserverPressure = 1013.25;  // mBar
  this.ObserverDistance = 0;
  this.TargetPressureAlt = 0;
  this.TargetPressure = 1013.25;    // mBar
  this.TargetDistance = 10000;

  this.ObserverAlt = [];
  this.ObserverTemp = [];

  this.TargetAlt = [];
  this.TargetTemp = [];

  // arrays of BaroDataItem computed by Update() from ObserverAlt, ObserverTemp and TargetAlt, TargetTemp
  this.ObserverBaroData = [];
  this.TargetBaroData = [];

  // constants

  this.NumBaroEntries = 5;
  this.NSplineSegments = 20;
  this.SplineSmoothness = 0.75;
  this.MaxAltInput = 9000;  // input altitude is restricted to blend into standard model
  this.MaxPressureVariationFactor = 0.05; // until MaxAltInput
  this.MaxTempVariation = 50; // until MaxAltInput

  // private

  this.graph = jsGraph;
  this.BaroModel = baroModel;
  this.RayTracer = rayTracer;  // used to calculate refraction coefficient

  this.ObserverDataFinder = new BaroDataFinder( this.ObserverBaroData );
  this.TargetDataFinder = new BaroDataFinder( this.TargetBaroData );

  // init interface values
  this.Clear();

}

BaroSettings.prototype.GetLimits = function( altMax, baroData ) {
  // find min and max values of baroData from alt = 0 to alt = altMax;
  // baroData = this.ObserverBaroData or this.TargetBaroData
  // require baroData.length > 1

  var d = baroData[0];
  var limits = {
    TempMin: d.Temp, TempMax: d.Temp,
    GradMin: d.Grad, GradMax: d.Grad,
    RefrMin: d.Refr, RefrMax: d.Refr,
    maxIx: 0,
    maxAlt: 0,
  };

  var limit = baroData.length;
  var i = 1;
  while (i < limit) {
    var d = baroData[i];
    if (d.Temp < limits.TempMin) limits.TempMin = d.Temp;
    if (d.Temp > limits.TempMax) limits.TempMax = d.Temp;
    if (d.Grad < limits.GradMin) limits.GradMin = d.Grad;
    if (d.Grad > limits.GradMax) limits.GradMax = d.Grad;
    if (d.Refr < limits.RefrMin) limits.RefrMin = d.Refr;
    if (d.Refr > limits.RefrMax) limits.RefrMax = d.Refr;
    i++;
    if (d.Alt > altMax) break;
  }
  limits.maxIx = i;
  limits.maxAlt = baroData[i-1].Alt;

  return limits;
}

BaroSettings.prototype.Clear = function() {

  for (var i = 0; i < this.NumBaroEntries; i++) {
    this.ObserverAlt.push( -1 );
    this.ObserverTemp.push( 15 );
    this.TargetAlt.push( -1 );
    this.TargetTemp.push( 15 );
  }
  this.Update();
}

BaroSettings.prototype.Update = function( controller ) {

  if (MatchesControllerPanel( controller, 'BaroSettingsObserverPanel' )) {

    if (MatchesControllerField( controller, 'ObserverDistance' )) {
      if (this.ObserverDistance < 0) this.ObserverDistance = 0;
      if (this.ObserverDistance + 1000 >= this.TargetDistance) this.ObserverDistance = this.TargetDistance - 1000;
    }

    if (MatchesControllerField( controller, 'NSplineSegments' )) {
      if (this.NSplineSegments < 3) this.NSplineSegments = 3;
      if (this.NSplineSegments > 25) this.NSplineSegments = 25;
    }

    if (MatchesControllerField( controller, 'SplineSmoothness' )) {
      if (this.SplineSmoothness < 0) this.SplineSmoothness = 0;
      if (this.SplineSmoothness > 1) this.SplineSmoothness = 1;
    }

    this.SortUserEntries( this.ObserverAlt, this.ObserverTemp );
    this.Validate( this.ObserverAlt, this.ObserverTemp, true );
    this.RemoveDoubleUserEntries( this.ObserverAlt, this.ObserverTemp );

    // if no observer baro data is provided, set alt = 0 and temp = 16
    if (this.ObserverAlt[0] == -1) {
      this.ObserverAlt[0] = 0;
      this.ObserverTemp[0] = 15;
    }

    // creates temperature, temperature gradient and refraction data from sorted user input

    this.ObserverBaroData.length = 0;

    this.CreateData( this.ObserverAlt, this.ObserverTemp, this.ObserverBaroData );
    this.AddPastTroposphereData( this.ObserverBaroData );
    this.CompRefraction( this.ObserverAlt, this.ObserverBaroData, this.ObserverPressureAlt, this.ObserverPressure );

  }

  if (MatchesControllerPanel( controller, 'BaroSettingsTargetPanel' )) {

    if (MatchesControllerField( controller, 'TargetDistance' )) {
      if (this.TargetDistance < this.ObserverDistance + 1000) this.TargetDistance = this.ObserverDistance + 1000;
    }

    this.SortUserEntries( this.TargetAlt, this.TargetTemp );
    this.Validate( this.TargetAlt, this.TargetTemp, false );
    this.RemoveDoubleUserEntries( this.TargetAlt, this.TargetTemp );

    // creates temperature, temperature gradient and refraction data from sorted user input

    this.TargetBaroData.length = 0;

    if (this.TargetAlt[0] != -1) {
      this.CreateData( this.TargetAlt, this.TargetTemp, this.TargetBaroData );
      this.AddPastTroposphereData( this.TargetBaroData );
      this.CompRefraction( this.TargetAlt, this.TargetBaroData, this.TargetPressureAlt, this.TargetPressure );
    }
  }

}

BaroSettings.prototype.Validate = function( alts, temps, isObserverData ) {

  for (var i = 0; i < alts.length; i++) {
    if (alts[i] < -1 || alts[i] > this.MaxAltInput) {

      // reject altitudes out of range
      alts.splice( i, 1 );
      alts.push( -1 );
      temps.splice( i, 1 );
      temps.push( 15 );
      i--;

    } else if (alts[i] != -1 ) {

      // restrict temperature according to altitude
      var stdTempC = this.BaroModel.TemperatureC( alts[i] );
      if (temps[i] < stdTempC - this.MaxTempVariation) temps[i] = stdTempC - this.MaxTempVariation;
      if (temps[i] > stdTempC + this.MaxTempVariation) temps[i] = stdTempC + this.MaxTempVariation;

    }
  }

  if (isObserverData) {

    // limit pressure altitude
    if (this.ObserverPressureAlt < 0) this.ObserverPressureAlt = 0;
    if (this.ObserverPressureAlt > this.MaxAltInput) this.ObserverPressureAlt = this.MaxAltInput;
    if (this.TargetPressureAlt < 0) this.TargetPressureAlt = 0;
    if (this.TargetPressureAlt > this.MaxAltInput) this.TargetPressureAlt = this.MaxAltInput;

    // limit pressure depending on altitude
    var stdPressure = this.BaroModel.Pressure( this.ObserverPressureAlt ) / 100;
    var pressureVar = (this.MaxAltInput - this.ObserverPressureAlt) / this.MaxAltInput * this.MaxPressureVariationFactor * stdPressure;
    if (this.ObserverPressure < stdPressure - pressureVar) this.ObserverPressure = stdPressure - pressureVar;
    if (this.ObserverPressure > stdPressure + pressureVar) this.ObserverPressure = stdPressure + pressureVar;

    var stdPressure = this.BaroModel.Pressure( this.TargetPressureAlt ) / 100;
    var pressureVar = (this.MaxAltInput - this.TargetPressureAlt) / this.MaxAltInput * this.MaxPressureVariationFactor * stdPressure;
    if (this.TargetPressure < stdPressure - pressureVar) this.TargetPressure = stdPressure - pressureVar;
    if (this.TargetPressure > stdPressure + pressureVar) this.TargetPressure = stdPressure + pressureVar;
  }

}

BaroSettings.prototype.SortUserEntries = function( alts, temps ) {
  // bubble sort

  function swap( arr, i, j ) {
    var tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }

  for (var i = 0; i < alts.length-1; i++) {
    for (var j = i+1; j < alts.length; j++) {
      if (alts[j] != -1) {
        if (alts[i] == -1 || alts[i] > alts[j]) {
          if (alts[i] == -1) {
            temps[i] = 15;
          }
          swap( alts, i, j );
          swap( temps, i, j );
        }
      }
    }
  }
}

BaroSettings.prototype.RemoveDoubleUserEntries = function( alts, temps ) {
  // require alts and temps sorted
  var limit = alts.length - 1;
  var i = 0;
  while (i < limit) {
    if (alts[i] != -1 && abs(alts[i] - alts[i+1]) < 0.01) {
      alts.splice( i+1, 1 );
      alts.push( -1 );
      temps.splice( i+1, 1 );
      temps.push( 15 );
    } else {
      i++;
    }
  }
}

BaroSettings.prototype.CreateData = function( alts, temps, data ) {
  // alts: user set altitudes
  // temps: user set temperatures
  // data: created spline interpolated data points

  // Create baro data from user input
  for (var i = 0; i < alts.length; i++) {
    if (alts[i] == -1) break;
    data.push( new BaroDataItem( alts[i], temps[i] ) );
  }

  // if no baro data is provided, use standard
  if (data.length == 0) {

    data.push( new BaroDataItem( 0, 15 ) );

  }

  // add top limit
  data.push( new BaroDataItem( BaroConst.hRefTab[1], BaroConst.TRefTab[1] - 273.15 ) );
  data[data.length-1].Grad = BaroConst.alphaTab[1];

  // if no data for zero altitude is provided, interpolate temperature from lowest gradient
  if (data[0].Alt > 0) {

    // estimate temp at zero by interpolation of temp above
    var grad0 = (data[1].Temp - data[0].Temp) / (data[1].Alt - data[0].Alt);
    var temp0 = data[0].Temp - grad0 * data[0].Alt;
    data.splice( 0, 0, new BaroDataItem( 0, temp0 ) );
  }

  // spline interpolation of temperature

  var altCoord = [];
  var tempCoord = [];
  for (var i = 0; i < data.length; i++) {
    altCoord[i] = data[i].Alt;
    tempCoord[i] = data[i].Temp;
  }
  data.length = 0;

  // add control point to blend curve into std atmo
  altCoord.push( this.MaxAltInput-1000 );
  tempCoord.push( BaroConst.TRefTab[1] - 273.15 );

  // interpolate between data points with a spline using the spline function of this.graph
  // mode: 32 -> last point is a control point, not a curve point
  // mode: 128 -> make symmetric control bezier control points so curvature at meeting bezier curves is the same!

  var poly = this.graph.MakeSplineCurve( altCoord, tempCoord, this.SplineSmoothness/2, 32+128, altCoord.length, this.NSplineSegments );

  // create ObserverBaroData from spline
  for (var i = 0; i < poly.Size; i++) {
    data[i] = new BaroDataItem( poly.X[i], poly.Y[i] );
  }

  this.RemoveInvalid( data );

  this.CompGradient( data );

}

BaroSettings.prototype.RemoveInvalid = function( data ) {
  // removes double entries with the same alt
  // removes entries with the wrong alt direction due to spline interpolation
  var i = 0;
  while (i < data.length-1) {
    if (abs(data[i].Alt - data[i+1].Alt) < 0.01) {
      data.splice( i, 1 );
    } else {
      i++;
    }
  }
  i = 0;
  while (i < data.length-1) {
    if (data[i+1].Alt < data[i].Alt) {
      data.splice( i+1, 1 );
    } else {
      i++;
    }
  }
}

BaroSettings.prototype.CompGradient = function( data ) {
  // computes the gradients from the data altitudes and temperatures
  // removes double entries

  if (data.length < 2) return;
  var limit = data.length - 2;

  for (var i = 0; i <= limit; i++) {
    data[i].Grad = (data[i+1].Temp - data[i].Temp) / (data[i+1].Alt - data[i].Alt);
  }

  // Note: at this point the last data entry should be at alt = 11000 m
  var last = limit+1;
  if (data[last].Alt >= BaroConst.hLimitTab[0]) {
    // above 11 km the gradient is 0, eg. constant tempertature until 18 km, but we keep it further up
    data[last].Grad = BaroConst.alphaTab[1];
  } else {
    data[last].Grad = data[limit].Grad;
  }
}

BaroSettings.prototype.AddPastTroposphereData = function( data ) {
  // Note: tropo top is already in data (see CreateData()), so start with index  i = 2
  var deltaAlt = 10;
  // replace last item
  data.splice( data.length-1 );
  var limit = BaroConst.hLimitTab.length - 1;
  for (var i = 1; i < limit; i++) {
    var item = new BaroDataItem( BaroConst.hRefTab[i], BaroConst.TRefTab[i]-273.15 );
    item.Grad = BaroConst.alphaTab[i];
    // insert an additional near item to reflect the jumps in the gradient when connecting points
    if (i > 1) {
      var prevItem = data[data.length-1];
      var alt2 = item.Alt - deltaAlt;
      var deltaTemp = prevItem.Grad * deltaAlt;
      var item2 = new BaroDataItem( alt2, item.Temp-deltaTemp );
      item2.Grad = prevItem.Grad;
      var itemInter = new BaroDataItem( (prevItem.Alt+item2.Alt)/2, (prevItem.Temp+item2.Temp)/2 );
      itemInter.Grad = item2.Grad;
      data.push( itemInter );
      data.push( item2 );
    }
    data.push( item );
  }
}

BaroSettings.prototype.CompRefraction = function( alts, data, pAlt, p ) {
  // computes refraction coefficient for each altitude in data from pressure, temperature and temp gradient
  // according to standard refraction model
  // Pressure is calculated from standard atmosphere model and shifted by p on pAlt proportionally

  var tropoAlt = BaroConst.hRefTab[1];
  var tropoP = BaroConst.pRefTab[1] / 100;
  var limit = data.length;
  for (var i = 0; i < limit; i++) {
    var item = data[i];
    if (item.Alt >= BaroConst.hMax) {
      // too high, no more refraction
      item.Refr = 0;
    } else {
      if (item.Alt >= tropoAlt) {
        // use standard atmo model
        item.Pres = this.BaroModel.Pressure( item.Alt ) / 100;
      } else {
        // correct std atmo pressure by pressure p measured at altitude pAlt proportionally
        item.Pres = (p - tropoP) / (tropoAlt - pAlt) * (tropoAlt - item.Alt) + tropoP;
      }
      item.Refr = this.RayTracer.RefractionCoefficient( item.Pres, item.Temp+273.15, item.Grad );
    }
  }

}

BaroSettings.prototype.GetRefraction = function( dist, alt ) {
  // finds refraction entries in BaroData and interpolates according to dist and alt.

  var i = this.ObserverDataFinder.Find( alt );
  if (i == -1) return 0;

  var refr1 = this.InterpolateRefraction( i, this.ObserverBaroData, alt );

  if (this.TargetBaroData.length == 0) return refr1;

  var i = this.TargetDataFinder.Find( alt );
  if (i == -1) return refr1;

  var refr2 = this.InterpolateRefraction( i, this.TargetBaroData, alt );

  if (dist < this.ObserverDistance) return refr1;
  if (dist > this.TargetDistance) return refr2;

  return this.Interpolate( dist, this.ObserverDistance, this.TargetDistance, refr1, refr2 );
}

BaroSettings.prototype.InterpolateRefraction = function( i, data, alt ) {

  var refr = data[i].Refr;
  if (i == 0 && alt < data[0].Alt) return refr;
  if (i >= data.length-1) return refr;

  refr = this.Interpolate( alt, data[i].Alt, data[i+1].Alt, refr, data[i+1].Refr );

  return refr;
}

BaroSettings.prototype.Interpolate = function( pos, xmin, xmax, ymin, ymax ) {
  return (ymax - ymin) / (xmax - xmin) * (pos - xmin) + ymin;
}

BaroSettings.prototype.IsObsAltDefined = function( i ) {
  return this.ObserverAlt[i] != -1;
}

BaroSettings.prototype.IsTrgAltDefined = function( i ) {
  return this.TargetAlt[i] != -1;
}

BaroSettings.prototype.IsTargetBaroSpecified = function() {
  for (var i = 0; i < this.TargetAlt.length; i++) {
    if (this.TargetAlt[i] != -1) return true;
  }
  return false;
}

BaroSettings.prototype.GetMaxInputAlt = function( userAlt ) {
  // searches to highest entry in ObserverAlt and TargetAlt

  if (userAlt != 0) {
    if (userAlt < 1) userAlt = 10;
    return userAlt;
  }
  var altMaxObs = 0;
  for (var i = this.ObserverAlt.length-1; i >= 0; i--) {
    var alt = this.ObserverAlt[i];
    if (alt != -1) {
      altMaxObs = alt;
      break;
    }
  }
  var altMaxTrg = 0;
  for (var i = this.TargetAlt.length-1; i >= 0; i--) {
    var alt = this.TargetAlt[i];
    if (alt != -1) {
      altMaxTrg = alt;
      break;
    }
  }
  if (altMaxTrg > altMaxObs) altMaxObs = altMaxTrg;
  if (altMaxObs < 10) altMaxObs = 10;
  return altMaxObs;
}

///////////////////////////////////////////////////////////
// GraphDisplayParams

var GraphDisplayParams = {

  AltitudeRange: 0,      // 0 -> auto range
  DistanceRange: 0,      // 0 -> auto range
  RayAngleRange: 2,      // degrees
  NRays: 100,
  RaysSpreading: 0,      // 0 -> radial, 1 -> parallel
  RaysDspAspectRatio: 0, // 0 -> auto, 1 -> streched, 2 -> equal scaling
  ShowHorizon: true,
  ShowEyeLevel: false,

  Update: function( controller ) {
    if (this.AltitudeRange > 0) {
      // 0 -> auto range
      if (this.AltitudeRange < 1) this.AltitudeRange = 1;
      if (this.AltitudeRange > 100000) this.AltitudeRange = 100000;
    } else {
      this.AltitudeRange = 0;
    }
    if (this.DistanceRange > 0) {
      // 0 -> auto range
      if (this.DistanceRange < 100) this.DistanceRange = 100;
      if (this.DistanceRange > 10000000) this.DistanceRange = 10000000;
    } else {
      this.DistanceRange = 0;
    }
    if (this.NRays < 1) this.NRays = 1;
    if (this.NRays > 500) this.NRays = 500;
    this.NRays = round( this.NRays );
    if (this.RayAngleRange < 0.01) this.RayAngleRange = 0.01;
    if (this.RayAngleRange > 170) this.RayAngleRange = 170;
  },
}

///////////////////////////////////////////////////////////
// RefrSimApp

var RefrSimApp = {

  Targets: null,        // link to RefractionRayTracer.Targets()
  TargetTemplate: null,
  CurrTarget: null,
  CurrTargetIndex: -1,

  LeftDspViewType: 0,        // 0 -> image, 1 -> Baro, 2 -> Rays
  LeftDspModel: 0,           // 0 -> globe, 1 -> flat earth
  LeftDspRefrType: 0,        // 0 -> no, 1 -> standard, 2 -> custom
  LeftDspLeftCurveType: 0,   // 0 -> Temperature, 1 -> Gradient, 2 -> Refraction
  LeftDspRightCurveType: 2,  // 0 -> Temperature, 1 -> Gradient, 2 -> Refraction

  RightDspViewType: 0,       // 0 -> image, 1 -> graphic
  RightDspModel: 0,          // 0 -> globe, 1 -> flat earth
  RightDspRefrType: 2,       // 0 -> no, 1 -> standard, 2 -> custom
  RightDspLeftCurveType: 0,  // 0 -> Temperature, 1 -> Gradient, 2 -> Refraction
  RightDspRightCurveType: 2, // 0 -> Temperature, 1 -> Gradient, 2 -> Refraction

  DisplayLeft: null,         // jsGraph
  DisplayRight: null,        // jsGraph
  Camera: null,              // RayTracerCamera
  Camera2: null,             // RayTracerCamera
  BaroDisplayLeft: null,     // BaroDisplay
  BaroDisplayRight: null,    // BaroDisplay
  RaysDisplayLeft: null,     // RaysDisplay
  RaysDisplayRight: null,    // RaysDisplay
  RayTracer: RefractionRayTracer,
  GraphDisplayParams: GraphDisplayParams,

  // private
  OnTargetImageLoad: null,
  RemovedTargets: [],
  RemovedTargetsIndexes: [],
  BaroData: null,
  BaroModel: null,

  Init: function( displayLeft, displayRight ) {
    // displayLeft, displayRight: jsGraph

    this.DisplayLeft = displayLeft;
    this.DisplayRight = displayRight;

    this.Targets = this.RayTracer.Targets;
    this.TargetTemplate = new Target( {
      Plane: { Pos: [ 10000, 0, 0 ], RotX: 0 },
      Width: 10,
      Height: 0,
      LimitType: 1,
    } );
    this.CurrTarget = this.TargetTemplate;

    // Baro data
    this.BaroData = new BaroSettings( this.DisplayLeft, this.RayTracer, BaroModel );
    this.BaroModel = BaroModel;

    // right main camera
    this.Camera = new RayTracerCamera( this.RayTracer );

    // left camera
    // The Camera2 parameters are cloned from this.Camera on redraw
    this.Camera2 = new RayTracerCamera( this.RayTracer, this.Camera );

    // Barometric displays
    this.BaroDisplayLeft = new BaroDisplay( this.BaroData );
    this.BaroDisplayRight = new BaroDisplay( this.BaroData );

    // Rays displays
    this.RaysDisplayLeft = new RaysDisplay( this.RayTracer, this.Camera, this.BaroData );
    this.RaysDisplayRight = new RaysDisplay( this.RayTracer, this.Camera, this.BaroData );

    var me = this;

    this.RayTracer.CustomRefrFunc = function( pos_surf3D ) {
      return me.BaroData.GetRefraction( pos_surf3D[0], pos_surf3D[1] );
    };

    this.OnTargetImageLoad = function RefrSimApp_TargetImageLoaded( target ) {
      me.Update( 'TargetPanel' );
    };

    this.Update();
  },

  IndicateTargetSelection: function() {
    // labels the current target in the TargetPanel

    var header = xElement( 'TargetPanelHeader' );
    if (header) {
      var txt = 'Target ';
      if (this.CurrTargetIndex == -1) {
        txt += 'Template';
      } else {
        txt += (this.CurrTargetIndex+1);
      }
      if (this.Targets.length > 0) {
        txt += ' of ' + this.Targets.length;
      }
      xInnerHTML( header, txt );
    }
  },

  IndicateImageLoadState: function() {
    var e = xElement( 'TargetPanel-ImageSrc-Unit' );
    if (e) {
      xInnerHTML( e, this.CurrTarget.ImageState );
    }
  },

  Clear: function() {
    // clears the App
    // Note: currently only the targets need to be deleted

    this.RayTracer.Clear();
    this.CurrTargetIndex = -1;
    this.CurrTarget = this.TargetTemplate;
    this.Update( 'TargetChanged' );
  },

  Update: function( controller ) {
    // controller: undefined, CpField or string

    var displayPanelMask = 3;

    // only left or right display is affected by the following panels
    if (xDef( controller )) {
      if (MatchesControllerPanel( controller, 'LeftDisplaySelectorPanel,LeftDisplayModelSelectorPanel,LeftDisplayCurveSelectorPanel' )) {
        displayPanelMask &= ~2;
      }
      if (MatchesControllerPanel( controller, 'RightDisplaySelectorPanel,RightDisplayModelSelectorPanel,RightDisplayCurveSelectorPanel' )) {
        displayPanelMask &= ~1;
      }
    }

    // only display the corresponding DisplayModelSelectorPanel or DisplayCurveSelectorPanel and hide the other depending on DspViewType
    if (MatchesControllerPanel( controller, 'LeftDisplaySelectorPanel' )) {
      if (this.LeftDspViewType == 1) {
        // show DisplayCurveSelectorPanel
        xDisplay( 'LeftDisplayModelSelectorPanel', 'none' );
        xDisplay( 'LeftDisplayCurveSelectorPanel', '' );
      } else {
        // show DisplayModelSelectorPanel
        xDisplay( 'LeftDisplayModelSelectorPanel', '' );
        xDisplay( 'LeftDisplayCurveSelectorPanel', 'none' );
      }
      // update panel to enable/disable ray parameters
      if (xDef(controller)) {
        ControlPanels.Update( 'GraphDisplayParamsPanel' );
      }
    }
    if (MatchesControllerPanel( controller, 'RightDisplaySelectorPanel' )) {
      if (this.RightDspViewType == 1) {
        // show DisplayCurveSelectorPanel
        xDisplay( 'RightDisplayModelSelectorPanel', 'none' );
        xDisplay( 'RightDisplayCurveSelectorPanel', '' );
      } else {
        // show DisplayModelSelectorPanel
        xDisplay( 'RightDisplayModelSelectorPanel', '' );
        xDisplay( 'RightDisplayCurveSelectorPanel', 'none' );
      }
      // update panel to enable/disable ray parameters
      if (xDef(controller)) {
        ControlPanels.Update( 'GraphDisplayParamsPanel' );
      }
    }

    if (MatchesControllerPanel( controller, 'BaroSettingsObserverPanel,BaroSettingsTargetPanel')) {

      this.BaroData.Update( controller );

      // only update image display showing custom refraction
      if (xDef( controller )) {
        if (this.LeftDspViewType == 0 && this.LeftDspRefrType != 2) {
          displayPanelMask &= ~1;
        }

        if (this.RightDspViewType == 0 && this.RightDspRefrType != 2) {
          displayPanelMask &= ~2;
        }
      }

    }

    if (MatchesControllerName( controller, 'TargetSelection,TargetChanged' )) {

      if (xDef(controller) && MatchesControllerName( controller, 'TargetSelection' )) {
        displayPanelMask = 0;
      }

      if (this.Targets.length == 0) {

        this.CurrTargetIndex = -1;
        this.CurrTarget = this.TargetTemplate;

      } else if (this.CurrTargetIndex < 0 || this.CurrTargetIndex >= this.Targets.length) {

        this.CurrTargetIndex = this.Targets.length - 1;
        this.CurrTarget = this.Targets[this.CurrTargetIndex];

      } else {

        this.CurrTarget = this.Targets[this.CurrTargetIndex];

      }
      var panels = [ 'TargetPanel', 'TargetPlanePanel' ];
      ControlPanels.Refresh( panels );
      ControlPanels.SetEnabled( panels, '', this.CurrTargetIndex >= 0 );
      ControlPanels.Update( panels );

      this.IndicateTargetSelection();
      this.IndicateImageLoadState();

    }

    if (MatchesControllerPanel( controller, 'CameraPanel' )) {

      this.Camera.Update( controller );

      // only update image displays
      if (xDef( controller ) && !MatchesControllerField( controller, 'Height' )) {
        if (this.LeftDspViewType != 0) {
          displayPanelMask &= ~1;
        }

        if (this.RightDspViewType != 0) {
          displayPanelMask &= ~2;
        }
      }
    }

    if (MatchesControllerPanel( controller, 'GraphDisplayParamsPanel' )) {

      GraphDisplayParams.Update( controller );

      // only update graph displays
      if (xDef( controller )) {
        if (this.LeftDspViewType == 0) {
          displayPanelMask &= ~1;
        }

        if (this.RightDspViewType == 0) {
          displayPanelMask &= ~2;
        }
      }
    }

    if (MatchesControllerPanel( controller, 'TargetPanel,TargetPlanePanel' )) {

      if (xDef(controller)) {

        var target = this.CurrTargetIndex == -1 ? this.TargetTemplate : this.Targets[this.CurrTargetIndex];

        if (MatchesControllerField( controller, 'Name' )) {
          // no redraw needed if Name is changed
          displayPanelMask = 0;
        }

        if (MatchesControllerField( controller, 'Color1,Color2,Param1,Param2,Pattern' ) && target.Image) {
          // no redraw needed if Patter Parameter is changed and target is an image
          displayPanelMask = 0;
        }

        target.Update( controller );

      } else {

        for (var i = 0; i < this.Targets.length; i++) {
          this.Targets[i].Update( controller );
        }
        this.TargetTemplate.Update( controller );

      }

      this.IndicateImageLoadState();
    }

    if (MatchesControllerPanel( controller, 'RayTracerPanel' )) {

      this.RayTracer.Update( controller );

    }

    ControlPanels.Update( GetControllerPanelNames( controller ) );

    if (displayPanelMask > 0) {
      this.Draw( displayPanelMask );
    }
  },

  Draw: function( displpayPanelMask ) {
    // displpayPanelMask: 1 -> draw left graph, 2 -> draw right graph, 0 or undefined -> draw both

    displpayPanelMask = xDefNum( displpayPanelMask, 0 );
    if (displpayPanelMask == 0) displpayPanelMask = 3; // both

    if (displpayPanelMask & 1) {
      this.Camera2.StopDrawing(); // Camera2 is on left display
      this.BaroDisplayLeft.StopDrawing();
      this.RaysDisplayLeft.StopDrawing();

      if (this.LeftDspViewType == 0) {

        this.Camera2.SetParams( this.Camera ); // clone settings from right camera
        this.Camera2.DrawImage( this.DisplayLeft, this.LeftDspRefrType, this.LeftDspModel );

      } else if (this.LeftDspViewType == 1) {

        this.BaroDisplayLeft.Draw( this.DisplayLeft, this.LeftDspLeftCurveType, this.LeftDspRightCurveType );

      } else {

        this.RaysDisplayLeft.Draw( this.DisplayLeft, this.LeftDspRefrType, this.LeftDspModel );

      }
    }

    if (displpayPanelMask & 2) {
      this.Camera.StopDrawing();
      this.BaroDisplayRight.StopDrawing();
      this.RaysDisplayRight.StopDrawing();

      if (this.RightDspViewType == 0) {

        this.Camera.DrawImage( this.DisplayRight, this.RightDspRefrType, this.RightDspModel );

      } else if (this.RightDspViewType == 1) {

        this.BaroDisplayRight.Draw( this.DisplayRight, this.RightDspLeftCurveType, this.RightDspRightCurveType );

      } else {

        this.RaysDisplayRight.Draw( this.DisplayRight, this.RightDspRefrType, this.RightDspModel );
      }
    }
  },

  NewTarget: function( params, posIx ) {
    // creates a new target and inserts it into the this.Targets at location posIx.
    // The new target gets selected.
    // If posIx is not defined, the new target is appended at this.Targets.
    // You can use a Target as params to copy the settings

    var target = new Target( params, this.OnTargetImageLoad );
    this.InsertTargetAtPosition( target, posIx );
  },

  CreateTarget: function() {
    // target constructor for load function DataX.SetAppState()
    // target data is loaded from json or stream and Target.Update() is called after

    return new Target( null, this.OnTargetImageLoad );
  },

  InsertTargetAtPosition: function( target, posIx ) {
    // adds target at posIx and selects it
    // if posIx is not defined or out of bound, target is appendet to the end

    posIx = this.RayTracer.InsertTarget( target, posIx );
    this.CurrTargetIndex = posIx;
    this.CurrTarget = target;
    this.Update( 'TargetChanged' );
  },

  AddTarget: function() {
    // Creates a new target from TargetTemplate and appends it to this.Targets.
    // The new target gets selected

    this.NewTarget( this.TargetTemplate );
  },

  AddCloneTarget: function() {
    // creates a copy of the selected or template target and adds it to the end of this.Targets
    // The new target gets selected

    if (this.CurrTargetIndex == -1) {
      this.AddTarget();
    } else {
      this.NewTarget( this.Targets[this.CurrTargetIndex] );
    }
  },

  InsertTarget: function() {
    // Creates a new target from TargetTemplate and inserts it to this.Targets at current position.
    // The new target gets selected

    this.NewTarget( this.TargetTemplate, this.CurrTargetIndex );
  },

  InsertCloneTarget: function() {
    // creates a copy of the selected or template target and inserts it to this.Targets at current position.
    // The new target gets selected

    if (this.CurrTargetIndex == -1) {
      this.InsertTarget();
    } else {
      this.NewTarget( this.Targets[this.CurrTargetIndex], this.CurrTargetIndex );
    }
  },

  DeleteTarget: function() {
    // removes selected target and places it in this.RemovedTargets for UndeleteTarget().
    // The target behind the removed target gets selected, or if there are no targets behind,
    // the target before the removed target gets selected, if there is any.

    if (this.CurrTargetIndex == -1) return;
    var target = this.RayTracer.RemoveTarget( this.CurrTargetIndex );
    if (target == null) return;

    this.RemovedTargetsIndexes.push( this.CurrTargetIndex );
    this.RemovedTargets.push( target );
    if (this.CurrTargetIndex >= this.Targets.length) {
      this.CurrTargetIndex = this.Targets.length-1;
    }
    this.CurrTarget = (this.CurrTargetIndex == -1) ? this.TargetTemplate : this.Targets[this.CurrTargetIndex];
    this.Update( 'TargetChanged' );
  },

  UndeleteTarget: function() {

    if (this.RemovedTargets.length == 0) return;
    this.InsertTargetAtPosition( this.RemovedTargets.pop(), this.RemovedTargetsIndexes.pop() );
  },

  DeleteAllTargets: function() {
    while (this.Targets.length > 0) {
      this.DeleteTarget();
    }
  },

  TargetUpDown: function( dir ) {
    // dir = +/- 1
    if (this.Targets.length < 2 || dir == 0) return false;
    var destPosIx = this.CurrTargetIndex + dir;
    if (destPosIx < 0 || destPosIx >= this.Targets.length) return false;

    var target = this.Targets.splice( this.CurrTargetIndex, 1 )[0];
    this.Targets.splice( destPosIx, 0, target );

    this.CurrTargetIndex = destPosIx;
    this.CurrTarget = this.Targets[this.CurrTargetIndex];
    this.Update( 'TargetChanged' );
    return true;
  },

  TargetUp: function() {
    return this.TargetUpDown( -1 );
  },

  TargetDown: function() {
    return this.TargetUpDown( 1 );
  },

  SelectTarget: function( posIx ) {
    if (posIx < 0 || posIx >= this.Targets.length) return false;
    this.CurrTargetIndex = posIx;
    this.CurrTarget = this.Targets[this.CurrTargetIndex];
    this.Update( 'TargetSelection' );
    return true;
  },

  SelectNextTarget: function() {
    return this.SelectTarget( this.CurrTargetIndex + 1 );
  },

  SelectPrevTarget: function() {
    return this.SelectTarget( this.CurrTargetIndex - 1 );
  },

  IsParameterUsed: function( parameterName ) {
    if (this.CurrTargetIndex == -1) return false;
    return this.CurrTarget.IsParameterUsed( parameterName );
  },

  IsObsAltDefined: function( i ) {
    return this.BaroData.IsObsAltDefined(i);
  },

  IsTrgAltDefined: function( i ) {
    return this.BaroData.IsTrgAltDefined(i);
  },

  IsCustomLimit: function() {
    // disabled if target template is selected
    if (this.CurrTargetIndex == -1) return false;
    return this.CurrTarget.LimitType == 0;
  },

  IsRayGraphDisplayed: function() {
    return (this.LeftDspViewType == 2) || (this.RightDspViewType == 2);
  },

  IsAnyGraphDisplayed: function() {
    return (this.LeftDspViewType != 0) || (this.RightDspViewType != 0);
  },

  IsTargetBaroSpecified: function() {
    return this.BaroData.IsTargetBaroSpecified();
  },

  // preset barometric settings for observer

  ClearObserverBaro: function( update ) {
    update = xDefBool( update, true );
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    for (var i = 0; i < 5; i++) {
      alts[i] = -1; temps[i] = 15;
    }
    alts[0] = 0;
    if (update) this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroLooming1: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =   0; temps[0] = 11;
    alts[1] = 100; temps[1] = 15;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroLooming2: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =   0; temps[0] = 2;
    alts[1] =  50; temps[1] = 5;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroInferiorMirage1: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =    0; temps[0] = 15.0;
    alts[1] =    1; temps[1] = 14.8;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroInferiorMirage2: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =    0; temps[0] = 12.3;
    alts[1] = 0.49; temps[1] = 12.01;
    alts[2] =  1.4; temps[2] = 11.95;
    alts[3] =  100; temps[3] = 12.48;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroSuperiorMirage1: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =   0; temps[0] = 14.75;
    alts[1] = 1.1; temps[1] = 14.49;
    alts[2] =   4; temps[2] = 14.94;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  SetObserverBaroSuperiorMirage2: function() {
    var alts = this.BaroData.ObserverAlt;
    var temps = this.BaroData.ObserverTemp;
    this.ClearObserverBaro( false );
    alts[0] =   0; temps[0] = 16.0;
    alts[1] =  30; temps[1] = 15.6;
    alts[2] =  50; temps[2] = 17.0;
    this.Update( 'BaroSettingsObserverPanel' );
  },

  // preset barometric settings for target

  ClearTargetBaro: function( update ) {
    update = xDefBool( update, true );
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    for (var i = 0; i < 5; i++) {
      alts[i] = -1; temps[i] = 15;
    }
    if (update) this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroLooming1: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =   0; temps[0] = 11;
    alts[1] = 100; temps[1] = 15;
    this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroLooming2: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =   0; temps[0] = 2;
    alts[1] =  50; temps[1] = 5;
    this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroInferiorMirage1: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =    0; temps[0] =   15;
    alts[1] =    1; temps[1] = 14.8;
    this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroInferiorMirage2: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =    0; temps[0] = 12.3;
    alts[1] = 0.49; temps[1] = 12.01;
    alts[2] =  1.4; temps[2] = 11.95;
    alts[3] =  100; temps[3] = 12.48;
    this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroSuperiorMirage1: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =   0; temps[0] = 14.75;
    alts[1] = 1.1; temps[1] = 14.49;
    alts[2] =   4; temps[2] = 14.94;
    this.Update( 'BaroSettingsTargetPanel' );
  },

  SetTargetBaroSuperiorMirage2: function() {
    var alts = this.BaroData.TargetAlt;
    var temps = this.BaroData.TargetTemp;
    this.ClearTargetBaro( false );
    alts[0] =   0; temps[0] = 16.0;
    alts[1] =  30; temps[1] = 15.6;
    alts[2] =  50; temps[2] = 17.0;
    this.Update( 'BaroSettingsTargetPanel' );
  },

};

////////////////////////////////////////////////////////////
// Save/Restore


DataX.AssignApp( 'RefrSimApp', RefrSimApp, RefrSimAppMetaData,
  function ClearRefrSimApp() { RefrSimApp.Clear(); },
  function UpdateRefrSimApp() { RefrSimApp.Update(); }
);
DataX.AssignSaveRestoreDomObj( 'SaveRestorePanel' );
DataX.SetupUrlStateHandler( 'App' );

///////////////////////////////////////////////////////////
// Scene Presets

function SceneDescription( txt ) {
  if (txt == '') txt = 'no description';
  xInnerHTML( 'SceneDescription', txt );
}

var IntroState = 0;
var NIntroStates = 5;

function Intro( i ) {

  IntroState = xDefNum( i, IntroState );

  switch( IntroState ) {

    case 0:
     Intro0();
     break;

    case 1:
     Intro1();
     break;

    case 2:
     Intro2();
     break;

    case 3:
     Intro3();
     break;

    case 4:
     Intro4();
     break;

  }

  // advance to next intro
  IntroState++;
  if (IntroState >= NIntroStates) IntroState = 0;
}

function PresetInitialScene() {
  IntroState = 0;
  SceneDescription( 'Click <b>Intro</b> repeatedly to get a brief introduction of what the App can do and the other buttons for some Demos.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 2, "RightDspModel": 0, "RightDspRefrType": 1, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 50, "FocalLength": 1900, "Tilt": -0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "RefrEqFact1": 503, "RefrEqFact2": 0.0343, "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": true } }'
  );
  SelectTab( 'Rendering' );
}

function PresetSceneChicagoStrongLooming() {
  SceneDescription( 'Chicago at 50 km distance with standard refraction (left) and strong refraction over cool water, so called <b>Looming</b> (right).  <a href="https://aty.sdsu.edu/mirages/mirsims/loom/loom.html#looming" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'BaroObserver' );
}

function Intro0() {
  SceneDescription( '1/5: Compare Refraction Simulation of <b>Globe Earth</b> with <b>Flat Earth</b>. Here Chicago at 50 km. Click <b>Intro</b> again for more.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 1, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Rendering' );
}

function Intro1() {
  SceneDescription( '2/5: Check Option <b>EyeLvl</b> and <b>Horizon</b> in the <b>Rendering</b> panel to display eye level and geometric horizon lines.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 1, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Rendering' );
}

function Intro2() {
  SceneDescription( '3/5: Enter some <b>Barometric Data</b> and let the App calculate the <b>Refraction Curve</b> and show the visual result using <b>Refraction = Custom</b>.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 1, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'BaroObserver' );
}

function Intro3() {
  SceneDescription( '4/5: Compare how the <b>Light Rays</b> bend on Standard and Custom Refraction (not to scale).' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 2, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 2, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Graphs' );
}

function Intro4() {
  SceneDescription( '5/5: Add additional Targets like a LASER to simulate "LASER over cool Water" experiments and compare Globe with FE.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 1, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "LASER", "Hidden": false, "Plane": { "Pos": [ 49000, 5, -200 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 100, "Height": 200, "LimitType": 1, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": 2, "Param1": 0.05, "Param2": 0.9, "Color1": [ 1, 1, 1, 1 ], "Color2": [ 0, 1, 0, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 2, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Targets' );
}


function PresetSceneGodRays() {
  SceneDescription( 'Crepuscular Rays using a plane with 7 <b>parallel waves</b> of <span style="border-bottom:1px solid black">unlimited length</span>, inclined 2°, proving the sun has not to be close, due to Perspective.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 2, "LeftDspModel": 1, "LeftDspRefrType": 0, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 1, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 50, "FocalLength": 400, "Tilt": 0.6, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 5000, "NRaySegments": 25, "MaxSegments": 10000, "RayDistanceLimit": 5e+6, "DomeHorizonColor": [ 0.9, 0.9, 0.8, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.2, 0.2, 0.6, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Crepuscular Rays", "Hidden": false, "Plane": { "Pos": [ 10000, 0, 0 ], "RotX": -90, "RotY": 0, "RotZ": -88 }, "ImageSrc": "", "Width": 200, "Height": 0, "LimitType": 0, "LimitLeft": 0, "LimitRight": null, "LimitTop": 700, "LimitBottom": -700, "Pattern": 0, "Param1": 1, "Param2": 1, "Color1": [ 1, 1, 1, 0 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 0.9, "ForceOversampling": true }, { "Name": "Ceiling", "Hidden": false, "Plane": { "Pos": [ 0, 1000, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 30000, "Height": 0, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": 0, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0.2, 0.2, 0.2, 1 ], "Color2": [ 0.9, 0.9, 0.8, 0.7 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 0.75, "ForceOversampling": true }, { "Name": "Sun", "Hidden": false, "Plane": { "Pos": [ 15000, 570, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 100, "Height": 0, "LimitType": 1, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": 2, "Param1": 0.15, "Param2": 0.5, "Color1": [ 1, 1, 1, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 50, -1, -1, -1 ], "ObserverTemp": [ 2, 5, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 10000, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Targets' );
}

function PresetSceneSunset() {
  SceneDescription( '<b>Sunset:</b> With Refraction (even with Standard Refraction) the apparent sun is still above the horizon while the real sun is below the horizon already.' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 0, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 1.5, "FocalLength": 1000, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 35, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 9000, "NRaySegments": 30, "MaxSegments": 10000, "RayDistanceLimit": 1.2e+6, "DomeHorizonColor": [ 0.5, 0, 0, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 1, 0.5, 0, 1 ], "Targets": [ { "Name": "Background", "Hidden": false, "Plane": { "Pos": [ 1.01e+6, 69000, 0 ], "RotX": 90, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 25000, "Height": 0, "LimitType": 0, "LimitLeft": 0, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": 0, "Param1": 1, "Param2": 0, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 0, 0, 0, 0 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 50, "Height": 1000, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": 10000, "LimitBottom": null, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 0.5, "ForceOversampling": true }, { "Name": "Sun", "Hidden": false, "Plane": { "Pos": [ 1e+6, 73500, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 8726, "Height": 0, "LimitType": 1, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": 2, "Param1": 0.95, "Param2": 1, "Color1": [ 1, 1, 1, 1 ], "Color2": [ 0.9, 0.7, 0, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 2, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 2, 5, 20, 100 ], "ObserverTemp": [ 11, 11.1, 10.8, 11.3, 11.9 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 50, "DistanceRange": 50000, "RayAngleRange": 1, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 1, "ShowHorizon": true, "ShowEyeLevel": true } } '
  );
  SelectTab( 'Rendering' );
}

function PresetSceneStandardRefraction2km() {
  SceneDescription( 'Standard Refraction (left) compared to zero Refraction of a 2 km far Target of 3 m height. Observer Height = 1.5 m. <a href="https://aty.sdsu.edu/mirages/mirsims/mirsimintro.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 1.5, "FocalLength": 7500, "Tilt": -0.02, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 35, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 10000, "DomeHorizonColor": [ 0.94, 0.98, 1, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.2, 0.4, 1, 1 ], "Targets": [ { "Name": "Ground", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 0, "Height": 1000, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": 1, "Param1": 0.5, "Param2": 0, "Color1": [ 0.7, 0.7, 0.7, 1 ], "Color2": [ 0.3, 0.3, 0.3, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target", "Hidden": false, "Plane": { "Pos": [ 2000, 0, 0 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 0.3, -1, -1, -1 ], "ObserverTemp": [ 15, 14.9, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 10000, "RayAngleRange": 0.25, "NRays": 50, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Targets' );
}

function PresetSceneStandardRefraction5km() {
  SceneDescription( 'Target at 4.8 km, that is at the refracted horizon. Zoomed in to the same size as in <b>Std 2km</b>. <a href="https://aty.sdsu.edu/mirages/mirsims/std/std.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 1.5, "FocalLength": 15000, "Tilt": -0.02, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 35, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 10000, "DomeHorizonColor": [ 0.94, 0.98, 1, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.2, 0.4, 1, 1 ], "Targets": [ { "Name": "Ground", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 0, "Height": 1000, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": 1, "Param1": 0.5, "Param2": 0, "Color1": [ 0.7, 0.7, 0.7, 1 ], "Color2": [ 0.3, 0.3, 0.3, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target", "Hidden": false, "Plane": { "Pos": [ 4800, 0, 0 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 0.3, -1, -1, -1 ], "ObserverTemp": [ 15, 14.9, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 10000, "RayAngleRange": 0.25, "NRays": 50, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Targets' );
}

function PresetSceneStandardRefraction10km() {
  SceneDescription( 'Refracted Target at 9.5 km is half hidden and clearly lifted (looming). No obvious vertical stretching or compression at Std Refraction. <a href="https://aty.sdsu.edu/mirages/mirsims/std/std.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 1, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 1.5, "FocalLength": 30000, "Tilt": -0.03, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 35, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0.94, 0.98, 1, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.2, 0.4, 1, 1 ], "Targets": [ { "Name": "Ground", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 0, "Height": 1000, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": 1, "Param1": 0.5, "Param2": 0, "Color1": [ 0.7, 0.7, 0.7, 1 ], "Color2": [ 0.3, 0.3, 0.3, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target", "Hidden": false, "Plane": { "Pos": [ 9500, 0, 0 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 0.3, -1, -1, -1 ], "ObserverTemp": [ 15, 14.9, 15, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 10000, "RayAngleRange": 0.25, "NRays": 50, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'Targets' );
}

var CommonSettingsPre = 'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 0, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 1.5, "FocalLength": 7500, "Tilt": -0.02, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 35, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 30, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0.94, 0.98, 1, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.2, 0.4, 1, 1 ], "Targets": [ { "Name": "Ground", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 0, "Height": 1000, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": 1, "Param1": 0.5, "Param2": 0, "Color1": [ 0.7, 0.7, 0.7, 1 ], "Color2": [ 0.3, 0.3, 0.3, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 2 km", "Hidden": false, "Plane": { "Pos": [ 2000, 0, -3 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 3 km", "Hidden": false, "Plane": { "Pos": [ 3000, 0, -2 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 1, 0, 0, 1 ], "Color2": [ 1, 1, 0, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 4 km", "Hidden": false, "Plane": { "Pos": [ 4000, 0, 0 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 5 km", "Hidden": false, "Plane": { "Pos": [ 5000, 0, 3 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 1, 0, 0, 1 ], "Color2": [ 1, 1, 0, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 7.5 km", "Hidden": false, "Plane": { "Pos": [ 7500, 0, 8 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Target 10 km", "Hidden": false, "Plane": { "Pos": [ 10000, 0, 15 ], "RotX": -45, "RotY": 0, "RotZ": 0 }, "ImageSrc": "", "Width": 0, "Height": 1.7, "LimitType": 0, "LimitLeft": -2.12, "LimitRight": 2.12, "LimitTop": 2.12, "LimitBottom": -2.12, "Pattern": 1, "Param1": 0.5, "Param2": 0.25, "Color1": [ 1, 0, 0, 1 ], "Color2": [ 1, 1, 0, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 6, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, ';

var CommonSettingsPost = '"TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 10000, "RayAngleRange": 0.25, "NRays": 50, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }';

function PresetSceneLooming() {
  SceneDescription( '<b>Looming</b> (lifting), due to cool surfaces, brings hidden objects partially into view. Select <b>Display = Baro</b> on the right display to see the Refraction Curve. <a href="https://aty.sdsu.edu/mirages/mirsims/loom/loom.html#looming" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    CommonSettingsPre + '"ObserverAlt": [ 0, 100, -1, -1, -1 ], "ObserverTemp": [ 11, 15, 15, 15, 15 ], ' + CommonSettingsPost
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneTowering() {
  SceneDescription( '<b>Towering</b> is caused by a curved Temperature Profile. Select <b>Display = Baro</b> to see the Temperature curve. <a href="https://aty.sdsu.edu/mirages/mirsims/loom/loom.html#towering" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    CommonSettingsPre + '"ObserverAlt": [ 0, 1.1, 100, -1, -1 ], "ObserverTemp": [ 12.015, 12, 16, 15, 15 ], ' + CommonSettingsPost
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneStooping() {
  SceneDescription( '<b>Stooping</b>, like Towering, is nearly always seen in mirages. Select <b>Display = Baro</b> to see the Temperature curve. <a href="https://aty.sdsu.edu/mirages/mirsims/loom/loom.html#stooping" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    CommonSettingsPre + '"ObserverAlt": [ 0, 2, 100, -1, -1 ], "ObserverTemp": [ 11.95, 12, 10, 15, 15 ], ' + CommonSettingsPost
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneInferiorMirage() {
  SceneDescription( '<b>Inferior Mirages</b> are caused by cool air over warm surfaces. Select <b>Display = Baro</b> to see the Temperature curve. <a href="https://aty.sdsu.edu/mirages/mirsims/inf-mir/inf-mir.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    CommonSettingsPre + '"ObserverAlt": [ 0, 0.49, 1.4, 100, -1 ], "ObserverTemp": [ 12.3, 12.01, 11.95, 12.48, 15 ], ' + CommonSettingsPost
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneSuperiorMirage() {
  SceneDescription( '<b>Superior Mirages</b>, like all mirages, depend strongly on the height of the eye and the distance from targets. <a href="https://aty.sdsu.edu/mirages/mirsims/sup-mir/supintro.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    CommonSettingsPre + '"ObserverAlt": [ 0, 1.5, 4, -1, -1 ], "ObserverTemp": [ 14.75, 14.49, 14.94, 12.48, 15 ], ' + CommonSettingsPost
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneChicagoSuperiorMirage() {
  SceneDescription( '<b>Superior Mirage</b> of Chicago, caused by an inversion layer at 40 m altitude as seen from within this layer. <a href="https://aty.sdsu.edu/mirages/mirsims/sup-mir/supintro.html" class="extern" target="_blank">more infos</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 1, "LeftDspModel": 0, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 0, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 39, "FocalLength": 2600, "Tilt": -0.07, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 70000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 30, 50, -1, -1 ], "ObserverTemp": [ 16, 15.3, 17, 15, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneCurvedFE() {
  SceneDescription( 'To get the same picture on the Flat Earth as on the Globe, there must be an impossible Temperature Gradient of -15°C/100m every day!' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 1, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 1, "RightDspModel": 1, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 2, "FocalLength": 1900, "Tilt": 0.1, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": true, "ShowHorizon": true, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0, 0.37, 0.68, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0, 0.37, 0.68, 1 ], "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Skyline", "Hidden": false, "Plane": { "Pos": [ 50000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/ChicagoSkyline.jpg", "Width": 995, "Height": 411, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 1, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 100, 200, 300, -1 ], "ObserverTemp": [ 29, 12, -3, -11, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ] }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": false } }'
  );
  SelectTab( 'BaroObserver' );
}

function PresetSceneSaltLakeCityStooping() {
  SceneDescription( '<b>Salt Lake City</b> from 40 km distance. Try height 0.5 m, 1.5 m and 7.5 m to see how the image matches the following: <a href="https://youtu.be/V-AbtmpR_qM?t=1380" class="extern" target="_blank">Observations</a>' );
  DataX.SetAppState(
    'RefrSimApp = { "LeftDspViewType": 0, "LeftDspModel": 0, "LeftDspRefrType": 2, "LeftDspLeftCurveType": 0, "LeftDspRightCurveType": 2, "RightDspViewType": 2, "RightDspModel": 0, "RightDspRefrType": 2, "RightDspLeftCurveType": 0, "RightDspRightCurveType": 2, "Camera": { "Height": 0.5, "FocalLength": 850, "Tilt": -0.05, "PixelSize": 1, "Oversampling": 1, "BoostOversampling": false, "EnableSmoothing": false, "Smoothing": 2, "ShowEyeLevel": false, "ShowHorizon": false, "StartPixelSize": 64, "SensorSize": 43.2, "ProgressBarWidth": 4 }, "RayTracer": { "Visibility": 0, "RayDelta": 0, "NRaySegments": 25, "MaxSegments": 5000, "RayDistanceLimit": 0, "DomeHorizonColor": [ 0.28, 0.42, 0.67, 1 ], "DomeGradientHeight": 0, "DomeZenithColor": [ 0.31, 0.51, 0.74, 1 ], "RefrEqFact1": 503, "RefrEqFact2": 0.0343, "Targets": [ { "Name": "Water", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "../blog/media/SeaWater.jpg", "Width": 100, "Height": 500, "LimitType": 0, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true }, { "Name": "Water Color", "Hidden": false, "Plane": { "Pos": [ 0, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": -90 }, "ImageSrc": "", "Width": 100, "Height": 500, "LimitType": 2, "LimitLeft": null, "LimitRight": null, "LimitTop": null, "LimitBottom": null, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0.5, 0.7, 1, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 0.4, "ForceOversampling": true }, { "Name": "Salt Lake City", "Hidden": false, "Plane": { "Pos": [ 40000, 0, 0 ], "RotX": 0, "RotY": 0, "RotZ": 0 }, "ImageSrc": "../blog/media/SaltLakeCity.jpg", "Width": 0, "Height": 737, "LimitType": 1, "LimitLeft": -497.5, "LimitRight": 497.5, "LimitTop": 411, "LimitBottom": 0, "Pattern": -1, "Param1": 0.5, "Param2": 0.5, "Color1": [ 0, 0, 0, 1 ], "Color2": [ 1, 1, 1, 1 ], "TransparentColor": [ 0, 0, 0, 0 ], "Alpha": 1, "ForceOversampling": true } ] }, "CurrTargetIndex": 2, "BaroData": { "ObserverPressureAlt": 0, "ObserverPressure": 1013.25, "ObserverDistance": 0, "TargetPressureAlt": 0, "TargetPressure": 1013.25, "TargetDistance": 10000, "ObserverAlt": [ 0, 5, 50, 100, -1 ], "ObserverTemp": [ 14.6, 14.5, 12.6, 11.7, 15 ], "TargetAlt": [ -1, -1, -1, -1, -1 ], "TargetTemp": [ 15, 15, 15, 15, 15 ], "NSplineSegments": 20, "SplineSmoothness": 0.7 }, "GraphDisplayParams": { "AltitudeRange": 0, "DistanceRange": 0, "RayAngleRange": 2, "NRays": 100, "RaysSpreading": 0, "RaysDspAspectRatio": 0, "ShowHorizon": true, "ShowEyeLevel": true } }'
  );
  SelectTab( 'Rendering' );
}

/*
function PresetSceneTemplate() {
  SceneDescription( 'text' );
  DataX.SetAppState(
    ''
  );
  SelectTab( 'Rendering' );
}
*/
</jscript>

<a id="App"></a>

{{div|id=ScenePresetButtons}}
[javascript:void Intro()|{{ButtonText|Intro|small|red}}]
[javascript:void PresetInitialScene()|{{ButtonText|Reset|small|red}}]
[javascript:void PresetSceneStandardRefraction2km()|{{ButtonText|Std 2km|small|black}}]
[javascript:void PresetSceneStandardRefraction5km()|{{ButtonText|4.8km|small|black}}]
[javascript:void PresetSceneStandardRefraction10km()|{{ButtonText|9.5km|small|black}}]
[javascript:void PresetSceneLooming()|{{ButtonText|Looming|small|black}}]
[javascript:void PresetSceneTowering()|{{ButtonText|Towering|small|black}}]
[javascript:void PresetSceneStooping()|{{ButtonText|Stooping|small|black}}]
[javascript:void PresetSceneInferiorMirage()|{{ButtonText|Inferior Mirage|small|black}}]
[javascript:void PresetSceneSuperiorMirage()|{{ButtonText|Superior Mirage|small|black}}]
[javascript:void PresetSceneChicagoStrongLooming()|{{ButtonText|Chicago strong Looming|small|green}}]
[javascript:void PresetSceneChicagoSuperiorMirage()|{{ButtonText|Chicago Superior Mirage|small|green}}]
[javascript:void PresetSceneSaltLakeCityStooping()|{{ButtonText|Salt Lake City Stooping|small|green}}]
[javascript:void PresetSceneSunset()|{{ButtonText|Sunset|small|green}}]
[javascript:void PresetSceneCurvedFE()|{{ButtonText|FE Curved|small|blue}}]
[javascript:void PresetSceneGodRays()|{{ButtonText|God Rays|small|blue}}]

{{end div}}

{{div|id=SceneDescription}}no scnene description{{end div}}

{{col|50|row=$GraphicPanels}}

<jscript>

///////////////////////////////////////////////////////////
// Graphic panels

var DisplayLeft = NewGraph2D( {
  Id:        'GraphPanelLeft',
  Width:     '100%',
  Height:    '66%',
  DrawFunc:  function(g) { RefrSimApp.Draw(1); },
  AutoReset: true,
  AutoClear: true,
} );

</jscript>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'LeftDisplaySelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'LeftDspViewType',
  Label: 'Display',
  ValueType: 'int',
  Items: [
    {
      Name: 'Image',
      Value: 0
    }, {
      Name: 'Rays',
      Value: 2
    }, {
      Name: 'Baro',
      Value: 1
    }
  ],
  Link: Help( 'Display Type' ),

} ).Render();

ControlPanels.NewPanel( {
    Name: 'LeftDisplayModelSelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'LeftDspModel',
  Label: 'Model',
  ValueType: 'int',
  Items: [
    {
      Name: 'Globe',
      Value: 0
    }, {
      Name: 'Flat Earth',
      Value: 1
    }
  ],
  Link: Help( 'Display Model' ),

} ).AddRadiobuttonField( {
  Name: 'LeftDspRefrType',
  Label: 'Refraction',
  ValueType: 'int',
  Items: [
    {
      Name: 'Zero',
      Value: 0
    }, {
      Name: 'Standard',
      Value: 1
    }, {
      Name: 'Custom',
      Value: 2
    }
  ],
  Link: Help( 'Display Refraction' ),

} ).Render();

ControlPanels.NewPanel( {
    Name: 'LeftDisplayCurveSelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'LeftDspLeftCurveType',
  Label: 'Left',
  ValueType: 'int',
  Items: [
    {
      Name: 'Temp',
      Value: 0
    }, {
      Name: 'Grad',
      Value: 1
    }, {
      Name: 'Refr',
      Value: 2
    }
  ],
  Link: Help( 'Display Curve' ),

} ).AddRadiobuttonField( {
  Name: 'LeftDspRightCurveType',
  Label: 'Right',
  ValueType: 'int',
  Items: [
    {
      Name: 'Temp',
      Value: 0
    }, {
      Name: 'Grad',
      Value: 1
    }, {
      Name: 'Refr',
      Value: 2
    }
  ],
  Link: Help( 'Display Curve' ),

} ).Render();

</jscript>

{{end scroll}}

{{col}}

<jscript>

var DisplayRight = NewGraph2D( {
  Id:        'GraphPanelRight',
  Width:     '100%',
  Height:    '66%',
  DrawFunc:  function(g){ RefrSimApp.Draw(2); },
  AutoReset: true,
  AutoClear: true,
} );

</jscript>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'RightDisplaySelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'RightDspViewType',
  Label: 'Display',
  ValueType: 'int',
  Items: [
    {
      Name: 'Image',
      Value: 0
    }, {
      Name: 'Rays',
      Value: 2
    }, {
      Name: 'Baro',
      Value: 1
    }
  ],
  Link: Help( 'Display Type' ),

} ).Render();

ControlPanels.NewPanel( {
    Name: 'RightDisplayModelSelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'RightDspModel',
  Label: 'Model',
  ValueType: 'int',
  Items: [
    {
      Name: 'Globe',
      Value: 0
    }, {
      Name: 'Flat Earth',
      Value: 1
    }
  ],
  Link: Help( 'Display Model' ),

} ).AddRadiobuttonField( {
  Name: 'RightDspRefrType',
  Label: 'Refraction',
  ValueType: 'int',
  Items: [
    {
      Name: 'Zero',
      Value: 0
    }, {
      Name: 'Standard',
      Value: 1
    }, {
      Name: 'Custom',
      Value: 2
    }
  ],
  Link: Help( 'Display Refraction' ),

} ).Render();

ControlPanels.NewPanel( {
    Name: 'RightDisplayCurveSelectorPanel',
    ModelRef: 'RefrSimApp',
    OnModelChange: function(field){ RefrSimApp.Update(field); },

} ).AddRadiobuttonField( {
  Name: 'RightDspLeftCurveType',
  Label: 'Left',
  ValueType: 'int',
  Items: [
    {
      Name: 'Temp',
      Value: 0
    }, {
      Name: 'Grad',
      Value: 1
    }, {
      Name: 'Refr',
      Value: 2
    }
  ],
  Link: Help( 'Display Curve' ),

} ).AddRadiobuttonField( {
  Name: 'RightDspRightCurveType',
  Label: 'Right',
  ValueType: 'int',
  Items: [
    {
      Name: 'Temp',
      Value: 0
    }, {
      Name: 'Grad',
      Value: 1
    }, {
      Name: 'Refr',
      Value: 2
    }
  ],
  Link: Help( 'Display Curve' ),

} ).Render();


</jscript>

{{end scroll}}

{{col|end}}

<jscript>

RefrSimApp.Init( DisplayLeft, DisplayRight );

///////////////////////////////////////////////////////////
// Target Presets

function CreateRealisticWaterTarget() {
  RefrSimApp.NewTarget( {
    Name:        'Water',
    Plane:       { Pos: [ 0, 0, 0 ], RotZ: -90 },
    ImageSrc:    '../blog/media/SeaWater.jpg',
    Width:       100,
    Height:      500,
    LimitType:   2,
  } );
  RefrSimApp.Update();
}

function CreateGroundPatternTarget() {
  RefrSimApp.NewTarget( {
    Name:        'Ground',
    Plane:       { Pos: [ 0, 0, 0 ], RotZ: -90 },
    Width:       0,
    Height:      1000,
    LimitType:   2,
    Pattern:     1,
    Param1:      0.5,
    Param2:      0,
    Color1:      [ 0.7, 0.7, 0.7, 1 ],
    Color2:      [ 0.3, 0.3, 0.3, 1 ],
  } );
  RefrSimApp.Update();
}

function CreateLaserTarget() {
  RefrSimApp.NewTarget( {
    Name:        'LASER',
    Plane:       { Pos: [ 10000, 5, 0 ] },
    Width:       100,
    Height:      200,
    LimitType:   1,
    Pattern:     2,
    Param1:      0.05,
    Param2:      0.9,
    Color1:      [ 1, 1, 1, 1 ],
    Color2:      [ 0, 1, 0, 1 ],
  } );
  RefrSimApp.Update();
}

function CreatePlateTarget() {
  RefrSimApp.NewTarget( {
    Name:        'Plate',
    Plane:       { Pos: [ 2000, 0, 0 ], RotX: -45 },
    Width:       0,
    Height:      1.7,
    LimitType:   0,
    LimitLeft:   -2.12,
    LimitRight:  2.12,
    LimitTop:    2.12,
    LimitBottom: -2.12,
    Pattern:     1,
    Param1:      0.5,
    Param2:      0.25,
    Color1:      [ 0, 0, 0, 1 ],
    Color2:      [ 1, 1, 1, 1 ],
  } );
  RefrSimApp.Update();
}

function CreateChicagoTarget() {
  RefrSimApp.NewTarget( {
    Name:         'Chicago Skyline',
    Plane:        { Pos: [ 10000, 0, 0 ] },
    ImageSrc:     '../blog/media/ChicagoSkyline.jpg',
    Width:        0, // auto -> 995
    Height:       411,
    LimitType:    1,
    LimitLeft:   -995/2,
    LimitRight:   995/2,
    LimitBottom:  0,
    LimitTop:     411,
  } );
  RefrSimApp.Update();
}

function CreateFogTarget() {
  RefrSimApp.NewTarget( {
    Name:         'Fog',
    Plane:        { Pos: [ 500, 2, 0 ] },
    Width:        0,
    Height:       5,
    LimitType:    0,
    LimitLeft:    null,
    LimitRight:   null,
    LimitBottom:  -1,
    LimitTop:     3,
    Pattern:      0,
    Param1:       1,
    Param2:       0.075,
    Color1:       [ 0, 0, 0, 0 ],
    Color2:       [ 1, 0.9, 0.8, 0.9 ],
    Alpha:        0.95,
  } );
  RefrSimApp.Update();
}

///////////////////////////////////////////////////////////
// Start Scene

function HandleUrlCommands() {

  var dataStr = DataX.GetUrlStr( 'demo' );
  switch( dataStr ) {
    case 'Intro':
      Intro();
      break;
    case 'Std2km':
      PresetSceneStandardRefraction2km();
      break;
    case 'Std5km':
      PresetSceneStandardRefraction5km();
      break;
    case 'Std10km':
      PresetSceneStandardRefraction10km();
      break;
    case 'Looming':
      PresetSceneLooming();
      break;
    case 'Towering':
      PresetSceneTowering();
      break;
    case 'Stooping':
      PresetSceneStooping();
      break;
    case 'InteriorMirage':
      PresetSceneInferiorMirage();
      break;
    case 'SuperiorMirage':
      PresetSceneSuperiorMirage();
      break;
    case 'ChicagoStrongLooming':
      PresetSceneChicagoStrongLooming();
      break;
    case 'ChicagoSuperiorMirage':
      PresetSceneChicagoSuperiorMirage();
      break;
    case 'SaltLakeCityStooping':
      PresetSceneSaltLakeCityStooping();
      break;
    case 'Sunset':
      PresetSceneSunset();
      break;
    case 'FECurved':
      PresetSceneCurvedFE();
      break;
    case 'GodRays':
      PresetSceneGodRays();
      break;
  }

  if (dataStr != '') {
    location.hash = "#App";
  } else {
    PresetInitialScene();
  }

  var dataStr = DataX.GetUrlStr( 'tab' );
  if (dataStr != '') {
    SelectTab( dataStr );
  }

}

xOnLoad( HandleUrlCommands );

/////////////////////////////////////////////
// Panel Functions

function AutoValueModelToPanel( v, scale ) {
  scale = xDefNum( scale, 1 );
  if (v == 0) return 'auto';
  return NumFormatter.Format( v/scale, 'std', DefaultDigits );
}

function AutoValuePanelToModel( s, scale, allowNegative ) {
  scale = xDefNum( scale, 1 );
  allowNegative = xDefBool( allowNegative, false );
  var v = parseFloat( s );
  if (isNaN(v) || (!allowNegative && v < 0)) return 0;
  return v * scale;
}

function LimitsModelToPanel( n ) {
  if (n == null) return 'no limit';
  return NumFormatter.Format( n, 'std', DefaultDigits );
}

function LimitsPanelToModel( s ) {
  var v = parseFloat( s );
  if (isNaN(v)) return null;
  return v;
}

function TranspColorModelToPanel( color ) {
  if (color[3] == 0) return 'not used';
  return ColorModelToPanel( color );
}

function TranspColorPanelToModel( s ) {
  s = s.replace( /^ +| +$/g, '' );
  var v = parseFloat( s );
  if (isNaN(v)) return [ 0, 0, 0, 0 ];
  return ColorPanelToModel( s );
}

function ColorModelToPanel( color ) {
  var s = '';
  for (var i = 0; i < 3; i++) {
    if (i > 0) s += '  ';
    s += NumFormatter.Format( color[i], 'std', 2 );
  }
  if (color[3] < 1) {
    s += '  ' + NumFormatter.Format( color[3], 'std', 2 );
  }
  return s;
}

function ColorPanelToModel( s ) {
  function limit( x ) {
    return x < 0 ? 0 : x > 1 ? 1 : x;
  }
  var color = [ 0, 0, 0, 1 ];
  s = s.replace( /[^#a-f\d\.]+/g, ' ' );
  s = s.replace( /^ +| +$/g, '' );
  if (s.indexOf('#') == 0 || s.indexOf('0x') == 0) {
    // decode hex color
    if (s.indexOf('#') == 0) {
      s = s.substr( 1 );
    } else {
      s = s.substr( 2 );
    }
    var v = parseInt( s, 16 );
    if (isNaN(v)) v = 0;
    color[2] = (v % 256) / 255;
    v = floor( v / 256 );
    color[1] = (v % 256) / 255;
    v = floor( v / 256 );
    if (v > 255) v = 255;
    color[0] = v / 255;
  } else {
    // decode color as 3..4 values
    var sl = s.split( ' ' );
    var max = 0;
    for (var i = 0; i < 4; i++) {
      if (i < sl.length) {
        // parse color value
        var v = parseFloat( sl[i] );
        if (isNaN(v)) v = 0;
        max = maxOf( max, v );
        color[i] = v;
      } else if (i == 0) {
        // first color component is 0 if not defined
        color[0] = 0;
      } else if (i == 3) {
        // alpha channel is 1 if not defined
        color[i] = 1;
      } else {
        // copy previous color channel if not defined
        color[i] = color[i-1];
      }
    }
    // if a value is greater than 1 imterpret all values as 0..255
    if (max > 1) {
      JsgColor.Scale( color, 1/255 );
    }
    JsgColor.Limit( color );
  }
  return color;
}

/////////////////////////////////////////////
// Panels

function SelectTab( tabName ) {

  if (tabName == 'Rendering') {
    Tabs.Select( 'SettingTabs', 0 );
  } else if (tabName == 'BaroObserver') {
    Tabs.Select( 'SettingTabs', 1 );
  } else if (tabName == 'BaroTarget') {
    Tabs.Select( 'SettingTabs', 2 );
  } else if (tabName == 'Graphs') {
    Tabs.Select( 'SettingTabs', 3 );
  } else if (tabName == 'Targets') {
    Tabs.Select( 'SettingTabs', 4 );
  } else if (tabName == 'SaveRestore') {
    Tabs.Select( 'SettingTabs', 5 );
  }
}

</jscript>

{{TabSelectorsTop| SettingTabs | MarginTop }}
{{TabSel| Rendering | RenderingTab }}
{{TabSel| Baro Observer | ObserverBaroTab }}
{{TabSel| Baro Target | TargetBaroTab }}
{{TabSel| Graphs | GraphTab }}
{{TabSel| Targets | TargetsTab }}
{{TabSel| Save/Restore | SaveRestoreTab }}
{{EndTabSelectors}}

{{TabBoxes| SettingTabs }} <comment> Rendering Panel </comment>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'CameraPanel',
    ModelRef: 'RefrSimApp.Camera',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddHeader( {
    Text: 'Camera Parameters',
    ColSpan: 4,
  }

).AddTextField( {
    Name: 'Height',
    Label: 'Observer Height',
    Units: 'm',
    Inc: 10,
    Link: Help( 'Camera Observer Height' ),
  }

).AddRadiobuttonField( {
    Name: 'PixelSize',
    Label: 'Pixel Size',
    ValueType: 'int',
    Items: [
      {
        Name: '1x1',
        Value: 1
      }, {
        Name: '2x2',
        Value: 2
      }, {
        Name: '4x4',
        Value: 4
      }, {
        Name: '8x8',
        Value: 8
      }
    ],
    Link: Help( 'Camera Pixel Size' ),
  }

).AddTextField( {
    Name: 'FocalLength',
    Label: 'Zoom',
    Units: 'mm',
    Inc: 100,
    Link: Help( 'Camera Zoom' ),
  }

).AddRadiobuttonField( {
    Name: 'Oversampling',
    ValueType: 'int',
    Items: [
      {
        Name: 'no',
        Value: 1
      }, {
        Name: '2x2',
        Value: 2
      }, {
        Name: '3x3',
        Value: 3
      }, {
        Name: '4x4',
        Value: 4
      }
    ],
    Link: Help( 'Camera Oversampling' ),
  }

).AddTextField( {
    Name: 'Tilt',
    Label: 'Tilt',
    Units: '°',
    Inc: 0.1,
    Link: Help( 'Camera Tilt' ),
  }

).AddCheckboxField( {
    Name: 'DisplayOptions',
    Label: 'Options',
    Items: [
      {
        Name: 'EnableSmoothing',
        Text: 'Smooth',
      },
      {
        Name: 'ShowEyeLevel',
        Text: 'EyeLvl',
      },
      {
        Name: 'ShowHorizon',
        Text: 'Horizon',
      }
    ],
    Link: Help( 'Camera Options' ),
  }

).Render();


ControlPanels.NewPanel( {
    Name: 'RayTracerPanel',
    ModelRef: 'RefractionRayTracer',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddHeader( {
    Text: 'Raytracer Parameters',
    ColSpan: 4,
  }

).AddTextField( {
    Name: 'RayDistanceLimit',
    Label: 'Horizon Dist',
    Units: 'km',
    Inc: 1000,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v, 1000 );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s, 1000 );
    },
    Link: Help( 'Ray Tracer Horizon Dist' ),
  }

).AddTextField( {
    Name: 'MaxSegments',
    Label: 'Max Segments',
    Inc: 100,
    Link: Help( 'Ray Tracer Max Segments' ),
  }

).AddTextField( {
    Name: 'RayDelta',
    Label: 'Ray Delta',
    Units: 'km',
    Inc: 1000,
    ConvFromModelFunc: function( v ){
      if (v <= 0) return 'Num Segments >>';
      return NumFormatter.Format( v/1000, 'std', DefaultDigits );
    },
    ConvToModelFunc: function( s ) {
      var v = parseFloat( s );
      if (isNaN(v) || v <= 0) return 0;
      return v*1000;
    },
    Link: Help( 'Ray Tracer Ray Delta' ),
  }

).AddTextField( {
    Name: 'NRaySegments',
    Label: 'Num Segments',
    Inc: 10,
    EnabledRef: function() { return RefrSimApp.RayTracer.RayDelta == 0; },
    ConvFromModelFunc: function( v ){
      if (RefrSimApp.RayTracer.RayDelta > 0) return '<< Ray Delta';
      return v;
    },
    Link: Help( 'Ray Tracer Num Segments' ),
  }

/*
).AddTextField( {
    Name: 'Visibility',
    Mult: 1000,
    Units: 'km',
  }
*/

).AddTextField( {
    Name: 'DomeHorizonColor',
    Label: 'Horizon Color',
    Units: 'rgb',
    ConvFromModelFunc: ColorModelToPanel,
    ConvToModelFunc: ColorPanelToModel,
    Link: Help( 'Ray Tracer Horizon' ),
  }

).AddTextField( {
    Name: 'DomeZenithColor',
    Label: 'Zenith Color',
    Units: 'rgb',
    ConvFromModelFunc: ColorModelToPanel,
    ConvToModelFunc: ColorPanelToModel,
    Link: Help( 'Ray Tracer Horizon' ),
  }

).AddTextField( {
    Name: 'DomeGradientHeight',
    Label: 'Gradient Height',
    Units: 'km',
    Inc: 100,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v, 1000 );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s, 1000 );
    },
    Link: Help( 'Ray Tracer Horizon' ),
  }

).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Observer Baro Panel </comment>

{{scroll}}

<jscript>

function BaroAltModelToPanel( n ) {
  if (n == -1) return 'undefined';
  return NumFormatter.Format( n, 'std', DefaultDigits );
}

function BaroAltPanelToModel( s ) {
  var v = parseFloat( s );
  if (isNaN(v)) return -1;
  return v;
}

function BaroObsTempModelToPanel( t, i ) {
  if (RefrSimApp.IsObsAltDefined(i)) {
    return NumFormatter.Format( t, 'std', DefaultDigits );
  } else {
    return 'undefined';
  }
}

function BaroTrgTempModelToPanel( t, i ) {
  if (RefrSimApp.IsTrgAltDefined(i)) {
    return NumFormatter.Format( t, 'std', DefaultDigits );
  } else {
    return 'undefined';
  }
}

ControlPanels.NewPanel( {
    Name: 'BaroSettingsObserverPanel',
    ModelRef: 'RefrSimApp.BaroData',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddTextField( {
    Name: 'ObserverPressureAlt',
    Label: 'Pressure Alt',
    Units: 'm',
    Link: Help( 'Baro Pressure' ),
  }

).AddTextField( {
    Name: 'ObserverPressure',
    Label: 'Pressure',
    Units: 'mBar',
    Link: Help( 'Baro Pressure' ),
  }

).AddTextField( {
    Name: 'ObserverDistance',
    Label: 'Measure Dist',
    Units: 'km',
    Mult: 1000,
    Link: Help( 'Baro Measure Dist' ),
  }

).AddTextField( {
    Name: 'SplineSmoothness',
    Label: 'Smoothness',
    Inc: 0.1,
    Link: Help( 'Baro Smoothness' ),
  }

/*
).AddHtmlField( {
    Name: 'Spacer',
    Label: '-',
    Html: '',
    ColSpan: 2
  }
*/
).AddTextField( {
    Name: 'ObserverAlt0',
    ValueRef: 'ObserverAlt[0]',
    Label: 'Altitude 1',
    Units: 'm',
    ConvFromModelFunc: BaroAltModelToPanel,
    ConvToModelFunc: BaroAltPanelToModel,
    Link: Help( 'Baro Altitude and Temperature' ),
  }

).AddTextField( {
    Name: 'ObserverTemp0',
    ValueRef: 'ObserverTemp[0]',
    Label: 'Temperature 1',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsObsAltDefined(0); },
    ConvFromModelFunc: function(v) { return BaroObsTempModelToPanel(v,0); },
    Link: Help( 'Baro Altitude and Temperature' ),
  }

).AddTextField( {
    Name: 'ObserverAlt1',
    ValueRef: 'ObserverAlt[1]',
    Label: 'Altitude 2',
    Units: 'm',
    ConvFromModelFunc: BaroAltModelToPanel,
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'ObserverTemp1',
    ValueRef: 'ObserverTemp[1]',
    Label: 'Temperature 2',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsObsAltDefined(1); },
    ConvFromModelFunc: function(v) { return BaroObsTempModelToPanel(v,1); }
  }

).AddTextField( {
    Name: 'ObserverAlt2',
    ValueRef: 'ObserverAlt[2]',
    Label: 'Altitude 3',
    Units: 'm',
    ConvFromModelFunc: BaroAltModelToPanel,
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'ObserverTemp2',
    ValueRef: 'ObserverTemp[2]',
    Label: 'Temperature 3',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsObsAltDefined(2); },
    ConvFromModelFunc: function(v) { return BaroObsTempModelToPanel(v,2); }
  }

).AddTextField( {
    Name: 'ObserverAlt3',
    ValueRef: 'ObserverAlt[3]',
    Label: 'Altitude 4',
    Units: 'm',
    ConvFromModelFunc: BaroAltModelToPanel,
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'ObserverTemp3',
    ValueRef: 'ObserverTemp[3]',
    Label: 'Temperature 4',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsObsAltDefined(3); },
    ConvFromModelFunc: function(v) { return BaroObsTempModelToPanel(v,3); }
  }

).AddTextField( {
    Name: 'ObserverAlt4',
    ValueRef: 'ObserverAlt[4]',
    Label: 'Altitude 5',
    Units: 'm',
    ConvFromModelFunc: BaroAltModelToPanel,
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'ObserverTemp4',
    ValueRef: 'ObserverTemp[4]',
    Label: 'Temperature 5',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsObsAltDefined(4); },
    ConvFromModelFunc: function(v) { return BaroObsTempModelToPanel(v,4); }
  }

).AddHtmlField( {
    Name: 'Presets',
    ColSpan: 3,
    Html:
      ControlPanels.SmallButton( 'Looming 1', 'void RefrSimApp.SetObserverBaroLooming1()', 'Black' ) +
      ControlPanels.SmallButton( 'Looming 2', 'void RefrSimApp.SetObserverBaroLooming2()', 'Black' ) +
      ControlPanels.SmallButton( 'Inferior Mirage 1', 'void RefrSimApp.SetObserverBaroInferiorMirage1()', 'Black' ) +
      ControlPanels.SmallButton( 'Inferior Mirage 2', 'void RefrSimApp.SetObserverBaroInferiorMirage2()', 'Black' ) +
      ControlPanels.SmallButton( 'Superior Mirage 1', 'void RefrSimApp.SetObserverBaroSuperiorMirage1()', 'Black' ) +
      ControlPanels.SmallButton( 'Superior Mirage 2', 'void RefrSimApp.SetObserverBaroSuperiorMirage2()', 'Black' ) +
      ControlPanels.SmallButton( 'Clear', 'void RefrSimApp.ClearObserverBaro()', 'Red' ),
  }

).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Target Baro Panel </comment>

{{scroll}}

<jscript>

function TargetBaroAltModelToPanel( v, i ) {
  if (RefrSimApp.IsTargetBaroSpecified()) {
    return BaroAltModelToPanel( v );
  } else {
    return '= Observer Alt ' + i;
  }
}

function TargetBaroTempModelToPanel( v, i ) {
  if (RefrSimApp.IsTargetBaroSpecified()) {
    return BaroTrgTempModelToPanel( v, i );
  } else {
    return '= Observer Temp ' + (i+1);
  }
}

ControlPanels.NewPanel( {
    Name: 'BaroSettingsTargetPanel',
    ModelRef: 'RefrSimApp.BaroData',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddTextField( {
    Name: 'TargetPressureAlt',
    Label: 'Pressure Alt',
    Units: 'm',
    Link: Help( 'Baro Pressure' ),
  }

).AddTextField( {
    Name: 'TargetPressure',
    Label: 'Pressure',
    Units: 'mBar',
    Link: Help( 'Baro Pressure' ),
  }

).AddTextField( {
    Name: 'TargetDistance',
    Label: 'Measure Dist',
    Units: 'km',
    Mult: 1000,
    Link: Help( 'Baro Measure Dist' ),
  }

).AddHtmlField( {
    Name: 'Spacer',
    Label: '-',
    Html: '',
    ColSpan: 2
  }

).AddTextField( {
    Name: 'TargetAlt0',
    ValueRef: 'TargetAlt[0]',
    Label: 'Altitude 1',
    Units: 'm',
    ConvFromModelFunc: function(v){ return TargetBaroAltModelToPanel(v,1); },
    ConvToModelFunc: BaroAltPanelToModel,
    Link: Help( 'Baro Altitude and Temperature' ),
  }

).AddTextField( {
    Name: 'TargetTemp0',
    ValueRef: 'TargetTemp[0]',
    Label: 'Temperature 1',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsTrgAltDefined(0); },
    ConvFromModelFunc: function(v){ return TargetBaroTempModelToPanel(v,0); },
    Link: Help( 'Baro Altitude and Temperature' ),
  }

).AddTextField( {
    Name: 'TargetAlt1',
    ValueRef: 'TargetAlt[1]',
    Label: 'Altitude 2',
    Units: 'm',
    ConvFromModelFunc: function(v){ return TargetBaroAltModelToPanel(v,2); },
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'TargetTemp1',
    ValueRef: 'TargetTemp[1]',
    Label: 'Temperature 2',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsTrgAltDefined(1); },
    ConvFromModelFunc: function(v){ return TargetBaroTempModelToPanel(v,1); },
  }

).AddTextField( {
    Name: 'TargetAlt2',
    ValueRef: 'TargetAlt[2]',
    Label: 'Altitude 3',
    Units: 'm',
    ConvFromModelFunc: function(v){ return TargetBaroAltModelToPanel(v,3); },
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'TargetTemp2',
    ValueRef: 'TargetTemp[2]',
    Label: 'Temperature 3',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsTrgAltDefined(2); },
    ConvFromModelFunc: function(v){ return TargetBaroTempModelToPanel(v,2); },
  }

).AddTextField( {
    Name: 'TargetAlt3',
    ValueRef: 'TargetAlt[3]',
    Label: 'Altitude 4',
    Units: 'm',
    ConvFromModelFunc: function(v){ return TargetBaroAltModelToPanel(v,4); },
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'TargetTemp3',
    ValueRef: 'TargetTemp[3]',
    Label: 'Temperature 4',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsTrgAltDefined(3); },
    ConvFromModelFunc: function(v){ return TargetBaroTempModelToPanel(v,3); },
  }

).AddTextField( {
    Name: 'TargetAlt4',
    ValueRef: 'TargetAlt[4]',
    Label: 'Altitude 5',
    Units: 'm',
    ConvFromModelFunc: function(v){ return TargetBaroAltModelToPanel(v,5); },
    ConvToModelFunc: BaroAltPanelToModel,
  }

).AddTextField( {
    Name: 'TargetTemp4',
    ValueRef: 'TargetTemp[4]',
    Label: 'Temperature 5',
    Units: '°C',
    EnabledRef: function(){ return RefrSimApp.IsTrgAltDefined(4); },
    ConvFromModelFunc: function(v){ return TargetBaroTempModelToPanel(v,4); },
  }

).AddHtmlField( {
    Name: 'Presets',
    ColSpan: 3,
    Html:
      ControlPanels.SmallButton( 'Looming 1', 'void RefrSimApp.SetTargetBaroLooming1()', 'Black' ) +
      ControlPanels.SmallButton( 'Looming 2', 'void RefrSimApp.SetTargetBaroLooming2()', 'Black' ) +
      ControlPanels.SmallButton( 'Inferior Mirage 1', 'void RefrSimApp.SetTargetBaroInferiorMirage1()', 'Black' ) +
      ControlPanels.SmallButton( 'Inferior Mirage 2', 'void RefrSimApp.SetTargetBaroInferiorMirage2()', 'Black' ) +
      ControlPanels.SmallButton( 'Superior Mirage 1', 'void RefrSimApp.SetTargetBaroSuperiorMirage1()', 'Black' ) +
      ControlPanels.SmallButton( 'Superior Mirage 2', 'void RefrSimApp.SetTargetBaroSuperiorMirage2()', 'Black' ) +
      ControlPanels.SmallButton( 'Clear', 'void RefrSimApp.ClearTargetBaro()', 'Red' ),
  }

).Render();

// finally update RefrSimApp to connect to the Control Panels created
// Note: Control Panels are initialized with xOnLoad(),
// so we have to call Update() with the same method to ensure panels are initialized

//xOnLoad( function(){ RefrSimApp.Update(); } );

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Graphs Panel </comment>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'GraphDisplayParamsPanel',
    ModelRef: 'GraphDisplayParams',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddHeader( {
    Text: 'Rays and Baro Graph Parameters',
    ColSpan: 4,
  }

).AddTextField( {
    Name: 'AltitudeRange',
    Label: 'Alt Range',
    Units: 'm',
    Inc: 100,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s );
    },
    EnabledRef: function(){
      return RefrSimApp.IsAnyGraphDisplayed();
    },
    Link: Help( 'Graphs Alt Range' ),
  }

).AddTextField( {
    Name: 'DistanceRange',
    Label: 'Dist Range',
    Units: 'km',
    Inc: 1000,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v, 1000 );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s, 1000 );
    },
    EnabledRef: function(){
      return RefrSimApp.IsRayGraphDisplayed();
    },
    Link: Help( 'Graphs Dist Range' ),
  }

).AddTextField( {
    Name: 'RayAngleRange',
    Label: 'Ray Angle',
    Units: '°',
    Inc: 0.1,
    EnabledRef: function(){ return RefrSimApp.IsRayGraphDisplayed(); },
    Link: Help( 'Graphs Ray Angle' ),
  }

).AddRadiobuttonField( {
    Name: 'RaysDspAspectRatio',
    Label: 'Aspect Ratio',
    EnabledRef: function(){ return RefrSimApp.IsRayGraphDisplayed(); },
    ValueType: 'int',
    Items: [
      {
        Name: 'auto',
        Value: 0
      }, {
        Name: 'streched',
        Value: 1
      }, {
        Name: '1:1',
        Value: 2
      }
    ],
    Link: Help( 'Graphs Aspect Ratio' ),
  }

).AddTextField( {
    Name: 'NRays',
    Label: 'Num of Rays',
    Inc: 1,
    EnabledRef: function(){ return RefrSimApp.IsRayGraphDisplayed(); },
    Link: Help( 'Graphs Num of Rays' ),
  }

).AddRadiobuttonField( {
    Name: 'RaysSpreading',
    Label: 'Ray Spreading',
    EnabledRef: function(){ return RefrSimApp.IsRayGraphDisplayed(); },
    ValueType: 'int',
    Items: [
      {
        Name: 'radial',
        Value: 0
      }, {
        Name: 'parallel',
        Value: 1
      }
    ],
    Link: Help( 'Graphs Ray Spreading' ),
  }

).AddHtmlField( {
    Name: 'Blank',
    Label: '-',
    Html: '',
  }

).AddCheckboxField( {
    Name: 'GraphOptions',
    Label: 'Show',
    EnabledRef: function(){ return RefrSimApp.IsRayGraphDisplayed(); },
    Items: [
      {
        Name: 'ShowHorizon',
        Text: 'Horizon',
      },
      {
        Name: 'ShowEyeLevel',
        Text: 'Eye Level',
      }
    ],
    Link: Help( 'Graphs Options' ),
  }

).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Targets Panel </comment>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'TargetPlanePanel',
    ModelRef: 'RefrSimApp.CurrTarget.Plane',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputMediumWidth',
    NCols: 3,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddHeader( {
    Text: 'Target',
    ColSpan: 1,
    Attr: 'id="TargetPanelHeader" style="white-space: nowrap;"',
  }

).AddHeader( {
    Text:
      ControlPanels.SmallButton( 'Prev', 'void RefrSimApp.SelectPrevTarget()', 'Blue' ) +
      ControlPanels.SmallButton( 'Next', 'void RefrSimApp.SelectNextTarget()', 'Blue' ) +
      ControlPanels.SmallButtonR( 'Undelete', 'void RefrSimApp.UndeleteTarget()', 'Green' ) +
      ControlPanels.SmallButtonR( 'Delete All', 'void RefrSimApp.DeleteAllTargets()', 'Red' ) +
      ControlPanels.SmallButtonR( 'Delete', 'void RefrSimApp.DeleteTarget()', 'Red' ) +
      ControlPanels.SmallButtonR( 'Move Down', 'void RefrSimApp.TargetDown()', 'Black' ) +
      ControlPanels.SmallButtonR( 'Move Up', 'void RefrSimApp.TargetUp()', 'Black' ) +
      ControlPanels.SmallButtonR( 'Clone', 'void RefrSimApp.AddCloneTarget()', 'Green' ) +
      ControlPanels.SmallButtonR( 'New', 'void RefrSimApp.AddTarget()', 'Green' ),
    ColSpan: 5,
  }

).AddHtmlField( {
  Name: 'TargetPresets',
  Label: 'Presets',
  ColSpan: 5,
  Html:
    ControlPanels.SmallButton( 'Water', 'void CreateRealisticWaterTarget()', 'Blue' ) +
    ControlPanels.SmallButton( 'Chicago', 'void CreateChicagoTarget()', 'Green' ) +
    ControlPanels.SmallButton( 'LASER', 'void CreateLaserTarget()', 'Green' ) +
    ControlPanels.SmallButton( 'Fog', 'void CreateFogTarget()', 'Green' ) +
    ControlPanels.SmallButton( 'Ground', 'void CreateGroundPatternTarget()', 'Black' ) +
    ControlPanels.SmallButton( 'Plate', 'void CreatePlateTarget()', 'Black' ),
  }

).AddTextField( {
    Name: 'Name',
    ValueRef: 'RefrSimApp.CurrTarget.Name',
    Mult: 0,
    Link: Help( 'Target Name' ),
  }

).AddCheckboxField( {
    Name: 'Hidden',
    Label: '-',
    Items: [
      {
        Name: 'Hidden',
        Text: 'hide',
        ValueRef: 'RefrSimApp.CurrTarget.Hidden',
      }
    ],
    Link: Help( 'Target Hide' ),
  }

).AddHtmlField( {
    Label: '-',
  }

).AddTextField( {
    Name: 'Pos0',
    ValueRef: 'Pos[0]',
    Label: 'Dist X',
    Mult: 1,
    Units: 'm',
    Inc: 100,
    Link: Help( 'Target Position' ),
  }

).AddTextField( {
    Name: 'Pos1',
    ValueRef: 'Pos[1]',
    Label: 'Up Y',
    Mult: 1,
    Units: 'm',
    Inc: 1,
  }

).AddTextField( {
    Name: 'Pos2',
    ValueRef: 'Pos[2]',
    Label: 'Right Z',
    Mult: 1,
    Units: 'm',
    Inc: 10,
  }

).AddTextField( {
    Name: 'RotX',
    Label: 'Rot X',
    Units: '°',
    Link: Help( 'Target Orientation' ),
  }

).AddTextField( {
    Name: 'RotY',
    Label: 'Rot Y',
    Units: '°',
  }

).AddTextField( {
    Name: 'RotZ',
    Label: 'Rot Z',
    Units: '°',
  }

).Render();


ControlPanels.NewPanel( {
    Name: 'TargetPanel',
    ModelRef: 'RefrSimApp.CurrTarget',
    OnModelChange: function(field){ RefrSimApp.Update(field); },
    PanelFormat: 'InputNormalWidth',
    NCols: 2,
    Format: 'std',
    Digits: DefaultDigits
  }

).AddTextField( {
    Name: 'ImageSrc',
    Label: 'Image URL',
    Mult: 0,
    ColSpan: 3,
    Link: Help( 'Target Image URL' ),
    Units: '&nbsp;',
  }

).AddTextField( {
    Name: 'Width',
    Label: 'Width',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v, 1 );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s, 1, true );
    },
    Link: Help( 'Target Width and Height' ),
  }

).AddTextField( {
    Name: 'Height',
    Label: 'Height',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: function( v ){
      return AutoValueModelToPanel( v, 1 );
    },
    ConvToModelFunc: function( s ) {
      return AutoValuePanelToModel( s, 1, true );
    },
    Link: Help( 'Target Width and Height' ),
  }

).AddRadiobuttonField( {
    Name: 'LimitType',
    Label: 'Limit Type',
    ColSpan: 3,
    ValueType: 'int',
    Items: [
      {
        Name: 'Custom',
        Value: 0,
      }, {
        Name: 'Width/Height',
        Value: 1,
      }, {
        Name: 'no Limits',
        Value: 2,
        Link: Help( 'Target Limits' ),
      }
    ],
    Link: Help( 'Target Limits' ),
  }

).AddTextField( {
    Name: 'LimitLeft',
    Label: 'Limit Left',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: LimitsModelToPanel,
    ConvToModelFunc: LimitsPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsCustomLimit(); },
    Link: Help( 'Target Limits' ),
    Description: 'Enter x for no limits',
  }

).AddTextField( {
    Name: 'LimitRight',
    Label: 'Limit Right',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: LimitsModelToPanel,
    ConvToModelFunc: LimitsPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsCustomLimit(); },
    Link: Help( 'Target Limits' ),
    Description: 'Enter x for no limits',
  }

).AddTextField( {
    Name: 'LimitBottom',
    Label: 'Limit Bottom',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: LimitsModelToPanel,
    ConvToModelFunc: LimitsPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsCustomLimit(); },
    Link: Help( 'Target Limits' ),
    Description: 'Enter x for no limits',
  }

).AddTextField( {
    Name: 'LimitTop',
    Label: 'Limit Top',
    Units: 'm',
    Inc: 10,
    ConvFromModelFunc: LimitsModelToPanel,
    ConvToModelFunc: LimitsPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsCustomLimit(); },
    Link: Help( 'Target Limits' ),
    Description: 'Enter x for no limits',
  }

).AddRadiobuttonField( {
    Name: 'Pattern',
    ColSpan: 3,
    ValueType: 'int',
    Items: TargetPatterns.GetRadioButtonItems(),
    EnabledRef: function() { return RefrSimApp.IsParameterUsed( 'Pattern' ); },
    Link: Help( 'Target Pattern' ),
    Description: 'Clear Image URL to enable',
  }

).AddTextField( {
    Name: 'Color1',
    Units: 'rgb[a]',
    ConvFromModelFunc: ColorModelToPanel,
    ConvToModelFunc: ColorPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsParameterUsed( 'Color1' ); },
    Link: Help( 'Target Pattern Parameters' ),
  }

).AddTextField( {
    Name: 'Color2',
    Units: 'rgb[a]',
    ConvFromModelFunc: ColorModelToPanel,
    ConvToModelFunc: ColorPanelToModel,
    EnabledRef: function() { return RefrSimApp.IsParameterUsed( 'Color2' ); },
    Link: Help( 'Target Pattern Parameters' ),
  }

).AddTextField( {
    Name: 'Param1',
    Inc: 0.1,
    EnabledRef: function() { return RefrSimApp.IsParameterUsed( 'Param1' ); },
    Link: Help( 'Target Pattern Parameters' ),
  }

).AddTextField( {
    Name: 'Param2',
    Inc: 0.1,
    EnabledRef: function() { return RefrSimApp.IsParameterUsed( 'Param2' ); },
    Link: Help( 'Target Pattern Parameters' ),
  }

).AddTextField( {
    Name: 'Alpha',
    Label: 'Alpha',
    Units: '1..0',
    Inc: 0.1,
    Link: Help( 'Target Alpha' ),
    Description: 'Overall Transparency, 1 -> opaque',
  }

).AddTextField( {
    Name: 'TransparentColor',
    Label: 'Transp Color',
    Units: 'rgb',
    ConvFromModelFunc: TranspColorModelToPanel,
    ConvToModelFunc: TranspColorPanelToModel,
    Link: Help( 'Target Transparent Color' ),
    Description: 'Tranparent Color: Enter x to ignore',
  }

).Render();

function ClearLog(){
  xInnerHTML('DebugLogPanel','');
}

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> SaveRestore Panel </comment>


{{form textarea|SaveRestorePanel|15|spellcheck=false|class=ListingDisplay|}}

[javascript:void DataX.GetAppState()|{{ButtonText|Get App State|blue}}]
[javascript:void DataX.GetAppStateUrl(ThisPageUrl)|{{ButtonText|Get App Url|blue}}]
[javascript:void DataX.SetAppState()|{{ButtonText|Set App State|green}}]
[javascript:void DataX.CompactSaveRestoreDomObj()|{{ButtonText|Compact|red}}]
[javascript:void DataX.ClearSaveRestoreDomObj()|{{ButtonText|Clear|red}}]

{{EndTabBoxes}}

<comment>
[javascript:void ClearLog()|{{ButtonText|Clear|red}}]
{{form textarea|DebugLogPanel|40|spellcheck=false|class=ListingDisplay|}}
</comment>



Weitere Infos zur Seite
Erzeugt Thursday, November 22, 2018
von wabis
Zum Seitenanfang
Geändert Thursday, November 22, 2018
von wabis