

//////////////////////////////////////////////////////////////////////////
// Air traffic control demo with a map displayed using the GLG Map Server.
// This demo uses Glg as a bean and may be used in a browser or stand-alone.
//////////////////////////////////////////////////////////////////////////

import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import javax.swing.*;
import com.genlogic.*;

//////////////////////////////////////////////////////////////////////////
public class GlgAirTrafficDemo extends GlgJBean implements ActionListener
{
   //////////////////////////////////////////////////////////////////////////
   // The main demo class
   //////////////////////////////////////////////////////////////////////////
   static final long serialVersionUID = 0;

   String drawing_name = "airtraffic.g";

   // If supplied, overrides the URL of the GIS object in the drawing
   static String SuppliedMapServerURL = null;

   static boolean StandAlone = false;
   boolean IsReady = false;
   double PlaneSpeed = 0.005; // Relative units   

   int FloridaZoomDelay1 = 3000; // Delay to zoom to Florida to show details.
   int FloridaZoomDelay2 = 1000; // Delay to remove Florida zooming message.

   // Define constants
   final int
      MAX_NUM_PLANES = 10000,
      NUM_NODE_TYPES = 2,
      NUM_PLANE_TYPES = 3;

   final double
      SMALL_SIZE = 0.5,
      MEDIUM_SIZE = 0.7,
      BIG_SIZE = 0.9;

   final int
      NORMAL = 0,
      WARNING = 1,
      ALARM = 2,
      SELECTED = 3;

   int
      MaxNumPlanes = 10000,
      NumNodeTypes = 2,
      NumPlaneTypes = 3;

   GlgObject
      Drawing,
      PositionObject,
      PositionArea,
      NodeTemplate[] = new GlgObject[ NumNodeTypes ],
      PlaneTemplate[] = new GlgObject[ NumPlaneTypes ],
      TrajectoryTemplate,
      NodePool[] = new GlgObject[ NumNodeTypes ],
      PlanePool[] = new GlgObject[ NumPlaneTypes ],
      TrajectoryPool,
      Map,
      GISObject,
      NodeGroup,
      PlaneGroup,
      TrajectoryGroup,
      DistancePolygon;

   // Store initial extent and center, used to reset.
   GlgPoint
      InitExtent = new GlgPoint(),
      InitCenter = new GlgPoint();

   double PlaneSize = SMALL_SIZE;

   int
      UpdateInterval = 100, // Update interval in msec.
      NumPlanes = 100,
      NumNodes,
      NumTrajectoryPoints,
      InitialMapProjection,
      MapProjection,
      PlaneType = 0,
      NodeType = 0,
      SelectedPlaneIndex = -1,
      NumDistancePoints;

   boolean
      OrthoOnly = false,
      NoFill = false,
      DoUpdate = true,
      PanMode = false,
      DistanceMode = false,
      RedoIcons = true,
      HasAngle = false,
      HasElevation = false,
      CityLabels = true,
      StateDisplay = true;

   PlaneData PlaneArray[];

   // Array of icons to place on the map as GLG objects in addition to the icons
   // defined in GIS server's data. The icons that use GLG objects may be selected
   // with the mouse and their attributes can be changed dynamically, based on 
   // data. When the mouse moves over an icon, it may be highlighted with a 
   // different color or a tooltip may be displayed. 
   NodeData NodeArray[] =
   {
      new NodeData( "Boston", -71.01789, 42.33602 ),
      new NodeData( "New York", -73.97213, 40.77436 ),
      new NodeData( "San Francisco", -122.55478, 37.79325 ),
      new NodeData( "Miami", -80.21084, 25.77566 ),
      new NodeData( "Seattle", -122.35032, 47.62180 ),
      new NodeData( "Houston", -95.38672, 29.76870 ),
      new NodeData( "Denver", -104.87265, 39.76803 ),
      new NodeData( "Minneapolis", -93.26684, 44.96185 ),
      new NodeData( "Chicago", -87.68496, 41.83705 ),
      new NodeData( "Dallas", -96.76524, 32.79415 )
   };

   // Temporary variables: allocate once
   GlgPoint
      lat_lon = new GlgPoint(),
      point = new GlgPoint(),
      world_point = new GlgPoint();

   Timer timer = null;
   Timer zoom_timer = null;

   //////////////////////////////////////////////////////////////////////////
   public GlgAirTrafficDemo()
   {
      super();

      SetDResource( "$config/GlgMouseTooltipTimeout", 0.05 );

      // Don't expand selection area for exact tooltips.
      SetDResource( "$config/GlgPickResolution", 0. );

      // Activate Trace callback.
      AddListener( GlgObject.TRACE_CB, this );
   }

   //////////////////////////////////////////////////////////////////////////
   // For use as a stand-alone java demo
   //////////////////////////////////////////////////////////////////////////
   public static void main( final String arg[] )
   {
      SwingUtilities.
        invokeLater( new Runnable(){ public void run() { Main( arg ); } } );
   }

   //////////////////////////////////////////////////////////////////////////
   // Optional arguments.
   // -num_planes <number>  - specify the number of planes to be displayed;
   //                          default: 100
   // -speed <number>       - plane speed in relative units, default: 0.005
   // -ortho                - use Orthographic projection only
   // -map_server_url       - specify MapServerURL; it may be supplied as an
   //                         applet parameter when used as an applet;
   //                         default: use GIS object's MapServerURL property 
   //                         from the drawing file.
   //////////////////////////////////////////////////////////////////////////
   public static void Main( String arg[] )
   {
      class DemoQuit extends WindowAdapter
      {
         public void windowClosing( WindowEvent e ) { System.exit( 0 ); }
      }

      JFrame frame = new JFrame();

      frame.setResizable( true );
      frame.setSize( 800, 700 );
      frame.setLocation( 100, 20 );

      GlgAirTrafficDemo air_traffic_demo = new GlgAirTrafficDemo();
      frame.getContentPane().add( air_traffic_demo );

      frame.addWindowListener( new DemoQuit() );
      frame.setVisible( true );

      // Process command line arguments.
      int num_arg = Array.getLength( arg );

      if( num_arg != 0 )
      {
         for( int skip = 0; skip < num_arg; ++skip )
         {
            if( arg[ skip ].equals( "-num_planes" ) )
            {
               ++skip;
               if( num_arg <= skip )
                  air_traffic_demo.error( "No plane number.", true );

               try
               {
                  air_traffic_demo.NumPlanes = new Integer( arg[ skip ] ).intValue();
               }
               catch( NumberFormatException e )
               {
                  air_traffic_demo.error( "No plane number.", true );
               }

               if( air_traffic_demo.NumPlanes > air_traffic_demo.MAX_NUM_PLANES )
                 air_traffic_demo.error( "Increase plane array size and run again.", true );
            }
            else if( arg[ skip ].equals( "-speed" ) )
            {
               ++skip;
               if( num_arg <= skip )
                  air_traffic_demo.error( "No plane speed.", true );

               try
               {
                  air_traffic_demo.PlaneSpeed =
                    new Double( arg[ skip ] ).doubleValue();
               }
               catch( NumberFormatException e )
               {
                  air_traffic_demo.error( "No plane speed.", true );
               }
            }
            else if( arg[ skip ].equals( "-ortho" ) )
            {
               air_traffic_demo.OrthoOnly = true;
            }
            else if( arg[ skip ].equals( "-map_server_url" ) )
            {
               ++skip;
               if( num_arg <= skip )
                  air_traffic_demo.error( "No map server URL.", true );

               GlgAirTrafficDemo.SuppliedMapServerURL = arg[ skip ];
            }
            else if( arg[ skip ].equals( "-help" ) )
            {
               System.out.println(
                   "Options: -num_planes <number> -speed <number> -ortho -map_server_url" );
               System.out.println(
                   "Defaults: -num_planes 100 -speed 0.005" );
               System.exit( 0 );
            }
            else
            {
               System.out.println (
                   "Invalid option. Use -help for the list of options." );
               System.exit( 1 );
            }
         }

      }

      air_traffic_demo.SetDrawingName( air_traffic_demo.drawing_name );
   }

   //////////////////////////////////////////////////////////////////////////
   // Invoked before the hierarchy setup.
   //////////////////////////////////////////////////////////////////////////
   public void HCallback( GlgObject viewport )
   {
      Drawing = viewport;
      Map = Drawing.GetResourceObject( "Map" );
      GISObject = Map.GetResourceObject( "GISObject" );

      if( !StandAlone )
      {
         String param = getParameter( "MapServerURL" );
         if( param != null )
           SuppliedMapServerURL = param;
      }

      Init();
   }

   //////////////////////////////////////////////////////////////////////////
   // Initializes icons in the drawing
   //////////////////////////////////////////////////////////////////////////
   void Init()
   {
      String icon_name;
      int i;






      if( SuppliedMapServerURL != null ) // Override URL in the drawing
         GISObject.SetSResource( "GISMapServerURL", SuppliedMapServerURL );

      // Set GIS Zoom mode for the drawing.
      Map.SetGISZoom( null, GISObject, null );

      // Query and store the GIS projection (ORTHOGRAPHIC or RECTANGULAR)
      int projection = GISObject.GetDResource( "GISProjection" ).intValue();
      InitialMapProjection = projection;
      MapProjection = projection;

      // Query and store GIS object's Extent and Center.
      InitExtent = GISObject.GetGResource( "GISExtent" );
      InitCenter = GISObject.GetGResource( "GISCenter" );

      // Make popup dialog and distance popup invisible.
      Drawing.SetDResource( "FlightInfoPopup/Visibility", 0. );
      Map.SetDResource( "DistancePopup/Visibility", 0. );

      // Disable color button: this functionality is not supported with
        // the remote map sever setup.
      Drawing.SetDResource( "ToggleColor/HandlerDisabled", 1. );

      // Get the palette containing templates for plane and node icons.
      GlgObject palette = Drawing.GetResourceObject( "Palette" );

       // Delete it from the drawing
      Drawing.DeleteObject( palette );

      // Get viewport's XYRatio.
      GlgObject xy_ratio = Map.GetResourceObject( "XYRatio" );

      // Make it global acrosss all icons, to set in one place.
      xy_ratio.SetDResource( "Global", (double) GlgObject.GLOBAL );

      // Get node and plane templates from the palette. A few sets of templates
      // with different level of details are used, depending on the zoom level.
      // Palette aproach is used to implement icon types instead of 
      // subdrawings, since icons are dynamically added/deleted from the 
      // drawing any way to show only the icons visible in the zoomed area.
        //
      for( i=0; i < NUM_PLANE_TYPES; ++i )
      {
         icon_name = "Plane" + i;
         PlaneTemplate[ i ] = palette.GetResourceObject( icon_name );

         // Scalable icons have transforms attached: wire up to maintain ratio.
         if( PlaneTemplate[ i ].HasResourceObject( "ScaleY" ) )
         {
            // Wire up constraints to make icon keeping its XY ratio when
              // windows is resized in a way that changes the ratio.
            GlgObject coeff =
              PlaneTemplate[ i ].GetResourceObject( "ScaleYXform/XformAttr2" );
            coeff.ConstrainObject( xy_ratio );
         }

         // Turn labels on initially.
         if( i != 0 )
            PlaneTemplate[ i ].SetDResource( "Label/Visibility", 1. );
      }

      TrajectoryTemplate = palette.GetResourceObject( "Trajectory" );
      NumTrajectoryPoints =
        TrajectoryTemplate.GetDResource( "Factor" ).intValue();

      for( i=0; i < NUM_NODE_TYPES; ++i )
      {
         icon_name = "Node" + i;
         NodeTemplate[ i ] = palette.GetResourceObject( icon_name );
      }

      NumNodes = Array.getLength( NodeArray );

      // Create and initialize plane structures used for simulation.
      PlaneArray = new PlaneData[ NumPlanes ];
      for( i =0; i < NumPlanes; ++i )
      {
         PlaneArray[ i ] = new PlaneData();
         PlaneArray[ i ].name = Integer.toString( i );
         PlaneArray[ i ].tooltip = null;
         PlaneArray[ i ].color_index = NORMAL;
         PlaneArray[ i ].graphics = null;
         PlaneArray[ i ].trajectory = null;
         PlaneArray[ i ].iteration = 0;
         StartPlane( PlaneArray[ i ], true );
      }

      // Create groups to hold nodes and planes.
      NodeGroup = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );
      NodeGroup.SetSResource( "Name", "NodeGroup" );

      TrajectoryGroup = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );
      TrajectoryGroup.SetSResource( "Name", "TrajectoryGroup" );

      PlaneGroup = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );
      PlaneGroup.SetSResource( "Name", "PlaneGroup" );

      Map.AddObjectToBottom( NodeGroup );
      Map.AddObjectToBottom( PlaneGroup );
      Map.AddObjectToBottom( TrajectoryGroup );

      // Create groups to keep pooled objects.
      for( i=0; i<NumNodeTypes; ++i )
         NodePool[i] = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );

      for( i=0; i<NumPlaneTypes; ++i )
         PlanePool[i] = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );

      TrajectoryPool = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );

      SetPlaneSize();


      // Demos starts with the whole word view, then zooms to the Florida area
      // in a few seconds to show more details. Set initial parameters
        // for the whole world view.
      HandleZoomLevel();

      // Store objects used to display lat/lon on mouse move.
      PositionArea = Drawing.GetResourceObject( "PositionArea" );
      PositionObject = Drawing.GetResourceObject( "PositionLabel/String" );
      PositionObject.SetSResource( null, "" );

      // Set Florida zooming message to OFF initially.
      Drawing.SetDResource( "Map/FloridaZoomingMessage/Visibility", 0. );
   }

   //////////////////////////////////////////////////////////////////////////
   // Invoked after the hierarchy setup.
   //////////////////////////////////////////////////////////////////////////
   public void VCallback( GlgObject viewport )
   {
      String message = "Loading map, please wait....";

      // Position icons before showing them.
      UpdateObjectsOnMap( message );
   }

   //////////////////////////////////////////////////////////////////////////
   // Starts updates
   //////////////////////////////////////////////////////////////////////////
   public void ReadyCallback( GlgObject viewport )
   {
      super.ReadyCallback();

      StartUpdates();
      IsReady = true;

      // Zoom to the Florida area after a few seconds to show details.        
      zoom_timer = new Timer( FloridaZoomDelay1, new ZoomPerformer( this ) );
      zoom_timer.setRepeats( false );
      zoom_timer.start();
   }


   //////////////////////////////////////////////////////////////////////////
   // Reposition icons when a new map is displayed. 
   //////////////////////////////////////////////////////////////////////////
   public void UpdateObjectsOnMap( String message )
   {
      int i;

      // SetupHierarchy causes the new map to be generated if necessary, 
      // so display the wait message while the map is being generated.
      SetStatus( message );

      if( RedoIcons ) // Zoom or pan.
      {
         DeleteNodes();
         DeletePlanes();

         HandleZoomLevel();
      }

      // Update the GIS object with new extent but don't draw it yet:
      // we want to update objects on the map first.
      Map.SetupHierarchy();

      for( i = 0; i < NumNodes; ++i ) // Position nodes
         PositionNode( NodeArray[ i ], i );

      // Add before positioning planes to setup trajectories' history
      if( RedoIcons )
         Map.AddObjectToBottom( TrajectoryGroup );

      for( i = 0; i < NumPlanes; ++i ) // Position planes
         PositionPlane( PlaneArray[ i ], i );

      if( RedoIcons )
      {
         Map.AddObjectToBottom( NodeGroup );
         Map.AddObjectToBottom( PlaneGroup );
         RedoIcons = false;
      }

      SetStatus( "" );
   }

   //////////////////////////////////////////////////////////////////////////
   // Set the node type depending on zoom level. Also change to rectangular
   // projection for high zooms.
   //////////////////////////////////////////////////////////////////////////
   void HandleZoomLevel()
   {
      GlgPoint extent = GetExtentDegrees();

      if( extent.x < 20. && extent.y < 20. )
      {
         // High Zoom: use the most detailed icon.
         NodeType = 1;
         PlaneType = 2; // Most detailed icon
         HasAngle = true; // Most detailed plane icons show angle
         HasElevation = true; // Most detailed plane icons show elevation
         CityLabels = true; // Use city names instead of airport labels.
      }
      else if( extent.x < 70. && extent.y < 70. )
      {
         // Zoom: use detailed icon 1 
         NodeType = 1;
         PlaneType = 1; // Detailed icon 
         HasAngle = true; // Detailed plane icons show angle 
         HasElevation = false;
         CityLabels = true; // Use city names instead of airport labels.
      }
      else
      {
         // Whole world view 
         NodeType = 1; // City icons are always visible 
         PlaneType = 0; // Simple icon 
         HasAngle = false;
         HasElevation = false;
         CityLabels = false; // Use airport labels instead of all city names.
      }

      SetGISLayers(); /* Set airport labels. */

      if( !OrthoOnly )
         ChangeProjection( extent );
   }

   //////////////////////////////////////////////////////////////////////////
   // Change projection to rectangular for zoomed views, and back to 
   // orthographics for high-level views.
   //////////////////////////////////////////////////////////////////////////
   void ChangeProjection( GlgPoint extent )
   {
      if( extent.x < 30. && extent.y < 30. )
      {
         if( MapProjection == GlgObject.RECTANGULAR_PROJECTION )
            return; // Already rect, no change.

         // Change to the rectangular projection
         MapProjection = GlgObject.RECTANGULAR_PROJECTION;

         GISObject.SetDResource( "GISProjection",
                                GlgObject.RECTANGULAR_PROJECTION );
         // Set extent in degrees
         GISObject.SetGResource( "GISExtent", extent );
      }
      else
      {
         if( MapProjection == GlgObject.ORTHOGRAPHIC_PROJECTION )
            return; // Already ortho, no change

         // Change to the orthographic projection
         MapProjection = GlgObject.ORTHOGRAPHIC_PROJECTION;

         GISObject.SetDResource( "GISProjection",
                                GlgObject.ORTHOGRAPHIC_PROJECTION );

         // Set extent in meters
         GISObject.SetGResource( "GISExtent",
                      extent.x / 90. * GlgObject.EQUATOR_RADIUS,
                      extent.y / 90. * GlgObject.POLAR_RADIUS, 0. );
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Handle user interaction.
   //////////////////////////////////////////////////////////////////////////
   public void InputCallback( GlgObject viewport, GlgObject message_obj )
   {
      String
        origin,
        format,
        action,
        subaction;

      super.InputCallback( viewport, message_obj );

      origin = message_obj.GetSResource( "Origin" );
      format = message_obj.GetSResource( "Format" );
      action = message_obj.GetSResource( "Action" );
      subaction = message_obj.GetSResource( "SubAction" );

      // Handle window closing if run stand-alone
      if( format.equals( "Window" ) && action.equals( "DeleteWindow" ) )
      {
         // Closing main window: exit.
         System.exit( 0 );
      }

      if( format.equals( "Button" ) )
      {
         if( !action.equals( "Activate" ) )
            return;

         PanMode = false;
         AbortDistanceMode();
         if( origin.equals( "CloseDialog" ) )
         {
            Drawing.SetDResource( "SelectionDialog/Visibility", 0. );
            Update();
         }
         else if( origin.equals( "ZoomIn" ) )
         {
            Zoom( 'i', 2. );
            Update();
         }
         else if( origin.equals( "ZoomOut" ) )
         {
            Zoom( 'o', 2. );
            Update();
         }
         else if( origin.equals( "ZoomReset" ) )
         {
            Zoom( 'n', 0. );
            Update();
         }
         else if( origin.equals( "ZoomTo" ) )
         {
            PanMode = false; // Abort Pan mode
            AbortDistanceMode();

            Map.SetZoom( null, 't', 0. ); // Start Zoom op
            SetStatus( "Define a rectangular area to zoom to." );
            Update();
         }
         else if( origin.equals( "Pan" ) )
         {
            Map.SetZoom( null, 'e', 0. ); // Abort ZoomTo mode

            PanMode = true;
            SetStatus( "Click to define a new center." );
            Update();
         }
         else if( origin.equals( "Up" ) )
         {
            Zoom( 'u', 0. );
            Update();
         }
         else if( origin.equals( "Down" ) )
         {
            Zoom( 'd', 0. );
            Update();
         }
         else if( origin.equals( "Left" ) )
         {
            Zoom( 'l', 0. );
            Update();
         }
         else if( origin.equals( "Right" ) )
         {
            Zoom( 'r', 0. );
            Update();
         }
         else if( origin.equals( "AirportLabels" ) )
         {
            CityLabels = !CityLabels;
            SetGISLayers();
            Update();
         }
         else if( origin.equals( "Planes" ) )
         {
            ToggleResource( Map, "PlaneGroup/Visibility" );
            Update();
         }
         else if( origin.equals( "ValueDisplay" ) )
         {
            if( PlaneType == 0 )
            {
               GlgObject.Bell();
               SetStatus( "Zoom in to see plane labels." );
            }
            else
            {
               // Visibility of all labels is constrained, set just one.
               for( int i=1; i<NUM_PLANE_TYPES; ++i )
                  ToggleResource( PlaneTemplate[ i ], "Label/Visibility" );
            }
            Update();
         }
         else if( origin.equals( "ToggleStates" ) )
         {
            StateDisplay = !StateDisplay;
            SetGISLayers();
            Update();
         }
         else if( origin.equals( "Update" ) )
         {
            DoUpdate = !DoUpdate;
            if( DoUpdate )
              StartUpdates();
         }
         else if( origin.equals( "PlaneSize" ) )
         {
            // Change plane icon's size.
            if( PlaneSize == SMALL_SIZE )
              PlaneSize = MEDIUM_SIZE;
            else if( PlaneSize == MEDIUM_SIZE )
              PlaneSize = BIG_SIZE;
            else // BIG_SIZE
              PlaneSize = SMALL_SIZE;

            SetPlaneSize();

            Update();
         }
         else if( origin.equals( "CloseFlightInfo" ) )
         {
            SelectPlane( -1 ); // Unselect the plane and erase popup display.
            Update();
         }
         else if( origin.equals("Distance") )
         {
            AbortDistanceMode(); // Abort prev. distance mode if any

            DistanceMode = true;
            SetStatus( "Click on the map to define distance to measure, right click to finish." );
            Update();
         }
      }
      else if( action.equals( "Zoom" ) && subaction.equals( "End" ) )
      {
         // Update icon positions after zooming. 
         RedoIcons = true;
         UpdateObjectsOnMap( "Zooming, please wait..." );
         Update();
      }
      else if( format.equals( "CustomEvent" ) &&
               action.equals( "MouseClick" ) ) // Mouse click on an icon
      {
         int zoom_mode, button_index;
         String custom_event;

         if( DistanceMode )
            return; // Ignore selection in the DistanceMode

         zoom_mode = Map.GetDResource( "ZoomToMode" ).intValue();
         if( zoom_mode != 0 )
            return; // Don't handle selection in ZoomTo mode. 

         button_index = message_obj.GetDResource( "ButtonIndex" ).intValue();
         if( button_index != 1 )
            return; // Ignore middle and right mouse button clicks

         custom_event = message_obj.GetSResource( "EventLabel" );

         if( custom_event.equals( "Plane" ) ) // Plane icon was selected
         {
            int plane_index;

            // Get plane index 
            plane_index =
              message_obj.GetDResource( "Object/DataIndex" ).intValue();
            SelectPlane( plane_index );

            // Show message in the bottom
            SetStatus( PlaneArray[ SelectedPlaneIndex ].tooltip );

            DisplayPlaneInfo(); // Display popup
            Update();
         }
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Used to obtain coordinates of the mouse click. 
   //////////////////////////////////////////////////////////////////////////
   public void TraceCallback( GlgObject viewport, GlgTraceData trace_info )
   {
      GlgObject point_obj;

      int event_type = trace_info.event.getID();

      // Use the Map area events only.
      if( trace_info.viewport != Map )
      {
         // Erase the current postion display when the mouse moves outside 
           // of the map.
         switch( event_type )
         {
          case MouseEvent.MOUSE_MOVED:
          case MouseEvent.MOUSE_DRAGGED:
            PositionObject.SetSResource( null, "" );
            PositionArea.Update();
            break;
         }
         return;
      }

      switch( event_type )
      {
       case MouseEvent.MOUSE_PRESSED:
         if( GetButton( trace_info.event ) != 1 )
         {
            AbortDistanceMode();
            return; // Use the left button clicks only.
         }

         point.x = (double) ((MouseEvent)trace_info.event).getX();
         point.y = (double) ((MouseEvent)trace_info.event).getY();

         // Handle paning: set the new map center to the location of 
           // the click.
         if( PanMode )
         {
            PanMode = false;

            // Converts X/Y to lat/lon using GIS object's current 
              // projection.
            GISObject.GISConvert( null, GlgObject.SCREEN_COORD,
                                 /* X/Y to Lat/Lon */ true, point, lat_lon );

            // Pan the map
            GISObject.SetGResource( "GISCenter", lat_lon );
            RedoIcons = true;
            UpdateObjectsOnMap( "Paning the map..." );
            Map.Update();
         }
         else if( DistanceMode )
         {
            if( DistancePolygon == null )
            {
               DistancePolygon = new GlgPolygon( 2, null );
               DistancePolygon.SetGResource( "EdgeColor", 1., 1., 0. );
               NumDistancePoints = 1;

               Map.ScreenToWorld( true, point, world_point );

               point_obj = (GlgObject) DistancePolygon.GetElement( 0 );
               point_obj.SetGResource( null, world_point );

               point_obj = (GlgObject) DistancePolygon.GetElement( 1 );
               point_obj.SetGResource( null, world_point );

               Map.AddObjectToBottom( DistancePolygon );
               Map.Update();
            }
            else // Not the first point
            {
               // Set current point to the coord. of the click.
               point_obj = (GlgObject)
                 DistancePolygon.GetElement( NumDistancePoints );
               Map.ScreenToWorld( true, point, world_point );
               point_obj.SetGResource( null, world_point );
               ++NumDistancePoints;

               DisplayDistance( DistancePolygon );

               // Add next point, same coords.
               point_obj = point_obj.CopyObject();
               DistancePolygon.AddObjectToBottom( point_obj );

               Map.Update();
            }
         }
         break;

       case MouseEvent.MOUSE_MOVED:
       case MouseEvent.MOUSE_DRAGGED:
         point.x = (double) ((MouseEvent)trace_info.event).getX();
         point.y = (double) ((MouseEvent)trace_info.event).getY();
         point.z = 0;

         if( DistanceMode && DistancePolygon != null )
         {
            Map.ScreenToWorld( true, point, world_point );
            point_obj = (GlgObject)
              DistancePolygon.GetElement( NumDistancePoints );
            point_obj.SetGResource( null, world_point );
            DisplayDistance( DistancePolygon );
            Map.Update();
         }

         // Report lat/lon position under the mouse.

         // Converts X/Y to lat/lon using GIS object's current projection.
         GISObject.GISConvert( null, GlgObject.SCREEN_COORD,
                              /* X/Y to Lat/Lon */ true, point, lat_lon );

         PositionObject.SetSResource( null, CreateLocationString( lat_lon ) );
         PositionArea.Update();
         break;

       case ComponentEvent.COMPONENT_RESIZED:
         // No need to adjust icon positions if the GIS object has
           // Stretch=YES

         // Component widget = (Component) Drawing.GetResource( "Widget" );
         // int width = widget.getSize().width;
         // int height = widget.getSize().height;
         break;

       default:
         return;
      }
   }

   //////////////////////////////////////////////////////////////////////////
   void AbortDistanceMode()
   {
      if( DistanceMode )
      {
         if( DistancePolygon != null ) // Delete distance polygon
         {
            if( Map.ContainsObject( DistancePolygon ) )
               Map.DeleteObject( DistancePolygon );

            DistancePolygon = null;
         }
         Map.SetDResource( "DistancePopup/Visibility", 0. );
         SetStatus( "" );
         DistanceMode = false;
      }
   }

   //////////////////////////////////////////////////////////////////////////
   void DisplayDistance( GlgObject polygon )
   {
      // Popup distance display
      Map.SetDResource( "DistancePopup/Visibility", 1. );

      // Last point is for dragging, not set yet - don't include.
      int size = polygon.GetSize();
      if( size < 2 )
         return;

      double distance = 0.;
      GlgObject point = null;
      for( int i=0; i<size; ++i )
      {
         GlgObject last_point = point;
         point = (GlgObject) polygon.GetElement( i );

         if( last_point != null )
            // Nautical mile = 1842m 
            distance += GetGlobeDistance( point, last_point ) / 1842.;
      }

      Map.SetDResource( "DistancePopup/Distance", distance );
   }

   //////////////////////////////////////////////////////////////////////////
   // Returns ength (in meters) of a direct line connecting the two
   // points in 3D. Use more complex math for curves around the Earth.
   //////////////////////////////////////////////////////////////////////////
   double GetGlobeDistance( GlgObject point1_obj, GlgObject point2_obj )
   {
      GlgPoint point1, point2, lat_lon1, lat_lon2, globe_point1, globe_point2;

      point1 = point1_obj.GetGResource( null );
      point2 = point2_obj.GetGResource( null );

      lat_lon1 = GetLatLon( point1 );
      lat_lon2 = GetLatLon( point2 );

      /* XYZ of the first point, in meters */
      globe_point1 = GetPointXYZ( lat_lon1 );

      /* XYZ of the second point, in meters */
      globe_point2 = GetPointXYZ( lat_lon2 );

      double dx = globe_point1.x - globe_point2.x;
      double dy = globe_point1.y - globe_point2.y;
      double dz = globe_point1.z - globe_point2.z;

      return Math.sqrt( dx * dx + dy * dy + dz * dz );
   }

   //////////////////////////////////////////////////////////////////////////
   GlgPoint GetPointXYZ( GlgPoint lat_lon )
   {
      double
         angle_x,
         angle_y;

      GlgPoint xyz = new GlgPoint();

      // Place [0,0] at the math's x axis for simplicity. 

      angle_x = DegToRad( lat_lon.x );
      angle_y = DegToRad( lat_lon.y );

      xyz.x =
        GlgObject.EQUATOR_RADIUS * Math.cos( angle_x ) * Math.cos( angle_y );
      xyz.y = GlgObject.EQUATOR_RADIUS * Math.sin( angle_y );
      xyz.z =
        GlgObject.EQUATOR_RADIUS * Math.sin( angle_x ) * Math.cos( angle_y );

      return xyz;
   }

   //////////////////////////////////////////////////////////////////////////
   // Convenience wrapper
   //////////////////////////////////////////////////////////////////////////
   GlgPoint GetLatLon( GlgPoint point_xy )
   {

      GlgPoint lat_lon = new GlgPoint();
      GISObject.GISConvert( null, GlgObject.OBJECT_COORD,
                     /* X/Y to Lat/Lon */ true,
                     point_xy, lat_lon );
      return lat_lon;
   }

   //////////////////////////////////////////////////////////////////////////
   void Zoom( char type, double value )
   {
      switch( type )
      {
       default:
         Map.SetZoom( null, type, value );
         RedoIcons = true;
         UpdateObjectsOnMap( "Zooming or panning, please wait..." );
         break;

       case 'n':
         // Reset map to the initial extent.
         GISObject.SetDResource( "GISProjection",
                                (double) GlgObject.ORTHOGRAPHIC_PROJECTION );
         GISObject.SetGResource( "GISCenter", InitCenter );
         GISObject.SetGResource( "GISExtent", InitExtent );
         MapProjection = GlgObject.ORTHOGRAPHIC_PROJECTION;

         RedoIcons = true;
         UpdateObjectsOnMap( "Reloading map, please wait..." );
         break;
      }
   }

   ////////////////////////////////////////////////////////////////////////
   // Changes plane color to indicate selection and displayes or erases the 
   // flight info popup dialog.
   ////////////////////////////////////////////////////////////////////////
   void SelectPlane( int selected_plane_index )
   {
      PlaneData plane;
      double popup_visibility;

      if( SelectedPlaneIndex != -1 ) // Unselect previously selected plane
      {
         plane = PlaneArray[ SelectedPlaneIndex ];
         if( plane.graphics != null ) // Restore color if plane is visible 
            plane.graphics.SetDResource( "ColorIndex",
                             (double) plane.color_index );
      }

      if( selected_plane_index != -1 ) // Select new plane
      {
         plane = PlaneArray[ selected_plane_index ];
         if( plane.graphics != null ) // Set selected color if plane is visible
            plane.graphics.SetDResource( "ColorIndex", (double) SELECTED );
      }

      // Display or erase the flight info popup.
      if( selected_plane_index == -1 ) // Unselected
         popup_visibility = 0.;
      else // Selected
         popup_visibility = 1.;
      Drawing.SetDResource( "FlightInfoPopup/Visibility", popup_visibility );

      SelectedPlaneIndex = selected_plane_index;
   }

   ////////////////////////////////////////////////////////////////////////
   void DisplayPlaneInfo()
   {
      PlaneData plane = PlaneArray[ SelectedPlaneIndex ];
      Drawing.SetSResource( "FlightInfoPopup/FlightInfo", plane.tooltip );
      Drawing.SetDResource( "FlightInfoPopup/Elevation",
                            GetPlaneElevation( plane ) );
      Drawing.SetDResource( "FlightInfoPopup/StatusIndex",
                           (double) plane.color_index );

      Drawing.SetSResource( "FlightInfoPopup/Location",
                           CreateLocationString( plane.lat_lon ) );
   }

   ////////////////////////////////////////////////////////////////////////
   // Delete node icons and place them into the object pool. 
   ////////////////////////////////////////////////////////////////////////
   void DeleteNodes()
   {
      GlgObject icon;
      int i;

      // Move node icons into the object pull 
      int size = NodeGroup.GetSize();
      for( i=0; i<size; ++i )
      {
         icon = (GlgObject) NodeGroup.GetElement( i );
         NodePool[ NodeType ].AddObjectToBottom( icon );
      }

      // Delete node icons from the drawing and node group 
      Map.DeleteObject( NodeGroup );
      for( i=0; i<size; ++i )
         NodeGroup.DeleteBottomObject();

      // Set nodes' graphics to null 
      if( size > 0 )
         for( i=0; i<NumNodes; ++i )
            NodeArray[i].graphics = null;
   }

   ////////////////////////////////////////////////////////////////////////
   // Delete plane icons and place them into the object pool. 
   ////////////////////////////////////////////////////////////////////////
   void DeletePlanes()
   {
      GlgObject icon;
      int i;

      /* Move plane icons into the object pull */
      int size = PlaneGroup.GetSize();
      for( i=0; i<size; ++i )
      {
         icon = (GlgObject) PlaneGroup.GetElement( i );
         PlanePool[ PlaneType ].AddObjectToBottom( icon );
      }

      // Delete plane icons from the drawing and plane group 
      Map.DeleteObject( PlaneGroup );
      for( i=0; i<size; ++i )
         PlaneGroup.DeleteBottomObject();

      // Set planes' graphics to null 
      if( size > 0 )
         for( i=0; i<NumPlanes; ++i )
            PlaneArray[i].graphics = null;

      DeleteTrajectories();
   }

   ////////////////////////////////////////////////////////////////////////
   // Delete trajectory objects and place them into the object pool. 
   ////////////////////////////////////////////////////////////////////////
   void DeleteTrajectories()
   {
      GlgObject icon;
      int i;

      // Move plane icons into the object pull
      int size = TrajectoryGroup.GetSize();
      for( i=0; i<size; ++i )
      {
         icon = (GlgObject) TrajectoryGroup.GetElement( i );
         TrajectoryPool.AddObjectToBottom( icon );
      }

      // Delete trajectory icons from the drawing and trajectory group
      Map.DeleteObject( TrajectoryGroup );
      for( i=0; i<size; ++i )
         TrajectoryGroup.DeleteBottomObject();

      // Set trajectorys' graphics to null
      if( size > 0 )
         for( i=0; i<NumPlanes; ++i )
            PlaneArray[i].trajectory = null;
   }

   ////////////////////////////////////////////////////////////////////////
   void PositionNode( NodeData node, int index )
   {
      // Converts node position from lat/lon to GLG coordinates.
      GetNodePosition( node );

      if( !IconVisible( node.xyz ) )
         return;

      if( node.graphics == null )
         AddNodeGraphics( node, NodeType, index );

      // Update node's icon in the drawing
      node.graphics.SetGResource( "Position", node.xyz );
   }

   ////////////////////////////////////////////////////////////////////////
   GlgObject CreateNodeIcon( int node_type )
   {
      GlgObject icon;

      int size = NodePool[ node_type ].GetSize();
      if( size > 0 ) // Return an icon from the pool
      {
         icon = (GlgObject) NodePool[ node_type ].GetElement( size - 1 );
         NodePool[ node_type ].DeleteBottomObject();
      }
      else // Create a new icon
      {
         icon =
           NodeTemplate[ node_type ].CloneObject( GlgObject.STRONG_CLONE );
      }
      return icon;
   }

   ////////////////////////////////////////////////////////////////////////
   GlgObject CreatePlaneIcon( int plane_type )
   {
      GlgObject icon;

      int size = PlanePool[ plane_type ].GetSize();
      if( size > 0 ) // Return an icon from the pool
      {
         icon = (GlgObject) PlanePool[ plane_type ].GetElement( size - 1 );
         PlanePool[ plane_type ].DeleteBottomObject();
      }
      else // Create a new icon 
      {
         icon =
           PlaneTemplate[ plane_type ].CloneObject( GlgObject.STRONG_CLONE );
      }
      return icon;
   }

   ////////////////////////////////////////////////////////////////////////
   GlgObject CreateTrajectoryIcon()
   {
      GlgObject icon;

      int size = TrajectoryPool.GetSize();
      if( size > 0 ) // Return an icon from the pool
      {
         icon = (GlgObject) TrajectoryPool.GetElement( size - 1 );
         TrajectoryPool.DeleteBottomObject();
      }
      else // Create a new icon 
      {
         icon = TrajectoryTemplate.CloneObject( GlgObject.STRONG_CLONE );
      }
      return icon;
   }

  ////////////////////////////////////////////////////////////////////////
   void PositionPlane( PlaneData plane, int index )
   {
      // Converts plane position (simulated or real data) from lat/lon to 
      // GLG coordinates.
      GetPlanePosition( plane );

      if( !IconVisible( plane.xyz ) )
      {
         // Delete graphics and place into the pool
         if( plane.graphics != null )
         {
            PlanePool[ PlaneType ].AddObjectToBottom( plane.graphics );
            PlaneGroup.DeleteObject( plane.graphics );
            plane.graphics = null;
         }

         // Delete trajectory and place into the pool
         if( plane.trajectory != null )
         {
            TrajectoryPool.AddObjectToBottom( plane.trajectory );
            TrajectoryGroup.DeleteObject( plane.trajectory );
            plane.trajectory = null;
         }
         return;
      }

      if( plane.graphics == null )
         AddPlaneGraphics( plane, PlaneType, index );

      // Update plane's icon in the drawing
      plane.graphics.SetGResource( "Position", plane.xyz );

      // Update icon's direction angle is necessary
      if( HasAngle )
      {
         plane.angle = GetPlaneAngle( plane );
         plane.graphics.SetDResource( "Angle", plane.angle );
      }

      if( HasElevation )
      {
         plane.elevation = GetPlaneElevation( plane );
         plane.graphics.SetDResource( "Height", plane.elevation );
      }

      if( plane.trajectory != null )
      {
         // For small speeds, skip a few iterations to increase the 
         // trajectory's length
         if( PlaneSpeed < 0.005 )
         {
            Double n_d = new Double( 0.005 / PlaneSpeed );
            int n = n_d.intValue();

            if( n != 0 )
            {
               ++plane.iteration;
               if( ( plane.iteration % n ) != 0 )
                  return; // Skip n iterations, update every n'th
            }
         }

         plane.trajectory.SetDResource( "VisEntryPoint", 1. );
         plane.trajectory.SetGResource( "XYEntryPoint", plane.xyz );
      }
   }

   ////////////////////////////////////////////////////////////////////////
   // Adds an airport icon, fills labels, tooltips, etc.
   ////////////////////////////////////////////////////////////////////////
   void AddNodeGraphics( NodeData node, int node_type, int index )
   {
      GlgObject icon = CreateNodeIcon( node_type );

      // Index for direct access 
      icon.SetDResource( "DataIndex", (double)index );

      if( node_type > 0 ) // More detailed icon
         icon.SetSResource( "LabelString", node.name );

      String tooltip = node.name + ", " + CreateLocationString( node.lat_lon );
      icon.SetSResource( "TooltipString", tooltip );

      node.graphics = icon;

      // The node will be positioned after the GIS object is setup.

      NodeGroup.AddObjectToBottom( icon );
   }

   ////////////////////////////////////////////////////////////////////////
   // Adds a plane icon, fills labels, tooltips, etc.
   ////////////////////////////////////////////////////////////////////////
   void AddPlaneGraphics( PlaneData plane, int plane_type, int index )
   {
      GlgObject icon = CreatePlaneIcon( plane_type );

      // Index for direct access
      icon.SetDResource( "DataIndex", (double)index );

      // Icon color
      icon.SetDResource( "ColorIndex", (double) plane.color_index );

      if( plane_type > 0 ) // More detailed icon
      {
         // Show the flight number as icon label
         String label = "Flight " + plane.flight_number;
         icon.SetSResource( "LabelString", label );
      }

      // Set the tooltip
      icon.SetSResource( "TooltipString", plane.tooltip );

      plane.graphics = icon;

      // The plane will be positioned after the GIS object is setup.
      // PositionPlane( plane, map );

      PlaneGroup.AddObjectToBottom( icon );

      if( plane_type == 2 ) // For detailed icon, create trajectory
      {
         icon = CreateTrajectoryIcon();
         plane.trajectory = icon;

         // Set entries invisible initially
         icon.SetDResource( "Marker/Visibility", 0. );

         TrajectoryGroup.AddObjectToBottom( icon );

         for( int i=0; i<NumTrajectoryPoints; ++i ) // Set fading
            icon.SetDResource( "BrightEntryPoint",
                             0.2 + 0.8 * i / (double) NumTrajectoryPoints );
      }
   }

   ////////////////////////////////////////////////////////////////////////
   // Check if the icon is visible in the current zoom region.
   ////////////////////////////////////////////////////////////////////////
   boolean IconVisible( GlgPoint position )
   {
      return position.z >= 0. &&
         position.x > -1000. && position.x < 1000. &&
         position.y > -1000. && position.y < 1000.;
   }

   ////////////////////////////////////////////////////////////////////////
   // Converts node's position from lat/lon to X/Y in GLG world coordinates.
   ////////////////////////////////////////////////////////////////////////
   void GetNodePosition( NodeData node )
   {
      // Converts lat/lon to X/Y using GIS object's current projection.
      GISObject.GISConvert( null, GlgObject.OBJECT_COORD,
                            /* Lat/Lon to XY */ false,
                            node.lat_lon, node.xyz );
   }

   ////////////////////////////////////////////////////////////////////////
   // Converts plane's position from lat/lon to X/Y in GLG world coordinates.
   ////////////////////////////////////////////////////////////////////////
   void GetPlanePosition( PlaneData plane )
   {
      // Gets the new plane position, simulated or from real data.
      GetPlaneLatLon( plane );

      // Converts lat/lon to X/Y using GIS object's current projection.
      GISObject.GISConvert( null, GlgObject.OBJECT_COORD,
                     /* Lat/Lon to XY */ false,
                     plane.lat_lon, plane.xyz );
   }

   ////////////////////////////////////////////////////////////////////////
   // Displays a message in the status area.
   ////////////////////////////////////////////////////////////////////////
   void SetStatus( String message )
   {
      Drawing.SetSResource( "StatusLabel/String", message );
      Drawing.GetResourceObject("StatusArea").Update();
   }

   ////////////////////////////////////////////////////////////////////////
   void SetPlaneSize()
   {
      for( int i=0; i<NUM_PLANE_TYPES; ++i )
      {
         GlgObject resource = PlaneTemplate[ i ].GetResourceObject( "Scale" );
         if( resource != null )
         {
            // Polygon icon: set scale.
            resource.SetDResource( null, PlaneSize );
         }
         else
         {
            resource = GetResourceObject( PlaneTemplate[ i ],
                                         "Marker/MarkerSize" );
            if( resource != null )
            {
               // Marker: set MarkerSize.
               if( PlaneSize == SMALL_SIZE )
                 resource.SetDResource( null, 7. );
               else if( PlaneSize == MEDIUM_SIZE )
                 resource.SetDResource( null, 9. );
               else
                 resource.SetDResource( null, 11. );
            }
         }
      }
   }

   ////////////////////////////////////////////////////////////////////////
   // Toggle resource between 0 and 1.
   ////////////////////////////////////////////////////////////////////////
   void ToggleResource( GlgObject object, String res_name )
   {
      GlgObject resource = object.GetResourceObject( res_name );
      if( resource == null )
         return;

      double value = resource.GetDResource( null ).doubleValue();
      resource.SetDResource( null, value != 0. ? 0. : 1. );
   }

   ////////////////////////////////////////////////////////////////////////
   // Toggle map layers: airport/city labels and states.
   ////////////////////////////////////////////////////////////////////////
   void SetGISLayers()
   {
      String layers;

      // Airport labels should be visible only when city labels are off.
      NodeTemplate[1].SetDResource( "Label/Visibility", CityLabels ? 0. : 1. );

      layers = "default_air";

      // Add city layers if they are on on the detailed map.
      if( CityLabels )
        layers = layers + ",us_cities";
      else
        layers = layers + ",-us_cities";

      if( StateDisplay ) // Add states layer if it is on.
        // Enable states regardless of the default.
        layers = layers + ",states_dcw";
      else
        // Disable state outline display.
        layers = layers + ",-states_dcw";

      GISObject.SetSResource( "GISLayers", layers );
   }

   ////////////////////////////////////////////////////////////////////////
   int GetButton( AWTEvent event )
   {
      if( ! ( event instanceof InputEvent ) )
        return 0;

      InputEvent input_event = (InputEvent) event;
      int modifiers = input_event.getModifiers();

      if( ( modifiers & InputEvent.BUTTON3_MASK ) != 0 )
        return 3;
      else if( ( modifiers & InputEvent.BUTTON2_MASK ) != 0 )
        return 2;
      else
        return 1;
   }

   ////////////////////////////////////////////////////////////////////////
   // Gets extent in lat/lon (converts from meters for ortho projection).
   ////////////////////////////////////////////////////////////////////////
   GlgPoint GetExtentDegrees()
   {
      GlgPoint extent = GISObject.GetGResource( "GISExtent" );

      if( MapProjection == GlgObject.ORTHOGRAPHIC_PROJECTION )
      {
         extent.x = extent.x / GlgObject.EQUATOR_RADIUS * 90.;
         extent.y = extent.y / GlgObject.POLAR_RADIUS * 90.;
      }

      return extent;
   }

   //////////////////////////////////////////////////////////////////////////
   double Rand( double low, double high )
   {
      return low + ( high - low ) * Math.random();
   }

   //////////////////////////////////////////////////////////////////////////
   double RELATIVE_TO_NEW_RANGE( double low, double high, double rel_value )
   {
      return ( (low) + ((high) - (low)) * rel_value );
   }

   //////////////////////////////////////////////////////////////////////////
   double VALUE_TO_RELATIVE( double low, double high, double value )
   {
      return ( high - low != 0. ? ((value) - (low)) / ((high) - (low)) : 0. );
   }

   //////////////////////////////////////////////////////////////////////////
   double DegToRad( double angle )
   {
      return angle / 180. * Math.PI;
   }

   //////////////////////////////////////////////////////////////////////////
   double RadToDeg( double angle )
   {
      return angle / Math.PI * 180.;
   }

   //////////////////////////////////////////////////////////////////////////
   void error( String message_str, boolean quit )
   {
      System.out.println( message_str );
      if( quit )
         System.exit( 0 );
   }

   //////////////////////////////////////////////////////////////////////////
   // UTILITY FUNCTION: Calculates an angle between the line defined by two 
   // points and the X axis.
   //////////////////////////////////////////////////////////////////////////
   double GetAngle( GlgPoint pt1, GlgPoint pt2 )
   {
      double length, angle;

      length = GetLength( pt1, pt2 );

      if( length == 0. )
         angle = 0.;
      else
      {
         angle = Math.acos( ( pt2.x - pt1.x ) / length );

         if( pt2.y - pt1.y < 0. ) // ScreenSpace Z axis points to the user.
            angle = - angle;
      }

      return RadToDeg( angle );
   }

   //////////////////////////////////////////////////////////////////////////
   // UTILITY FUNCTION: Calculates a distance between two points in 2D.
   //////////////////////////////////////////////////////////////////////////
   double GetLength( GlgPoint pt1, GlgPoint pt2 )
   {
      return Math.sqrt( ( pt2.x - pt1.x ) * ( pt2.x - pt1.x ) +
                   ( pt2.y - pt1.y ) * ( pt2.y - pt1.y ) );
   }

   //////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Calculates plane icon's directional angle.
   //////////////////////////////////////////////////////////////////////////
   double GetPlaneAngle( PlaneData plane )
   {
      GlgPoint last_lat_lon = new GlgPoint();
      double angle;

      if( MapProjection == GlgObject.RECTANGULAR_PROJECTION ||
          plane.path_position == plane.path_position_last ) // Just started 
      {
         // Rectangular projection preserves straight lines, we can use the 
         // angle of the line connecting the start and end nodes. For the
         // orthographic projection, use this case if the plane has just 
         // started and there is no previous position stored.
         angle =
            GetAngle( plane.from_node.xyz, plane.to_node.xyz );
      }
      else // In orthographic projection straight lines are drawn as curves.
            // Use the angle of the line connecting the current and last 
            // positions of the plane.
      {
         double stored_position;

         stored_position = plane.path_position; // Store current position.

         // Get the coordinates of the plane's previous position 
         plane.path_position = plane.path_position_last;
         GetPlaneLatLon( plane );
         GISObject.GISConvert( null, GlgObject.OBJECT_COORD,
                        /* Lat/Lon to XY */ false,
                        plane.lat_lon, last_lat_lon );

         // Get angle of a line connecting the previous and current position.
         angle = GetAngle( last_lat_lon, plane.xyz );

         // Restore the plane's current position
         plane.path_position = stored_position;
         GetPlaneLatLon( plane );
      }
      return angle;
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Calculates plane icon's elevation (zero at the start 
   // and end, max in the middle.
   ////////////////////////////////////////////////////////////////////////
   double GetPlaneElevation( PlaneData plane )
   {
      return ( 0.5 - Math.abs( plane.path_position - 0.5 ) ) * 2. * 10000.;
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Performs one step of the simulation to move the planes.
   // Change this code to use real data.
   ////////////////////////////////////////////////////////////////////////
   void UpdatePlanes()
   {

      if( !DoUpdate )
         return;

      for( int i = 0; i < NumPlanes; ++i )
         UpdatePlane( PlaneArray[ i ], i );

      if( SelectedPlaneIndex != -1 ) // Update selected plane info if any 
         DisplayPlaneInfo();

      Update();
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Calculates new plane position.
   ////////////////////////////////////////////////////////////////////////
   void UpdatePlane( PlaneData plane, int index )
   {

      if( (plane.from_node == null) || (plane.to_node == null) )
         return;

      if( plane.path_position == 1. ) // Finished old path, start a new one.
      {
         if( index == SelectedPlaneIndex )
            SelectPlane( -1 ); // Unselect the plane: it reached destination

         StartPlane( plane, false );
      }
      else
      {
         double speed = PlaneSpeed;

         // Store last position for calculating angle in ORTHO projection. 
         plane.path_position_last = plane.path_position;

         plane.path_position += plane.speed * speed;
         if( plane.path_position > 1. )
            plane.path_position = 1.;
      }

      SetPlaneColor( plane );

      PositionPlane( plane, index ); // Position plane on the map
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Simulate data to change plane color to show warnings 
   // and alarms.
   ////////////////////////////////////////////////////////////////////////
   void SetPlaneColor( PlaneData plane )
   {
      int new_color_index = NORMAL;

      // Set random color
      double random_value = Rand( 0., 1. );
      if( plane.color_index == NORMAL )
      {
         if( random_value <= 0.999 )
            new_color_index = NORMAL;
         else if( random_value > 0.9999 )
            new_color_index = ALARM;
         else if( random_value > 0.999 )
            new_color_index = WARNING;
      }
      else if( plane.color_index == WARNING )
      {
         if( random_value > 0.99 )
            new_color_index = NORMAL;
         else
            new_color_index = plane.color_index; // Keep alarm for a while
      }
      else if( plane.color_index == ALARM )
      {
         if( random_value > 0.999 )
            new_color_index = NORMAL;
         else
            new_color_index = plane.color_index; // Keep alarm for a while
      }

      if( plane.graphics != null &&
          new_color_index != plane.color_index )
         plane.graphics.SetDResource( "ColorIndex", (double)new_color_index );

      plane.color_index = new_color_index;
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Starts simulation for a plane, selects its start 
   // and end nodes. 
   ////////////////////////////////////////////////////////////////////////
   void StartPlane( PlaneData plane, boolean init )
   {
      int
         to_index,
         from_index;

      if( NumNodes < 2 )
         error( "Less then two nodes: can't start planes.", true );

      from_index = (int) Rand( 0., NumNodes - 0.001 );
      do
         {
            to_index = (int) Rand( 0., NumNodes - 0.001 );
         } while( to_index == from_index );

      plane.from_node = NodeArray[ from_index ];
      plane.to_node = NodeArray[ to_index ];
      plane.flight_number = (int) Rand( 101., 1999. );
      plane.speed = Rand( 0.4, 1. ); // Vary plane speed 

      if( init ) // Init the demo: position randomly along the path 
      {
         plane.path_position = Rand( 0.1, 0.9 );
         plane.path_position_last = plane.path_position - 0.05; // For angle
      }
      else // Position at the beginning of the path 
      {
         plane.path_position = 0.;
         plane.path_position_last = 0.;
      }

      String flight_name = "Flight " + plane.flight_number;

      // Add from/to node info to the tooltip.
      plane.tooltip = flight_name + " from " +
         plane.from_node.name + " to " + plane.to_node.name;

      // Set all trajectory points invisible, if any 
      if( plane.trajectory != null )
         plane.trajectory.SetDResource( "Marker%/Visibility", 0. );
   }

   ////////////////////////////////////////////////////////////////////////
   // SIMULATION ONLY: Calculates plane's lat/lon using simulated data. 
   //
   // The simulation moves the plane from the start to the end node/city
   // as controlled by the path_position parameter. The path_position changes
   // in the range from from 0 (start node) to 1 (end node).
   ////////////////////////////////////////////////////////////////////////
   void GetPlaneLatLon( PlaneData plane )
   {
      plane.lat_lon.x =
         RELATIVE_TO_NEW_RANGE( plane.from_node.lat_lon.x,
                           plane.to_node.lat_lon.x, plane.path_position );
      plane.lat_lon.y =
         RELATIVE_TO_NEW_RANGE( plane.from_node.lat_lon.y,
                                plane.to_node.lat_lon.y, plane.path_position );
   }

   //////////////////////////////////////////////////////////////////////////
   // Generate a location info string by converting +- sign info into the
   // N/S, E/W suffixes, and decimal fraction to deg, min, sec.
   //////////////////////////////////////////////////////////////////////////
   String CreateLocationString( GlgPoint point )
   {
      int
        x_deg, y_deg,
        x_min, y_min,
        x_sec, y_sec;
      char
        char_x,
        char_y;
      double lat, lon;

      if( point.z < 0. )
        return "";

      lon = point.x;
      lat = point.y;

      if( lon < 0. )
      {
         lon = -lon;
         char_x = 'W';
      }
      else if( lon >= 360. )
      {
         lon -= 360.;
         char_x = 'E';
      }
      else if( lon >= 180. )
      {
         lon = 180. - ( lon - 180. );
         char_x = 'W';
      }
      else
        char_x = 'E';

      if( lat < 0. )
      {
         lat = -lat;
         char_y = 'S';
      }
      else
        char_y = 'N';

      x_deg = (int) lon;
      x_min = (int) ( ( lon - x_deg ) * 60. );
      x_sec = (int) ( ( lon - x_deg - x_min / 60. ) * 3600. );

      y_deg = (int) lat;
      y_min = (int) ( ( lat - y_deg ) * 60. );
      y_sec = (int) ( ( lat - y_deg - y_min / 60. ) * 3600. );

      return "Lon=" + x_deg + "\u00B0" + x_min + "\'" + x_sec + "\"" + char_x +
            "  Lat=" + y_deg + "\u00B0" + y_min + "\'" + y_sec + "\"" + char_y;
   }

   //////////////////////////////////////////////////////////////////////////
   // Stops updates
   //////////////////////////////////////////////////////////////////////////
   public void stop()
   {
      StopUpdates();

      if( zoom_timer != null )
      {
         zoom_timer.stop();
         zoom_timer = null;
      }

      IsReady = false;
      super.stop();
   }

   //////////////////////////////////////////////////////////////////////////
   void StopUpdates()
   {
      if( timer != null )
      {
         timer.stop();
         timer = null;
      }
   }

   //////////////////////////////////////////////////////////////////////////
   void StartUpdates()
   {
      if( timer == null )
      {
         timer = new Timer( UpdateInterval, this );
         timer.setRepeats( true );
         timer.start();
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // ActionListener method to use the bean as update timer's ActionListener.
   //////////////////////////////////////////////////////////////////////////
   public void actionPerformed( ActionEvent e )
   {
      if( timer != null )
         UpdatePlanes();
   }

   //////////////////////////////////////////////////////////////////////////
   // Show zoom message.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToFloridaStart()
   {
      Drawing.SetDResource( "Map/FloridaZoomingMessage/Visibility", 1. );
      Drawing.Update();
   }

   //////////////////////////////////////////////////////////////////////////
   // Zoom to the Florida area after a few seconds to show details.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToFlorida()
   {
      // Zoom to the Florida boundaries on detailed map.
      GISObject.SetGResource( "GISExtent", 1169530., 1169530., 0. );
      GISObject.SetGResource( "GISCenter" , -82.8239, 28.9382, 0. );

      // Update icon positions after zooming.
      RedoIcons = true;
      UpdateObjectsOnMap( "Zooming, please wait..." );

      // Reorder Florida zoom message to top, otherwise airplane icons 
        // would be flying on top of it.
      GlgObject florida_message =
        Drawing.GetResourceObject( "Map/FloridaZoomingMessage" );
      Map.ReorderElement( Map.GetIndex( florida_message ),
                         Map.GetSize() - 1 );

      Drawing.Update();
   }

   //////////////////////////////////////////////////////////////////////////
   // Remove the Florida zooming message after a few seconds.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToFloridaEnd()
   {
      Drawing.SetDResource( "Map/FloridaZoomingMessage/Visibility", 0. );
      Drawing.Update();
   }

   class NodeData
   {
      String name;
      GlgPoint lat_lon;

      GlgObject graphics;
      GlgPoint xyz = new GlgPoint(); // Position in GLG world coords

      NodeData( String name_p, double lon, double lat )
      {
         name = name_p;
         lat_lon = new GlgPoint( lon, lat, 0. );
      }
   }

   class PlaneData
   {
      String name;
      GlgPoint lat_lon = new GlgPoint();
      int flight_number;
      String tooltip;
      GlgObject graphics;
      GlgObject trajectory;
      NodeData from_node;
      NodeData to_node;
      double path_position;
      double path_position_last;
      double speed;
      GlgPoint xyz = new GlgPoint(); // Position in GLG world coords  
      double angle;
      int has_angle;
      int color_index;
      double elevation;
      int iteration;

      PlaneData()
      {
      }
   }

   class ZoomPerformer implements ActionListener
   {
      GlgAirTrafficDemo bean;
      int stage;

      ZoomPerformer( GlgAirTrafficDemo bean_p )
      {
         bean = bean_p;
         stage = 0;
      }

      public void actionPerformed( ActionEvent e )
      {
         switch( stage )
         {
          case 0: // Display zoom message, yield to let event thread draw it.
            bean.ZoomToFloridaStart();

            stage = 1;
            bean.zoom_timer.setInitialDelay( 1 );
            bean.zoom_timer.start();
            break;

          case 1: // Zoom to the Florida area
            bean.ZoomToFlorida();

            stage = 2;
            bean.zoom_timer.setInitialDelay( FloridaZoomDelay2 );
            bean.zoom_timer.start();
            break;

          case 2: // Erase zoom message after a delay
            bean.ZoomToFloridaEnd();
            zoom_timer = null;
            break;
         }
      }
   }
}
