#INCLUDE JsGraphX3D.inc
#INCLUDE ControlPanel.inc
<jscript>
function EarthModel() {
this.rEarth = 6371000;
this.rFEarth = this.rEarth * Math.PI / 2;
this.nLines = 45;
this.showModel = 1; // 1 -> globe, 2 -> flat, 3 -> both
this.showGrid = 1; // 0 -> none, 1 -> globe, 3 -> both
this.showFlatHorizon = false;
this.showEquator = false;
this.showEyeLevel = true;
this.deviceRatio = 3 / 2; // width / height of device screen
this.sceneWidth = 0;
this.sceneHeight = 0;
this.HeightSlider = 0;
this.HeightSliderLast = 0;
this.HeightRange = 0; // 0 -> log, > 0 -> linear
this.Height = 10000;
this.ViewAngle = 60; // viewAngle in deg
this.ViewAngleField = 60;
this.ViewAngleSlider = 0;
this.ViewAngleSliderLast = 0;
this.Roll = 0;
this.Nick = 0;
this.FocalLength = 0;
this.FocalLengthField = 0;
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.aEarth = 0; // 180 - 2 * alpha
this.dView = 0; // v
this.aDelta = 0; // aDip / nLines
this.posEarth = [ 0, 0, -this.rEarth ];
this.camViewAngle = 0.1; // rad
this.camPos = [ 0, 0, 0 ];
this.camUp = [ 0, 0, 1 ];
this.camViewCenter = [ 0, 1, 0 ];
this.camSceneSize = 1;
this.drawBackGGrid = false;
this.drawBackFGrid = false;
this.drawBackFE = false;
this.Update();
}
EarthModel.prototype.Update = function() {
var pi180 = Math.PI;
var pi90 = pi180 / 2;
var pi45 = pi180 / 4;
var toRad = pi180 / 180;
if ( this.rEarth < 100000 ) this.rEarth = 100000;
this.rFEarth = this.rEarth * Math.PI / 2;
// handle height changes
if ( this.HeightSliderLast != this.HeightSlider ) {
if ( this.HeightRange == 0 ) {
this.Height = Math.pow( 10, 1 + 8 * this.HeightSlider );
} else {
this.Height = this.HeightRange * this.HeightSlider;
}
}
if ( this.Height < 0.1 ) this.Height = 0.1;
if ( this.Height > 1000000000 ) this.Height = 1000000000;
if ( this.HeightRange == 0 ) {
this.HeightSlider = ( Math.log10( this.Height ) - 1 ) / 8;
} else {
this.HeightSlider = this.Height / this.HeightRange;
}
this.HeightSliderLast = this.HeightSlider;
// handle ViewAngle and FocalLength changes
if ( this.FocalLengthSlider != this.FocalLengthSliderLast ) {
this.ViewAngle = 2 * Math.atan( 43.2 / 2 / this.FocalLengthSlider ) / toRad;
} else if ( this.FocalLengthField != this.FocalLength ) {
this.ViewAngle = 2 * Math.atan( 43.2 / 2 / this.FocalLengthField ) / toRad;
} 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 = this.ViewAngle * toRad;
this.FocalLength = 43.2 / ( 2 * Math.tan( this.camViewAngle / 2 ) );
this.FocalLengthField = this.FocalLength;
this.FocalLengthSlider = this.FocalLength;
this.FocalLengthSliderLast = this.FocalLengthSlider;
this.ViewAngleField = this.ViewAngle;
this.ViewAngleSlider = this.ViewAngle;
this.ViewAngleSliderLast = this.ViewAngle;
// compute diverse values
this.aDip = Math.acos( this.rEarth / (this.rEarth + this.Height) );
this.aEarth = pi180 - 2 * this.aDip;
this.rDisk = this.rEarth * Math.sin( this.aDip );
this.dHorizon = this.rEarth * this.aDip;
this.hDisk = this.rEarth * Math.cos( this.aDip );
this.hDip = this.rEarth - this.hDisk;
this.dView = ( this.Height + this.rEarth ) * Math.sin( this.aDip );
this.aDelta = this.aDip / this.nLines;
this.dDelta = this.showModel & 1 ? this.aDelta * this.rEarth : 0;
this.posEarth = [ 0, 0, -(this.rEarth + this.Height) ];
this.zDisk = (this.rEarth + this.Height) - ( this.rEarth * Math.cos( this.aDip ) );
// compute camViewCenter from panning
if ( Math.abs( this.Nick ) < 30 / 30 ) this.Nick = 0;
if ( Math.abs( this.Roll ) < 30 / 30 ) this.Roll = 0;
var dvc = Math.sqrt( this.rDisk * this.rDisk + this.zDisk * this.zDisk );
var avc = this.aDip - this.Nick * toRad;
if ( avc > pi90 ) avc = pi90;
if ( avc < 0 ) avc = 0;
var yvc = dvc * Math.cos( avc );
var zvc = - dvc * Math.sin( avc );
this.camViewCenter = [ 0, yvc, zvc ];
// compute camera up and pos
var a = this.Roll * toRad;
this.camUp = [ Math.sin(a), 0.7, Math.cos(a) ];
this.camPos = [ 0, 0, 0 ];
// 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;
}
// looking down
var vpan = this.Nick * toRad / 2;
this.drawBackGGrid = this.aDip > pi45 + vpan;
this.drawBackFGrid = Math.atan( this.Height / this.rDisk ) > pi45 + vpan;
this.drawBackFE = Math.atan( this.Height / (2 * this.rEarth) ) > pi45 + vpan;
}
var Model = new EarthModel();
function UpdateAll() {
Model.Update();
ControlPanels.Update();
graph.Redraw();
}
var graph = NewGraphX3D( {
Id: 'JsGraph1',
Width: '100%',
Height: '66.67%',
DrawFunc: DrawModel,
AutoReset: true,
AutoClear: true,
AutoScalePix: true
} );
function DrawModel( g ) {
g.SetAngleMeasure( 'rad' );
g.SetViewport( 0, 1, -0.5, -2 );
g.SetGraphClipping( true, '', 0 );
g.SetWindowToCameraScreen();
g.SetCamera( {
SceneSize: Model.camSceneSize,
CamPos: Model.camPos,
CamUp: Model.camUp,
CamViewCenter: Model.camViewCenter,
} );
g.SetCameraZoom( 1 );
g.SetLineAttr( 'black', 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( -Model.sceneWidth/2, -Model.sceneHeight/2, Model.sceneWidth/2, Model.sceneHeight/2, 1 );
// Globe Earth
if ( Model.showModel & 1 ) {
var alpha = 0.6 - 0.5 * (Math.log10( Model.Height ) / 9);
g.SetAlpha( alpha );
g.SetLineAttr( 'blue', 1 );
// show globe grid
if ( Model.showGrid & 1 ) {
// latitude lines
var latMax = Model.aDip;
var latStart = -( Math.floor( latMax / Model.aDelta ) * Model.aDelta );
if (!Model.drawBackGGrid) latStart = 0;
for ( var lat = latStart; lat < latMax; lat += Model.aDelta ) {
var dLatPlaneDisk = Model.hDisk / Math.cos( lat );
var longMax = Math.acos( dLatPlaneDisk / Model.rEarth );
var longStart = -( Math.floor( longMax / Model.aDelta ) * Model.aDelta );
g.NewPoly();
for ( var long = longStart; long < longMax; long += Model.aDelta ) {
g.AddPointToPoly3D( PointOnEarth( lat, long ) );
}
g.AddPointToPoly3D( PointOnEarth( lat, longMax ) );
g.DrawPoly( 1 );
}
// longitude lines
var longMax = Model.aDip;
var longStart = -( Math.floor( longMax / Model.aDelta ) * Model.aDelta );
for ( var long = longStart; long < longMax; long += Model.aDelta ) {
var rLong = Model.rEarth * Math.cos( long );
var latMax = Math.acos( Model.hDisk / rLong );
var latStart = -( Math.floor( latMax / Model.aDelta ) * Model.aDelta );
if (!Model.drawBackGGrid) latStart = 0;
g.NewPoly();
if (Model.drawBackGGrid) g.AddPointToPoly3D( PointOnEarth( -latMax, long ) );
for ( var lat = latStart; lat < latMax; lat += Model.aDelta ) {
g.AddPointToPoly3D( PointOnEarth( lat, long ) );
}
g.AddPointToPoly3D( PointOnEarth( latMax, long ) );
g.DrawPoly( 1 );
}
} // end show globe grid
g.SetLineAttr( 'red', 1 );
// show flat grid
if ( Model.showGrid & 2 ) {
// latitude lines on flat model
var latMax = Model.aDip;
var latStart = -( Math.floor( latMax / Model.aDelta ) * Model.aDelta );
if (!Model.drawBackFGrid) latStart = 0;
for ( var lat = latStart; lat < latMax; lat += Model.aDelta ) {
var dLatPlaneDisk = Model.hDisk / Math.cos( lat );
var longMax = Math.acos( dLatPlaneDisk / Model.rEarth );
var longStart = -( Math.floor( longMax / Model.aDelta ) * Model.aDelta );
g.NewPoly();
for ( var long = longStart; long < longMax; long += Model.aDelta ) {
g.AddPointToPoly3D( PointOnPlane( lat, long ) );
}
g.AddPointToPoly3D( PointOnPlane( lat, longMax ) );
g.DrawPoly( 1 );
}
// longitude lines on flat model
var longMax = Model.aDip;
var longStart = -( Math.floor( longMax / Model.aDelta ) * Model.aDelta );
for ( var long = longStart; long < longMax; long += Model.aDelta ) {
var rLong = Model.rEarth * Math.cos( long );
var latMax = Math.acos( Model.hDisk / rLong );
var latStart = -( Math.floor( latMax / Model.aDelta ) * Model.aDelta );
if (!Model.drawBackFGrid) latStart = 0;
g.NewPoly();
if (Model.drawBackFGrid) g.AddPointToPoly3D( PointOnPlane( -latMax, long ) );
for ( var lat = latStart; lat < latMax; lat += Model.aDelta ) {
g.AddPointToPoly3D( PointOnPlane( lat, long ) );
}
g.AddPointToPoly3D( PointOnPlane( latMax, long ) );
g.DrawPoly( 1 );
}
} // end flat grid
if ( (Model.showGrid & 2) || Model.showFlatHorizon ) {
// horizon on flat model
var aMax = Model.drawBackFGrid ? Math.PI * 2 : Math.PI;
g.SetPlane( [ 0, 0, -Model.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
g.SetAlpha( 1 );
g.SetLineAttr( 'red', 2 );
g.ArcOnPlane( 0, 0, Model.rDisk, 0, aMax, 1 );
}
if ( Model.showEquator ) {
// equator
var aMax = Model.drawBackFGrid ? Math.PI * 2 : Math.PI;
g.SetPlane( [ 0, 0, -Model.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
g.SetAlpha( 1 );
g.SetLineAttr( 'black', 1 );
g.ArcOnPlane( 0, 0, Model.rFEarth, 0, aMax, 1 );
}
// Globe Horizon
var aMax = Model.drawBackGGrid ? Math.PI * 2 : Math.PI;
g.SetAlpha( 1 );
g.SetLineAttr( 'blue', 2 );
g.SetPlane( [ 0, 0, -Model.zDisk ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
g.ArcOnPlane( 0, 0, Model.rDisk, 0, aMax, 1 );
} // end model globe
// Flat Earth
if ( Model.showModel & 2 ) {
var alpha = 0.6 - 0.5 * (Math.log10( Model.Height ) / 9);
g.SetAlpha( alpha );
g.SetLineAttr( 'black', 1 );
// circle lines
var aMax = Model.drawBackFE ? Math.PI * 2 : Math.PI;
var crDelta = Model.rFEarth / 12;
var crMax = 2 * Model.rFEarth - crDelta / 2;
g.SetPlane( [ 0, 0, -Model.Height ], [ 1, 0, 0 ], [ 0, 1, 0 ] );
for ( var cr = crDelta; cr < crMax; cr += crDelta ) {
g.ArcOnPlane( 0, 0, cr, 0, aMax, 1 );
}
if (Model.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 (Model.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 (Model.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 (Model.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 );
}
}
g.SetAlpha( 1 );
g.ArcOnPlane( 0, 0, Model.rFEarth, 0, aMax, 1 );
g.SetLineAttr( 'black', 2 );
g.ArcOnPlane( 0, 0, 2*Model.rFEarth, 0, aMax, 1 );
// ray lines
g.SetAlpha( alpha );
g.SetLineAttr( 'black', 1 );
var caDelta = Math.PI / 12;
var caMax = Model.drawBackFE ? 2 * Math.PI : Math.PI;
caMax -= caDelta / 2;
var r = 2 * Model.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 );
}
} // end nLines > 0
if ( Model.showEyeLevel ) {
g.SetLineAttr( 'magenta', 1 );
g.SetAlpha( 1 );
g.Line3D( [ -Model.sceneWidth/2, Model.rDisk, 0 ], [ Model.sceneWidth/2, Model.rDisk, 0 ] );
g.SetTextAttr( 'Arial', 12, 'magenta', 'normal', 'normal', 'venter', 'bottom', 6 );
g.SetTextRotation( -Model.Roll*Math.PI/180 );
g.Text3D( 'Eye-Level', [ 0, Model.rDisk, 0 ] );
}
}
function PointOnEarth( lat, long ) {
var x = Model.rEarth * Math.sin( long );
var rr = Model.rEarth * Math.cos( long );
var y = rr * Math.sin( lat );
var z = rr * Math.cos( lat );
return [ x, y, z - (Model.rEarth + Model.Height) ];
}
function PointOnPlane( lat, long ) {
var x = Model.rEarth * Math.sin( long );
var rr = Model.rEarth * Math.cos( long );
var y = rr * Math.sin( lat );
return [ x, y, -Model.Height ];
}
ControlPanels.NewSliderPanel( {
ModelRef: 'Model',
OnModelChange: UpdateAll,
Format: 'std',
Digits: 3,
ReadOnly: false,
PanelFormat: 'InputMediumWidth'
} ).AddValueSliderField( {
Name: 'Height',
ValueRef: 'Height',
SliderValueRef: 'HeightSlider',
Mult: 1000,
Units: 'km',
Color: 'blue',
Min: 0,
Max: 1
} ).AddValueSliderField( {
Name: 'ViewAngle',
Label: 'View∠',
ValueRef: 'ViewAngleField',
SliderValueRef: 'ViewAngleSlider',
Units: '°',
Color: 'black',
Min: 5,
Max: 90
} ).AddValueSliderField( {
Name: 'FocalLength',
ValueRef: 'FocalLengthField',
SliderValueRef: 'FocalLengthSlider',
Label: 'f',
Units: 'mm',
Color: 'black',
Min: 21,
Max: 500
} ).Render();
ControlPanels.NewSliderPanel( {
ModelRef: 'Model',
OnModelChange: UpdateAll,
NCols: 2,
Format: 'fix0',
Digits: 0,
ReadOnly: true,
PanelFormat: 'InputMediumWidth'
} ).AddValueSliderField( {
Name: 'Nick',
Format: 'std',
Digits: 3,
Units: '°',
Color: 'green',
Min: -45,
Max: 30
} ).AddValueSliderField( {
Name: 'Roll',
Format: 'std',
Digits: 3,
Units: '°',
Color: 'green',
Min: -45,
Max: 45
} ).Render();
ControlPanels.NewPanel( {
Name: 'Options',
ModelRef: 'Model',
NCols: 2,
OnModelChange: UpdateAll
} ).AddRadiobuttonField( {
Name: 'showModel',
Label: 'Model',
ValueType: 'int',
Items: [
{
Name: 'Globe',
Value: 1
}, {
Name: 'Globe+Flat',
Value: 3
}, {
Name: 'Flat',
Value: 2
}
]
} ).AddRadiobuttonField( {
Name: 'HeightRange',
ValueType: 'int',
Items: [
{
Name: '50',
Text: '50',
Value: 50000
}, {
Name: '500',
Text: '500',
Value: 500000
}, {
Name: '20000',
Text: '20 000',
Value: 20000000
}, {
Name: 'Log',
Value: 0
}
]
} ).AddRadiobuttonField( {
Name: 'showGrid',
Label: 'Grid',
ValueType: 'int',
Items: [
{
Name: 'None',
Value: 0
}, {
Name: 'Globe',
Value: 1
}, {
Name: 'Globe+Flat',
Value: 3
}
]
} ).AddRadiobuttonField( {
Name: 'nLines',
Label: 'Lines',
ValueType: 'int',
Items: [
{
Name: '15',
Value: 15
}, {
Name: '30',
Value: 30
}, {
Name: '45',
Value: 45
}, {
Name: '60',
Value: 60
}, {
Name: '90',
Value: 90
}
]
} ).AddCheckboxField( {
Name: 'Show',
Label: 'Show',
Items: [
{
Name: 'showEyeLevel',
Text: 'EyeLevel',
}, {
Name: 'showFlatHorizon',
Text: 'FE-Horizon',
}, {
Name: 'showEquator',
Text: 'FE-Equator',
}
]
} ).AddRadiobuttonField( {
Name: 'deviceRatio',
Label: 'AspectRatio',
ValueType: 'num',
Items: [
{
Name: '3:2',
Value: 3/2
}, {
Name: '2:3',
Value: 2/3
}, {
Name: '16:9',
Value: 16/9
}, {
Name: '9:16',
Value: 9/16
}
]
} ).Render();
ControlPanels.NewPanel( {
Name: 'Output',
ModelRef: 'Model',
OnModelChange: UpdateAll,
NCols: 2,
ReadOnly: true,
Format: 'std',
Digits: 4
} ).AddTextField( {
Name: 'hDip',
Label: 'DipHeight(b)',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'aDip',
Label: 'DipAngle(α)',
Mult: Math.PI / 180,
Units: '°'
} ).AddTextField( {
Name: 'dHorizon',
Label: 'HorDist(s)',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'aEarth',
Label: 'AngDiameter',
Mult: Math.PI / 180,
Units: '°'
} ).AddTextField( {
Name: 'rDisk',
Label: 'HorDistX(d)',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'dDelta',
Label: 'LineSpacing',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'zDisk',
Label: 'HorDistZ(p)',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'dView',
Label: 'HorDistView(v)',
Mult: 1000,
Units: 'km'
} ).AddTextField( {
Name: 'rEarth',
Label: 'RadiusPlanet',
Mult: 1000,
Units: 'km',
ReadOnly: false
} ).Render();
</jscript>