WaBis

walter.bislins.ch

Source Code: Advanced Earth Curvature Calculator

This page shows the source code of the Advanced Earth Curvature Calculator which is stored in the private page Special: Advanced Curvature Calculator Code. This code is an extract of the Source Code: Curvature App, lived out the demos and most models, but added some computations and implemented variable units.

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

#INCLUDE JsGraphX3D.inc
#INCLUDE ControlPanel.inc
#INCLUDE Tabs.inc
#INCLUDE DataX.inc
#INCLUDE TextControl.inc


<style>
textarea.ListingDisplay {
  font-family: Courier;
  margin-bottom: 1em;
}
#JsGraph1 { margin-top: 0.25em; }
#ResetButton { margin-left: 1.2em; }
#UnitsTab { margin-left: 1.2em; }
#OptionsPanel { margin-top: 0; }
#BasicsPanel { margin-bottom: 0; }
#TargetDataPanel th { background: #dfd; }
#HorizonDataPanel th { background: #ddf; }
</style>

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

<jscript>

var DefaultDigits = 6;
var DeviceRatioOff = 1.5;
var PI90 = Math.PI / 2;

function toRad( a ) { return a * Math.PI / 180; }
function toDeg( a ) { return a * 180 / Math.PI; }

// meta data for Json generation and loading

var CurveAppMetaData = {
  Compact: false,
  DefaultPrec: 8,
  Properties: [
    { Name: 'DemoText',          Type: 'str',  Default: '' },
    { Name: 'Description',       Type: 'str',  Default: '' },

    { Name: 'Height',            Type: 'num',  Default: 2 },
    { Name: 'FocalLengthField',  Type: 'num',  Default: 3000 },

    { Name: 'showModel',         Type: 'int',  Default: 1 },
    { Name: 'deviceRatio',       Type: 'num',  Default: DeviceRatioOff },
    { Name: 'viewcenterHorizon', Type: 'int',  Default: 0 },

    { Name: 'ObjType',           Type: 'arr', Size: 2, ArrayType: 'int', Default: [ 0, 0 ] },
    { Name: 'NObjects',          Type: 'arr', Size: 2, ArrayType: 'int', Default: [ 1, 0 ] },
    { Name: 'ObjDist',           Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 12000, 20000 ] },
    { Name: 'ObjDeltaDist',      Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 300, 300 ] },
    { Name: 'ObjSideType',       Type: 'arr', Size: 2, ArrayType: 'int', Default: [ 0, 0 ] },
    { Name: 'ObjSidePos',        Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 0, 0 ] },
    { Name: 'ObjSideVar',        Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 0, 0 ] },
    { Name: 'ObjSizeType',       Type: 'arr', Size: 2, ArrayType: 'int', Default: [ 1, 1 ] },
    { Name: 'ObjSize',           Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 10, 10 ] },
    { Name: 'ObjSizeVar',        Type: 'arr', Size: 2, ArrayType: 'num', Default: [ 0, 0 ] },

    { Name: 'refractionCoeff',   Type: 'num',  Default: 0 },
    { Name: 'tempGradient',      Type: 'num',  Default: -0.0065 },
    { Name: 'refractionSync',    Type: 'int',  Default: 0 },
    { Name: 'pressure',          Type: 'num',  Default: 1013.25 },
    { Name: 'temperatureC',      Type: 'num',  Default: 15 },
    { Name: 'refractionFactMin', Type: 'num',  Default: 0.5 },
    { Name: 'refractionFactMax', Type: 'num',  Default: 10000 },

    { Name: 'rEarth',            Type: 'num',  Default: 6371000 },
    { Name: 'rFEarth',           Type: 'num',  Default: 10007543 },
    { Name: 'showTheodolite',    Type: 'bool', Default: false },
    { Name: 'OverlayImage',      Type: 'str',  Default: '' },
    { Name: 'OverlayImageAlpha', Type: 'num',  Default: 0.5 },

    { Name: 'UnitsType',         Type: 'int',  Default: 0 },
    { Name: 'Tilt',              Type: 'num',  Default: 0 },
    { Name: 'Pan',               Type: 'num',  Default: 0 },

    { Name: 'Flerspective',      Type: 'int',  Default: 0 },
    { Name: 'RefDistance',       Type: 'num',  Default: 10000 },
    { Name: 'showDataObject',    Type: 'bool', Default: true },
    { Name: 'showDataRefraction',Type: 'bool', Default: true },
    { Name: 'showDataHorizon',   Type: 'bool', Default: true },
  ],
};


// Curve App

function CurveAppClass() {

  // choosen units tables
  this.UnitsType = 0;  // 0 -> m, 1 -> mi/ft, 2 -> ft
  this.LengthUnits = {
    Selection: '.UnitsType',
    Units: [ 'm', 'mi', 'ft' ],
    Mults: [ 1, 1609.344, 0.3048 ]
  };
  this.BigLengthUnits = {
    Selection: '.UnitsType',
    Units: [ 'km', 'mi', 'ft' ],
    Mults: [ 1000, 1609.344, 0.3048 ]
  };
  this.HeightUnits = {
    Selection: '.UnitsType',
    Units: [ 'm', 'ft', 'ft' ],
    Mults: [ 1, 0.3048, 0.3048 ]
  };
  this.GradientUnits = {
    Selection: '.UnitsType',
    Units: [ '°C/m', '°C/ft', '°C/ft' ],
    Mults: [ 1, 1/0.3048, 1/0.3048 ]
  };

  this.DemoText = '';
  this.Description = '';
  this.AllStatesChanged = false;
  this.OverlayImage = '';
  this.OverlayImageAlpha = 0.5;
  this.AlphaOpaque = 1;
  this.pause = 0; // dummy for animation delays

  this.rEarth = 6371000;
  this.rFEarth = 10007543; // this.rEarth * PI90;
  this.nLines = 45;
  this.showModel = 1; // 1 -> globe, 2 -> flat, 3 -> both mirrored, 4 -> both side by side
  this.showGrid = 1; // 0 -> none, 1 -> on, 3 -> projection of globe to flat
  this.showData = true; // true if any of the 3 following values are true (see Update)
  this.showDataObject = true;
  this.showDataRefraction = true;
  this.showDataHorizon = true;
  this.showTangent = false;
  this.showEyeLevel = true;
  this.showTheodolite = false;
  this.deviceRatio = 3 / 2; // width / height of device screen
  this.sceneWidth = 0;
  this.sceneHeight = 0;
  this.viewcenterHorizon = 0;  // 0 -> globe, 1 -> flat earth, 2 -> between, 3 -> eye level

  // refraction
  this.BaroModel = null;
  // refractionSync = 0 -> off, 1 -> T,P, 2 -> std, 3 -> k=0.13, 4 -> k=0.17, 5 -> a=7/6, 6 -> a=7/2
  this.refractionSync = 0;
  this.refractionFactMax = 10000;
  this.refractionFactMin = 0.5;
  this.pressure = 1013.25;      // mBar
  this.pressureLast = 1013.25;
  this.temperature = 288.15;    // K
  this.temperatureC = 15;       // Celsius
  this.temperatureCLast = 15;
  this.tempGradient = -0.0065;  // K/m
  this.tempGradientLast = -0.0065;
  this.refractionCoeff = 0;
  this.refractionCoeffLast = 0;
  this.refractionFact = 1;
  this.refractionFactLast = 1;
  this.refractionRadius = this.rEarth;
  this.refractionRadiusLast = this.rEarth;
  this.refractionSlider = 0;
  this.refractionSliderLast = 0;

  this.HeightSlider = 0;
  this.HeightSliderLast = 0;
  this.Height = 2;

  this.ViewAngle = 0;  // viewAngle in deg (field of view)
  this.ViewAngleField = 0;
  this.ViewAngleSlider = 0;
  this.ViewAngleSliderLast = 0;
  this.Roll = 0;
  this.Tilt = 0;
  this.LastTilt = 0;
  this.TiltSlider = 0;
  this.LastTiltSlider = 0;
  this.Pan = 0;
  this.LastPan = 0;
  this.PanSlider = 0;
  this.LastPanSlider = 0;
  this.PanRad = 0;
  this.FocalLength = 0;
  this.FocalLengthField = 3000;
  this.FocalLengthSlider = 0;
  this.FocalLengthSliderLast = 0;

  this.rDisk = 0;          // d
  this.dHorizon = 0;       // s
  this.zDisk = 0;          // p
  this.hDisk = 0;          // R - b
  this.hDip = 0;           // b
  this.aDip = 0;           // alpha
  this.bulge = 0;
  this.objDrop = 0;
  this.aEarth = 0;         // 180 - 2 * alpha
  this.dView = 0;          // v
  this.aDelta = 0;         // about aDip / nLines
  this.posEarth = [ 0, 0, -this.refractionRadius ];
  this.camViewAngle = 0.1; // rad
  this.camPos = [ 0, 0, 0 ];
  this.camUp = [ 0, 0, 1 ];
  this.camViewCenter = [ 0, 1, 0 ];
  this.camSceneSize = 1;
  this.TheodoliteTilt = 0;

  // special additional flat earth perspective transformations (flerspective)
  this.Flerspective = 0;    // 0 -> normal, 1 -> flerspective
  this.RefDistance = 10000;

  this.FlerspectiveFunc = function( x, y, z, camera, data, p ) {
    if (data.Flerspective == 1) {
      var rh = data.rEarth + data.Height;
      var vpd = Math.sqrt( rh * rh - data.rEarth * data.rEarth ) + data.RefDistance;
      if (vpd < 1) vpd = 1;
      var r = JsgVect3.ScalarProd( data.FEviewDir, [x,y,0] );
      z *= (vpd - r) / vpd;
    }
    /*
    if (data.Flerspective == 2) {
      // scale vertically and horizontally dependent on distance.
      // horizontal scale is around the center of the viewing axis.
      var r = Math.sqrt( x*x + y*y );
      var scale = data.RefDistance / r;
      z = (z + data.Height) * scale - data.Height;
    }
    */
    p[0] = x;
    p[1] = y;
    p[2] = z;
  };

  // object settings
  this.NearObjIx = 0;
  this.ObjType = [ 0, 0 ]; // 0 = M-Rod, 1 = mountain
  this.ObjSideType = [ 0, 0 ]; // 0 = lin, 1 = 2col, 2 = rand, 3 = cos, 4 = sin
  this.ObjSizeType = [ 1, 1 ]; // 1 = lin, 2 = rand, 3 = cos, 4 = sin

  this.ObjDist = [ 12000, 20000 ];
  this.Slider_ObjDist_Log = [ 0, 0 ];
  this.Slider_ObjDist_Log_Last = [ 0, 0 ];

  this.ObjSidePos = [ 0, 0 ];
  this.Slider_ObjSidePos_Log = [ 0, 0 ];
  this.Slider_ObjSidePos_Log_Last = [ 0, 0 ];

  this.ObjSize = [ 10, 10 ];
  this.Slider_ObjSize_Log = [ 0, 0 ];
  this.Slider_ObjSize_Log_Last = [ 0, 0 ];

  this.ObjDeltaDist = [ 300, 300 ];
  this.Slider_ObjDeltaDist_Log = [ 0, 0 ];
  this.Slider_ObjDeltaDist_Log_Last = [ 0, 0 ];

  this.ObjSideVar = [ 0, 0 ];
  this.Slider_ObjSideVar_Log = [ 0, 0 ];
  this.Slider_ObjSideVar_Log_Last = [ 0, 0 ];

  this.ObjSizeVar = [ 0, 0 ];
  this.NObjects = [ 1, 0 ];

  // object draw loop variables
  this.aObjDelta = [ 0, 0 ];
  this.aObjFirst = [ 0, 0 ];
  this.aObjLast = [ 0, 0 ];
  this.NObjectsDrawn = [ 0, 0 ];
  this.MaxNObjectsToDraw = [ 0, 0 ];
  this.aObjCurr = [ 0, 0 ];
  this.isHiddenObj = [ false, false ];
  this.LastPosValid = [ false, false ];

  // computed object values
  this.HorizonRefrAng = 0;
  this.ObjRefrAng = 0;
  this.ObjLiftAbs = 0;
  this.ObjLiftRel = 0;
  this.HorizonLift = 0;
  this.ObjNearSize = 0;
  this.ObjNearTilt = 0;
  this.ObjAngSize = 0;
  this.ObjAngHidden = 0;
  this.ObjAngVisible = 0;
  this.ObjRealDist = 0;  // distance along surface taking refraction, ObjDist and ObjSidePos into account
  this.ObjAngDist = 0;
  this.ObjHidden = 0;
  this.ObjVisi = 0;
  this.VerticalAngle = 0;  // angle from observer tangent to top of object (90 - zenith angle) in deg
  this.VerticalAngleFE = 0;

  // colors; for some elements '' -> do not draw element
  this.EyeLvlCol = 'magenta';
  this.FEEqCol = 'black';
  this.FEGridCol = 'black';
  this.GlobeGridCol = 'blue';
  this.GlobeFGridCol = 'red';
  this.TangentCol = 'black';
  this.FrameCol = 'black';

  this.Update();
}

CurveAppClass.prototype.IsShowGlobe = function() {
  return (this.showModel & 1) || this.showModel == 4;
}

CurveAppClass.prototype.IsShowFlatEarth = function() {
  return (this.showModel & 2) || this.showModel == 4;
}

CurveAppClass.prototype.IsShowBothModels = function() {
  return this.showModel >= 3;
}

CurveAppClass.prototype.IsShowBothModelsMirror = function() {
  return this.showModel == 3;
}

CurveAppClass.prototype.IsNearestObject = function( objIx ) {
  // returns true if the current object drawn is the nearest object to the observer
  return this.NObjectsDrawn[objIx] == this.NObjects[objIx]-1;
}

CurveAppClass.prototype.IsFurthestObject = function( objIx ) {
  // returns true if the current object drawn is the furthest object to the observer
  return this.NObjectsDrawn[objIx] == 0;
}

CurveAppClass.prototype.GetObjectSizeVar = function( objIx ) {
  // some object sizes like M-Rod are dependent on ObjSizeVar
  var sizeVar = 1;
  if (this.ObjType[objIx] == 0) {
    // M-Rod make size: var 0.1..11 for values of -1..+1 where 0 -> 1
    sizeVar = this.ObjSizeVar[objIx];
    if (sizeVar < 0) {
      sizeVar = 1 + sizeVar;
      if (sizeVar < 0.1) sizeVar = 0.1;
    } else {
      sizeVar = 10 * sizeVar + 1;
    }
  }
  return sizeVar;
}

CurveAppClass.prototype.IsVariableSizeObject = function( objIx ) {
  // M-Rod is never variable, all the same size, but ObjSizeVar defines its size too, see GetObjectSizeVar()
  return this.ObjType[objIx] != 0 && Math.abs(this.ObjSizeVar[objIx]) > 0.01;
}

CurveAppClass.prototype.Update = function() {

  this.showData = this.showDataObject || this.showDataRefraction || this.showDataHorizon;

  // input validation
  if ( this.rEarth  < 100000 ) this.rEarth  = 100000;
  if ( this.rFEarth < 100000 ) this.rFEarth = 100000;
  if (this.nLines > 200) this.nLines = 200;
  if (this.nLines < 0) this.nLines = 0;
  this.showModel = Math.floor(this.showModel);
  if (this.showModel < 1 || this.showModel > 4) this.showModel = 1;
  this.showGrid = Math.floor(this.showGrid);
  if (this.showGrid < 0 || this.showGrid > 3) this.showGrid = 1;
  if (this.deviceRatio <= 0) this.deviceRatio = DeviceRatioOff;
  this.viewcenterHorizon = Math.floor(this.viewcenterHorizon);
  if (this.viewcenterHorizon < 0 || this.viewcenterHorizon > 3) this.viewcenterHorizon = 0;
  if (this.RefDistance < 100) this.RefDistance = 100;

  // handle height changes
  if ( this.HeightSliderLast != this.HeightSlider ) {
    this.Height = Math.pow( 10, -1 + 6 * this.HeightSlider );
  }
  if ( this.Height < 0.001 ) this.Height = 0.001;
  if ( this.Height > 1000000000 ) this.Height = 1000000000;
  this.HeightSlider = ( Math.log10( this.Height ) + 1 ) / 6;
  this.HeightSliderLast = this.HeightSlider;
  if (this.BaroModel) {
    // BaroModel depends on Height
    this.BaroModel.Update();
  }

  // handle refraction changes
  // refractionSync: 0 -> off, 1 -> T,P, 2 -> std, 3 -> k=0.13, 4 -> k=0.17, 5 -> a=7/6, 6 -> a=7/2

  this.refractionSync = Math.floor(this.refractionSync);
  if (this.refractionSync < 0 || this.refractionSync > 6) this.refractionSync = 1;
  if (this.refractionFactMin < 0.1) this.refractionFactMin = 0.1;
  if (this.refractionFactMin > 1) this.refractionFactMin = 1;
  if (this.refractionFactMax < 10000) this.refractionFactMax = 10000;
  var k_min = 1 - 1 / this.refractionFactMin;
  var k_max = 1 - 1 / this.refractionFactMax;
  if (this.refractionCoeff < k_min) this.refractionCoeff = k_min;
  if (this.refractionCoeff > k_max) this.refractionCoeff = k_max;
  if (this.pressure < 0) this.pressure = 0;
  if (this.pressure > 1200) this.pressure = 1200;
  if (this.temperatureC < -100) this.temperatureC = -100;
  if (this.temperatureC > 100) this.temperatureC = 100;

  // revert sync mode to T,P or off if some refraction values have changed manually
  if (this.AllStatesChanged) {
    // refractionsSync has priority if multiple values have changed
    if (this.refractionSync == 2 || this.refractionSync == 0) {
      // ensure that tempGradient has priority
      this.tempGradientLast = this.tempGradient + 1;
      this.refractionCoeffLast = this.refractionCoeff;
    } else {
      // ensure that refractionCoeff has priority
      this.refractionCoeffLast = this.refractionCoeff + 1;
      this.tempGradientLast = this.tempGradient;
    }
  } else {
    if (this.refractionSync >= 2 &&
        (this.refractionSlider != this.refractionSliderLast ||
         this.tempGradient != this.tempGradientLast ||
         this.refractionCoeff != this.refractionCoeffLast ||
         this.refractionFact != this.refractionFactLast ||
         this.refractionRadius != this.refractionRadiusLast)
    )
    {
      this.refractionSync = 1;
    } else if (this.refractionSync > 0 &&
               (this.temperatureC != this.temperatureCLast ||
                this.pressure != this.pressureLast)
    )
    {
      this.refractionSync = 0;
    }
  }

  // synchronize refraction pressure, temp and gradient with baro settings
  if (this.refractionSync >= 1) {
    if (this.Height > 84852) {
      this.temperatureC = -86.204;
      this.pressure = 0.00373383;
      if (this.refractionSync == 2) {
        this.tempGradient = -0.002;
        this.tempGradientLast = this.tempGradient - 1; // indicate a change
        this.refractionCoeffLast = this.refractionCoeff;
      }
    } else if (this.BaroModel) {
      var baroModel = this.BaroModel;
      this.temperatureC = baroModel.T_C;
      this.pressure = baroModel.p / 100;
      if (this.refractionSync == 2) {
        this.tempGradient = baroModel.alpha;
        this.tempGradientLast = this.tempGradient - 1; // indicate a change
        this.refractionCoeffLast = this.refractionCoeff;
      }
    }
  }

  // handle sync modes
  if (this.refractionSync == 3) {
    this.refractionCoeff = 0.13;
    this.refractionCoeffLast = this.refractionCoeff + 1; // indicate change
  } else if (this.refractionSync == 4) {
    this.refractionCoeff = 0.17;
    this.refractionCoeffLast = this.refractionCoeff + 1; // indicate change
  } else if (this.refractionSync == 5) {
    this.refractionFact = 7 / 6;
    this.refractionFactLast = this.refractionFact + 1; // indicate change
  } else if (this.refractionSync == 6) {
    this.refractionFact = 7 / 2;
    this.refractionFactLast = this.refractionFact + 1; // indicate change
  }

  // handle changes of refraction input fields and sliders
  this.temperature = this.temperatureC + 273.15;
  if (this.refractionSlider != this.refractionSliderLast) {
    // slider changed
    if (Math.abs(this.refractionSlider) < 0.01) this.refractionSlider = 0; // snap to 0
    this.refractionCoeff = k_max * this.refractionSlider;
  } else if (this.refractionCoeff != this.refractionCoeffLast) {
    // k changed
  } else if (this.tempGradient != this.tempGradientLast) {
    // dT/dh changed
    if (this.temperature < 3) this.temperature = 3;
    this.refractionCoeff = 503 * (this.pressure / (this.temperature*this.temperature)) * (0.0343 + this.tempGradient);
  } else if (this.refractionFact != this.refractionFactLast) {
    // a changed
    if (this.refractionFact < this.refractionFactMin) this.refractionFact = this.refractionFactMin;
    if (this.refractionFact > this.refractionFactMax) this.refractionFact = this.refractionFactMax;
    this.refractionCoeff = 1 - 1 / this.refractionFact;
  } else if (this.refractionRadius != this.refractionRadiusLast) {
    // R' changed
    this.refractionFact = this.refractionRadius / this.rEarth;
    if (this.refractionFact < this.refractionFactMin) this.refractionFact = this.refractionFactMin;
    if (this.refractionFact > this.refractionFactMax) this.refractionFact = this.refractionFactMax;
    this.refractionCoeff = 1 - 1 / this.refractionFact;
  }

  // limit some inputs
  if (this.refractionCoeff < k_min) this.refractionCoeff = k_min;
  if (this.refractionCoeff > k_max) this.refractionCoeff = k_max;
  if (this.temperature < 3) this.temperature = 3;
  if (this.temperature > 10000) this.temperature = 10000;
  this.temperatureC = this.temperature - 273.15;
  if (this.pressure < 0.001) this.pressure = 0.001;
  if (this.pressure > 10000) this.pressure = 10000;
  if (Math.abs(this.refractionCoeff) < 0.000002) this.refractionCoeff = 0;

  // compute refraction values
  this.tempGradient = (this.refractionCoeff * this.temperature * this.temperature) / (503 * this.pressure) - 0.0343;
  if (Math.abs(this.tempGradient) < 0.000001) this.tempGradient = 0;
  this.refractionFact = 1 / (1 - this.refractionCoeff);
  this.refractionRadius = this.rEarth * this.refractionFact;
  this.refractionSlider = this.refractionCoeff / k_max;

  // store current refration values for detecting user changes
  this.refractionSliderLast = this.refractionSlider;
  this.refractionCoeffLast = this.refractionCoeff;
  this.tempGradientLast = this.tempGradient;
  this.refractionFactLast = this.refractionFact;
  this.refractionRadiusLast = this.refractionRadius;
  this.temperatureCLast = this.temperatureC;
  this.pressureLast = this.pressure;

  // handle ViewAngle and FocalLength changes
  if ( this.FocalLengthSlider != this.FocalLengthSliderLast ) {
    var f = (10000-21) * Math.pow(this.FocalLengthSlider,2) + 21;  // f = 21..10000
    this.ViewAngle = toDeg( 2 * Math.atan( 43.2 / 2 / f ) );
  } else if ( this.FocalLengthField != this.FocalLength ) {
    this.ViewAngle = toDeg( 2 * Math.atan( 43.2 / 2 / this.FocalLengthField ) );
  } else if ( this.ViewAngleSlider != this.ViewAngleSliderLast ) {
    this.ViewAngle = this.ViewAngleSlider;
  } else if ( this.ViewAngleField != this.ViewAngle ) {
    this.ViewAngle = this.ViewAngleField;
  }
  if ( this.ViewAngle < 0.1 ) this.ViewAngle = 0.1;
  if ( this.ViewAngle > 160 ) this.ViewAngle = 160;
  this.camViewAngle = toRad( this.ViewAngle );
  this.FocalLength = 43.2 / ( 2 * Math.tan( this.camViewAngle / 2 ) );
  this.FocalLengthField = this.FocalLength;
  var f = this.FocalLength - 21;
  if (f < 0) f = 0;
  this.FocalLengthSlider = Math.pow(f/(10000-21),1/2);
  this.FocalLengthSliderLast = this.FocalLengthSlider;
  this.ViewAngleField = this.ViewAngle;
  this.ViewAngleSlider = this.ViewAngle;
  this.ViewAngleSliderLast = this.ViewAngle;

  this.UpdateObjectInput(0);
  this.UpdateObjectInput(1);

  // compute diverse values
  this.aDip = Math.acos( this.refractionRadius / (this.refractionRadius + this.Height) );
  this.aEarth = Math.PI - 2 * this.aDip;
  this.rDisk = this.refractionRadius * Math.sin( this.aDip );
  this.dHorizon = this.refractionRadius * this.aDip;
  this.hDisk = this.refractionRadius * Math.cos( this.aDip );
  this.hDip = this.refractionRadius - this.hDisk;
  this.dView = ( this.Height + this.refractionRadius ) * Math.sin( this.aDip );

  var exp2 = Math.floor( Math.log( PI90 / this.aDip ) / Math.LN2 );
  this.aDelta = (PI90 / Math.pow( 2, exp2 ) / this.nLines);

  this.dDelta = this.IsShowGlobe() ? this.aDelta * this.refractionRadius : 0;
  this.posEarth = [ 0, 0, -(this.refractionRadius + this.Height) ];
  this.zDisk = (this.refractionRadius + this.Height) - ( this.refractionRadius * Math.cos( this.aDip ) );

  // Tilt and Pan slider synchronization
  if (this.Tilt != this.LastTilt) {
    if (this.Tilt < -85) this.Tilt = -85;
    if (this.Tilt >  45) this.Tilt =  45;
    if (this.Tilt > 0) {
      this.TiltSlider = Math.sqrt( this.Tilt / 45 );
    } else {
      this.TiltSlider = - Math.sqrt( -this.Tilt / 85 );
    }
  } else if (this.TiltSlider != this.LastTiltSlider) {
    if (this.TiltSlider > 0) {
      this.Tilt = Math.pow( this.TiltSlider, 2 ) * 45;
    } else {
      this.Tilt = - Math.pow( this.TiltSlider, 2 ) * 85;
    }
  }
  this.LastTilt = this.Tilt;
  this.LastTiltSlider = this.TiltSlider;

  if (this.Pan != this.LastPan) {
    if (this.Pan < -180) this.Pan = -180;
    if (this.Pan >  180) this.Pan =  180;
    if (this.Pan > 0) {
      this.PanSlider = Math.sqrt( this.Pan / 90 );
    } else {
      this.PanSlider = - Math.sqrt( -this.Pan / 90 );
    }
  } else if (this.PanSlider != this.LastPanSlider) {
    if (this.PanSlider > 0) {
      this.Pan = Math.pow( this.PanSlider, 2 ) * 90;
    } else {
      this.Pan = - Math.pow( this.PanSlider, 2 ) * 90;
    }
  }
  this.PanRad = toRad( this.Pan );
  this.LastPan = this.Pan;
  this.LastPanSlider = this.PanSlider;

  // compute camViewCenter from panning
  if (this.Roll < -180) this.Roll = -180;
  if (this.Roll >  180) this.Roll = 180;
  this.CompCameraParams( this.PanRad, this.Tilt, this.Roll );

  // compute scene size taking device ratio into account
  var vpRatio = 3 / 2;
  var diag = 2 * this.dView * Math.tan( this.camViewAngle / 2 );
  this.sceneHeight = diag / Math.sqrt( 1 + this.deviceRatio*this.deviceRatio );
  this.sceneWidth = this.deviceRatio * this.sceneHeight;
  if ( this.deviceRatio > vpRatio ) {
    // device is wider then viewport
    this.camSceneSize = this.sceneWidth / vpRatio;
  } else {
    this.camSceneSize = this.sceneHeight;
  }

  function compObjVect( dist, side, size, rad, h ) {
    var R = rad + size;
    var a1 = side / rad;
    var a2 = dist / rad;
    var r = R * Math.cos( a1 );
    var vx = R * Math.sin( a1 );
    var vy = r * Math.sin( a2 );
    var vz = r * Math.cos( a2 ) - (rad + h);
    return [ vx, vy, vz ];
  }

  function compViewDist( v ) {
    return Math.sqrt( v[0]*v[0] + v[1]*v[1] + v[2]*v[2] );
  }

  function compVectAng( v1, v2 ) {
    var sp = JsgVect3.ScalarProd( JsgVect3.Norm(v1), JsgVect3.Norm(v2) );
    if (sp >  1) sp =  1; // handle rounding errors
    if (sp < -1) sp = -1; // handle rounding errors
    return Math.acos( sp );
  }

  // horizon refraction
  if (Math.abs(this.rEarth - this.refractionRadius) > 1e-5) {
    var aDip_norm = Math.acos( this.rEarth / (this.rEarth + this.Height) );
    var dView_norm = ( this.Height + this.rEarth ) * Math.sin( aDip_norm );
    var v_norm = compObjVect( dView_norm, 0, 0, this.rEarth, this.Height );
    var v_refr = compObjVect( this.dView, 0, 0, this.refractionRadius, this.Height );
    this.HorizonRefrAng = toDeg( compVectAng( v_norm, v_refr ) );
    if (this.refractionCoeff < 0) this.HorizonRefrAng *= -1;
  } else {
    this.HorizonRefrAng = 0;
  }

  // some computed object values
  var objIx = 0;
  if (this.NObjects[0] == 0) objIx = 1;
  if (this.NObjects[0] > 0 && this.NObjects[1] > 0 && this.ObjDist[0] > this.ObjDist[1]) objIx = 1;
  this.NearObjIx = objIx;

  if (this.NObjects[objIx] > 0) {

    // ObjRealDist = distance from origin to object base along surface
    var v_pos = compObjVect( this.ObjDist[objIx], this.ObjSidePos[objIx], 0, this.refractionRadius, -this.refractionRadius );
    this.ObjAngDist = compVectAng( v_pos, [ 0, 0, this.refractionRadius ] );
    this.ObjRealDist = this.ObjAngDist * this.refractionRadius;

    // bulge and drop
    var d2r = Math.sin( this.ObjRealDist / (2 * this.refractionRadius) );
    this.bulge = this.refractionRadius * (1 - Math.sqrt(1 - d2r*d2r));
    this.objDrop = this.refractionRadius * (1 - Math.cos( this.ObjRealDist / this.refractionRadius ) );

    this.ObjNearSize = this.GetObjectSizeVar(objIx) * this.ObjSize[objIx];
    var v_base_refr = compObjVect( this.ObjDist[objIx], this.ObjSidePos[objIx], 0, this.refractionRadius, this.Height );
    var v_top_refr  = compObjVect( this.ObjDist[objIx], this.ObjSidePos[objIx], this.ObjNearSize, this.refractionRadius, this.Height );
    var v_base_norm = compObjVect( this.ObjDist[objIx], this.ObjSidePos[objIx], 0, this.rEarth, this.Height );

    var objUpDir = JsgVect3.Sub( v_top_refr, v_base_refr );
    this.ObjNearTilt = toDeg( compVectAng( [0,0,1], objUpDir ) );
    this.ObjAngSize = toDeg( compVectAng( v_base_refr, v_top_refr ) );
    if (Math.abs(this.ObjAngSize) < 1e-5) this.ObjAngSize = 0;
    this.ObjRefrAng = toDeg( compVectAng( v_base_norm, v_base_refr ) );
    if (Math.abs(this.ObjRefrAng) < 1e-5) this.ObjRefrAng = 0;
    if (this.refractionCoeff < 0) this.ObjRefrAng *= -1;

    this.ObjLiftAbs = 0;
    this.ObjLiftRel = 0;
    this.HorizonLift = 0;
    if (this.ObjAngSize != 0) {
      this.ObjLiftAbs = this.ObjNearSize * this.ObjRefrAng / this.ObjAngSize;
      this.HorizonLift = this.ObjNearSize * this.HorizonRefrAng / this.ObjAngSize;
      this.ObjLiftRel = this.ObjLiftAbs - this.HorizonLift;
    }

    // compute hidden part
    if (this.ObjAngDist > this.aDip) {

      // object lies behind horizon
      var cosaHorObj = Math.cos( this.ObjAngDist - this.aDip );
      this.ObjHidden = this.refractionRadius * ( 1 - cosaHorObj ) / cosaHorObj;
      this.ObjVisi = this.ObjNearSize - this.ObjHidden;
      if (this.ObjVisi < 0) this.ObjVisi = 0;
      this.ObjAngHidden = this.ObjAngSize * this.ObjHidden / this.ObjNearSize;
      this.ObjAngVisible = this.ObjAngSize * this.ObjVisi / this.ObjNearSize;

    } else {

      // object lies in front of horizon
      this.ObjHidden = 0;
      this.ObjVisi = this.ObjNearSize;
      this.ObjAngHidden = 0;
      this.ObjAngVisible = this.ObjAngSize;
    }

    // compute vertical angle from horizontal to target top (= 90 deg - zenith angle)
    // c = line of sight observer to target top
    var a = this.refractionRadius + this.Height;
    var b = this.refractionRadius + this.ObjNearSize;
    var c = Math.sqrt( a*a + b*b -2*a*b * Math.cos( this.ObjRealDist / this.refractionRadius ) );
    var a1 = (c*c - b*b + a*a) / (2 * a);
    this.VerticalAngle = -Math.asin( a1 / c ) * 180 / Math.PI;
    this.VerticalAngleFE = Math.atan( (this.ObjSize[objIx] - this.Height) / this.ObjRealDist ) * 180 / Math.PI;


  } else {

    this.bulge = 0;
    this.objDrop = 0;
    this.ObjRefrAng = this.HorizonRefrAng;
    this.ObjLiftAbs = 0;
    this.HorizonLift = 0;
    this.ObjLiftRel = 0;
    this.ObjAngSize = 0;
    this.ObjHidden = 0;
    this.ObjVisi = 0;
    this.ObjAngHidden = 0;
    this.ObjAngVisible = 0;
    this.ObjRealDist = 0;
    this.ObjAngDist = 0;

  }
  this.AllStatesChanged = false;

}

CurveAppClass.prototype.CompCameraParams = function( pan, tilt, roll ) {
  var dvc, avc;
  dvc = Math.sqrt( this.rDisk * this.rDisk + this.zDisk * this.zDisk );
  if (this.viewcenterHorizon == 0) {
    // view center is globe horizon
    avc = this.aDip;
  } else if (this.viewcenterHorizon == 1) {
    // view center is flat earth equator
    avc = Math.atan( this.Height / this.rFEarth );
  } else if (this.viewcenterHorizon == 2) {
    // view center is between globe horizon and flat earth equator
    avc = (Math.atan( this.Height / this.rFEarth ) + this.aDip) / 2;
  } else {
    // view center is eye level
    avc = 0;
  }
  avc -= toRad( tilt );
  if ( avc > 0.9999*PI90 ) avc = 0.9999*PI90;
  if ( avc < -0.9999*PI90 ) avc = -0.9999*PI90;
  this.TheodoliteTilt = -avc * 180 / Math.PI;
  var zvc = - dvc * Math.sin( avc );
  var rvc = dvc * Math.cos( avc );
  var xvc = rvc * Math.sin( pan );
  var yvc = rvc * Math.cos( pan );
  this.camViewCenter = [ xvc, yvc, zvc ];
  var nvc = JsgVect3.Norm( this.camViewCenter );
  var v = JsgVect3.Norm( [ xvc, yvc, 0 ] );
  var n = JsgVect3.Mult( v, [ 0, 0, 1 ] );
  var up = JsgVect3.Mult( n, nvc );

  // compute camera up and pos
  var a = toRad( roll );
  this.camUp = JsgVect3.Add( JsgVect3.Scale( n, Math.sin(a) ), JsgVect3.Scale( up, Math.cos(a) ) );
  this.camPos = [ 0, 0, 0 ];
}

CurveAppClass.prototype.UpdateObjectInput = function( objIx ) {

  // object settings
  this.NObjects[objIx] = Math.round(this.NObjects[objIx]);
  if (this.NObjects[objIx] < 0) this.NObjects[objIx] = 0;
  if (this.NObjects[objIx] > 500) this.NObjects[objIx] = 500;

  if (this.Slider_ObjDist_Log_Last[objIx] != this.Slider_ObjDist_Log[objIx]) {
    var distSign = this.Slider_ObjDist_Log[objIx] < 0 ? -1 : 1;
    var distVal = Math.abs( this.Slider_ObjDist_Log[objIx] );
    if (distVal < 1) {
      this.ObjDist[objIx] = 100 * distVal;
    } else {
      this.ObjDist[objIx] = 10 * Math.pow( 10, this.Slider_ObjDist_Log[objIx] );
    }
    this.ObjDist[objIx] *= distSign;
  }
  var distLimit = this.refractionRadius * Math.PI / 2;
  if (this.ObjDist[objIx] < -distLimit) this.ObjDist[objIx] = -distLimit;
  if (this.ObjDist[objIx] >  distLimit) this.ObjDist[objIx] =  distLimit;
  var distSign = this.ObjDist[objIx] < 0 ? -1 : 1;
  var distVal  = Math.abs( this.ObjDist[objIx] );
  if (distVal < 100) {
    this.Slider_ObjDist_Log[objIx] = distVal / 100;
  } else {
    this.Slider_ObjDist_Log[objIx] = Math.log10( distVal / 10 );
  }
  this.Slider_ObjDist_Log[objIx] *= distSign;
  this.Slider_ObjDist_Log_Last[objIx] = this.Slider_ObjDist_Log[objIx];

  if (this.Slider_ObjSidePos_Log[objIx] != this.Slider_ObjSidePos_Log_Last[objIx]) {
    var sidePosSign = this.Slider_ObjSidePos_Log[objIx] < 0 ? -1 : 1;
    var sidePosVal = Math.abs( this.Slider_ObjSidePos_Log[objIx] );
    if (sidePosVal < 1) {
      this.ObjSidePos[objIx] = 100 * sidePosVal;
    } else {
      this.ObjSidePos[objIx] = 10 * Math.pow( 10, sidePosVal );
    }
    this.ObjSidePos[objIx] *= sidePosSign;
  }
  var sidePosLimit = this.refractionRadius * Math.PI / 4;
  if (this.ObjSidePos[objIx] < -sidePosLimit) this.ObjSidePos[objIx] = -sidePosLimit;
  if (this.ObjSidePos[objIx] > sidePosLimit) this.ObjSidePos[objIx] = sidePosLimit;
  var sidePosSign = this.ObjSidePos[objIx] < 0 ? -1 : 1;
  var sidePosVal = Math.abs( this.ObjSidePos[objIx] );
  if (sidePosVal < 100) {
    this.Slider_ObjSidePos_Log[objIx] = sidePosVal / 100;
  } else {
    this.Slider_ObjSidePos_Log[objIx] = Math.log10( sidePosVal / 10 );
  }
  this.Slider_ObjSidePos_Log[objIx] *= sidePosSign;
  this.Slider_ObjSidePos_Log_Last[objIx] = this.Slider_ObjSidePos_Log[objIx];

  if (this.Slider_ObjSideVar_Log[objIx] != this.Slider_ObjSideVar_Log_Last[objIx]) {
    var sideVarSign = this.Slider_ObjSideVar_Log[objIx] < 0 ? -1 : 1;
    var sideVarVal = Math.abs( this.Slider_ObjSideVar_Log[objIx] );
    if (sideVarVal < 1) {
      this.ObjSideVar[objIx] = 10 * sideVarVal;
    } else {
      this.ObjSideVar[objIx] = Math.pow( 10, sideVarVal );
    }
    this.ObjSideVar[objIx] *= sideVarSign;
  }
  var sideVarLimit = this.refractionRadius * Math.PI / 4;
  if (this.ObjSideVar[objIx] < -sideVarLimit) this.ObjSideVar[objIx] = -sideVarLimit;
  if (this.ObjSideVar[objIx] > sideVarLimit) this.ObjSideVar[objIx] = sideVarLimit;
  var sideVarSign = this.ObjSideVar[objIx] < 0 ? -1 : 1;
  var sideVarVal = Math.abs( this.ObjSideVar[objIx] );
  if (sideVarVal < 10) {
    this.Slider_ObjSideVar_Log[objIx] = sideVarVal / 10;
  } else {
    this.Slider_ObjSideVar_Log[objIx] = Math.log10( sideVarVal );
  }
  this.Slider_ObjSideVar_Log[objIx] *= sideVarSign;
  this.Slider_ObjSideVar_Log_Last[objIx] = this.Slider_ObjSideVar_Log[objIx];

  if (this.Slider_ObjSize_Log[objIx] != this.Slider_ObjSize_Log_Last[objIx]) {
    this.ObjSize[objIx] = Math.pow( 10, this.Slider_ObjSize_Log[objIx] );
  }
  if (this.ObjSize[objIx] < 0.001) this.ObjSize[objIx] = 0.001;
  if (this.ObjSize[objIx] > 1e9) this.ObjSize[objIx] = 1e9;
  this.Slider_ObjSize_Log[objIx] = Math.log10( this.ObjSize[objIx] );
  this.Slider_ObjSize_Log_Last[objIx] = this.Slider_ObjSize_Log[objIx];

  if (this.Slider_ObjDeltaDist_Log[objIx] != this.Slider_ObjDeltaDist_Log_Last[objIx]) {
    this.ObjDeltaDist[objIx] = 10 * Math.pow( 10, this.Slider_ObjDeltaDist_Log[objIx] );
  }
  var deltaDistLimit = this.refractionRadius * PI90;
  if (this.ObjDeltaDist[objIx] < 0.001) this.ObjDeltaDist[objIx] = 0.001;
  if (this.ObjDeltaDist[objIx] > deltaDistLimit) this.ObjDeltaDist[objIx] = deltaDistLimit;
  this.Slider_ObjDeltaDist_Log[objIx] = Math.log10( this.ObjDeltaDist[objIx] / 10 );
  this.Slider_ObjDeltaDist_Log_Last[objIx] = this.Slider_ObjDeltaDist_Log[objIx];

}

var CurveApp = new CurveAppClass();
var UpdateAllRunning = false;

DataX.AssignApp( 'AdvCurveApp', CurveApp, CurveAppMetaData, null, UpdateAllChanged );
DataX.AssignSaveRestoreDomObj( 'SaveRestorePanel' );
DataX.SetupUrlStateHandler( 'App' );

function UpdateAll() {
  if (UpdateAllRunning) return;
  UpdateAllRunning = true;
  try {
    CurveApp.Update();
    ControlPanels.Update();
    graph.Redraw();
  }
  finally {
    UpdateAllRunning = false;
  }
}

function UpdateAllChanged() {
  CurveApp.AllStatesChanged = true;
  UpdateAll();
}

xOnLoad(
  function() {
    Tabs.AddButtonClickHandler( 'CurveSettingsTabs', 'ResetButton',
      function( buttonData ) {
        ResetApp( true );
      }
    );
    Tabs.AddButtonClickHandler( 'CurveSettingsTabs', 'StdRefrButton',
      function( buttonData ) {
        SetStdRefraction();
      }
    );
    Tabs.AddButtonClickHandler( 'CurveSettingsTabs', 'ZeroRefrButton',
      function( buttonData ) {
        Set0Refraction();
      }
    );
    HandleUrlCommands();
    UpdateAll();
  }
);

function LVal( x ) {
  return x / CurveApp.LengthUnits.Mults[CurveApp.UnitsType];
}

function LUnit() {
  return ' ' + CurveApp.LengthUnits.Units[CurveApp.UnitsType];
}

function HVal( x ) {
  return x / CurveApp.HeightUnits.Mults[CurveApp.UnitsType];
}

function HUnit() {
  return ' ' + CurveApp.HeightUnits.Units[CurveApp.UnitsType];
}

function BVal( x ) {
  return x / CurveApp.BigLengthUnits.Mults[CurveApp.UnitsType];
}

function BUnit() {
  return ' ' + CurveApp.BigLengthUnits.Units[CurveApp.UnitsType];
}

function AVal( x ) {
  return x / CurveApp.GradientUnits.Mults[CurveApp.UnitsType];
}

function AUnit() {
  return CurveApp.GradientUnits.Units[CurveApp.UnitsType];
}

var graph = NewGraphX3D( {
  Id: 'JsGraph1',
  Width: '100%',
  Height: '66.67%',
  DrawFunc: DrawModel,
  AutoReset: true,
  AutoClear: true,
  AutoScalePix: true
} );

function DrawModel( g ) {

  var m = CurveApp;
  g.MaxCurveSegments = 256;
  g.SetAngleMeasure( 'rad' );
  g.SetViewport( 0, 1, -0.5, -2 );
  g.SetGraphClipping( true, '', 0 );
  g.SetCameraClipping( 0.001 );
  g.SetWindowToCameraScreen();

  g.SetCamera( {
    SceneSize: m.camSceneSize,
    CamPos: m.camPos,
    CamUp: m.camUp,
    CamViewCenter: m.camViewCenter,
  } );
  g.SetCameraZoom( 1 );

  var specialLineWidth = 1.5;

  // draw eye level
  var rotz = JsgMat3.RotatingZ( -m.PanRad );
  if ( m.showEyeLevel || m.showTheodolite || (m.Flerspective > 0 && m.IsShowFlatEarth())) {
    g.SetLineAttr( m.EyeLvlCol, 1 );
    g.SetAlpha( m.AlphaOpaque );
    g.Line3D( JsgMat3.Trans( rotz, [ -2*m.sceneWidth, m.rDisk, 0 ] ), JsgMat3.Trans( rotz, [ 2*m.sceneWidth, m.rDisk, 0 ] ) );
    g.SetTextAttr( 'Arial', 12, m.EyeLvlCol, 'normal', 'normal', 'right', 'bottom', 30, 4 );
    g.SetTextRotation( toRad(-m.Roll) );
    g.Text3D( 'Eye-Level', JsgMat3.Trans( rotz, [ 0, m.rDisk, 0 ] ) );
    g.SetTextRotation( 0 );
  }

  // Flat Earth

  var objIx = 0;
  if (m.NObjects[0] == 0) objIx = 1;

  if ( m.IsShowFlatEarth() ) {

    // set clipping if both models are shown to restrict graphic to one half of the screen
    if (m.IsShowBothModels()) {
      var xpos = 0;
      g.SetClipRect( xpos, 0, g.CanvasWidth/2, g.CanvasHeight, 'canvas' );
    }
    if (m.IsShowBothModelsMirror()) {
      m.CompCameraParams( -m.PanRad, m.Tilt, m.Roll );
      g.SetCamera( {
        CamPos: m.camPos,
        CamUp: m.camUp,
        CamViewCenter: m.camViewCenter,
      } );
    }

    // set flerspective func
    if (m.Flerspective > 0) {

      g.SetCameraCustomTrans( m.FlerspectiveFunc, null, m );

      var vdir = JsgVect3.Sub( g.Camera.CamViewCenter, g.Camera.CamPos );
      vdir[2] = 0;
      vdir = JsgVect3.Norm( vdir );
      m.FEviewDir = vdir;

      if (m.Flerspective == 1) {
        // calculate clipping plane at the vanishing distance in view direction
        var rh = m.rEarth + m.Height;
        var vpd = Math.sqrt( rh * rh - m.rEarth * m.rEarth ) + m.RefDistance;
        var pos = JsgVect3.Scale( vdir, vpd );
        var xdir = JsgVect3.Norm( JsgVect3.Mult( vdir, [ 0, 0, 1 ] ) );
        var ydir = JsgVect3.Mult( xdir, vdir );
        g.AddClipPlane( pos, xdir, ydir );
      }
    }

    // draw flat earth horizon and equator
    g.SetAlpha( m.AlphaOpaque );
    g.SetPlane( [ 0, 0, -m.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
    g.SetLineAttr( m.FEEqCol, 1 );
    g.CircleOnPlane( 0, 0, m.rFEarth, 1 );
    g.SetLineWidth( 2 );
    g.CircleOnPlane( 0, 0, 2*m.rFEarth, 1 );

    var aMax = 2 * Math.PI;
    var alpha = m.AlphaOpaque * (0.6 - 0.5 * (Math.log10( m.Height ) / 9));
    g.SetAlpha( alpha );
    g.SetLineAttr( m.FEGridCol, 1 );

    if (m.showGrid > 0) {

      // circle lines
      var crDelta = m.rFEarth / 12;
      var crMax = 2 * m.rFEarth - crDelta / 2;
      g.SetPlane( [ 0, 0, -m.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
      for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
        g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
      }
      if (m.Height < 700000) {
        crMax = crDelta;
        crDelta /= 10;
        crMax -= crDelta / 2;
        for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
          g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
        }
      }
      if (m.Height < 30000) {
        crMax = crDelta;
        crDelta /= 10;
        crMax -= crDelta / 2;
        for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
          g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
        }
      }
      if (m.Height < 3000) {
        crMax = crDelta;
        crDelta /= 10;
        crMax -= crDelta / 2;
        for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
          g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
        }
      }
      if (m.Height < 300) {
        crMax = crDelta;
        crDelta /= 10;
        crMax -= crDelta / 2;
        for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
          g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
        }
      }

      // ray lines
      g.SetAlpha( alpha );
      g.SetLineAttr( m.FEGridCol, 1 );
      var caDelta = Math.PI / 12;
      var caMax = 2 * Math.PI;
      caMax -= caDelta / 2;
      var r = 2 * m.rFEarth;
      for ( var ca = 0; ca < caMax; ca += caDelta ) {
        var c = Math.cos( ca );
        var s = Math.sin( ca );
        g.LineOnPlane( r * c, r * s, 0, 0 );
      }
    }

    var drawObjIx = 1;
    if (m.ObjDist[0] > m.ObjDist[1]) drawObjIx = 0;
    m.LastPosValid[0] = false;
    m.LastPosValid[1] = false;
    DrawObjectsBeforeHorizon( drawObjIx, g, false );
    drawObjIx = (drawObjIx + 1) % 2;
    DrawObjectsBeforeHorizon( drawObjIx, g, false );

    // reset inverse panning
    if (m.IsShowBothModelsMirror()) {
      m.CompCameraParams( m.PanRad, m.Tilt, m.Roll );
      g.SetCamera( {
        CamPos: m.camPos,
        CamUp: m.camUp,
        CamViewCenter: m.camViewCenter,
      } );
    }
    // reset clipping
    if (m.IsShowBothModels()) {
      g.SetClipping( 'canvas' );
    }

    if (m.Flerspective > 0) {
      g.SetCameraCustomTrans( null, null, null );
      if (m.Flerspective == 1) {
        g.DeleteClipPlanes();
      }
    }

  } // end nLines > 0

  // Globe Earth

  if (m.IsShowGlobe()) {

    // set clipping if both models are shown to restrict graphic to one half of the screen
    if (m.IsShowBothModels()) {
      var xpos = g.CanvasWidth / 2;
      g.SetClipRect( xpos, 0, g.CanvasWidth/2, g.CanvasHeight, 'canvas' );
    }

    var drawObjIx = 1;
    if (m.ObjDist[0] > m.ObjDist[1]) drawObjIx = 0;
    m.LastPosValid[0] = false;
    m.LastPosValid[1] = false;
    DrawObjectsBehindHorizon( drawObjIx, g );
    drawObjIx = (drawObjIx + 1) % 2;
    DrawObjectsBehindHorizon( drawObjIx, g );

    var alpha = m.AlphaOpaque * (0.6 - 0.5 * (Math.log10( m.Height ) / 9) );
    g.SetAlpha( alpha );
    g.SetLineAttr( m.GlobeGridCol, 1 );

    // show globe grid
    if ( m.showGrid & 1 ) {

      // latitude lines
      var latMax = m.aDip;
      var latStart = -( Math.floor( latMax / m.aDelta ) * m.aDelta );
      for ( var lat = latStart; lat < latMax; lat += m.aDelta ) {
        var dLatPlaneDisk = m.hDisk / Math.cos( lat );
        var longMax = Math.acos( dLatPlaneDisk / m.refractionRadius );
        var longStart = -( Math.floor( longMax / m.aDelta ) * m.aDelta );
        g.NewPoly3D();
        for ( var long = longStart; long < longMax; long += m.aDelta ) {
          g.AddPointToPoly3D( PointOnEarth( lat, long ) );
        }
        g.AddPointToPoly3D( PointOnEarth( lat, longMax ) );
        g.DrawPoly3D( 1 );
      }

      // longitude lines
      var longMax = m.aDip;
      var longStart = -( Math.floor( longMax / m.aDelta ) * m.aDelta );
      for ( var long = longStart; long < longMax; long += m.aDelta ) {
        var rLong = m.refractionRadius * Math.cos( long );
        var latMax = Math.acos( m.hDisk / rLong );
        var latStart = -( Math.floor( latMax / m.aDelta ) * m.aDelta );
        g.NewPoly3D();
        g.AddPointToPoly3D( PointOnEarth( -latMax, long ) );
        for ( var lat = latStart; lat < latMax; lat += m.aDelta ) {
          g.AddPointToPoly3D( PointOnEarth( lat, long ) );
        }
        g.AddPointToPoly3D( PointOnEarth( latMax, long ) );
        g.DrawPoly3D( 1 );
      }

    } // end show globe grid

    g.SetLineAttr( m.GlobeFGridCol, 1 );

    // show flat grid
    if ( m.showGrid & 2 ) {

      // latitude lines on flat model
      var latMax = m.aDip;
      var latStart = -( Math.floor( latMax / m.aDelta ) * m.aDelta );
      for ( var lat = latStart; lat < latMax; lat += m.aDelta ) {
        var dLatPlaneDisk = m.hDisk / Math.cos( lat );
        var longMax = Math.acos( dLatPlaneDisk / m.refractionRadius );
        var longStart = -( Math.floor( longMax / m.aDelta ) * m.aDelta );
        g.NewPoly3D();
        for ( var long = longStart; long < longMax; long += m.aDelta ) {
          g.AddPointToPoly3D( PointOnPlane( lat, long ) );
        }
        g.AddPointToPoly3D( PointOnPlane( lat, longMax ) );
        g.DrawPoly3D( 1 );
      }

      // longitude lines on flat model
      var longMax = m.aDip;
      var longStart = -( Math.floor( longMax / m.aDelta ) * m.aDelta );
      for ( var long = longStart; long < longMax; long += m.aDelta ) {
        var rLong = m.refractionRadius * Math.cos( long );
        var latMax = Math.acos( m.hDisk / rLong );
        var latStart = -( Math.floor( latMax / m.aDelta ) * m.aDelta );
        g.NewPoly3D();
        g.AddPointToPoly3D( PointOnPlane( -latMax, long ) );
        for ( var lat = latStart; lat < latMax; lat += m.aDelta ) {
          g.AddPointToPoly3D( PointOnPlane( lat, long ) );
        }
        g.AddPointToPoly3D( PointOnPlane( latMax, long ) );
        g.DrawPoly3D( 1 );
      }

    } // end flat grid

    if ( m.showGrid & 2 ) {

      // horizon on flat model
      g.SetPlane( [ 0, 0, -m.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
      g.SetAlpha( m.AlphaOpaque );
      g.SetLineAttr( m.GlobeFGridCol, specialLineWidth );
      g.CircleOnPlane( 0, 0, m.rDisk, 1 );

    }

    // Globe Horizon
    g.SetAlpha( m.AlphaOpaque );
    g.SetLineAttr( m.GlobeGridCol, specialLineWidth );
    g.SetPlane( [ 0, 0, -m.zDisk ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
    g.CircleOnPlane( 0, 0, m.rDisk, 1 );

    // show tangent line to globe horizon
    if ( m.showTangent || m.showDataHorizon ) {
      g.SetLineAttr( m.TangentCol, 1 );
      g.Line3D( JsgMat3.Trans( rotz, [ -2*m.sceneWidth, m.rDisk, -m.zDisk ] ), JsgMat3.Trans( rotz, [ 2*m.sceneWidth, m.rDisk, -m.zDisk ] ) );
    }

    // draw all objects in the foreground
    var drawObjIx = 1;
    if (m.ObjDist[0] > m.ObjDist[1]) drawObjIx = 0;
    DrawObjectsBeforeHorizon( drawObjIx, g, true );
    drawObjIx = (drawObjIx + 1) % 2;
    DrawObjectsBeforeHorizon( drawObjIx, g, true );

    // reset clipping
    if (m.IsShowBothModels()) {
      g.SetClipping( 'canvas' );
    }

  } // end model globe

  if ( m.showTheodolite ) {
    var xDir = JsgVect3.Mult( g.Camera.ViewDir, g.Camera.CamUp );
    var yDir = JsgVect3.Mult( g.Camera.ViewDir, xDir );
    var diag = Math.sqrt( m.sceneWidth*m.sceneWidth + m.sceneHeight*m.sceneHeight );
    g.SetPlane( g.Camera.CamViewCenter, xDir, yDir, true );

    // measurements
    var numFormat = { Mode: 'fix0', Precision: 4, UsePrefix: false, Units: '' };

    var txt = NumFormatter.NumToString( m.TheodoliteTilt, numFormat ) + '°';
    g.SetBgColor( 'white' );
    g.SetTextAttr( 'Arial', 12, 'black', 'normal', 'normal', 'left', 'bottom', 5, 2 );
    g.SetAlpha( 0.8 * m.AlphaOpaque );
    g.Rect( g.GetTextBoxOnPlane( txt, 0.1*m.sceneWidth, 0 ), 2 );
    g.SetAlpha( m.AlphaOpaque );
    g.TextOnPlane( txt, 0.1*m.sceneWidth, 0 );

    var txt = NumFormatter.NumToString( m.Pan, numFormat ) + '°';
    g.SetTextAttr( 'Arial', 12, 'black', 'normal', 'normal', 'left', 'top', 5, 2 );
    g.SetAlpha( 0.8 * m.AlphaOpaque );
    g.Rect( g.GetTextBoxOnPlane( txt, 0, -0.35*m.sceneHeight ), 2 );
    g.SetAlpha( m.AlphaOpaque );
    g.TextOnPlane( txt, 0, -0.35*m.sceneHeight );

    // crosshair
    g.SetAlpha( 0.8 * m.AlphaOpaque );
    g.SetLineAttr( 'white', 3 );
    g.LineOnPlane( -0.7*m.sceneWidth/2, 0, -0.05*m.sceneWidth/2, 0 );
    g.LineOnPlane( 0.05*m.sceneWidth/2, 0, 0.7*m.sceneWidth/2, 0 );
    g.LineOnPlane( 0, -0.7*m.sceneHeight/2, 0, -0.05*m.sceneHeight/2 );
    g.LineOnPlane( 0, 0.7*m.sceneHeight/2, 0, 0.05*m.sceneHeight/2 );
    g.SetAlpha( m.AlphaOpaque );
    g.SetLineAttr( 'black', 1 );
    g.LineOnPlane( -0.7*m.sceneWidth/2, 0, -0.02*m.sceneWidth/2, 0 );
    g.LineOnPlane(  0.7*m.sceneWidth/2, 0,  0.02*m.sceneWidth/2, 0 );
    g.LineOnPlane( 0, -0.7*m.sceneHeight/2, 0, -0.02*m.sceneHeight/2 );
    g.LineOnPlane( 0,  0.7*m.sceneHeight/2, 0,  0.02*m.sceneHeight/2 );

    // theodolite scope border
    g.SetAreaAttr( 'gray', 'gray', 2 );
    g.SetAlpha( 0.8 * m.AlphaOpaque );
    g.OpenPath3D();
    g.RectOnPlane( -diag, -diag, diag, diag );
    g.CircleOnPlane( 0, 0, -0.35*diag );
    g.Path3D( 3 );
    g.SetAlpha( m.AlphaOpaque );
  }

  // label split screen
  if (m.IsShowBothModels()) {
    var oldTrans = g.SelectTrans( 'viewport' );
    // label left
    var txt = 'Flat Earth';
    g.SetTextAttr( 'Arial', 16, 'black', 'bold', 'normal', 'left', 'top', 10 );
    g.SetAreaAttr( 'yellow', 'white', 1 );
    g.TextBox( txt, 0, 0, 3 );
    g.Text( txt, 0, 0 );
    // label right
    var txt = 'Globe';
    g.SetTextAttr( 'Arial', 16, 'black', 'bold', 'normal', 'right', 'top', 10 );
    g.SetAreaAttr( 'yellow', 'white', 1 );
    g.TextBox( txt, g.VpInnerWidth, 0, 3 );
    g.Text( txt, g.VpInnerWidth, 0 );
    // split line
    g.SetLineAttr( '#ddd', 1 );
    g.Line( g.VpInnerWidth/2, 0, g.VpInnerWidth/2, g.VpInnerHeight );
    g.SelectTrans( oldTrans );
  }

  // draw frame
  if (m.deviceRatio != DeviceRatioOff && m.FrameCol != '') {
    g.SetAlpha( m.AlphaOpaque );
    g.SetLineAttr( m.FrameCol, 2 );
    var xDir = JsgVect3.Mult( g.Camera.ViewDir, g.Camera.CamUp );
    var yDir = JsgVect3.Mult( g.Camera.ViewDir, xDir );
    g.SetPlane( g.Camera.CamViewCenter, xDir, yDir, true );
    g.RectOnPlane( -m.sceneWidth/2, -m.sceneHeight/2, m.sceneWidth/2, m.sceneHeight/2, 1 );
  }

  var numFormat = {
    Mode: 'std',
    Precision: 4,
    UsePrefix: false,
    Units: '',
  };

  var numFormat4 = {
    Mode: 'std',
    Precision: 4,
    UsePrefix: false,
    Units: '',
  };

  // draw data
/*
  var textSize = 12;
  var lineHeight = g.ScalePix(19);
*/
  var textSize = 14;
  var lineHeight = g.ScalePix(22);
  var tm = g.ScalePix(5); // text margin

  if (m.showData && m.IsShowGlobe()) {

    var posHorizon, align, offset;
    align = 'right';

    if (m.showDataHorizon) {
      posHorizon = JsgMat3.Trans( rotz, [ 0.44 * m.sceneWidth, m.rDisk, -m.zDisk ] );
      offset = g.ScalePix(40);
      var posHorizonWin = g.VTransPoint3D( posHorizon );
      var posHorizonVp = g.TransXY( posHorizonWin[0], posHorizonWin[1] );
      var posHorizonX = posHorizonVp.x;
      var posHorizonY = posHorizonVp.y - g.ScalePix(2);
      var posTextX = posHorizonX + offset;
      var posTextY = posHorizonY - g.ScalePix(30);
      if (posTextX < tm) posTextX = tm;
      if (posTextX > g.VpInnerWidth-tm) posTextX = g.VpInnerWidth-tm;

      // show horizon data
      g.SetTextAttr( 'Arial', textSize, 'black', 'normal', 'normal', align, 'bottom', 4 );
      g.SetMarkerAttr( 'Arrow1', 6, 'black', 'black', 1 );
      var oldTrans = g.SelectTrans( 'viewport' );

      g.Arrow( posHorizonX, posTextY, posHorizonX, posHorizonY, 9 );
      g.SetBgColor( 'white' );

      var txt = 'Grid Spacing = ' + NumFormatter.NumToString( LVal(m.dDelta), numFormat ) + LUnit();
      var ty = posTextY;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      var txt = 'Bulge Height = ' + NumFormatter.NumToString( HVal(m.bulge), numFormat ) + HUnit();
      ty -= lineHeight;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      var txt = 'Drop from Surface = ' + NumFormatter.NumToString( HVal(m.hDip), numFormat ) + HUnit();
      ty -= lineHeight;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      var txt = 'Drop from Eye-Level = ' + NumFormatter.NumToString( HVal(m.zDisk), numFormat ) + HUnit();
      ty -= lineHeight;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      var txt = 'Horizon Dip Angle = ' + NumFormatter.NumToString( toDeg( m.aDip ), numFormat ) + '°';
      ty -= lineHeight;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      var txt = 'Distance on Surface = ' + NumFormatter.NumToString( LVal(m.dHorizon), numFormat ) + LUnit();
      ty -= lineHeight;
      g.TextBox( txt, posTextX, ty, 2 );
      g.Text( txt, posTextX, ty );

      g.SelectTrans( oldTrans );
    }

    if (m.showDataObject && m.NObjects[objIx] > 0) {

      // show object data
      var oldTrans = g.SelectTrans( 'viewport' );

      var ty = 1.25 * lineHeight;
      var tx = tm;
      var hAl = 'left';
      if (m.IsShowBothModels()) {
        tx += g.VpInnerWidth / 2;
      }
      g.SetTextAttr( 'Arial', textSize, 'black', 'normal', 'normal', hAl, 'bottom', 4 );
      g.SetBgColor( 'white' );

      var txt = 'Target ' + (m.NearObjIx+1) + ' Visible = ' + NumFormatter.NumToString( HVal(m.ObjVisi), numFormat4 ) + HUnit() + '; Hidden = ' + NumFormatter.NumToString( HVal(m.ObjHidden), numFormat4 ) + HUnit();
      g.SetBgColor( '#ffddff' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var eq = ' = ';
      var a = m.ObjAngSize;
      if (a < 1e-5) a = 0;
      if (m.IsVariableSizeObject(objIx)) eq = ' <= ';
      var txt = 'Size ' + eq + NumFormatter.NumToString( HVal(m.ObjNearSize), numFormat4 ) + HUnit() + '; AngSize = ' + NumFormatter.NumToString( a, numFormat ) + '°';
      g.SetBgColor( 'white' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var va = m.VerticalAngle;
      if (Math.abs(va) < 1e-5) va = 0;
      var txt = 'TopAng = ' + NumFormatter.NumToString( va, numFormat ) + '°; Drop = ' + NumFormatter.NumToString( HVal(m.objDrop), numFormat ) + HUnit() + '; Tilt = ' + NumFormatter.NumToString( m.ObjNearTilt, numFormat4 ) + '°';
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      g.SelectTrans( oldTrans );

    }

    if (m.showDataRefraction && m.NObjects[objIx] > 0) {

      var oldTrans = g.SelectTrans( 'viewport' );

      // show refraction data
      var ty = g.VpInnerHeight - 5.25 * lineHeight;
      var tx = tm;
      var hAl = 'left';
      if (m.IsShowBothModels()) {
        tx += g.VpInnerWidth / 2;
      }
      g.SetTextAttr( 'Arial', textSize, 'black', 'normal', 'normal', hAl, 'bottom', 4 );
      g.SetBgColor( 'white' );

      var txt = 'Lift Relativ to Horizon = ' + NumFormatter.NumToString( HVal(m.ObjLiftRel), numFormat4 ) + HUnit();
      g.SetBgColor( '#ffddff' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var txt = 'Lift Absolute = ' + NumFormatter.NumToString( HVal(m.ObjLiftAbs), numFormat4 ) + HUnit();
      g.SetBgColor( 'white' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var txt = 'Horizon Lift = ' + NumFormatter.NumToString( HVal(m.HorizonLift), numFormat4 ) + HUnit();
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      g.SetTextColor( 'darkred' );
      var txt = 'Refraction Angle = ' + NumFormatter.NumToString( m.ObjRefrAng, numFormat ) + '°';
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      g.SelectTrans( oldTrans );
    }

  }

  if (m.showData && m.IsShowFlatEarth()) {

    var oldTrans = g.SelectTrans( 'viewport' );

    if (m.showDataObject && m.NObjects[objIx] > 0) {

      // show object data
      var ty = 1.25 * lineHeight;
      var tx = tm;
      var hAl = 'left';
      if (m.IsShowBothModels()) {
        tx = g.VpInnerWidth / 2 - tm;
        hAl = 'right';
      }
      g.SetTextAttr( 'Arial', textSize, 'black', 'normal', 'normal', hAl, 'bottom', 4 );
      g.SetBgColor( 'white' );

      var txt = 'Target ' + (m.NearObjIx+1) + ' Visible = ' + NumFormatter.NumToString( HVal(m.ObjSize[objIx]), numFormat4 ) + HUnit() + '; Hidden = ' + NumFormatter.NumToString( HVal(0), numFormat4 ) + HUnit();
      g.SetBgColor( '#ffddff' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var eq = ' = ';
      var a = m.ObjAngSize;
      if (a < 1e-5) a = 0;
      if (m.IsVariableSizeObject(objIx)) eq = ' <= ';
      var txt = 'Size ' + eq + NumFormatter.NumToString( HVal(m.ObjNearSize), numFormat4 ) + HUnit() + '; AngSize = ' + NumFormatter.NumToString( a, numFormat ) + '°';
      g.SetBgColor( 'white' );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

      var va = m.VerticalAngleFE;
      if (Math.abs(va) < 1e-5) va = 0;
      var txt = 'TopAng = ' + NumFormatter.NumToString( va, numFormat ) + '°';
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
      ty += lineHeight;

    }

    g.SelectTrans( oldTrans );

  }

  // show refraction data

  if (m.showDataRefraction && m.IsShowGlobe() && (Math.abs(m.refractionCoeff) > 0.001 || (m.showData && m.NObjects[objIx] > 0))) {

    function refractionClass( k ) {
      var r_class = '';
      if (k >= 0.78) {
        r_class = 'extreme';
      } else if (k >= 0.58) {
        r_class = 'severe';
      } else if (k >= 0.38) {
        r_class = 'strong';
      } else if (k >= 0.18) {
        r_class = 'medium';
      } else if (k >= 0.12) {
        r_class = 'standard';
      } else if (k > 0.0001) {
        r_class = 'weak';
      } else if (k < 0) {
        r_class = 'negative';
      }
      if (r_class != '') r_class = '(' + r_class + ')';
      return r_class;
    }

    function tempGradClass( dt ) {
      var t_class = '';
      if (dt < -0.01) {
        t_class = 'instable Layer';
      } else {
        t_class = 'stable Layer';
      }
      if (dt > 0) {
        t_class += '; Inversion';
      }
      if (t_class != '') t_class = '(' + t_class + ')';
      return t_class;
    }

    var oldTrans = g.SelectTrans( 'viewport' );

    var tx = tm;
    var hAl = 'left';
    if (m.IsShowBothModels()) {
      tx += g.VpInnerWidth / 2;
    }

    g.SetTextAttr( 'Arial', textSize, 'darkred', 'normal', 'normal', hAl, 'bottom', 4 );
    if (m.refractionCoeff < -1e-5) {
      g.SetBgColor( '#ffeeaa' );
    } else if (m.refractionCoeff > 1e-5) {
      g.SetBgColor( '#ffff80' );
    } else {
      g.SetBgColor( 'white' );
    }

    var oldFormatMode = numFormat.Mode;
    var oldPrecision = numFormat.Precision;
    numFormat.Mode = 'fix0';

    var ty = g.VpInnerHeight - lineHeight - g.ScalePix(5);
    var txt = 'Refraction k = ' + NumFormatter.NumToString( m.refractionCoeff, numFormat );
    if (m.refractionSync == 2) {
      txt += ' (Standard Atmosphere)';
    } else {
      txt += ' ' + refractionClass( m.refractionCoeff );
    }
    g.TextBox( txt, tx, ty, 2 );
    g.Text( txt, tx, ty );

    if (m.Height < 84852) {
      numFormat.Precision = 5;
      var txt = '';
      if (m.tempGradient < 1) {
        txt = 'Temp. Gradient dT/dh = ' + NumFormatter.NumToString( AVal(m.tempGradient), numFormat ) + AUnit()+ ' ';
      }
      ty += lineHeight;
      txt += tempGradClass( m.tempGradient );
      g.TextBox( txt, tx, ty, 2 );
      g.Text( txt, tx, ty );
    }

    numFormat.Mode = oldFormatMode;
    numFormat.Precision = oldPrecision;

    g.SelectTrans( oldTrans );
  }

  // draw overlay image
  if (m.OverlayImage != '' && m.OverlayImageAlpha > 0) {
    g.SetAlpha( m.OverlayImageAlpha );
    var imgUrl = m.OverlayImage;
    if (imgUrl.indexOf('http') != 0) imgUrl = MEDIA_FOLDER + imgUrl;
    g.DrawImage( imgUrl, 'canvas-in' );
    g.SetAlpha( m.AlphaOpaque )
  }

  // draw demo text and description
  if (m.DemoText != '') {
    var oldTrans = g.SelectTrans( 'viewport' );

    g.SetTextAttr( 'Arial', 20, 'black', 'normal', 'normal', 'center', 'top', 10 );
    g.SetBgColor( 'white' );

    if (m.OverlayImage != '') {
      g.TextBox( m.DemoText, g.VpInnerWidth/2, 0, 2 );
    }
    g.Text( m.DemoText, g.VpInnerWidth/2, 0 );

    if (m.Description != '') {
      g.SetTextAttr( 'Arial', 16, 'black', 'normal', 'normal', 'center', 'top', 10 );
      if (m.OverlayImage != '') {
        g.TextBox( m.Description, g.VpInnerWidth/2, g.ScalePix(30), 2 );
      }
      g.Text( m.Description, g.VpInnerWidth/2, g.ScalePix(30) );
    }

    g.SelectTrans( oldTrans );
  }

}

function DrawObjectsBehindHorizon( objIx, g ) {
  var m = CurveApp;

  // init some loop variables used in DrawObjectsBeforeHorizon too
  if (m.NObjects[objIx] == 0) return;
  m.aObjDelta[objIx] = m.ObjDeltaDist[objIx] / m.refractionRadius;
  m.aObjFirst[objIx] = m.ObjDist[objIx] / m.refractionRadius;
  m.aObjLast[objIx] = (m.NObjects[objIx]-1) * m.aObjDelta[objIx] + m.aObjFirst[objIx];
  m.MaxNObjectsToDraw[objIx] = m.NObjects[objIx];
  m.NObjectsDrawn[objIx] = 0;
  if (m.aObjLast[objIx] > Math.PI) {
    m.MaxNObjectsToDraw[objIx] = Math.floor( Math.PI / m.aObjDelta[objIx] );
    m.aObjLast[objIx] = (m.MaxNObjectsToDraw[objIx]-1) * m.aObjDelta[objIx] + m.aObjFirst[objIx];
    m.NObjectsDrawn[objIx] = m.NObjects[objIx] - m.MaxNObjectsToDraw[objIx];
  }
  m.aObjCurr[objIx] = m.aObjLast[objIx];

  // if only objects before horizon exist skip this function
  var aSide = m.ObjSidePos[objIx] / m.refractionRadius;
  var sideOnDisk = (m.refractionRadius - m.hDip) * Math.tan( aSide );
  var aClip = 0;
  if (m.rDisk > Math.abs(sideOnDisk)) {
    // not all objects are behind the horizon, so find the first object that is behind
    var distOnDisk = Math.sqrt( m.rDisk * m.rDisk - sideOnDisk * sideOnDisk );
    aClip = Math.asin( distOnDisk / m.refractionRadius );
  }
  if (m.aObjCurr[objIx] <= aClip) return;
  m.isHiddenObj[objIx] = true;

  // draw all objects behind horizon only and clip then at horizon
  g.SetAlpha( m.AlphaOpaque );
  g.SetAreaAttr( '#ffa', '#666', 1 );
  g.SaveTrans3D();
  var aLimit = m.aObjFirst[objIx] - m.aObjDelta[objIx]/2;
  while (m.aObjCurr[objIx] > aClip && m.aObjCurr[objIx] > aLimit ) {
    DrawShapeVariants( objIx, g, m, true );
    m.NObjectsDrawn[objIx]++;
    m.aObjCurr[objIx] -= m.aObjDelta[objIx];
  }
  g.RestoreTrans3D();

  // clip at horizon
  if (m.NObjectsDrawn[objIx] > 0) {
    g.SetPlane( [0,0,-m.zDisk], [1,0,0], [0,1,0] );
    g.SetBgColor( 'white' );
    g.CircleOnPlane( 0, 0, m.rDisk, 2 );
  }
}

function DrawObjectsBeforeHorizon( objIx, g, bOnGlobe ) {
  var m = CurveApp;
  if (m.NObjects[objIx] == 0) return;

  // Init parameters for flat earth here, because DrawObjectsBehindHorizon is not caller in that case
  if (!bOnGlobe) {
    m.aObjDelta[objIx] = m.ObjDeltaDist[objIx] / m.refractionRadius;
    m.aObjFirst[objIx] = m.ObjDist[objIx] / m.refractionRadius;
    m.aObjLast[objIx] = (m.NObjects[objIx]-1) * m.aObjDelta[objIx] + m.aObjFirst[objIx];
    m.MaxNObjectsToDraw[objIx] = m.NObjects[objIx];
    m.aObjCurr[objIx] = m.aObjLast[objIx];
    m.NObjectsDrawn[objIx] = 0;
  }

  var aLimit = m.aObjFirst[objIx] - m.aObjDelta[objIx]/2;
  if (m.aObjCurr[objIx] < aLimit) return;
  m.isHiddenObj[objIx] = false;

  g.SetAlpha( m.AlphaOpaque );
  g.SetAreaAttr( 'yellow', 'black', 1 );
  g.SaveTrans3D();
  while (m.aObjCurr[objIx] >= aLimit) {
    DrawShapeVariants( objIx, g, m, bOnGlobe );
    m.aObjCurr[objIx] -= m.aObjDelta[objIx];
    m.NObjectsDrawn[objIx]++;
  }
  g.RestoreTrans3D();
}

function SetTrans( objIx, g, m, lng, lat, size, alt, bOnGlobe ) {
  var d = 0;
  var s = 0;
  var xs = m.IsShowBothModelsMirror() && !bOnGlobe ? -m.ObjSize[objIx] : m.ObjSize[objIx];
  g.ResetTrans3D();
  g.TransScale3D( xs, m.ObjSize[objIx], m.ObjSize[objIx] );
  g.TransMove3D( 0, 0, m.refractionRadius+alt );
  if (bOnGlobe) {
    g.TransRotateY3D( lat );
    g.TransRotateX3D( -lng );
  } else {
    d = lng * m.refractionRadius;
    s = lat * m.refractionRadius;
    if (m.IsShowBothModelsMirror()) s *= -1;
  }
  g.TransMove3D( s, d, -(m.refractionRadius+m.Height) );
}

function DrawShapeVariants( objIx, g, m, bOnGlobe ) {

  // size: 0 -> lin, 1 -> alt, 2 -> rand, 3 -> cos, 4 -> sin

  var size = 1;
  var sizeType = m.ObjSizeType[objIx];
  var objType = m.ObjType[objIx];

  if (objType == 0) {
    // M-Rod only scales linearly with size, size type is ignored and ObjSizeVar defines the scale size (num of divisions)

  } else if (sizeType == 1) {
    // lin
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      size = 1 + m.ObjSizeVar[objIx] * (2 / (m.MaxNObjectsToDraw[objIx] - 1) * (m.MaxNObjectsToDraw[objIx]-1 - m.NObjectsDrawn[objIx]) - 1);
    }

  } else if (sizeType == 1) {
    // alt
    size = 1 + m.ObjSizeVar[objIx] * Math.cos( Math.PI * m.NObjectsDrawn[objIx] );

  } else if (sizeType == 2) {
    // rand
    size = 1 + m.ObjSizeVar[objIx] * ( (64894678.9798467 * Math.sqrt(m.NObjectsDrawn[objIx])) % 2 - 1 );

  } else if (sizeType == 3) {
    // cos
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      size = 1 - m.ObjSizeVar[objIx] * Math.cos( 2 * Math.PI * m.NObjectsDrawn[objIx]/(m.MaxNObjectsToDraw[objIx]-1) );
    } else {
      size = 1 + m.ObjSizeVar[objIx];
    }

  } else if (sizeType == 4) {
    // sin
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      size = 1 - m.ObjSizeVar[objIx] * Math.sin( 2 * Math.PI * m.NObjectsDrawn[objIx]/(m.MaxNObjectsToDraw[objIx]-1) );
    } else {
      size = 1;
    }
  }

  // make size varying from 0..1
  size = (size + 1 - Math.abs(m.ObjSizeVar[objIx])) / 2;
  if (size < 0) size *= -1;

  if (objType == 0) {
    // dont scale M-Rod by sizeVar
    size = 1;
  }

  var sizeHor = 1;
  if (objType == 0) {
    // M-Rod: scale width proportionaly
    sizeHor = size;
  }

  var sideVar = m.ObjSideVar[objIx] / m.refractionRadius / 2;
  var side = m.ObjSidePos[objIx] / m.refractionRadius;
  var sideType = m.ObjSideType[objIx];
  if (sideType == 0) {
    // lin
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      sideVar *= (2 / (m.MaxNObjectsToDraw[objIx] - 1) * (m.MaxNObjectsToDraw[objIx]-1 - m.NObjectsDrawn[objIx]) - 1);
    }

    SetTrans( objIx, g, m, m.aObjCurr[objIx], side+sideVar, m.ObjSize[objIx], 0, bOnGlobe );
    g.SetPlane( [0,0,0], [sizeHor,0,0], [0,0,size] );
    DrawShape( objIx, g, m, bOnGlobe );

  } else if (sideType == 1) {
    // 2col
    var pos1 = side - sideVar;
    var pos2 = side + sideVar;
    if (Math.abs(pos1) < Math.abs(pos2)) {
      // invert drawing sequence
      var tmp = pos1;
      pos1 = pos2;
      pos2 = tmp;
    }
    // assert: pos1 is farther to the side than pos2 -> |pos1| >= |pos2|
    m.Col = 1;
    g.SetPlane( [0,0,0], [sizeHor,0,0], [0,0,size] );
    SetTrans( objIx, g, m, m.aObjCurr[objIx], pos1, m.ObjSize[objIx], 0, bOnGlobe );
    DrawShape( objIx, g, m, bOnGlobe );
    m.Col = 2;
    SetTrans( objIx, g, m, m.aObjCurr[objIx], pos2, m.ObjSize[objIx], 0, bOnGlobe );
    g.SetPlane( [0,0,0], [sizeHor,0,0], [0,0,size] );
    DrawShape( objIx, g, m, bOnGlobe );

  } else if (sideType == 2) {
    // rand
    sideVar *= (186573.6498496 * Math.sqrt(m.NObjectsDrawn[objIx]) % 2) - 1;
    g.SetPlane( [0,0,0], [sizeHor,0,0], [0,0,size] );
    SetTrans( objIx, g, m, m.aObjCurr[objIx], side+sideVar, m.ObjSize[objIx], 0, bOnGlobe );
    DrawShape( objIx, g, m, bOnGlobe );

  } else if (sideType == 3) {
    // cos
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      sideVar *= Math.cos( 2 * Math.PI * m.NObjectsDrawn[objIx]/(m.MaxNObjectsToDraw[objIx]-1) );
    }
    g.SetPlane( [side,0,0], [sizeHor,0,0], [0,0,size] );
    SetTrans( objIx, g, m, m.aObjCurr[objIx], side+sideVar, m.ObjSize[objIx], 0, bOnGlobe );
    DrawShape( objIx, g, m, bOnGlobe );

  } else if (sideType == 4) {
    // sin
    if (m.MaxNObjectsToDraw[objIx] > 1) {
      sideVar *= Math.sin( 2 * Math.PI * m.NObjectsDrawn[objIx]/(m.MaxNObjectsToDraw[objIx]-1) );
    } else {
      sideVar = 0;
    }
    g.SetPlane( [side,0,0], [sizeHor,0,0], [0,0,size] );
    SetTrans( objIx, g, m, m.aObjCurr[objIx], side+sideVar, m.ObjSize[objIx], 0, bOnGlobe );
    DrawShape( objIx, g, m, bOnGlobe );

  }
}

function DrawShape( objIx, g, m, bOnGlobe ) {
  // plane and transformations are set

  function setBgColor( normalColor, hiddenColor ) {
    if (m.isHiddenObj[objIx]) {
      g.SetBgColor( hiddenColor );
    } else {
      g.SetBgColor( normalColor );
    }
  }

  // ObjType: 0 = M-Rod, 1 = Mountain
  var objType = m.ObjType[objIx];

  if (objType == 0) {
    // M-Rod
    var sizeVar = m.GetObjectSizeVar( objIx );
    setBgColor( 'yellow', '#ff4' );
    g.RectOnPlane( -0.1, 0, 0.1, sizeVar, 2 );
    setBgColor( 'red', '#f66' );
    for (var z = 0.5; z < sizeVar; z += 1) {
      var t = z + 0.5;
      if (t > sizeVar) t = sizeVar;
      g.RectOnPlane( -0.1, z, 0, t, 2 );
    }
    for (var z = 0.1; z < sizeVar; z += 0.2 ) {
      var t = z + 0.1;
      if (t > sizeVar) t = sizeVar;
      g.RectOnPlane( 0, z, 0.1, t, 2 );
    }
    g.RectOnPlane( -0.1, 0, 0.1, sizeVar, 1 );

    if (m.showData && m.IsNearestObject(objIx)) {
      // label object size
      var x1 = 0.12;
      var x2 = 0.22;
      if (m.IsShowBothModelsMirror() && !bOnGlobe) {
        x1 = -0.12;
        x2 = -0.22;
      }
      g.LineOnPlane( x1, sizeVar, x2, sizeVar );
      g.SetTextAttr( 'Arial', 12, 'black', 'normal', 'normal', 'left', 'bottom', 0 );
      var numFormat = { Mode: 'fix', Precision: 2, UsePrefix: false, Units: '' };
      var txt = NumFormatter.NumToString( HVal(m.ObjSize[objIx]*sizeVar), numFormat ) + HUnit();
      g.TextOnPlane( txt, x2, sizeVar );
    }

  } else if (objType == 1) {
    // mountain
    var mi = m.NObjectsDrawn[objIx] % 3;
    var mr = m.NObjectsDrawn[objIx] % 6;
    mi = (mi + objIx) % 3;
    if (mr > 2) {
      // mirror x axes
      g.Plane.XDir[0] *= -1;
    }
    if (mi == 0) {
      setBgColor( '#dbe2ed', '#e9f0fc' );
      g.PolygonOnPlane( [ -1.3, -0.8, -0.6, -0.3, -0.1, 0.1, 0.2, 0.4, 0.5, 0.6, 1.3, -1.3 ], [ 0, 0.6, 0.6, 1, 0.9, 0.7, 0.7, 0.9, 0.8, 0.85, 0, 0 ], 3 );
      g.PolygonOnPlane( [ -0.3, 0, 0.3, 0.5, 0.8 ], [ 0.1, 0.2, 0.5, 0.5, 0.4 ], 1 );
      g.PolygonOnPlane( [ -0.6, -0.3, 0.1 ], [ 0.6, 0.4, 0.3 ], 1 );
      g.PolygonOnPlane( [ -0.2, -0.1, 0.1 ], [ 0.5, 0.6, 0.7 ], 1 );
      g.PolygonOnPlane( [ 0.1, 0.2, 0.4, 0.5 ], [ 0.5, 0.6, 0.7, 0.8 ], 1 );

    } else if (mi == 1) {
      setBgColor( '#92aace', '#c4d2e8' );
      g.PolygonOnPlane( [ -1.3, -0.8, -0.7, -0.2, 0.1, 0.2, 0.5, 0.6, 0.7, 1.3, -1.3 ], [ 0, 0.5, 0.5, 1, 0.8, 0.7, 0.7, 0.6, 0.6, 0, 0 ], 3 );
      g.PolygonOnPlane( [ -0.7, -0.6, -0.3 ], [ 0.5, 0.5, 0.2 ], 1 );
      g.LineOnPlane( 0.2, 0.7, 0.7, 0.2 );
      g.LineOnPlane( 0.6, 0.6, 1.1, 0.1 );

    } else {
      setBgColor( '#abacb1', '#c8c9cf' );
      g.PolygonOnPlane( [ -1.3, -0.7, -0.6, -0.3, -0.3, -0.2, -0.1, 0.1, 0.3, 0.5, 0.6, 0.8, 1, 1.1, 1.3, -1.3 ],  [ 0, 0.6, 0.6, 0.8, 0.9, 1, 1, 0.8, 0.5, 0.4, 0.5, 0.5, 0.4, 0.2, 0, 0 ], 3 );
      g.PolygonOnPlane( [ -1, -0.6, -0.4, -0.2, 0 ], [ 0.1, 0.4, 0.5, 0.5, 0.3 ], 1 );
      g.PolygonOnPlane( [ -0.6, -0.5, -0.4 ], [ 0.6, 0.6, 0.5 ], 1 );
      g.PolygonOnPlane( [ -0.2, -0.1, 0 ], [ 1, 0.8, 0.4 ], 1 );
      g.PolygonOnPlane( [ 0, 0.3, 0.6, 0.8, 0.9 ], [ 0.1, 0.2, 0.5, 0.4, 0.3 ], 1 );
    }
  }
}

function PointOnEarth( lat, long ) {
  var x = CurveApp.refractionRadius * Math.sin( long );
  var rr = CurveApp.refractionRadius * Math.cos( long );
  var y = rr * Math.sin( lat );
  var z = rr * Math.cos( lat );
  return [ x, y, z - (CurveApp.refractionRadius + CurveApp.Height) ];
}

function PointOnPlane( lat, long ) {
  var x = CurveApp.refractionRadius * Math.sin( long );
  var rr = CurveApp.refractionRadius * Math.cos( long );
  var y = rr * Math.sin( lat );
  return [ x, y, -CurveApp.Height ];
}

</jscript>

{{TabSelectorsTop| CurveSettingsTabs | MarginTop }}
{{TabSel| Basics | BasicsTab }}
{{TabSel| View | ViewsTab }}
{{TabSel| Target 1 | Obj1Tab }}
{{TabSel| Target 2 | Obj2Tab }}
{{TabSel| Refraction&rArr; | RefractionTab }}
{{TabSelButton| Std | StdRefrButton }}
{{TabSelButton| 0 | ZeroRefrButton }}
{{TabSel| Units | UnitsTab }}
{{TabSel| Save/Restore | SaveRestoreTab }}
{{TabSelButton| Reset All | ResetButton }}
{{EndTabSelectors}}

{{TabBoxes| CurveSettingsTabs }} <comment> Basics Tab </comment>

{{scroll|600px}}

<jscript>

ControlPanels.NewSliderPanel( {
  Name: 'BasicsPanel',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  Format: 'std',
  Digits: DefaultDigits,
  ReadOnly: false,
  PanelFormat: 'InputMediumWidth'

} ).AddValueSliderField( {
  Name: 'Height',
  Label: 'Observer Height',
  ValueRef: 'Height',
  SliderValueRef: 'HeightSlider',
  UnitsData: 'HeightUnits',
  Color: 'blue',
  Min: 0,
  Max: 1,
  Inc: 0.1,

} ).AddValueSliderField( {
  Name: 'ObjDist',
  Label: 'Target Distance',
  ValueRef: 'ObjDist[0]',
  SliderValueRef: 'Slider_ObjDist_Log[0]',
  UnitsData: 'LengthUnits',
  Color: 'orange',
  Min: 0,
  Max: 5,
  Inc: 100,

} ).AddValueSliderField( {
  Name: 'TargetSize',
  Label: 'Target Size',
  ValueRef: 'ObjSize[0]',
  SliderValueRef: 'Slider_ObjSize_Log[0]',
  UnitsData: 'HeightUnits',
  Color: 'green',
  Min: 0,
  Max: 4,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'Refraction',
  ValueRef: 'refractionCoeff',
  SliderValueRef: 'refractionSlider',
  Color: 'red',
  Min: -1,
  Max: 1,
  Inc: 0.01,

} ).AddValueSliderField( {
  Name: 'FocalLength',
  ValueRef: 'FocalLengthField',
  SliderValueRef: 'FocalLengthSlider',
  Label: 'Zoom f',
  Units: 'mm',
  Color: 'black',
  Min: 0,
  Max: 1,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ViewAngle',
  Label: 'View&ang;',
  ValueRef: 'ViewAngleField',
  SliderValueRef: 'ViewAngleSlider',
  Units: '°',
  Color: 'black',
  Min: 0.247517,
  Max: 91.6,
  Inc: 0.1,

} ).AddValueSliderField( {
  Name: 'RefDistance',
  Label: 'VP Dist',
  EnabledRef: function() { return CurveApp.Flerspective > 0 && CurveApp.IsShowFlatEarth(); },
  Format: 'std',
  Digits: 5,
  Units: 'km',
  Mult: 1000,
  Color: 'green',
  Min: 1000,
  Max: 200000,
  Inc: 1,

} ).Render();

ControlPanels.NewPanel( {
  Name: 'OptionsPanel',
  ModelRef: 'CurveApp',
  NCols: 2,
  OnModelChange: UpdateAll

} ).AddRadiobuttonField( {
  Name: 'showModel',
  Label: 'Model',
  ValueType: 'int',
  Items: [
    {
      Name: 'Globe',
      Value: 1
    }, {
      Name: 'FE',
      Value: 2
    }, {
      Name: 'Globe+FE',
      Value: 3
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'viewcenterHorizon',
  Label: 'Camera Aim',
  ValueType: 'int',
  Items: [
    {
      Name: 'Horizon',
      Value: 0
    }, {
      Name: 'Eye-Level',
      Value: 3
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'Flerspective',
  Label: 'Perspective',
  EnabledRef: function() { return CurveApp.IsShowFlatEarth(); },
  Link: '[[Flat Earth Perspective]]',
  ValueType: 'int',
  Items: [
    {
      Name: 'Normal',
      Value: 0
    }, {
/*
      Name: 'Correction',
      Value: 2
    }, {
*/
      Name: 'Flerspective',
      Value: 1
    }
  ]

} ).AddCheckboxField( {
  Name: 'ShowData',
  Label: 'Show Data',
  Items: [
    {
      Name: 'showDataObject',
      Text: 'Object',
    }, {
      Name: 'showDataRefraction',
      Text: 'Refr.',
    }, {
      Name: 'showDataHorizon',
      Text: 'Horizon',
    }
  ]

} ).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Views Tab </comment>

{{scroll|600px}}

<jscript>

ControlPanels.NewSliderPanel( {
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  Format: 'std',
  Digits: DefaultDigits,
  ReadOnly: false,
  PanelFormat: 'InputMediumWidth'

} ).AddValueSliderField( {
  Name: 'Height',
  ValueRef: 'Height',
  SliderValueRef: 'HeightSlider',
  UnitsData: 'HeightUnits',
  Color: 'blue',
  Min: 0,
  Max: 1,
  Inc: 0.1,

} ).AddValueSliderField( {
  Name: 'FocalLength',
  ValueRef: 'FocalLengthField',
  SliderValueRef: 'FocalLengthSlider',
  Label: 'Zoom f',
  Units: 'mm',
  Color: 'black',
  Min: 0,
  Max: 1,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ViewAngle',
  Label: 'View&ang;',
  ValueRef: 'ViewAngleField',
  SliderValueRef: 'ViewAngleSlider',
  Units: '°',
  Color: 'black',
  Min: 0.82,
  Max: 91.6,
  Inc: 0.1,

} ).AddValueSliderField( {
  Name: 'Pan',
  ValueRef: 'Pan',
  SliderValueRef: 'PanSlider',
  Format: 'std',
  Digits: 5,
  Units: '°',
  Color: 'green',
  Min: -1,
  Max: 1,
  Inc: 0.1,

} ).AddValueSliderField( {
  Name: 'Tilt',
  ValueRef: 'Tilt',
  SliderValueRef: 'TiltSlider',
  Format: 'std',
  Digits: 5,
  Units: '°',
  Color: 'green',
  Min: -1,
  Max: 1,
  Inc: 0.1,

} ).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Objects-1 Tab </comment>

{{scroll|600px}}

<jscript>

ControlPanels.NewSliderPanel( {
  Name: 'Object-Sliders0',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  Format: 'std',
  Digits: DefaultDigits,
  ReadOnly: false,
  PanelFormat: 'InputMediumWidth'

} ).AddValueSliderField( {
  Name: 'NObjects',
  ValueRef: 'NObjects[0]',
  Label: 'NObjects',
  Color: 'black',
  Format: 'fix',
  Digits: 0,
  Min: 0,
  Max: 200,
  Steps: 200,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ObjDist',
  Label: 'Dist',
  ValueRef: 'ObjDist[0]',
  SliderValueRef: 'Slider_ObjDist_Log[0]',
  UnitsData: 'LengthUnits',
  Color: 'blue',
  Min: 0,
  Max: 5,
  Inc: 100,

} ).AddValueSliderField( {
  Name: 'ObjSize',
  ValueRef: 'ObjSize[0]',
  SliderValueRef: 'Slider_ObjSize_Log[0]',
  UnitsData: 'HeightUnits',
  Color: 'red',
  Min: 0,
  Max: 4,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ObjDeltaDist',
  Label: 'DeltaDist',
  ValueRef: 'ObjDeltaDist[0]',
  SliderValueRef: 'Slider_ObjDeltaDist_Log[0]',
  UnitsData: 'LengthUnits',
  Color: 'blue',
  Min: 0,
  Max: 4,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSidePos',
  Label: 'SidePos',
  ValueRef: 'ObjSidePos[0]',
  SliderValueRef: 'Slider_ObjSidePos_Log[0]',
  UnitsData: 'LengthUnits',
  Color: 'green',
  Min: -3,
  Max: 3,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSideVar',
  Label: 'SideVar',
  ValueRef: 'ObjSideVar[0]',
  SliderValueRef: 'Slider_ObjSideVar_Log[0]',
  UnitsData: 'LengthUnits',
  Color: 'green',
  Min: -5,
  Max: 5,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSizeVar',
  ValueRef: 'ObjSizeVar[0]',
  Label: 'SizeVar',
  Units: '%',
  Mult: 0.01,
  Color: 'red',
  Min: -1,
  Max: 1,
  Inc: 0.01,

} ).Render();

</jscript>

{{end scroll}}

<style>
#Object-Sliders0 .Row6 { background-color: #dfd; }
#Object-Sliders0 .Row7 { background-color: #fdd; }
#Object-Options0 .Row2 { background-color: #dfd; }
#Object-Options0 .Row2 .FieldGrid { background-color: #dfd; }
#Object-Options0 .Row2 .FieldCell { border-color: #dfd; }
#Object-Options0 .Row3 { background-color: #fdd; }
#Object-Options0 .Row3 .FieldGrid { background-color: #fdd; }
#Object-Options0 .Row3 .FieldCell { border-color: #fdd; }
</style>


{{scroll}}

<jscript>

ControlPanels.NewPanel( {
  Name: 'Object-Options0',
  ModelRef: 'CurveApp',
  NCols: 1,
  OnModelChange: UpdateAll

} ).AddRadiobuttonField( {
  Name: 'ObjType[0]',
  Label: 'ObjType',
  ValueType: 'int',
  Items: [
    {
      Name: 'M-Rod',
      Value: 0
    }, {
      Name: 'Mountain',
      Value: 1
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'ObjSideType[0]',
  Label: 'SideVar',
  ValueType: 'int',
  Items: [
    {
      Name: 'Lin',
      Value: 0
    }, {
      Name: '2-Col',
      Value: 1
    }, {
      Name: 'Rand',
      Value: 2
    }, {
      Name: 'Cos',
      Value: 3
    }, {
      Name: 'Sin',
      Value: 4
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'ObjSizeType[0]',
  Label: 'SizeVar',
  ValueType: 'int',
  Items: [
    {
      Name: 'Lin',
      Value: 1
    }, {
      Name: 'Rand',
      Value: 2
    }, {
      Name: 'Cos',
      Value: 3
    }, {
      Name: 'Sin',
      Value: 4
    }
  ]

} ).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Objects-2 Tab </comment>

{{scroll|600px}}

<jscript>

ControlPanels.NewSliderPanel( {
  Name: 'Object-Sliders1',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  Format: 'std',
  Digits: DefaultDigits,
  ReadOnly: false,
  PanelFormat: 'InputMediumWidth'

} ).AddValueSliderField( {
  Name: 'NObjects',
  ValueRef: 'NObjects[1]',
  Label: 'NObjects',
  Color: 'black',
  Format: 'fix',
  Digits: 0,
  Min: 0,
  Max: 200,
  Steps: 200,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ObjDist',
  Label: 'Dist',
  ValueRef: 'ObjDist[1]',
  SliderValueRef: 'Slider_ObjDist_Log[1]',
  UnitsData: 'LengthUnits',
  Color: 'blue',
  Min: 0,
  Max: 5,
  Inc: 100,

} ).AddValueSliderField( {
  Name: 'ObjSize',
  ValueRef: 'ObjSize[1]',
  SliderValueRef: 'Slider_ObjSize_Log[1]',
  UnitsData: 'HeightUnits',
  Color: 'red',
  Min: 0,
  Max: 4,
  Inc: 1,

} ).AddValueSliderField( {
  Name: 'ObjDeltaDist',
  Label: 'DeltaDist',
  ValueRef: 'ObjDeltaDist[1]',
  SliderValueRef: 'Slider_ObjDeltaDist_Log[1]',
  UnitsData: 'LengthUnits',
  Color: 'blue',
  Min: 0,
  Max: 4,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSidePos',
  Label: 'SidePos',
  ValueRef: 'ObjSidePos[1]',
  SliderValueRef: 'Slider_ObjSidePos_Log[1]',
  UnitsData: 'LengthUnits',
  Color: 'green',
  Min: -3,
  Max: 3,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSideVar',
  Label: 'SideVar',
  ValueRef: 'ObjSideVar[1]',
  SliderValueRef: 'Slider_ObjSideVar_Log[1]',
  UnitsData: 'LengthUnits',
  Color: 'green',
  Min: -5,
  Max: 5,
  Inc: 10,

} ).AddValueSliderField( {
  Name: 'ObjSizeVar',
  ValueRef: 'ObjSizeVar[1]',
  Label: 'SizeVar',
  Units: '%',
  Mult: 0.01,
  Color: 'red',
  Min: -1,
  Max: 1,
  Inc: 0.01,

} ).Render();

</jscript>

{{end scroll}}

<style>
#Object-Sliders1 .Row6 { background-color: #dfd; }
#Object-Sliders1 .Row7 { background-color: #fdd; }
#Object-Options1 .Row2 { background-color: #dfd; }
#Object-Options1 .Row2 .FieldGrid { background-color: #dfd; }
#Object-Options1 .Row2 .FieldCell { border-color: #dfd; }
#Object-Options1 .Row3 { background-color: #fdd; }
#Object-Options1 .Row3 .FieldGrid { background-color: #fdd; }
#Object-Options1 .Row3 .FieldCell { border-color: #fdd; }
</style>


{{scroll}}

<jscript>

ControlPanels.NewPanel( {
  Name: 'Object-Options1',
  ModelRef: 'CurveApp',
  NCols: 1,
  OnModelChange: UpdateAll

} ).AddRadiobuttonField( {
  Name: 'ObjType[1]',
  Label: 'ObjType',
  ValueType: 'int',
  Items: [
    {
      Name: 'M-Rod',
      Value: 0
    }, {
      Name: 'Mountain',
      Value: 1
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'ObjSideType[1]',
  Label: 'SideVar',
  ValueType: 'int',
  Items: [
    {
      Name: 'Lin',
      Value: 0
    }, {
      Name: '2-Col',
      Value: 1
    }, {
      Name: 'Rand',
      Value: 2
    }, {
      Name: 'Cos',
      Value: 3
    }, {
      Name: 'Sin',
      Value: 4
    }
  ]

} ).AddRadiobuttonField( {
  Name: 'ObjSizeType[1]',
  Label: 'SizeVar',
  ValueType: 'int',
  Items: [
    {
      Name: 'Lin',
      Value: 1
    }, {
      Name: 'Rand',
      Value: 2
    }, {
      Name: 'Cos',
      Value: 3
    }, {
      Name: 'Sin',
      Value: 4
    }
  ]

} ).Render();

</jscript>

{{end scroll}}

{{NextTabBox}} <comment> Refraction Tab </comment>

{{scroll|600px}}

<jscript>

var BaroConst = {
  hIx: 0,

  // 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 - 1;
  },

  // 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 ConvertUnit = {
  ms_kt: function( v ) { return v * 1.944; },
  kt_ms: function( v ) { return v / 1.944; },
  ft_m:   function( h ) { return h * 0.3048; },
  m_ft:   function( h ) { return h / 0.3048; },
  K_C:    function( t ) { return t - 273.15; },
  C_K:    function( t ) { return t + 273.15; },
  K_F:    function( t ) { return t * 1.8 - 459.67; },
  F_K:    function( t ) { return (t + 459.67) / 1.8; }
}

var BaroModel = {

  h: -1,  // used for optimisation, CurveApp.Height is used instead,
  T_C: 0,
  T: 0,
  p: 0,
  rho: 0,
  alpha: 0,

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

  PressureOfH: function( 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;
    }
  },

  DensityOfH: function( 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() {
    if (this.h == CurveApp.Height) return;
    BaroConst.SetAltRange( CurveApp.Height );
    this.alpha = BaroConst.alpha();
    this.T = this.TempOfH( CurveApp.Height );
    this.T_C = this.T - 273.15;
    this.p = this.PressureOfH( CurveApp.Height );
    //this.rho = this.DensityOfH( CurveApp.Height );  not used
    this.h = CurveApp.Height;
  }

};

CurveApp.BaroModel = BaroModel;

ControlPanels.NewPanel( {
  Name: 'Refraction-Panel',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  NCols: 2,
  ReadOnly: false,
  Format: 'std',
  Digits: DefaultDigits

} ).AddSliderField( {
  Name: 'refractionSlider',
  Label: 'Refraction',
  Color: 'red',
  ColSpan: 3,
  Min: -1,
  Max: 1,

} ).AddTextField( {
  Name: 'refractionCoeff',
  Label: 'Coeff. k',
  Inc: 0.01,

} ).AddTextField( {
  Name: 'tempGradient',
  Label: 'dT/dh',
  UnitsData: 'GradientUnits',
  Inc: 0.0001,

} ).AddTextField( {
  Name: 'refractionFact',
  Label: 'Factor a',
  Inc: 0.001,

} ).AddTextField( {
  Name: 'temperatureC',
  Label: 'Temp. T',
  Units: '°C',
  Inc: 0.1,

} ).AddTextField( {
  Name: 'refractionRadius',
  Label: 'Radius R\'',
  UnitsData: 'BigLengthUnits',
  Inc: 100000,

} ).AddTextField( {
  Name: 'pressure',
  Label: 'Press. P',
  Units: 'mBar',
  Inc: 10,

} ).AddRadiobuttonField( {
  Name: 'refractionSync',
  Label: 'BaroLink',
  ColSpan: 3,
  ValueType: 'int',
  Items: [
    {
      Name: 'off',
      Value: 0
    }, {
      Name: 'T,P',
      Value: 1
    }, {
      Name: 'Std-Atm',
      Value: 2
    }, {
      Name: 'k=0.13',
      Value: 3
    }, {
      Name: 'k=0.17',
      Value: 4
    }, {
      Name: 'a=7/6',
      Value: 5
    }, {
      Name: 'a=7/2',
      Value: 6
    }
  ]
} ).Render();

ControlPanels.NewPanel( {
  Name: 'RefractionBaro-Panel',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  NCols: 2,
  ReadOnly: true,
  Format: 'std',
  Digits: DefaultDigits

} ).AddHeader( {
  Text: 'Std-Atmosphere Barometer: use BaroLink = Std-Atm to link with Refraction',
  ColSpan: 4,

} ).AddSliderField( {
  Name: 'HeightSlider',
  Label: 'Height h',
  Color: 'blue',
  ColSpan: 3,
  Min: 0,
  Max: 1,

} ).AddTextField( {
  Name: 'Height',
  Label: 'Height h',
  UnitsData: 'HeightUnits',
  ReadOnly: false,
  Inc: 0.1,

} ).AddTextField( {
  Name: 'BaroModel.T_C',
  Label: 'T(h)',
  Units: '°C',

} ).AddTextField( {
  Name: 'BaroModel.alpha',
  Label: 'Td/dh(h)',
  UnitsData: 'GradientUnits',

} ).AddTextField( {
  Name: 'BaroModel.p',
  Label: 'P(h)',
  Units: 'mBar',
  Mult: 100,

} ).Render();

</jscript>

{{end scroll}}

Please read the paragraph on [[Flat-Earth: Finding the curvature of the Earth#Refraction|Refraction]] to get familiar with this panel.


{{NextTabBox}} <comment> Units-Calculator Tab </comment>

<jscript>

function CLengthModel() {
  this.m = 0;
  this.km = 0;
  this.sm = 0;
  this.mile = 0;
  this.in = 0;
  this.cm = 0;
  this.ft = 0;
  this.fl = 0;
  this.mLast = 0;
  this.kmLast = 0;
  this.smLast = 0;
  this.mileLast = 0;
  this.inLast = 0;
  this.cmLast = 0;
  this.ftLast = 0;
  this.flLast = 0;
}

CLengthModel.prototype.calcOthersFromMeter = function() {
  this.km = this.m / 1000;
  this.sm = this.m / 1852;
  this.mile = this.m / 1609.344;
  this.in = this.m / 0.0254;
  this.cm = this.m * 100;
  this.ft = this.m / 0.3048;
  this.fl = this.ft / 100;
}

CLengthModel.prototype.Update = function() {
  if (this.m != this.mLast) {
    this.calcOthersFromMeter();
  }
  else if (this.km != this.kmLast) {
    this.m = this.km * 1000;
    this.calcOthersFromMeter();
  }
  else if (this.sm != this.smLast) {
    this.m = this.sm * 1852;
    this.calcOthersFromMeter();
  }
  else if (this.mile != this.mileLast) {
    this.m = this.mile * 1609.344;
    this.calcOthersFromMeter();
  }
  else if (this.in != this.inLast) {
    this.m = this.in * 0.0254;
    this.calcOthersFromMeter();
  }
  else if (this.cm != this.cmLast) {
    this.m  = this.cm / 100;
    this.calcOthersFromMeter();
  }
  else if (this.ft != this.ftLast) {
    this.m = this.ft * 0.3048;
    this.calcOthersFromMeter();
  }
  else if (this.fl != this.flLast) {
    this.m = this.fl * 100 * 0.3048;
    this.calcOthersFromMeter();
  }
  this.mLast = this.m;
  this.kmLast = this.km;
  this.smLast = this.sm;
  this.mileLast = this.mile;
  this.inLast = this.in;
  this.cmLast = this.cm;
  this.ftLast = this.ft;
  this.flLast = this.fl;
}

var LengthModel = new CLengthModel();

function UpdateLengthModel() {
  LengthModel.Update();
  ControlPanels.Update( 'Units-Calculator' );
}

xOnLoad( UpdateLengthModel );

</jscript>

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
    Name: 'Units-Calculator',
    ModelRef: 'LengthModel',
    OnModelChange: UpdateLengthModel,
    NCols: 2,
    Format: 'std',
    Digits: 10
  }

).AddTextField( {
    Name: 'm',
    Label: 'Meter',
    Units: 'm',
    Inc: 1,
  }

).AddTextField( {
    Name: 'km',
    Label: 'Kilometer',
    Units: 'km',
    Inc: 1,
  }

).AddTextField( {
    Name: 'sm',
    Label: 'Nautical Mile',
    Units: 'sm',
    Inc: 1,
  }

).AddTextField( {
    Name: 'mile',
    Label: 'Statute Mile',
    Units: 'mi',
    Inc: 1,
  }

).AddTextField( {
    Name: 'in',
    Label: 'Inch',
    Units: 'in',
    Inc: 1,
  }

).AddTextField( {
    Name: 'cm',
    Label: 'Centimeter',
    Units: 'cm',
    Inc: 1,
  }

).AddTextField( {
    Name: 'ft',
    Label: 'Feet',
    Units: 'ft',
    Inc: 1,
  }

).AddTextField( {
    Name: 'fl',
    Label: 'Flight Level FL',
    Units: '',
    Inc: 10,
  }

).Render();

</jscript>

{{end scroll}}

Use this Formular to convert between different lengh units. You can Copy/Paste the results into input fields in the other Forms.

{{NextTabBox}} <comment> Save/Restore Tab </comment>

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

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

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

Use '''Get App Url''' to get an URL containing the current App State. Click '''Set App State''' oder copy the URL into any browser address field to go to this page and display the current App State.

<jscript>

function ResetApp() {
  DataX.SetAppState(
    'AdvCurveApp = { "DemoText": "", "Description": "", "Height": 2, "FocalLengthField": 3000, "showModel": 1, "deviceRatio": 1.5, "viewcenterHorizon": 0, "ObjType": [ 0, 0 ], "NObjects": [ 1, 0 ], "ObjDist": [ 12000, 20000 ], "ObjDeltaDist": [ 300, 300 ], "ObjSideType": [ 0, 0 ], "ObjSidePos": [ 0, 0 ], "ObjSideVar": [ 0, 0 ], "ObjSizeType": [ 1, 1 ], "ObjSize": [ 10, 10 ], "ObjSizeVar": [ 0, 0 ], "refractionCoeff": 0, "tempGradient": -0.0343, "refractionSync": 0, "pressure": 1013.25, "temperatureC": 15, "refractionFactMin": 0.5, "refractionFactMax": 10000, "rEarth": 6371000, "rFEarth": 10007543, "showTheodolite": false, "OverlayImage": "", "OverlayImageAlpha": 0.5 }' );
}

function SetStdRefraction() {
  CurveApp.refractionSync = 2;
  UpdateAll();
}

function Set0Refraction() {
  CurveApp.refractionSync = 1;
  CurveApp.refractionCoeff = 0;
  UpdateAll();
}

function HandleUrlCommands() {

  var dataStr = DataX.GetUrlStr( 'tab' );
  if (dataStr != '') {
    if (dataStr == 'View') {
      Tabs.Select( 'CurveSettingsTabs', 0 );
    } else if (dataStr == 'Obj1') {
      Tabs.Select( 'CurveSettingsTabs', 1 );
    } else if (dataStr == 'Obj2') {
      Tabs.Select( 'CurveSettingsTabs', 2 );
    } else if (dataStr == 'Refr') {
      Tabs.Select( 'CurveSettingsTabs', 3 );
    } else if (dataStr == 'Unit') {
      Tabs.Select( 'CurveSettingsTabs', 4 );
    } else if (dataStr == 'SaveRest') {
      Tabs.Select( 'CurveSettingsTabs', 5 );
    }
  }

  Animations.TimeStrech = 1 / DataX.GetUrlNum( 'speed', 1 );
  if (Animations.TimeStrech < 0.01) Animations.TimeStrech = 0.01;
  if (Animations.TimeStrech > 100) Animations.TimeStrech = 100;

}

</jscript>

{{EndTabBoxes}}

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
  Name: 'UnitsPanel',
  ModelRef: 'CurveApp',
  NCols: 1,
  OnModelChange: UpdateAll

} ).AddRadiobuttonField( {
  Name: 'UnitsType',
  Label: 'Units',
  ValueType: 'int',
  Items: [
    {
      Name: 'm (Metric)',
      Value: 0
    }, {
      Name: 'mi/ft (Imperial)',
      Value: 1
    }, {
      Name: 'ft/ft (Imperial)',
      Value: 2
    }
  ]

} ).Render();

</jscript>

{{end scroll}}

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
  Name: 'TargetDataPanel',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  NCols: 2,
  ReadOnly: true,
  Format: 'std',
  Digits: DefaultDigits

} ).AddHeader( {
  Text: 'Nearest Target Data',
  ColSpan: 4,

} ).AddTextField( {
  Name: 'ObjVisi',
  Label: 'Visible',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'ObjHidden',
  Label: 'Hidden',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'ObjAngVisible',
  Label: 'Angular Visible',
  ReadOnly: true,
  Units: '°',

} ).AddTextField( {
  Name: 'ObjAngHidden',
  Label: 'Angular Hidden',
  ReadOnly: true,
  Units: '°',

} ).AddTextField( {
  Name: 'ObjAngSize',
  Label: 'Angular Size',
  ReadOnly: true,
  Units: '°',

} ).AddTextField( {
  Name: 'ObjRefrAng',
  Label: 'Refraction Angle',
  ReadOnly: true,
  Units: '°',

} ).AddTextField( {
  Name: 'ObjLiftAbs',
  Label: 'Lift Absolute',
  ReadOnly: true,
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'ObjLiftRel',
  Label: 'Relative to Horizon',
  ReadOnly: true,
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'VerticalAngle',
  Label: 'Target Top Angle',
  Units: '°',

} ).AddTextField( {
  Name: 'VerticalAngleFE',
  Label: 'Target Top Angle FE',
  Units: '°',

} ).AddTextField( {
  Name: 'ObjAngDist',
  Label: 'Angular Distance &theta;<sup>*</sup>',
  Mult: Math.PI/180,
  Units: '°',

} ).AddTextField( {
  Name: 'ObjNearTilt',
  Label: 'Tilt &theta;<sup>*</sup>',
  Units: '°'

} ).AddTextField( {
  Name: 'objDrop',
  Label: 'Drop<sup>*</sup>',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'bulge',
  Label: 'Bulge Height<sup>*</sup>',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'ObjRealDist',
  Label: 'Distance',
  UnitsData: 'LengthUnits',

} ).Render();

</jscript>

{{end scroll}}

{{scroll}}

<jscript>

ControlPanels.NewPanel( {
  Name: 'HorizonDataPanel',
  ModelRef: 'CurveApp',
  OnModelChange: UpdateAll,
  NCols: 2,
  ReadOnly: true,
  Format: 'std',
  Digits: DefaultDigits

} ).AddHeader( {
  Text: 'Horizon Data',
  ColSpan: 4,

} ).AddTextField( {
  Name: 'dHorizon',
  Label: 'Dist on Surf',
  UnitsData: 'LengthUnits',

} ).AddTextField( {
  Name: 'aDip',
  Label: 'Dip Angle',
  Mult: Math.PI / 180,
  Units: '°'

} ).AddTextField( {
  Name: 'dView',
  Label: 'Dist from Eye',
  UnitsData: 'LengthUnits',

} ).AddTextField( {
  Name: 'rDisk',
  Label: 'Dist on Eye-Lvl',
  UnitsData: 'LengthUnits',

} ).AddTextField( {
  Name: 'hDip',
  Label: 'Drop from Surf',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'zDisk',
  Label: 'Drop from Eye-Lvl',
  UnitsData: 'HeightUnits',

} ).AddTextField( {
  Name: 'sceneWidth',
  Label: 'Horizon Width',
  UnitsData: 'LengthUnits',

} ).AddTextField( {
  Name: 'dDelta',
  Label: 'Grid Spacing',
  UnitsData: 'LengthUnits',

} ).AddTextField( {
  Name: 'rEarth',
  Label: 'Radius Earth',
  UnitsData: 'BigLengthUnits',
  ReadOnly: false

} ).Render();

</jscript>

{{end scroll}}



Weitere Infos zur Seite
Erzeugt Friday, June 22, 2018
von wabis
Zum Seitenanfang
Geändert Sunday, November 25, 2018
von wabis