

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

//////////////////////////////////////////////////////////////////////////
// GIS 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.
//////////////////////////////////////////////////////////////////////////

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

   String map_name = "gis_demo.g";

   double PlaneSpeed = 0.005; // Relative units
   double MaxZoomSpeed = 5.; // In XY Coordinates

   int USZoomDelay1 = 3000; // Delay to zoom to the US area to show details.
   int USZoomDelay2 = 1000; // Delay to remove US zooming message.

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

   GlgObject
     Drawing,
     PositionObject,
     PositionArea,
     NodeTemplate[] = new GlgObject[ 2 ],
     PlaneTemplate[] = new GlgObject[ 2 ],
     Map[] = new GlgObject[ 2 ],
     GISObject[] = new GlgObject[ 2 ],
     NodeGroup[] = new GlgObject[ 2 ],
     PlaneGroup[] = new GlgObject[ 2 ];

   final double // Plane size constants
     SMALL_SIZE = 0.6,
     MEDIUM_SIZE = 0.8,
     BIG_SIZE = 1.0;

   double
     PlaneSize = MEDIUM_SIZE,
     // Dimensions of the map viewport windows
     window_width[] = new double[ 2 ],
     window_height[] = new double[ 2 ];

   int
     NumNodes,
     NumPlanes = 10,
     MapServer,
     MapProjection[] = new int[ 2 ];

   // If true, pan the map to make the selected plane visible in the current
     // zoomed area
   boolean LockSelectedPlane;

   boolean
     CityLabels = true,
     StateDisplay = true;

   GlgPoint
     // Store initial extent and center, used to reset
     InitExtent[] = new GlgPoint[ 2 ],
     InitCenter[] = new GlgPoint[ 2 ],
     // Temp vars: allocate once
     lat_lon = new GlgPoint(),
     last_lat_lon = new GlgPoint(),
     old_position = new GlgPoint(),
     point = new GlgPoint(),
     rect_point = new GlgPoint(),
     util_point = new GlgPoint();

   static boolean StandAlone = false;
   boolean MapIsReady = false;
   boolean TopMapIsReady = false;
   boolean PanMode = false;

   Timer timer = null;
   Timer zoom_timer = null;
   boolean DoUpdate = true;

   PlaneData PlaneArray[];
   PlaneData SelectedPlane;

   // 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 )
   };

   //////////////////////////////////////////////////////////////////////////
   public GlgGISDemo()
   {
      super();
      SetDResource( "$config/GlgSwingUsage", 1. );
      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 );
   }

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

   //////////////////////////////////////////////////////////////////////////
   public static void Main( final String arg[] )
   {
      class DemoQuit extends WindowAdapter
      {
         public void windowClosing( WindowEvent e ) { System.exit( 0 ); }
      }

      GlgGISDemo.StandAlone = true;

      // Map server URL from the command line
      if( Array.getLength( arg ) != 0 )
         GlgGISDemo.SuppliedMapServerURL = arg[ 0 ];

      JFrame frame = new JFrame();

      frame.setResizable( true );
      frame.setSize( 800, 650 );
      frame.setLocation( 200, 20 );

      GlgGISDemo map_demo = new GlgGISDemo();

      // Use getContentPane() for GlgJBean
      frame.getContentPane().add( map_demo );

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

      // The ReadyCallback will start updates when the drawing is loaded.
      map_demo.SetDrawingName( map_demo.map_name );
   }

   //////////////////////////////////////////////////////////////////////////
   // Invoked before the hierarchy setup.
   //////////////////////////////////////////////////////////////////////////
   public void HCallback( GlgObject viewport )
   {
      Drawing = viewport;
      Map[ 0 ] = Drawing.GetResourceObject( "TopMap" ); // Thumbnail map
      Map[ 1 ] = Drawing.GetResourceObject( "Map" ); // Detailed map

      // Display thumbnail map in a separate window. It is kept as a child
        // window in the drawing for the convinience of editing.
      Map[ 0 ].SetDResource( "ShellType", (double) GlgObject.DIALOG_SHELL );

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

      Init();
   }

   //////////////////////////////////////////////////////////////////////////
   // Initializes icons in the drawing
   //////////////////////////////////////////////////////////////////////////
   void Init()
   {
      GlgObject resource;
      int i, j;

      for( i=0; i<2; ++i ) // For each map window
      {
         // Get IDs of the GIS map objects in each of the map viewports. 
         GISObject[ i ] = Map[ i ].GetResourceObject( "GISObject" );





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

         // Query and store the GIS projection (ORTHOGRAPHIC or RECTANGULAR)
           // used to render the map.
         MapProjection[ i ] = (int)
           GetDResource( GISObject[ i ], "GISProjection" );

         // Set GIS Zoom mode: generate a new map request for a new area on 
           // zoom/pan.
         Map[ i ].SetGISZoom( null, GISObject[ i ], null );

         // Store initial map extent for resetting after zooming.
         InitExtent[i] = GISObject[ i ].GetGResource( "GISExtent" );
         InitCenter[i] = GISObject[ i ].GetGResource( "GISCenter" );
      }

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

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

      // Get node and plane templates from the palette. Two sets of templates
      // are used: smaller icons for the thumbnail view and more elaborate 
        // ones for the detailed map.
      for( i=0; i<2; ++i )
      {
         NodeTemplate[ i ] =
           palette.GetResourceObject( i == 0 ? "Node1" : "Node2" );

         PlaneTemplate[ i ] =
           palette.GetResourceObject( i == 0 ? "Plane1" : "Plane2" );

         // If the icon is not a marker (Scale resource exists), set the icon's
           // size.
         resource = PlaneTemplate[ i ].GetResourceObject( "Scale" );
         if( resource != null )
           resource.SetDResource( null, PlaneSize );
      }

      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 );

         StartPlane( PlaneArray[ i ], true );
      }

      // Add node and plane icons to both thumbnail and detailed map.
      for( i=0; i<2; ++i )
      {
         CreateAirportIcons( i ); // Add airport icons
         CreatePlaneIcons( i ); // Add plane icons

         // Check if the icon has an angle to indicate its direction.
         if( PlaneTemplate[ i ].GetResourceObject( "Angle" ) != null )
           for( j=0; j < NumPlanes; ++j )
             PlaneArray[ j ].has_angle[ i ] = true;
      }

      // Selected area annotates the currently viewed area of the detailed map 
      // in the thumbnail map view. Reorder SelectedArea on top of icons
        // (last in the array).
      GlgObject selected_area = Map[ 0 ].GetResourceObject( "SelectedArea" );
      Map[ 0 ].ReorderElement( Map[ 0 ].GetIndex( selected_area ),
                              Map[ 0 ].GetSize() - 1 );

      // Set state display on the thumbnail map. Airport labels on the detailed
        // map are handled by HandleZoomLevel(). 
      SetGISLayers( 0 );

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

      InitSelection(); // Zoom on the US and select some plane.

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

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

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

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

   //////////////////////////////////////////////////////////////////////////
   // Initializes the drawing and starts updates.
   //////////////////////////////////////////////////////////////////////////
   public void ReadyCallback( GlgObject viewport )
   {
      super.ReadyCallback();

      StartUpdates();

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

   //////////////////////////////////////////////////////////////////////////
   // Creates NodeGroup to hold airport icons and adds the icons to it.
   //////////////////////////////////////////////////////////////////////////
   void CreateAirportIcons( int map )
   {
      NodeGroup[ map ] = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );
      NodeGroup[ map ].SetSResource( "Name", "NodeGroup" );

      // Add city/node icons
      for( int i = 0; i < NumNodes; ++i )
        AddNode( NodeArray[ i ], map, i );

      Map[ map ].AddObjectToBottom( NodeGroup[ map ] );
   }

   //////////////////////////////////////////////////////////////////////////
   // Creates PlaneGroup to hold plane icons and adds the icons to it.
   //////////////////////////////////////////////////////////////////////////
   void CreatePlaneIcons( int map )
   {
      PlaneGroup[ map ] = new GlgDynArray( GlgObject.GLG_OBJECT, 0, 0 );
      PlaneGroup[ map ].SetSResource( "Name", "PlaneGroup" );

      // Add plane icons
      for( int i=0; i < NumPlanes; ++i )
        AddPlane( PlaneArray[ i ], map, i );

      Map[ map ].AddObjectToBottom( PlaneGroup[ map ] );
   }

   //////////////////////////////////////////////////////////////////////////
   void AddNode( NodeData node_data, int map, int index )
   {
      // Create a copy of a node.
      GlgObject node =
        NodeTemplate[ map ].CloneObject( GlgObject.STRONG_CLONE );

      node.SetSResource( "Name", node_data.name ); // Set object name

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

      if( map == 1 ) // On the detailed map, show node name label.
        node.SetSResource( "LabelString", node_data.name );

      String tooltip;
      if( map == 0 ) // On the thumbnail map, show node name in the tooltip.
        tooltip = node_data.name;
      else // On the detailed map, include lat/lon into the tooltip.
        tooltip =
          node_data.name + ", " + CreateLocationString( node_data.lat_lon );
      node.SetSResource( "TooltipString", tooltip );

      node_data.graphics[ map ] = node;

      // The node will be positioned after the GIS object is setup.
        // PositionNode( node_data, map );  

      NodeGroup[ map ].AddObjectToBottom( node );
   }

   //////////////////////////////////////////////////////////////////////////
   // Adds a plane icon, fills labels, tooltips, etc.
   //////////////////////////////////////////////////////////////////////////
   void AddPlane( PlaneData plane_data, int map, int index )
   {
      // Create a copy of a node.
      GlgObject plane =
        PlaneTemplate[ map ].CloneObject( GlgObject.STRONG_CLONE );

      plane.SetSResource( "Name", plane_data.name ); // Object name

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

      if( map == 1 ) // On detailed map, show the flight number as icon label
        plane.SetSResource( "LabelString",
                           "Flight " + plane_data.flight_number );

      // Set the tooltip, created by StartPlane method.
      plane.SetSResource( "TooltipString", plane_data.tooltip[ map ] );

      plane_data.graphics[ map ] = plane;

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

      PlaneGroup[ map ].AddObjectToBottom( plane );
   }

   //////////////////////////////////////////////////////////////////////////
   void PositionNode( NodeData node, int map )
   {
      if( node.graphics == null )
        return;

      // Converts node position from lat/lon to GLG coordinates.
      GetNodePosition( node, map );

      // Update node's icon in the drawing
      node.graphics[ map ].SetGResource( "Position", node.adj_xyz[ map ] );
   }

   //////////////////////////////////////////////////////////////////////////
   void PositionPlane( PlaneData plane, int map )
   {
      if( plane.graphics[ map ] == null ||
         plane.from_node == null || plane.to_node == null )
        return;

      // Converts plane position (simulated or real data) from lat/lon to 
        // GLG coordinates.
      GetPlanePosition( plane, map, true );

      // Update plane's icon in the drawing
      plane.graphics[ map ].SetGResource( "Position", plane.adj_xyz[ map ] );

      // Update icon's direction angle is necessary
      if( plane.has_angle[ map ] )
        plane.graphics[ map ].SetDResource( "Angle", plane.angle[ map ] );
   }

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

      // Prevent wrap-around errors under big zoom factors. Also handles
      // visibility of hidden nodes on another side of the globe
        // (z < 0 in ORTHOGRAPHIC projection ).
      if( node.xyz[ map ].z < 0. ||
         !GetVisibility( node.xyz[ map ], 1.1 ) )
      {
         // Not visible. Use smaller coordinates just outside the visible area
         // to prevent wrap-around errors. We could remove the node from the 
           // drawing and show only the visible ones.
         node.adj_xyz[ map ].x = 2000.;
         node.adj_xyz[ map ].y = 2000.;
      }
      else
        node.adj_xyz[ map ].CopyFrom( node.xyz[ map ] );
   }

   //////////////////////////////////////////////////////////////////////////
   // Converts plane's position from lat/lon to X/Y in GLG world coordinates.
   //////////////////////////////////////////////////////////////////////////
   void GetPlanePosition( PlaneData plane, int map, boolean get_angle )
   {
      // 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[ map ].GISConvert( null, GlgObject.OBJECT_COORD,
                                     /* Lat/Lon to XY */ false,
                                     plane.lat_lon, plane.xyz[ map ] );

      // Prevent wrap-around errors under big zoom factors. Also handles
        // visibility of hidden planes on another side of the globe
        // (z < 0 in ORTHOGRAPHIC projection ).
      if( plane.xyz[ map ].z < 0. ||
         !GetVisibility( plane.xyz[ map ], 1.1 ) )
      {
         plane.adj_xyz[ map ].x = 2000.;
         plane.adj_xyz[ map ].y = 2000.;
         plane.angle[ map ] = 0.;
      }
      else
      {
         plane.adj_xyz[ map ].CopyFrom( plane.xyz[ map ] );

         if( get_angle && plane.has_angle[ map ] )
           plane.angle[ map ] = GetPlaneAngle( plane, map );
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Calculates plane icon's directional angle.
   //////////////////////////////////////////////////////////////////////////
   double GetPlaneAngle( PlaneData plane, int map )
   {
      double angle;

      if( MapProjection[ map ] == 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[ map ],
                          plane.to_node.xyz[ map ] );
      }
      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[ map ].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[ map ] );

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

   //////////////////////////////////////////////////////////////////////////
   // Checks if the object is visible in the current zoom region.
   // This prevents wrap-around errors under big zoom factors.
   //////////////////////////////////////////////////////////////////////////
   boolean GetVisibility( GlgPoint position, double adj )
   {
      // Use adj as a gap
        return
          position.x > -1000. * adj && position.x < 1000. * adj &&
            position.y > -1000. * adj && position.y < 1000. * adj;
   }

   //////////////////////////////////////////////////////////////////////////
   // FOR SIMULATION: 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 ] );

      if( LockSelectedPlane )
        UpdateLocking();

      Update();
   }

   //////////////////////////////////////////////////////////////////////////
   // FOR SIMULATION: Calculates new plane position.
   //////////////////////////////////////////////////////////////////////////
   void UpdatePlane( PlaneData plane )
   {
      if( plane.graphics == null ||
         plane.from_node == null || plane.to_node == null )
        return;

      if( plane.path_position == 1. )
        StartPlane( plane, false ); // Finished old path, start a new one. 
      else
      {
         double speed = PlaneSpeed;

         // Slow the selected plane down when zoomed on it for a nice
           // demo effect.
         if( plane == SelectedPlane && LockSelectedPlane )
         {
            double stored_position = plane.path_position;

            GetPlanePosition( plane, 1, false );
            old_position.CopyFrom( plane.xyz[ 1 ] );

            plane.path_position += plane.speed * speed;

            GetPlanePosition( plane, 1, false );

            // Distance between the old and current position of the plane
            double dist = GetLength( old_position, plane.xyz[ 1 ] );

            if( dist > MaxZoomSpeed )
            {
               double slow_down = dist / MaxZoomSpeed;
               speed /= slow_down;
            }

            plane.path_position = stored_position; // restore
         }

         // 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.;
      }

      for( int i =0; i<2; ++i )
        PositionPlane( plane, i ); // Position the plane on both maps
   }


   //////////////////////////////////////////////////////////////////////////
   // In the lock mode, pans the map to keep selected plane visible when the 
   // plane moves out of the detailed map area.
   //////////////////////////////////////////////////////////////////////////
   void UpdateLocking()
   {
      int map = 1; // Checking is done on the detailed map

      UpdateStatus(); // Update selected plane lat/lon display

      GetPlanePosition( SelectedPlane, map, false );

      // If selected plane goes on another side of the globe or 
      // off the visible portion of the map, pan to re-center
        // on the selected plane

      if( MapProjection[ map ] == GlgObject.ORTHOGRAPHIC_PROJECTION &&
         SelectedPlane.xyz[ 1 ].z < 0.1 ||
         !GetVisibility( SelectedPlane.xyz[ 1 ], 0.9 ) )
      {
         String message =
           "Loading new map to keep the selected plane in sight, " +
             "please wait...";

         CenterOnPlane( SelectedPlane, 1 );
         UpdateObjectsOnMap( 1, message );

         if( MapProjection[ map ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
         {
            CenterOnPlane( SelectedPlane, 0 );
            UpdateObjectsOnMap( 0, message );
         }
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Compensate plane icons for X/Y stretch. Screen coord. extent may be 
   // used instead.
   //////////////////////////////////////////////////////////////////////////
   void AdjustPlane( int map )
   {
      GlgObject first_plane = PlaneArray[ 0 ].graphics[ map ];

      GlgObject scale_x = first_plane.GetResourceObject( "ScaleX" );
      GlgObject scale_y = first_plane.GetResourceObject( "ScaleY" );

      if( scale_x != null && scale_y != null )
      {
         double ratio_half;
         double wh_ratio = window_width[ map ] / window_height[ map ];

         if( wh_ratio > 1. )
         {
            ratio_half = Math.sqrt( wh_ratio );

            scale_y.SetDResource( null, ratio_half );
            scale_x.SetDResource( null, 1. / ratio_half );
         }
         else
         {
            ratio_half = Math.sqrt( 1. / wh_ratio );

            scale_y.SetDResource( null, 1. / ratio_half );
            scale_x.SetDResource( null, ratio_half );
         }
      }
   }

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

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

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

      for( i = 0; i < NumNodes; ++i )
        PositionNode( NodeArray[ i ], map );

      for( i = 0; i < NumPlanes; ++i )
        PositionPlane( PlaneArray[ i ], map );

      SetStatus( "" );

      // Adjust selected region on the thumbnail map to match detailed map.
      SetSelectedArea();
   }

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

      super.InputCallback( vp, 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( format.equals( "Window" ) )
      {
         if( action.equals( "DeleteWindow" ) )
         {
            if( origin.equals( "SelectionDialog" ) )
            {
               // Close selection dialog
               Drawing.SetDResource( "SelectionDialog/Visibility", 0. );
               Update();
               return;
            }
            else if( origin.equals( "TopMap" ) )
            {
               // Close top map window
               Drawing.SetDResource( "TopMap/Visibility", 0. );
               Update();
               return;
            }
            else
              // Closing main window when stand-alone: exit.
              System.exit( 0 );
            return;
         }
         else if( action.equals( "FirstExposure" ) )
         {
            origin = message_obj.GetSResource( "Origin" );
            if( origin.equals( "Map" ) )
              MapIsReady = true;
            else if( origin.equals( "TopMap" ) )
              TopMapIsReady = true;
         }
      }

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

         PanMode = false; // Abort Pan mode

         if( origin.equals( "CloseDialog" ) )
         {
            Drawing.SetDResource( "SelectionDialog/Visibility", 0. );
            Update();
         }
         else if( origin.equals( "ToggleLock" ) )
         {
            SetLocking( !LockSelectedPlane );
            Update();
         }
         else if( origin.equals( "ZoomIn" ) )
         {
            // Allow selected plane to be outside of the visible area: we
              // are zooming on different area.
            SetLocking( false );
            Zoom( 'i', 2. );
            HandleZoomLevel();
            Update();
         }
         else if( origin.equals( "ZoomOut" ) )
         {
            Zoom( 'o', 2. );
            HandleZoomLevel();
            Update();
         }
         else if( origin.equals( "ZoomReset" ) )
         {
            SetLocking( false );

            Zoom( 'n', 0. );
            HandleZoomLevel();
            Update();
         }
         else if( origin.equals( "ZoomTo" ) )
         {
            SetLocking( false );

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

            PanMode = true;
            SetLocking( false );
            SetStatus( "Click to define a new center." );
            Update();
         }
         else if( origin.equals( "Up" ) )
         {
            // Allow selected plane to be outside of the visible area: we
              // are panning out. */
            SetLocking( false );
            Zoom( 'u', 0. );
            Update();
         }
         else if( origin.equals( "Down" ) )
         {
            SetLocking( false );
            Zoom( 'd', 0. );
            Update();
         }
         else if( origin.equals( "Left" ) )
         {
            SetLocking( false );
            Zoom( 'l', 0. );
            Update();
         }
         else if( origin.equals( "Right" ) )
         {
            SetLocking( false );
            Zoom( 'r', 0. );
            Update();
         }
         else if( origin.equals( "AirportLabels" ) )
         {
            CityLabels = !CityLabels;
            SetGISLayers( 1 );
            Update();
         }
         else if( origin.equals( "Planes" ) )
         {
            ToggleResource( Map[ 0 ], "PlaneGroup/Visibility" );
            ToggleResource( Map[ 1 ], "PlaneGroup/Visibility" );
            Update();
         }
         else if( origin.equals( "ValueDisplay" ) )
         {
            // Visibility of all labels is constrained, set just one.
            ToggleResource( PlaneArray[ 0 ].graphics[ 1 ],
                           "Label/Visibility" );
            Update();
         }
         else if( origin.equals( "ToggleStates" ) )
         {
            StateDisplay = !StateDisplay;
            SetGISLayers( 0 ); // Thumbnail map
            SetGISLayers( 1 ); // Detailed map 

            Update();
         }
         else if( origin.equals( "Update" ) )
         {
            DoUpdate = !DoUpdate;
         }
         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( action.equals( "Zoom" ) && subaction.equals( "End" ) )
      {
         // Update icon positions after zooming.
         UpdateObjectsOnMap( 1, "Zooming or panning, please wait..." );
         Map[ 1 ].Update();

         // Get the center of the detailed map.
         GlgPoint center = GISObject[ 1 ].GetGResource( "GISCenter" );

         // Rotate the thumbnail globe to show the same area.
         GISObject[ 0 ].SetGResource( "GISCenter", center );

         HandleZoomLevel();

         // Updates icons and selected area display on the thumbnail globe.
         UpdateObjectsOnMap( 0, "Zooming, please wait..." );
         Update();
      }
      else if( format.equals( "CustomEvent" ) &&
              action.equals( "MouseClick" ) )
      {
         if( Map[ 1 ].GetDResource( "ZoomToMode" ).intValue() != 0 )
           return; // Don't handle selection in ZoomTo mode.

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

         String custom_event = message_obj.GetSResource( "EventLabel" );

         if( custom_event.equals( "Plane" ) )
         {
            int data_index =
              message_obj.GetDResource( "Object/DataIndex" ).intValue();

            if( SelectedPlane != PlaneArray[ data_index ] )
            {
               SelectPlane( SelectedPlane, 0 ); // Unhighlight old
               SelectedPlane = PlaneArray[ data_index ];
               SelectPlane( SelectedPlane, 1 ); // Highlight new 
               Update();
            }
         }
      }
   }

   //////////////////////////////////////////////////////////////////////////
   void Zoom( char type, double value )
   {
      switch( type )
      {
       default:
         Map[ 1 ].SetZoom( null, type, value );
         UpdateObjectsOnMap( 1, "Zooming, please wait..." );

         // After "1:1" zoom reset, the maps' centers differ, sync the centers
           // when zooming in the first time. */      
         switch( type )
         {
          case 'i':
          case 'u':
          case 'd':
          case 'l':
          case 'r':
            // Get the center of the detailed map.
            GlgPoint center = GISObject[ 1 ].GetGResource( "GISCenter" );

            // Get the center of the thumbnail globe.
            GlgPoint globe_center = GISObject[ 0 ].GetGResource( "GISCenter" );

            // First time: centers differ, sync up.
            if( globe_center.x != center.x || globe_center.y != center.y ||
               globe_center.z != center.z )
            {
               // Rotate the thumbnail globe to show the same area
               GISObject[ 0 ].SetGResource( "GISCenter", center );
               UpdateObjectsOnMap( 0, "Zooming, please wait..." );
            }
         }
         break;

       case 'n':
         if( MapProjection[ 0 ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
         {
            // Reset thumbnail globe to initial position.
            GISObject[ 0 ].SetGResource( "GISCenter", InitCenter[0] );
            GISObject[ 0 ].SetGResource( "GISExtent", InitExtent[0] );

            UpdateObjectsOnMap( 0, "Reloading map, please wait..." );
         }

         // Reset detailed map to initial extent.
         GISObject[ 1 ].SetGResource( "GISCenter", InitCenter[1] );
         GISObject[ 1 ].SetGResource( "GISExtent", InitExtent[1] );
         UpdateObjectsOnMap( 1, "Reloading map, please wait..." );

         // Make selected area rectangle invisible when no zoom
         Map[ 0 ].SetDResource( "SelectedArea/Visibility", 0. );
         break;
      }
   }

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

      if( !MapIsReady || !TopMapIsReady )
        return;

      int event_type = trace_info.event.getID();

      // Use the Map area events only.
      if( trace_info.viewport == Map[ 0 ] )
        map = 0;
      else if( trace_info.viewport == Map[ 1 ] )
        map = 1;
      else
      {
         // 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_MOVED: // Report lat/lon position under the mouse.
       case MouseEvent.MOUSE_DRAGGED:
         point.x = (double) ((MouseEvent)trace_info.event).getX();
         point.y = (double) ((MouseEvent)trace_info.event).getY();
         point.z = 0.;

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

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

       case MouseEvent.MOUSE_PRESSED:
         // Handle paning: set the new map center to the location of the click.
           // Handles paning on both maps. 
         if( !PanMode )
           return;

         if( GetButton( trace_info.event ) != 1 )
            return; // Use the left button clicks only.

         PanMode = false;

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

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

         if( MapProjection[ 0 ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
         {
            // Pan/Rotate globe on the thumbnail map as well
            // Don't do anything for the rectangular projection: the whole 
              // world is displayed anyway.
            GISObject[ 0 ].SetGResource( "GISCenter", lat_lon );
            UpdateObjectsOnMap( 0, "Paning, please wait..." );
            Map[ 0 ].Update();
         }

         // Pan detailed map
         GISObject[ 1 ].SetGResource( "GISCenter", lat_lon );
         UpdateObjectsOnMap( 1, "Paning the map..." );
         Update();
         break;

       case ComponentEvent.COMPONENT_RESIZED:
         Component widget = (Component) Map[ map ].GetResource( "Widget" );
         int width = widget.getSize().width;
         int height = widget.getSize().height;

         if( width == window_width[ map ] && height == window_height[ map ] )
           return;

         window_width[ map ] = width;
         window_height[ map ] = height;

         /* Adjust icons to maintain X/Y ratio when the window is resized. */
         AdjustPlane( map );

         // No need to adjust icon positions if the GIS object has Stretch=YES
         break;

       default: return;
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Adjust selected region on the thumbnail map to match detailed map.
   //////////////////////////////////////////////////////////////////////////
   void SetSelectedArea()
   {
      GlgObject point_obj[] = new GlgObject[ 16 ];
      GlgPoint lat_lon[] = new GlgPoint[ 16 ];
      int i;

      // Set the coordinates of the SelectedArea polygon.
      GlgObject rect = Map[ 0 ].GetResourceObject( "SelectedArea" );

      GlgPoint extent = GetExtentDegrees( 1 );

      if( extent.x >= 120. )
      {
         // Big area: don't need to show.
         rect.SetDResource( "Visibility", 0. );
      }
      else
      {
         rect.SetDResource( "Visibility", 1. );

         // Get polygon points
         for( i=0; i<16; ++i )
           point_obj[ i ] = (GlgObject) rect.GetElement( i );

         // Get lat/lon on detailed map
         lat_lon[ 0 ] = GetLatLon( -1000., -1000., 1 );
         lat_lon[ 1 ] = GetLatLon( -1000., -500., 1 );
         lat_lon[ 2 ] = GetLatLon( -1000., 0., 1 );
         lat_lon[ 3 ] = GetLatLon( -1000., 500., 1 );
         lat_lon[ 4 ] = GetLatLon( -1000., 1000., 1 );
         lat_lon[ 5 ] = GetLatLon( -500., 1000., 1 );
         lat_lon[ 6 ] = GetLatLon( 0., 1000., 1 );
         lat_lon[ 7 ] = GetLatLon( 500., 1000., 1 );
         lat_lon[ 8 ] = GetLatLon( 1000., 1000., 1 );
         lat_lon[ 9 ] = GetLatLon( 1000., 500., 1 );
         lat_lon[ 10 ] = GetLatLon( 1000., 0., 1 );
         lat_lon[ 11 ] = GetLatLon( 1000., -500., 1 );
         lat_lon[ 12 ] = GetLatLon( 1000., -1000., 1 );
         lat_lon[ 13 ] = GetLatLon( 500., -1000., 1 );
         lat_lon[ 14 ] = GetLatLon( 0., -1000., 1 );
         lat_lon[ 15 ] = GetLatLon( -500., -1000., 1 );

         for( i=0; i<16; ++i )
         {
            // Converts lat/lon on the detailed map to X/Y on the thumbnail map
              // using GIS object's current projection.
            GISObject[ 0 ].GISConvert( null, GlgObject.OBJECT_COORD,
                                      /* Lat/Lon to X/Y */ false,
                                      lat_lon[ i ], rect_point );

            point_obj[ i ].SetGResource( null, rect_point );
         }
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // FOR SIMULATION ONLY: Starts simulation for a plane, selects its start 
   // and end nodes.
   //////////////////////////////////////////////////////////////////////////
   void StartPlane( PlaneData plane, boolean init )
   {
      if( NumNodes < 2 )
      {
         System.out.println( "Less then two nodes: can't start planes." );
         return;
      }

      int to_index;
      int 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 )
      {
         plane.path_position = Rand( 0.1, 0.2 );
         plane.path_position_last = plane.path_position - 0.05; // For angle
      }
      else
      {
         plane.path_position = 0.;
         plane.path_position_last = 0.;
      }

      plane.tooltip[ 0 ] = "Flight " + plane.flight_number;

      // On the detailed map, add from/to node info to the tooltip.
      plane.tooltip[ 1 ] = plane.tooltip[ 0 ] +
        " from " + plane.from_node.name + " to " + plane.to_node.name;

      for( int i = 0; i < 2; ++i )
        if( plane.graphics[ i ] != null )
          plane.graphics[ i ].SetSResource( "TooltipString",
                                           plane.tooltip[ i ] );

      // Stop tracking the selected flight when it reaches destination
      if( plane == SelectedPlane )
        SetLocking( false );
   }

   //////////////////////////////////////////////////////////////////////////
   // FOR 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 );
   }

   //////////////////////////////////////////////////////////////////////////
   // FOR SIMULATION ONLY: select a plane to zoom on on the initial appearance.
   //////////////////////////////////////////////////////////////////////////
   void InitSelection()
   {
      // Select the first plane
      SelectedPlane = PlaneArray[ 0 ];
      SelectPlane( SelectedPlane, 1 );

      // Lock on the selected plane, pan the map to keep it visible.
      SetLocking( true );

      // Rotate thumbnail globe too to show the same location.
      if( MapProjection[ 0 ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
        GISObject[ 0 ].SetGResource( "GISCenter", -95.35, 37.37, 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 ) );
   }

   //////////////////////////////////////////////////////////////////////////
   // Turns plane icons' labels ON or OFF on the detailed map.
   //////////////////////////////////////////////////////////////////////////
   void SetPlaneLabels( boolean on )
   {
      GlgObject label =
        PlaneArray[ 0 ].graphics[ 1 ].GetResourceObject( "Label" );
      if( label != null )
        label.SetDResource( "Visibility", on ? 1. : 0. );
   }

   //////////////////////////////////////////////////////////////////////////
   // Sets locking mode ON or OFF. If locking is ON, the map is automatically
   // scrolled to keep the selected plane icon in view.
   void SetLocking( boolean lock )
   {
      LockSelectedPlane = lock;
      UpdateStatus();
   }

   //////////////////////////////////////////////////////////////////////////
   // Displays locking status.
   //////////////////////////////////////////////////////////////////////////
   void UpdateStatus()
   {
      String message;

      if( LockSelectedPlane )
      {
         GetPlaneLatLon( SelectedPlane );

         message = "Locked on Flight " + SelectedPlane.flight_number +
           " " + CreateLocationString( SelectedPlane.lat_lon );
      }
      else
        message = "Selected Plane Locking is off.";
      Drawing.SetSResource( "StatusLabel/String", message );
   }

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

   //////////////////////////////////////////////////////////////////////////
   // Centers the map on the selected plane, used when locking mode is ON.
   //////////////////////////////////////////////////////////////////////////
   void CenterOnPlane( PlaneData plane, int map )
   {
      GetPlaneLatLon( plane );

      // Center the map on the plane
      GISObject[ map ].SetGResource( "GISCenter", plane.lat_lon );
   }

   //////////////////////////////////////////////////////////////////////////
   // Highlights the selected plane on both maps by changing its 
   // SelectedIndex value.
   //////////////////////////////////////////////////////////////////////////
   void SelectPlane( PlaneData plane, int selected )
   {
      for( int i=0; i < 2; ++i )
        if( plane.graphics[ i ] != null )
          plane.graphics[ i ].SetDResource( "SelectedIndex",
                                           (double)selected );
   }

   //////////////////////////////////////////////////////////////////////////
   void SetPlaneSize()
   {
      for( int i=0; i<2; ++i )
      {
         GlgObject resource = PlaneTemplate[ i ].GetResourceObject( "Scale" );
         if( resource != null )
           resource.SetDResource( null, PlaneSize );
      }
   }

   //////////////////////////////////////////////////////////////////////////
   // Toggle resource between 0 and 1.
   //////////////////////////////////////////////////////////////////////////
   void ToggleResource( GlgObject object, String res_name )
   {
      double value = object.GetDResource( res_name ).doubleValue();
      object.SetDResource( res_name, value != 0. ? 0. : 1. );
   }

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

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

      layers = "default_gis";
      // Add city layers if they are on on the detailed map.
      if( map == 1 )
        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";
      else
        // Disable state outline display.
        layers = layers + ",-states";
      GISObject[ map ].SetSResource( "GISLayers", layers );
   }
   ////////////////////////////////////////////////////////////////////////
   // Convenience wrapper
   ////////////////////////////////////////////////////////////////////////
   GlgPoint GetLatLon( double x, double y, int map )
   {
      GlgPoint lat_lon = new GlgPoint();
      util_point.x = x;
      util_point.y = y;
      util_point.z = 0.;
      GISObject[ map ].GISConvert( null, GlgObject.OBJECT_COORD,
                                  /* X/Y to Lat/Lon */ true,
                                  util_point, lat_lon );
      return lat_lon;
   }
   ////////////////////////////////////////////////////////////////////////
   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;
   }
   //////////////////////////////////////////////////////////////////////////
   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.;
   }
   //////////////////////////////////////////////////////////////////////////
   // Stops updates.
   //////////////////////////////////////////////////////////////////////////
   public void stop()
   {
      StopUpdates();
      if( zoom_timer != null )
      {
         zoom_timer.stop();
         zoom_timer = null;
      }
      MapIsReady = false;
      TopMapIsReady = false;
      super.stop();
   }
   //////////////////////////////////////////////////////////////////////////
   void StopUpdates()
   {
      if( timer != null )
      {
         timer.stop();
         timer = null;
      }
   }
   //////////////////////////////////////////////////////////////////////////
   void StartUpdates()
   {
      if( timer == null )
      {
         timer = new Timer( 30, 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 && MapIsReady && TopMapIsReady )
        UpdatePlanes();
   }
   //////////////////////////////////////////////////////////////////////////
   // Turn airport and plane labels on or off depending on the zoom level and
   // adjust plane icon size. 
   //////////////////////////////////////////////////////////////////////////
   void HandleZoomLevel()
   {
      GlgPoint extent;
      boolean high_zoom;
      extent = GetExtentDegrees( 1 );
      high_zoom = ( extent.x < 100. && extent.y < 50. );
      SetPlaneLabels( high_zoom ); // Plane labels.
      CityLabels = true;
      SetGISLayers( 1 ); /* Airport labels. */
      // Plane icons size.
      PlaneSize = ( high_zoom ? MEDIUM_SIZE : SMALL_SIZE );
      SetPlaneSize();
   }
   //////////////////////////////////////////////////////////////////////////
   // Gets extent in lat/lon.
   // For the ortho projection, roughly converts from meters to lat/lon.
   //////////////////////////////////////////////////////////////////////////
   GlgPoint GetExtentDegrees( int map )
   {
      GlgPoint extent = GetGResource( GISObject[ map ], "GISExtent" );
      if( MapProjection[ map ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
      {
         extent.x = extent.x / GlgObject.EQUATOR_RADIUS * 90.;
         extent.y = extent.y / GlgObject.POLAR_RADIUS * 90.;
      }
      return extent;
   }
   //////////////////////////////////////////////////////////////////////////
   // 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;
   }
   //////////////////////////////////////////////////////////////////////////
   // Show zoom message.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToUSStart()
   {
      Drawing.SetDResource( "Map/USZoomingMessage/Visibility", 1. );
      Drawing.Update();
   }
   //////////////////////////////////////////////////////////////////////////
   // Zoom to the US area after a few seconds to show details.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToUS()
   {
      // Zoom to the US boundaries on detailed map.
      GISObject[ 1 ].SetGResource( "GISCenter", -95.35, 37.37, 0. );
      GISObject[ 1 ].SetGResource( "GISExtent", 69.71, 34.85, 0. );
      if( MapProjection[ 0 ] == GlgObject.ORTHOGRAPHIC_PROJECTION )
        // Rotate thumbnail globe too to show the same location.
        GISObject[ 0 ].SetGResource( "GISCenter", -95.35, 37.37, 0. );
      HandleZoomLevel();
      // Update icon positions after zooming.
      UpdateObjectsOnMap( 1, "Zooming, please wait..." );
      UpdateObjectsOnMap( 0, "Zooming, please wait..." );
      // Reorder US zoom message to top, otherwise airplane icons 
        // would be flying on top of it.
      GlgObject florida_message =
        Drawing.GetResourceObject( "Map/USZoomingMessage" );
      Map[1].ReorderElement( Map[1].GetIndex( florida_message ),
                            Map[1].GetSize() - 1 );
      Drawing.Update();
   }
   //////////////////////////////////////////////////////////////////////////
   // Remove the US zooming message after a few seconds.
   //////////////////////////////////////////////////////////////////////////
   void ZoomToUSEnd()
   {
      Drawing.SetDResource( "Map/USZoomingMessage/Visibility", 0. );
      Drawing.Update();
   }
   class NodeData
   {
      String name;
      GlgPoint lat_lon;
      GlgObject graphics[] = new GlgObject[ 2 ];
      // Position in GLG world coords on both maps
      GlgPoint xyz[] = new GlgPoint[ 2 ];
      // Adjusted position to avoid overflow and wrap-around errors.
      GlgPoint adj_xyz[] = new GlgPoint[ 2 ];
      NodeData( String name_p, double lon, double lat )
      {
         name = name_p;
         lat_lon = new GlgPoint( lon, lat, 0. );
         for( int i=0; i<2; ++i )
         {
            xyz[ i ] = new GlgPoint();
            adj_xyz[ i ] = new GlgPoint();
         }
      }
   }
   class PlaneData
   {
      String name;
      GlgPoint lat_lon;
      int flight_number;
      String tooltip[] = new String[ 2 ];
      GlgObject graphics[] = new GlgObject[ 2 ];
      NodeData from_node;
      NodeData to_node;
      double path_position;
      double path_position_last;
      double speed;
      // Position in GLG world coords on both maps
      GlgPoint xyz[] = new GlgPoint[ 2 ];
      // Adjusted position to avoid overflow and wrap-around errors.
      GlgPoint adj_xyz[] = new GlgPoint[ 2 ];
      boolean has_angle[] = new boolean[ 2 ];
      double angle[] = new double[ 2 ];
      PlaneData()
      {
         lat_lon = new GlgPoint();
         for( int i=0; i<2; ++i )
         {
            xyz[ i ] = new GlgPoint();
            adj_xyz[ i ] = new GlgPoint();
         }
      }
   }
   class ZoomPerformer implements ActionListener
   {
      GlgGISDemo bean;
      int stage;
      ZoomPerformer( GlgGISDemo 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.ZoomToUSStart();
            stage = 1;
            bean.zoom_timer.setInitialDelay( 1 );
            bean.zoom_timer.start();
            break;
          case 1: // Zoom to the US area
            bean.ZoomToUS();
            stage = 2;
            bean.zoom_timer.setInitialDelay( USZoomDelay2 );
            bean.zoom_timer.start();
            break;
          case 2: // Erase zoom message after a delay
            bean.ZoomToUSEnd();
            zoom_timer = null;
            break;
         }
      }
   }
}
