/*=============================================================================

  Program:   Visualization Toolkit
  Module:    vtkGeoCamera.cxx

  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
  All rights reserved.
  See Copyright.txt or http://www.kitware.com/Copyright.htm for details.

     This software is distributed WITHOUT ANY WARRANTY; without even
     the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
     PURPOSE.  See the above copyright notice for more information.

=============================================================================*/
/*-------------------------------------------------------------------------
  Copyright 2008 Sandia Corporation.
  Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
  the U.S. Government retains certain rights in this software.
-------------------------------------------------------------------------*/

#include "vtkObjectFactory.h"
#include "vtkGeoCamera.h"
#include "vtkGeoMath.h"
#include "vtkGeoTerrainNode.h"
#include "vtkMath.h"
#include "vtkCamera.h"
#include "vtkTransform.h"
#include <float.h>

vtkStandardNewMacro(vtkGeoCamera);


//----------------------------------------------------------------------------
vtkGeoCamera::vtkGeoCamera()
{
  this->VTKCamera = vtkSmartPointer<vtkCamera>::New();
  this->Transform = vtkSmartPointer<vtkTransform>::New();

  this->OriginLongitude = 0.0;
  this->OriginLatitude = 0.0;

  // Initial state will be looking at earth over the American continent.
  //this->Longitude = -90.0;
  this->Longitude = 0.0;
  this->Latitude = 0.0;
  this->Distance = vtkGeoMath::EarthRadiusMeters() * 5.0;
  this->Heading = 0.0;
  this->Tilt = 90.0;
  this->LockHeading = true;

  this->ComputeRectilinearOrigin();

  this->UpdateAngleRanges();
  this->UpdateVTKCamera();

  this->Position[0] = this->Position[1] = this->Position[2] = 0.0;
  this->Origin[0] = this->Origin[1] = this->Origin[2] = 0.0;
}

//-----------------------------------------------------------------------------
vtkGeoCamera::~vtkGeoCamera()
{
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::PrintSelf(ostream& os, vtkIndent indent)
{
  this->Superclass::PrintSelf(os,indent);
  os << indent << "Distance: " << this->Distance << endl;
  os << indent << "Tilt: " << this->Tilt << endl;
  os << indent << "Heading: " << this->Heading << endl;
  os << indent << "Latitude: " << this->Latitude << endl;
  os << indent << "Longitude: " << this->Longitude << endl;
  os << indent << "LockHeading: " << (this->LockHeading ? "on" : "off");
  os << indent << "Origin: {" << this->Origin[0] << ", " << this->Origin[1]
    << ", " << this->Origin[2] << "}" << endl;
  os << indent << "OriginLatitude: " << this->OriginLatitude << endl;
  os << indent << "OriginLongitude: " << this->OriginLongitude << endl;
  os << indent << "Position: {" << this->Position[0] << ", " << this->Position[1]
    << ", " << this->Position[2] << "}" << endl;
  os << indent << "VTKCamera: " << endl;

  this->VTKCamera->PrintSelf(os, indent.GetNextIndent());
}
//-----------------------------------------------------------------------------
void vtkGeoCamera::SetOriginLatitude(double oLat)
{
  if (this->OriginLatitude == oLat)
    {
    return;
    }
  this->OriginLatitude = oLat;
  this->Modified();
  this->ComputeRectilinearOrigin();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::SetOriginLongitude(double oLon)
{
  if (this->OriginLongitude == oLon)
    {
    return;
    }
  this->OriginLongitude = oLon;
  this->Modified();
  this->ComputeRectilinearOrigin();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::ComputeRectilinearOrigin()
{
  double tmp = cos(this->OriginLatitude * vtkMath::Pi() / 180.0);

  this->Origin[1] = tmp * cos(this->OriginLongitude * vtkMath::Pi() / 180.0);
  this->Origin[0] = -tmp * sin(this->OriginLongitude * vtkMath::Pi() / 180.0);

  this->Origin[2] = sin(this->OriginLatitude * vtkMath::Pi() / 180.0);

  this->Origin[0] *= vtkGeoMath::EarthRadiusMeters();
  this->Origin[1] *= vtkGeoMath::EarthRadiusMeters();
  this->Origin[2] *= vtkGeoMath::EarthRadiusMeters();

  this->UpdateVTKCamera();
}


//-----------------------------------------------------------------------------
void vtkGeoCamera::SetLongitude(double longitude)
{
  if (this->Longitude == longitude)
    {
    return;
    }
  this->Modified();
  this->Longitude = longitude;
  this->UpdateAngleRanges();
  this->UpdateVTKCamera();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::SetLatitude(double latitude)
{
  if (this->Latitude == latitude)
    {
    return;
    }
  this->Modified();
  this->Latitude = latitude;
  this->UpdateAngleRanges();
  this->UpdateVTKCamera();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::SetDistance(double altitude)
{
  if (this->Distance == altitude)
    {
    return;
    }
  this->Modified();
  this->Distance = altitude;
  this->UpdateVTKCamera();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::SetHeading(double heading)
{
  if (this->Heading == heading)
    {
    return;
    }
  while (heading < -180)
    {
    heading += 360;
    }
  while (heading > 180)
    {
    heading -= 360;
    }
  this->Modified();
  this->Heading = heading;
  this->UpdateAngleRanges();
  this->UpdateVTKCamera();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::SetTilt(double tilt)
{
  if (this->Tilt == tilt)
    {
    return;
    }
  this->Modified();
  this->Tilt = tilt;
  this->UpdateVTKCamera();
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::UpdateAngleRanges()
{
  while (this->Heading > 180.0)
    {
    this->Heading -= 360.0;
    }
  while (this->Heading < -180.0)
    {
    this->Heading += 360.0;
    }
  while (this->Longitude > 180.0)
    {
    this->Longitude -= 360.0;
    }
  while (this->Longitude < -180.0)
    {
    this->Longitude += 360.0;
    }
  if (this->Latitude > 90.0)
    {
    this->Latitude = 180.0 - this->Latitude;
    }
  if (this->Latitude < -90.0)
    {
    this->Latitude = -180.0 - this->Latitude;
    }
}


//-----------------------------------------------------------------------------
void vtkGeoCamera::UpdateVTKCamera()
{
  this->Transform->PostMultiply();
  this->Transform->Identity();
  // Tilt
  this->Transform->RotateX(this->Tilt);
  // Heading
  this->Transform->RotateY(-this->Heading);
  // to surface of earth
  this->Transform->Translate(0.0, vtkGeoMath::EarthRadiusMeters(), 0.0);
  // Latitude
  this->Transform->RotateX(this->Latitude);
  // Longitude
  this->Transform->RotateZ(this->Longitude);

  // Consider origin.
  this->Transform->Translate(-this->Origin[0],
                             -this->Origin[1],
                             -this->Origin[2]);


  double* pt;
  double tmp[3];
  // Find focal point of the camera.
  pt = this->Transform->TransformDoublePoint(0.0, 0.0, 0.0);
  this->VTKCamera->SetFocalPoint(pt);
  // Find position
  this->VTKCamera->GetFocalPoint(tmp);
  // Use arbitrary magnitude.
  pt = this->Transform->TransformDoublePoint(0.0, 0.0, -this->Distance);
  this->VTKCamera->SetPosition(pt);
  // Save the position with out the origin shift so the evaluation works.
  this->Position[0] = pt[0] + this->Origin[0];
  this->Position[1] = pt[1] + this->Origin[1];
  this->Position[2] = pt[2] + this->Origin[2];

  if (this->LockHeading)
    {
    // Now find view up using heading.
    pt = this->Transform->TransformDoublePoint(0.0, 1.0, 0.0);
    tmp[0] = pt[0] - tmp[0];
    tmp[1] = pt[1] - tmp[1];
    tmp[2] = pt[2] - tmp[2];
    this->VTKCamera->SetViewUp(tmp);
    }
  else
    {
    // Find heading using view up.
    this->VTKCamera->OrthogonalizeViewUp();
    double up[3];
    this->VTKCamera->GetViewUp(up);
    // Project vector to north pole and view up to the following plane:
    // Normal = dir. out of center of earth from focal point, Point = origin.

    // Compute the plane normal (center of earth at -origin).
    double dir[3];
    dir[0] = -tmp[0] - this->Origin[0];
    dir[1] = -tmp[1] - this->Origin[1];
    dir[2] = -tmp[2] - this->Origin[2];
    vtkMath::Normalize(dir);

    // Compute direction to north pole (at -origin + (0,0,earth_radius))
    // from focal point.
    double north[3];
    north[0] = -this->Origin[0] - tmp[0];
    north[1] = -this->Origin[1] - tmp[1];
    north[2] = vtkGeoMath::EarthRadiusMeters() - this->Origin[2] - tmp[2];
    double northDot = vtkMath::Dot(north, dir);

    // Project direction to north pole to our plane.
    double northProj[3];
    northProj[0] = north[0] - northDot*dir[0];
    northProj[1] = north[1] - northDot*dir[1];
    northProj[2] = north[2] - northDot*dir[2];
    vtkMath::Normalize(northProj);

    // Project view up vector to the same plane.
    double upDot = vtkMath::Dot(up, dir);
    double upProj[3];
    upProj[0] = up[0] - upDot*dir[0];
    upProj[1] = up[1] - upDot*dir[1];
    upProj[2] = up[2] - upDot*dir[2];
    vtkMath::Normalize(upProj);

    // Determine the angle between the vectors.
    // Do some sin/cos trickery to get it to the full range
    // [-180, 180].
    double dotProd = vtkMath::Dot(northProj, upProj);
    double crossProd[3];
    vtkMath::Cross(northProj, upProj, crossProd);
    this->Heading = vtkMath::DegreesFromRadians( asin( vtkMath::Norm( crossProd ) ) );
    if (dotProd < 0)
      {
      this->Heading = 180.0 - this->Heading;
      }
    if (vtkMath::Dot(crossProd, dir) < 0)
      {
      this->Heading = -this->Heading;
      }
    }
}

//-----------------------------------------------------------------------------
void vtkGeoCamera::InitializeNodeAnalysis(int rendererSize[2])
{
  this->Aspect[1] = tan( vtkMath::RadiansFromDegrees( this->VTKCamera->GetViewAngle() ) * 0.5 );
  this->Aspect[0] = this->Aspect[1] * rendererSize[0]
                       / static_cast<double>( rendererSize[1] );

  this->VTKCamera->GetViewPlaneNormal(this->ForwardNormal);
  this->ForwardNormal[0] = - this->ForwardNormal[0];
  this->ForwardNormal[1] = - this->ForwardNormal[1];
  this->ForwardNormal[2] = - this->ForwardNormal[2];

  this->VTKCamera->GetViewUp(this->UpNormal);
  vtkMath::Normalize(this->UpNormal);

  vtkMath::Cross(this->ForwardNormal, this->UpNormal, this->RightNormal);

  // It may not be necessary to keep the above as instance variables.
  for (int ii = 0; ii < 3; ++ii)
    {
    this->LeftPlaneNormal[ii] =  - this->ForwardNormal[ii]*this->Aspect[0]
                                   - this->RightNormal[ii];
    this->RightPlaneNormal[ii] = - this->ForwardNormal[ii]*this->Aspect[0]
                                   + this->RightNormal[ii];
    this->DownPlaneNormal[ii] =  - this->ForwardNormal[ii]*this->Aspect[1]
                                   - this->UpNormal[ii];
    this->UpPlaneNormal[ii] =    - this->ForwardNormal[ii]*this->Aspect[1]
                                   + this->UpNormal[ii];
    }
  vtkMath::Normalize(this->LeftPlaneNormal);
  vtkMath::Normalize(this->RightPlaneNormal);
  vtkMath::Normalize(this->DownPlaneNormal);
  vtkMath::Normalize(this->UpPlaneNormal);

}


//-----------------------------------------------------------------------------
// This has to be as fast as possible.
// It is called for each node every render.
// These comparisons in rectilinear coordinates are not perfect.
// I look at a viewplain containing the sphere center.
// I compate a circle with the view bounds.
// Another option is to save 4 frustum plane normals in the camera.
// This would require 5 dot products (15 multiplications).
double vtkGeoCamera::GetNodeCoverage(vtkGeoTerrainNode* node)
{
  double sphereCenter[3];
  double sphereRadius;
  double camPosition[3];

  this->GetPosition(camPosition);

  // Lets take care of nodes on the opposite side of the earth.
  // I think there should be a better way of figuring this out.
  if (vtkMath::Dot(this->ForwardNormal,node->GetCornerNormal00())>0.0 &&
      vtkMath::Dot(this->ForwardNormal,node->GetCornerNormal01())>0.0 &&
      vtkMath::Dot(this->ForwardNormal,node->GetCornerNormal10())>0.0 &&
      vtkMath::Dot(this->ForwardNormal,node->GetCornerNormal11())>0.0)
    { // Node is hidden by the earth.
    return 0.0;
    }

  sphereRadius = node->GetBoundingSphereRadius();
  node->GetBoundingSphereCenter(sphereCenter);

  // Put the camera's position at the origin.
  sphereCenter[0] = sphereCenter[0] - camPosition[0];
  sphereCenter[1] = sphereCenter[1] - camPosition[1];
  sphereCenter[2] = sphereCenter[2] - camPosition[2];

  double left  = vtkMath::Dot(this->LeftPlaneNormal,sphereCenter);
  double right = vtkMath::Dot(this->RightPlaneNormal,sphereCenter);
  double down  = vtkMath::Dot(this->DownPlaneNormal,sphereCenter);
  double up    = vtkMath::Dot(this->UpPlaneNormal,sphereCenter);
  double forward = vtkMath::Dot(this->ForwardNormal,sphereCenter);
  if ( left > sphereRadius || right > sphereRadius ||
       down > sphereRadius || up > sphereRadius ||
       forward < -sphereRadius)
    {
    return 0.0;
    }

  if (forward < sphereRadius)
    {  // Camera is probably in the sphere.
    return 1.0;
    }

  left = -left;
  if (left > sphereRadius) { left = sphereRadius;}
  up = -up;
  if (up > sphereRadius) { up = sphereRadius;}
  right = -right;
  if (right > sphereRadius) { right = sphereRadius;}
  down = -down;
  if (down > sphereRadius) { down = sphereRadius;}


  double coverage = (left+right)*(up+down) / (4.0 * forward*forward*this->Aspect[0]*this->Aspect[1]);
  return coverage;
}

//-----------------------------------------------------------------------------
vtkCamera* vtkGeoCamera::GetVTKCamera()
{
  return this->VTKCamera;
}

