// @configure_input@

/**************************************************************************\
 * Copyright (c) Kongsberg Oil & Gas Technologies AS
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of the copyright holder nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\**************************************************************************/

/*!
  \class So@Gui@FlyViewer Inventor/@Gui@/viewers/So@Gui@FlyViewer.h
  \brief The So@Gui@FlyViewer class implements controls for moving
  the camera in a "flying" motion.

  \ingroup viewers

  Controls:
  <ul>

  <li>Left mouse button increases the speed.</li>

  <li>Middle mouse button decreases the speed.</li>

  <li>Left and middle mouse button together sets the speed to zero.</li>

  <li>"s" puts the viewer in seek mode. Click some geometry with the
      left mouse button to start the seek zoom animation. (Hitting "s"
      again before clicking will cancel the seek operation.)</li>

  <li>"u" puts the viewer in up-vector pick mode. Click some geometry
      with the left mouse button to set the camera's up-vector to the
      normal vector of the face you pick.
      (Hitting "u" again before clicking will cancel the pick operation.)</li>

  <li>The control key stops the flying and lets you tilt the camera by moving
      the pointer.</li>

  </ul>
*/

/*
  FIXME:
  - animate camera when setting up-vector so the scene doesn't just
    suddenly change.
*/

#include <so@gui@defs.h>
#include <Inventor/@Gui@/viewers/So@Gui@FlyViewer.h>
#include <Inventor/events/SoKeyboardEvent.h>
#include <Inventor/events/SoLocation2Event.h>
#include <Inventor/events/SoMouseButtonEvent.h>
#include <Inventor/nodes/SoCamera.h>
#include <Inventor/nodes/SoCallback.h>
#include <Inventor/nodes/SoCoordinate3.h>
#include <Inventor/nodes/SoSwitch.h>
#include <Inventor/nodes/SoScale.h>
#include <Inventor/nodes/SoTranslation.h>
#include <Inventor/errors/SoDebugError.h>
#include <Inventor/actions/SoSearchAction.h>
#include <Inventor/actions/SoHandleEventAction.h>
#include <Inventor/actions/SoGetBoundingBoxAction.h>
#include <cstring> // strlen() etc
#include <cstdlib> // abs() 
#include <Inventor/@Gui@/So@Gui@Cursor.h>


// ************************************************************************

#ifndef DOXYGEN_SKIP_THIS

// FIXME: We should probably move this class out of this
// impl-file. There is code here that could be factored out and reused
// in other modules, for example camera handling. Now there is a
// duplication with some of the code in So@Gui@ConstrainedViewerP, for
// example. 20021017 rolvs
class So@Gui@FlyViewerP {
public:
  So@Gui@FlyViewerP(So@Gui@FlyViewer * owner);
  ~So@Gui@FlyViewerP();

  enum ViewerMode {
    FLYING, TILTING, WAITING_FOR_SEEK, WAITING_FOR_UP_PICK
  };

  void constructor(SbBool build);

  void dolly(const float delta) const;
  void updateCursorRepresentation(void); // in SoNativeFlyViewer.cpp
  void setMode(ViewerMode newmode);
  int getMode(void) { return this->viewermode; }

#define SO@GUI@_MIN_STEP     0.2f
#define SO@GUI@_INC_FACTOR   1.2f
#define SO@GUI@_MAX_SPEED   20.0f

  SbTime * lastrender;

  float currentspeed;
  
  // Maximum speed, target for currentspeed during
  // acceleration/deceleration.
  float maxspeed; 

  // Scales speed. Calculated in updateSpeedScalingFactor.
  float speed_scaling_factor; 
  
  // Used to calculate a new max_speed, on the basis on 'where' we are
  // in the speed landscape, see {increment|decrement}MaxSpeed(). 
  int max_speed_factor; 

  // Speed
  void incrementMaxSpeed();
  void decrementMaxSpeed();
  void updateMaxSpeed();
  void updateSpeedScalingFactor();
  void stopMoving();

  void updateSpeedIndicator(void);
  void updateCameraPosition( SoCamera * camera, float speed, float dt ); 
  void updateCameraOrientation( SoCamera * camera, 
                                float d_tilt, 
                                float d_pan, 
                                float dt );
  double calculateChangeInTime();
  void  updateCurrentSpeed(double dt);


  // Current keyboard state.
  SbBool button1down;
  SbBool button3down;
  int lctrldown;
  int rctrldown;
  SbBool lshiftdown;
  SbBool rshiftdown;

  // View, speed display, renderingstate.
  SoSearchAction * searcher;

  SoNode * superimposition;
  SoCoordinate3 * sgeometry;

  SoScale * sscale;
  SoScale * crossscale;

  SoTranslation * stranslation;
  SoTranslation * crossposition;

  SoSwitch * smaxspeedswitch; 
  SoSwitch * scurrentspeedswitch;
  SoSwitch * crossswitch;

  SoNode * getSuperimpositionNode(const char * name);

  void superimpositionevent(SoAction * action);
  static void superimposition_cb(void * closure, SoAction * action);


  // 
  float tilt_increment; // Angle-adjustment between View-up and direction 
  float pan_increment;  // Rotation-adjustment around View-up

  SbVec2s mouseloc;
  SbVec2s lastpos;
  SbVec2s tiltpos;

  // FIXME: Refactor event handlers and PUBLIC(this)->processSoEvent
  // in a way similar to that in SoGuiExaminerViewer, where only
  // internal state is red/set. 20021017 rolvs
  SbBool processKeyboardEvent(const SoKeyboardEvent * const kevt);
  SbBool processMouseButtonEvent(const SoMouseButtonEvent * const mevt);
  SbBool processLocation2Event(const SoLocation2Event * const levt);
private:
  So@Gui@FlyViewer * publ;
  ViewerMode viewermode;
};

So@Gui@FlyViewerP::So@Gui@FlyViewerP(So@Gui@FlyViewer * owner)
{
  this->searcher = NULL;
  this->publ = owner;
  this->viewermode = FLYING;
  this->currentspeed = 0.0f;
  this->maxspeed = 0.0f;
  this->speed_scaling_factor = 0.4f;
  this->max_speed_factor = 0;
  this->stranslation = NULL;
  this->sscale = NULL;
  this->button1down = FALSE;
  this->button3down = FALSE;
  this->lctrldown = 0;
  this->rctrldown = 0;
  this->lshiftdown = FALSE;
  this->rshiftdown = FALSE;
  this->lastrender = new SbTime;
  this->tilt_increment = 0.0f;
  this->pan_increment = 0.0f;
}

So@Gui@FlyViewerP::~So@Gui@FlyViewerP(void)
{
  delete this->searcher;
  delete this->lastrender;

  // superimposition unrefed in other destructor
}

#define PRIVATE(o) (o->pimpl)
#define PUBLIC(o) (o->publ)


// Common constructor code.
void
So@Gui@FlyViewerP::constructor(SbBool build)
{
  PUBLIC(this)->setClassName(PUBLIC(this)->getDefaultWidgetName());

  static const char * superimposed[] = {
    "#Inventor V2.1 ascii",
    "",
    "Separator {",
    "  MaterialBinding {",
    "    value OVERALL",
    "  }",
    "  OrthographicCamera {",
    "    height 1",
    "    nearDistance 0",
    "    farDistance 1",
    "  }",
    "  DEF so@gui@->callback Callback { }",
    "  Separator {",
    "    DEF so@gui@->translation Translation {",
    "      translation 0 0 0",
    "    }",
    "    DEF so@gui@->scale Scale {",
    "      scaleFactor 1 1 1",
    "    }",
    "    DEF so@gui@->geometry Coordinate3 {",
    "      point [",
    "       -0.8 -0.04 0,",
    "       -0.8  0    0,",
    "       -0.8  0.04 0,",
    "        0   -0.04 0,",
    "        0    0    0,",
    "        0    0.04 0,",
    "        0.8 -0.04 0,",
    "        0.8  0    0,",
    "        0.8  0.04 0,", 
    "        0    0.02 0,", // idx 9
    "        0.8  0.02 0,",
    "        0.8 -0.02 0,",
    "        0   -0.02 0,",
    "        0    0.01 0,", // idx 13
    "        0.4  0.01 0,",
    "        0.4 -0.01 0,",
    "        0   -0.01 0",
    "      ]",
    "    }",
    "    DEF so@gui@->maxspeedswitch Switch {",
    "      whichChild -3",
    // max speed indicator
    "      Material {",
    "        emissiveColor 1 0 0",
    "      }",
    "      IndexedFaceSet {",
    "        coordIndex [",
    "          12, 11, 10, 9, -1",
    "        ]",
    "      }",
    "    }",
    // the coordinate system
    "    BaseColor {",
    "      rgb 1 1 1",
    "    }",
    "    IndexedLineSet {",
    "      coordIndex [",
    "        0, 2, -1,",
    "        3, 5, -1,",
    "        6, 8, -1,",
    "        1, 7, -1",
    "      ]",
    "    }",
    // current speed indicator
    "    DEF so@gui@->currentspeedswitch Switch {",
    "      whichChild -3",
    "      Material {",
    "        emissiveColor 0 0 1",
    "      }",
    "      IndexedFaceSet {",
    "        coordIndex [",
    "          16, 15, 14, 13, -1",
    "        ]",
    "      }",
    "    }",
    "  }",
    // cross
    "  DEF so@gui@->crossswitch Switch {",
    "    whichChild -1",
    "    DEF so@gui@->crossposition Translation {",
    "      translation 0 0 0",
    "    }",
    "    DEF so@gui@->crossscale Scale {",
    "      scaleFactor 1 1 1",
    "    }",
    "    BaseColor {",
    "      rgb 1 0 0",
    "    }",
    "    Coordinate3 {",
    "      point [",
    "        0 -1  0,",
    "        0  1  0,",
    "       -1  0  0,",
    "        1  0  0",
    "      ]",
    "    }",
    "    IndexedLineSet {",
    "      coordIndex [",
    "        0, 1, -1,",
    "        2, 3, -1",
    "      ]",
    "    }",
    "  }",
    "}",
    NULL
  };

  int i;
  size_t bufsize;
  for (i = 0, bufsize = 0; superimposed[i]; i++)
    bufsize += strlen(superimposed[i]) + 1;
  char * buf = new char [bufsize + 1];
  for (i = 0, bufsize = 0; superimposed[i]; i++) {
    strcpy(buf + bufsize, superimposed[i]);
    bufsize += strlen(superimposed[i]);
    buf[bufsize] = '\n';
    bufsize++;
  }
  SoInput * input = new SoInput;
  input->setBuffer(buf, bufsize);
  SbBool ok = SoDB::read(input, this->superimposition);
  assert(ok);
  delete input;
  delete [] buf;
  this->superimposition->ref();


  this->sscale = (SoScale *)
    this->getSuperimpositionNode("so@gui@->scale");
  this->stranslation = (SoTranslation *)
    this->getSuperimpositionNode("so@gui@->translation");
  this->sgeometry = (SoCoordinate3 *)
    this->getSuperimpositionNode("so@gui@->geometry");
  this->smaxspeedswitch = (SoSwitch *)
    this->getSuperimpositionNode("so@gui@->maxspeedswitch");
  this->scurrentspeedswitch = (SoSwitch *)
    this->getSuperimpositionNode("so@gui@->currentspeedswitch");
  this->crossswitch = (SoSwitch *)
    this->getSuperimpositionNode("so@gui@->crossswitch");
  this->crossposition = (SoTranslation *)
    this->getSuperimpositionNode("so@gui@->crossposition");
  this->crossscale = (SoScale *)
    this->getSuperimpositionNode("so@gui@->crossscale");

  SoCallback * cb = (SoCallback *)
    this->getSuperimpositionNode("so@gui@->callback");
  cb->setCallback(So@Gui@FlyViewerP::superimposition_cb, this);

  this->updateSpeedIndicator();

  PUBLIC(this)->addSuperimposition(this->superimposition);
  PUBLIC(this)->setSuperimpositionEnabled(this->superimposition,TRUE);

  if (build) {
    @WIDGET@ viewer = PUBLIC(this)->buildWidget(PUBLIC(this)->getParentWidget());
    PUBLIC(this)->setBaseWidget(viewer);
  }
}

// This method dollies the camera back and forth in the scene.
void
So@Gui@FlyViewerP::dolly(const float delta) const
{
  SoCamera * const camera = PUBLIC(this)->getCamera();
  if (camera == NULL) { return; } // if there's no scene graph, for instance

  SbPlane walkplane(PUBLIC(this)->getUpDirection(), 
		    camera->position.getValue());

  SbVec3f campos = camera->position.getValue();
  SbVec3f camvec;
  camera->orientation.getValue().multVec(SbVec3f(0, 0, -1), camvec);
  SbLine cross(campos + camvec,
                campos + camvec + PUBLIC(this)->getUpDirection());
  SbVec3f intersect;
  walkplane.intersect(cross, intersect);
  SbVec3f dir = intersect - campos;
  dir.normalize();

  camera->position = campos - dir * delta;
}

// The viewer is a state machine, and all changes to the current state
// are made through this call.
void
So@Gui@FlyViewerP::setMode(ViewerMode newmode)
{
  this->viewermode = newmode;
  this->updateCursorRepresentation();
}

// This method locates a named node in the superimposed scene.
SoNode *
So@Gui@FlyViewerP::getSuperimpositionNode(const char * name)
{
  if (! this->searcher)
    this->searcher = new SoSearchAction;
  searcher->reset();
  searcher->setName(SbName(name));
  searcher->setInterest(SoSearchAction::FIRST);
  searcher->setSearchingAll(TRUE);
  searcher->apply(this->superimposition);
  assert(searcher->getPath());
  return searcher->getPath()->getTail();
}

SbBool
So@Gui@FlyViewerP::processKeyboardEvent(const SoKeyboardEvent * const ke)
{
  assert( ke != NULL );
  switch (ke->getState()) {
  case SoButtonEvent::UP:
    switch (ke->getKey()) {
    case SoKeyboardEvent::U:
      do {
	// either to switch to up-vector pick mode, or back to fly
	// mode if pick-mode already activated (i.e. cancel the
	// up-vector pick operation)
	SbBool uppickmode =
	  this->getMode() == So@Gui@FlyViewerP::WAITING_FOR_UP_PICK;
	this->setMode(uppickmode ? So@Gui@FlyViewerP::FLYING :
			       So@Gui@FlyViewerP::WAITING_FOR_UP_PICK);

        this->stopMoving();

	this->updateSpeedIndicator();
	PUBLIC(this)->scheduleRedraw();
	return TRUE;
      } while (FALSE);
      break;
      
    case SoKeyboardEvent::S:
      this->stopMoving();
      this->updateSpeedIndicator();
      PUBLIC(this)->scheduleRedraw();
      return FALSE;
      
    case SoKeyboardEvent::LEFT_SHIFT:
      this->lshiftdown = FALSE;
      if (this->lshiftdown < 0) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKeyboardEvent",
			   "left shift key count < 0");
#endif
	this->lshiftdown = 0;
      }
      break;
    case SoKeyboardEvent::RIGHT_SHIFT:
      this->rshiftdown = FALSE;
      if (this->rshiftdown < 0) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKeyboardEvent",
			   "right shift key count < 0");
#endif
	this->rshiftdown = 0;
      }
      break;
    case SoKeyboardEvent::LEFT_CONTROL:
      this->lctrldown -= 1;
      if (this->lctrldown < 0) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKyeboardEvent",
			   "left control key count < 0");
#endif
	this->lctrldown = 0;
      }
      break;
    case SoKeyboardEvent::RIGHT_CONTROL:
      this->rctrldown -= 1;
      if (this->rctrldown < 0) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKyeboardEvent",
			   "right control key count < 0");
#endif
	this->rctrldown = 0;
      }
      break;
    default:
      break;
    }
    break;
  case SoButtonEvent::DOWN:
    switch (ke->getKey()) {
    case SoKeyboardEvent::LEFT_SHIFT:
      this->lshiftdown += 1;
      if (this->lshiftdown > 2) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKeyboardEvent",
			   "left shift key count > 2");
#endif
	this->lshiftdown = 2;
      }
      break;
    case SoKeyboardEvent::RIGHT_SHIFT:
      this->rshiftdown += 1;
      if (this->rshiftdown > 2) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKeyboardEvent",
			   "right shift key count > 2");
#endif
	this->rshiftdown = 2;
      }
      break;
    case SoKeyboardEvent::LEFT_CONTROL:
      this->lctrldown += 1;
      if (this->lctrldown > 2) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewerP::processKeyboardEvent",
			   "left control key count > 2");
#endif
	this->lctrldown = 2;
      }
      break;
    case SoKeyboardEvent::RIGHT_CONTROL:
      this->rctrldown += 1;
      if (this->rctrldown > 2) {
#if SO@GUI@_DEBUG
	SoDebugError::post("So@Gui@FlyViewer::processSoEvent",
			   "right control key count > 2");
#endif
	this->rctrldown = 2;
      }
      break;
    default:
      break; 
    }
    break;
  default:
    break;
  }

  if ((this->getMode() == So@Gui@FlyViewerP::FLYING) &&
      (this->lctrldown || this->rctrldown)) {
    this->setMode(So@Gui@FlyViewerP::TILTING);
    
    this->tiltpos = this->mouseloc;
    this->lastpos = this->mouseloc;

    this->stopMoving();
    this->updateSpeedIndicator();
    this->crossswitch->whichChild.setValue(SO_SWITCH_ALL);
    PUBLIC(this)->scheduleRedraw();
    // NOTE; this could be optimized to only draw the superimposition in
    // question if speed is zero.
  } else if ((this->getMode() == So@Gui@FlyViewerP::TILTING) &&
	     !this->lctrldown && !this->rctrldown) {
    this->setMode(So@Gui@FlyViewerP::FLYING);
    assert(this->crossswitch != NULL);
    this->crossswitch->whichChild.setValue(SO_SWITCH_NONE);
    PUBLIC(this)->scheduleRedraw();
  }
  return FALSE;
}


SbBool
So@Gui@FlyViewerP::processMouseButtonEvent( const SoMouseButtonEvent * const me )
{
  assert( me != NULL );
  
  // FIXME: only for fly mode
  switch (this->getMode()) {
  case So@Gui@FlyViewerP::WAITING_FOR_UP_PICK:
    if ((me->getButton() == SoMouseButtonEvent::BUTTON1) &&
	(me->getState() == SoButtonEvent::DOWN)) {
      PUBLIC(this)->findUpDirection(me->getPosition());
      this->setMode(So@Gui@FlyViewerP::FLYING);
      return TRUE;
    }
    break;
  case So@Gui@FlyViewerP::FLYING:
    switch (me->getButton()) {
    case SoMouseButtonEvent::BUTTON1:

      switch (me->getState()) {
        
      case SoButtonEvent::DOWN:
        // Incrementing speed.
	this->button1down = TRUE;
	if (this->button3down) {
          this->stopMoving();
	} 
        else {
          this->incrementMaxSpeed();
	}
	this->updateSpeedIndicator();
	PUBLIC(this)->scheduleRedraw();
	return TRUE;
      case SoButtonEvent::UP:
	this->button1down = FALSE;
	return TRUE;
      default:
	break;
      }
      break;

    case SoMouseButtonEvent::BUTTON3:

      switch (me->getState()) {
      case SoButtonEvent::DOWN:
	this->button3down = TRUE;
	
        if (this->button1down) {
          this->stopMoving();
	} 
        else
          this->decrementMaxSpeed();
     
	this->updateSpeedIndicator();
	PUBLIC(this)->scheduleRedraw();
	return TRUE;
      case SoButtonEvent::UP:
	this->button3down = FALSE;
	return TRUE;
      default:
	break;
      }
      break;
    default:
      break;
    }
  default:
    break;
  }
  return FALSE;
}

SbBool
So@Gui@FlyViewerP::processLocation2Event(const SoLocation2Event * const lev)
{  
  this->mouseloc = lev->getPosition();

  if (this->getMode() == So@Gui@FlyViewerP::TILTING) {

    float pan = (this->lastpos[0] - this->mouseloc[0])/100.0f;
    float tilt = (this->lastpos[1] - this->mouseloc[1])/100.0f;
    
    SoCamera * camera = PUBLIC(this)->getCamera();
    if (camera == NULL) 
      return TRUE; // probably sceneless
    
    this->updateCameraOrientation( camera, tilt, pan, 1.0f );
    this->lastpos = this->mouseloc;
  }

  // FIXME: The size of the glcanvas only changes when the viewer is
  // resized. The GLSize should be set from the FlyViewer, to remove
  // the dependency on the PUBLIC(this) class. 20021021 rolvs
  SbVec2s glsize( PUBLIC(this)->getGLSize() );

  // NOTE: The values are normalized, so that the FlyViewer behaves
  // the same way no matter the screen-size. The old way to do it made
  // the possible range of pan and tilt increment depend on the canvas
  // size. 20021022 rolvs.
  this->pan_increment = 0.5f - float(this->mouseloc[0])/glsize[0]; 
  this->tilt_increment = 0.5f - float(this->mouseloc[1])/glsize[1];

  return TRUE;
}

void
So@Gui@FlyViewerP::superimpositionevent(SoAction * action)
{
  if (!action->isOfType(SoGLRenderAction::getClassTypeId())) return;
  SbViewportRegion vpRegion =
    ((SoGLRenderAction *) action)->getViewportRegion();
  SbVec2s viewport = vpRegion.getViewportSizePixels();
  float aspect = float(viewport[0]) / float(viewport[1]);
  float factorx = 1.0f/float(viewport[1]) * 220.0f;
  float factory = factorx;
  if (aspect > 1.0f) {
    this->stranslation->translation.setValue(SbVec3f(0.0f, -0.4f, 0.0f));
  } else {
    this->stranslation->translation.setValue(SbVec3f(0.0f, -0.4f / aspect, 0.0f));
    factorx /= aspect;
    factory /= aspect;
  }
  if (viewport[0] > 500)
    factorx *= 500.0f / 400.0f;
  else
    factorx *= float(viewport[0]) / 400.0f;
  this->sscale->scaleFactor.setValue(SbVec3f(factorx, factory, 1.0f));

  if (this->getMode() == TILTING) {
    assert(this->crossposition != NULL);
    assert(this->crossscale != NULL);
    float tx = float(this->tiltpos[0]-float(viewport[0])/2.0f)/(float(viewport[0]));
    float ty = float(this->tiltpos[1]-float(viewport[1])/2.0f)/(float(viewport[1]));
    if (aspect > 1.0f) tx *= aspect;
    else ty /= aspect;
    this->crossposition->translation.setValue(SbVec3f(tx, ty, 0));

    float sx = (1.0f/float(viewport[0])) * 15.0f;
    float sy = (1.0f/float(viewport[1])) * 15.0f;
    if (aspect > 1.0f) sx *= aspect;
    else sy /= aspect;
    this->crossscale->scaleFactor.setValue(SbVec3f(sx, sy, 0));
  }
}

void
So@Gui@FlyViewerP::superimposition_cb(void * closure, SoAction * action)
{
  assert(closure != NULL);
  ((So@Gui@FlyViewerP *) closure)->superimpositionevent(action);
}

void
So@Gui@FlyViewerP::updateSpeedIndicator(void)
{
  assert(this->sgeometry != NULL);

  SbVec3f * points = this->sgeometry->point.startEditing();

  if (points[10][0] == 0.0f)
    this->smaxspeedswitch->whichChild.setValue(SO_SWITCH_ALL);
  if (points[14][0] == 0.0f)
    this->scurrentspeedswitch->whichChild.setValue(SO_SWITCH_ALL);
  points[10][0] = this->maxspeed / (SO@GUI@_MAX_SPEED / 0.8f);
  points[11][0] = this->maxspeed / (SO@GUI@_MAX_SPEED / 0.8f);
  points[14][0] = this->currentspeed / (SO@GUI@_MAX_SPEED / 0.8f);
  points[15][0] = this->currentspeed / (SO@GUI@_MAX_SPEED / 0.8f);
  this->sgeometry->point.finishEditing();

  if (this->maxspeed == 0.0f)
    this->smaxspeedswitch->whichChild.setValue(SO_SWITCH_NONE);
  if (this->currentspeed == 0.0f)
    this->scurrentspeedswitch->whichChild.setValue(SO_SWITCH_NONE);
}

double So@Gui@FlyViewerP::calculateChangeInTime()
{
  SbTime thisrender; 
  thisrender.setToTimeOfDay();

  if (this->currentspeed == 0.0f)
    this->lastrender->setValue(thisrender.getValue() - 0.01);

  // We've had a report on Coin-support that floats may have too low
  // precision for the subtraction of these two values (i.e. it becomes
  // zero), so don't cast to float.
  //
  // FIXME: it doesn't sound likely that this was the real cause of
  // the problem. First of all, it seems improbably that precision
  // could be so bad for floats, as the time between render frames
  // should almost be guaranteed to be milliseconds, at least.  It is
  // suspicious that the error only shows up with the Intel C++
  // compiler, and not when the reported built with MSVC++ instead.
  //
  // Second, the fix is not sufficient. What if the
  // SbTime::getTimeOfDay() resolution is too low on the particular
  // system, so we often get zero difference here? That case must be
  // handled, and from the original bug report, it sounds like it
  // isn't, which is a separate bug in itself.
  //
  // Third, what's up with that magic multiplication factor of 10?
  // That doesn't seem to make sense.
  //
  // 20061212 mortene.

  // This is only a problem on release builds which makes it sound
  // like it's just some value that is not properly
  // initialized. (20061212 frodo)

  double t = (thisrender.getValue() - this->lastrender->getValue()) * 10.0;

  if (t >= 1.0) 
    t = 1.0;

  return t;
}

void So@Gui@FlyViewerP::updateCurrentSpeed(double dt)
{
  float speedscale = 
    1.0f - (this->pan_increment * this->pan_increment 
            + this->tilt_increment * this->tilt_increment);
  
  // NOTE: I don't believe that this boundary condition could ever
  // happen. 20021022 rolvs
  if (speedscale < 0.0f)
    speedscale = 0.0f;
  
  this->currentspeed +=
    (((this->currentspeed +
       this->maxspeed * speedscale) / 2.0f) - 
     this->currentspeed) * float(dt);
}

void So@Gui@FlyViewerP::updateCameraPosition(SoCamera * camera, 
                                             float current_speed,
                                             float dt)
{
  assert(camera != NULL);
  SbVec3f dir;
  camera->orientation.getValue().multVec(SbVec3f(0, 0, -1), dir);
  dir.normalize();
  camera->position.setValue(camera->position.getValue() +
                            dir * (current_speed * dt));
}
 
void So@Gui@FlyViewerP::updateCameraOrientation(SoCamera * camera, 
                                                float d_tilt, 
                                                float d_pan, 
                                                float dt)
{
  assert(camera != NULL);
  // FIXME: Make sure that the angle between direction and up-vector
  // stays larger than zero, or else it gets 'locked' in an undefined
  // state and starts to act weird. This should probably be done in
  // parent class. 20021017 rolvs
  PUBLIC(this)->tiltCamera(d_tilt * dt);
  
  camera->orientation = camera->orientation.getValue() *
    SbRotation(PUBLIC(this)->getUpDirection(), d_pan * dt);
}

void
So@Gui@FlyViewerP::incrementMaxSpeed(void)
{
  this->max_speed_factor++;
  this->updateMaxSpeed();
}


void
So@Gui@FlyViewerP::decrementMaxSpeed(void)
{	
  this->max_speed_factor--;
  this->updateMaxSpeed();
}


void 
So@Gui@FlyViewerP::updateSpeedScalingFactor(void)
{
  SoNode * n = PUBLIC(this)->getSceneGraph();
  if(n == NULL)
    return; // Scenegraph not set yet?

  SoGetBoundingBoxAction bbact(PUBLIC(this)->getViewportRegion());
  bbact.apply(n);
  
  SbBox3f bbox = bbact.getBoundingBox();
  float bbox_diagonal = (bbox.getMax() - bbox.getMin()).length();

  // FIXME: It should be possible to create a simple scaling function,
  // based on some logarithmic evaluation. 20021017 rolvs.
  if (bbox_diagonal > 100)
    this->speed_scaling_factor = 1.0f; // log(bbox_diagonal);
  else if (bbox_diagonal > 10 && bbox_diagonal < 100)
    this->speed_scaling_factor = 0.4f; 
  else if (bbox_diagonal > 1 && bbox_diagonal < 10)
    this->speed_scaling_factor = 0.3f; 
  else if (bbox_diagonal > 0.1 && bbox_diagonal < 1)
    this->speed_scaling_factor = 0.1f; 
  else
    this->speed_scaling_factor = 0.1f * bbox_diagonal;
}

void So@Gui@FlyViewerP::stopMoving(void)
{
  this->maxspeed = 0.0f;
  this->currentspeed = 0.0f; 
  this->max_speed_factor = 0;
}

void So@Gui@FlyViewerP::updateMaxSpeed(void)
{
  if (this->max_speed_factor == 0) {
    this->stopMoving();
    return;
  }

  // FIXME: Move this methodcall so that it is called only
  // once. (e.g. when scene graph is set) 20021021 rolvs
  this->updateSpeedScalingFactor();

  this->maxspeed = 
    this->max_speed_factor 
    * float(pow(SO@GUI@_INC_FACTOR, abs( this->max_speed_factor)))
    * this->speed_scaling_factor;

  if (this->maxspeed > SO@GUI@_MAX_SPEED)
    this->maxspeed = SO@GUI@_MAX_SPEED;
  else if (this->maxspeed < -1*SO@GUI@_MAX_SPEED)
    this->maxspeed = -1 * SO@GUI@_MAX_SPEED;
}



// Set cursor graphics according to mode.
void
So@Gui@FlyViewerP::updateCursorRepresentation(void)
{
  if (!PUBLIC(this)->isCursorEnabled()) {
    PUBLIC(this)->setComponentCursor(So@Gui@Cursor::getBlankCursor());
    return;
  }

  switch (this->viewermode) {
  case So@Gui@FlyViewerP::FLYING:
    PUBLIC(this)->setComponentCursor(So@Gui@Cursor(So@Gui@Cursor::DEFAULT));
    break;

  case So@Gui@FlyViewerP::WAITING_FOR_SEEK:
    PUBLIC(this)->setComponentCursor(So@Gui@Cursor(So@Gui@Cursor::CROSSHAIR));
    break;

  case So@Gui@FlyViewerP::WAITING_FOR_UP_PICK:
    PUBLIC(this)->setComponentCursor(So@Gui@Cursor(So@Gui@Cursor::UPARROW));
    break;

  case So@Gui@FlyViewerP::TILTING:
    PUBLIC(this)->setComponentCursor(So@Gui@Cursor::getPanCursor());
    break;

  default:
    assert(0 && "unknown mode");
    break;
  }
}

#endif // DOXYGEN_SKIP_THIS

// ************************************************************************

SO@GUI@_OBJECT_SOURCE(So@Gui@FlyViewer);

// ************************************************************************

/*!
  Public constructor.
*/
So@Gui@FlyViewer::So@Gui@FlyViewer(@WIDGET@ parent,
                                   const char * name, 
                                   SbBool embed, 
                                   So@Gui@FullViewer::BuildFlag flag,
                                   So@Gui@Viewer::Type type)
  : inherited(parent, name, embed, flag, type, FALSE)
{
  PRIVATE(this) = new So@Gui@FlyViewerP(this);
  PRIVATE(this)->constructor(TRUE);
}

// ************************************************************************

/*!
  Protected constructor, used by viewer components derived from the
  So@Gui@FlyViewer.
*/
So@Gui@FlyViewer::So@Gui@FlyViewer(@WIDGET@ parent,
                                   const char * const name,
                                   SbBool embed, 
                                   So@Gui@FullViewer::BuildFlag flag, 
                                   So@Gui@Viewer::Type type, 
                                   SbBool build)
  : inherited(parent, name, embed, flag, type, FALSE)
{
  PRIVATE(this) = new So@Gui@FlyViewerP(this);
  PRIVATE(this)->constructor(build);
}

// ************************************************************************

/*!
  Virtual constructor.
*/
So@Gui@FlyViewer::~So@Gui@FlyViewer()
{
  if (PRIVATE(this)->superimposition != NULL) {
    this->removeSuperimposition(PRIVATE(this)->superimposition);
    PRIVATE(this)->superimposition->unref();
    PRIVATE(this)->superimposition = NULL;
  }
  delete PRIVATE(this);
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::setViewing(SbBool enable)
{
  if (enable != this->isViewing())
    PRIVATE(this)->stopMoving();

  inherited::setViewing(enable);
  this->setSuperimpositionEnabled(PRIVATE(this)->superimposition, enable);
  this->scheduleRedraw();
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::resetToHomePosition(void)
{
  PRIVATE(this)->stopMoving();
  inherited::resetToHomePosition();
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::viewAll(void)
{
  PRIVATE(this)->stopMoving();
  inherited::viewAll();
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::setCamera(SoCamera * camera)
{
  PRIVATE(this)->stopMoving();

  inherited::setCamera(camera);
  // FIXME: do something with up-direction? 
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::setCursorEnabled(SbBool enable)
{
  inherited::setCursorEnabled(enable);
  PRIVATE(this)->updateCursorRepresentation();
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::setCameraType(SoType type)
{
  PRIVATE(this)->stopMoving();
  inherited::setCameraType(type);
  // FIXME: what else? 20010907 mortene.
}

// ************************************************************************

// doc in super
const char *
So@Gui@FlyViewer::getDefaultWidgetName(void) const
{
  static const char defaultWidgetName[] = "So@Gui@FlyViewer";
  return defaultWidgetName;
}

// ************************************************************************

// doc in super
const char *
So@Gui@FlyViewer::getDefaultTitle(void) const
{
  static const char defaultTitle[] = "Fly Viewer";
  return defaultTitle;
}

// ************************************************************************

// doc in super
const char *
So@Gui@FlyViewer::getDefaultIconTitle(void) const
{
  static const char defaultIconTitle[] = "Fly Viewer";
  return defaultIconTitle;
}

// ************************************************************************

// Documented in superclass.
SbBool
So@Gui@FlyViewer::processSoEvent(const SoEvent * const event)
{
  // FIXME: Refactor the event-handling so that it uses the same
  // strategy as in So@Gui@ExaminerViewer, where the event-handler
  // only checks the state and the mode from that. 20021016 rolvs.

  // We're in "interact" mode (i.e. *not* the camera modification mode),
  // so don't handle the event here. It should either be forwarded to
  // the scene graph, or caught by So@Gui@Viewer::processSoEvent() if
  // it's an ESC and ALT press (to switch modes).
  if (!this->isViewing()) { return inherited::processSoEvent(event); }

  // Events when in "ready-to-seek" mode are ignored, except those
  // which influence the seek mode itself -- these are handled further
  // up the inheritance hierarchy.
  if (this->isSeekMode()) { return inherited::processSoEvent(event); }
  
  // FIXME: There is more parts of the code in
  // So@Gui@*FlyViewer::processEvent that should go in to the
  // processKeyboardEvent function; to be fixed later. 
  // 20021015 rolvs

  // Keyboard handling
  if (event->isOfType(SoKeyboardEvent::getClassTypeId())) {
    SbBool result =
      PRIVATE(this)->processKeyboardEvent( (SoKeyboardEvent*)event );

    if( result ){
      return TRUE;
    }
    // Else: Do nothing, and proceed as usual
  }

  // Mousebutton handling
  // See FIXME and comment for keyboardhandler.
  else if (event->isOfType(SoMouseButtonEvent::getClassTypeId())) {
    // FIXME: only for fly mode
    const SoMouseButtonEvent * const me = 
      (const SoMouseButtonEvent *const) event;
    SbBool result = PRIVATE( this )->processMouseButtonEvent( me );
    if( result ) 
      return TRUE;
  }

  else if (event->isOfType(SoLocation2Event::getClassTypeId())) {
    const SoLocation2Event * const le = 
      (const SoLocation2Event * const) event;
    SbBool result = PRIVATE( this )->processLocation2Event( le );
    if( result ) 
      return TRUE;
  }

  return inherited::processSoEvent(event);
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::setSeekMode(SbBool enable)
{
  // Note: this method is almost identical to the setSeekMode() in the
  // So@Gui@ExaminerViewer, so migrate any changes.

#if SO@GUI@_DEBUG
  if (enable == this->isSeekMode()) {
    SoDebugError::postWarning("So@Gui@FlyViewer::setSeekMode",
                              "seek mode already %sset", enable ? "" : "un");
    return;
  }
#endif // SO@GUI@_DEBUG

  // FIXME: what if we're in the middle of a seek already? 20010910 mortene.
  // larsa - either stop the seek (on false) or reset timer to two new secs

  inherited::setSeekMode(enable);
  PRIVATE(this)->setMode(enable ? So@Gui@FlyViewerP::WAITING_FOR_SEEK :
                            So@Gui@FlyViewerP::FLYING);
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::actualRedraw(void)
{
  if (!this->isViewing()) {
    inherited::actualRedraw();
    return;
  }

  switch (PRIVATE(this)->getMode()) {
  case So@Gui@FlyViewerP::FLYING:
    {
      PRIVATE(this)->updateCurrentSpeed(PRIVATE(this)->calculateChangeInTime());
      PRIVATE(this)->updateSpeedIndicator();

      SbTime thisrender; 
      thisrender.setToTimeOfDay();

      if (PRIVATE(this)->currentspeed != 0.0f) {
        // We've had a report on Coin-support that floats may have too
        // low precision for the subtraction of these two values (i.e.
        // it becomes zero), so don't cast to float.
        // 
        // Note: there's some additional information about this, see
        // the FIXME comment in the
        // So@Gui@FlyViewerP::calculateChangeInTime() function.
        double t = (thisrender.getValue() -
                    PRIVATE(this)->lastrender->getValue()) * 2.0;
        if (t > 0.0) {
          SoCamera * camera = this->getCamera();

          if (camera){ // could be a sceneless viewer
            PRIVATE(this)->updateCameraPosition
              ( camera,
                PRIVATE(this)->currentspeed*
                PRIVATE(this)->speed_scaling_factor,
                float(t) );
            PRIVATE(this)->updateCameraOrientation
              ( camera, 
                PRIVATE(this)->tilt_increment, 
                PRIVATE(this)->pan_increment, 
                float(t) );
          }
        }
      }
      
      inherited::actualRedraw();
      
      PRIVATE(this)->lastrender->setValue(thisrender.getValue());
      
      if (PRIVATE(this)->currentspeed != 0.0f ||
          PRIVATE(this)->maxspeed != 0.0f)
        this->scheduleRedraw();
    }
    break;
  default:
    inherited::actualRedraw();
    break;
  }
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::rightWheelMotion(float value)
{
  PRIVATE(this)->dolly(value - this->getRightWheelValue());
  inherited::rightWheelMotion(value);
}

// ************************************************************************

// doc in super
void
So@Gui@FlyViewer::afterRealizeHook(void)
{
  PRIVATE(this)->updateCursorRepresentation();
  inherited::afterRealizeHook();
}

// ************************************************************************

#undef PRIVATE
#undef PUBLIC

