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

  Copyright (c) Kitware, Inc.
  All rights reserved.

     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.

=========================================================================*/
#include <sstream>
#include <cassert>

#include "mvActor.h"
#include "mvAudioHandler.h"
#include "mvDataObject.h"
#include "mvFlagpole.h"
#include "mvLobby.h"
#include "mvLobbyClient.h"
#include "mvPhotoSphere.h"
#include "mvPoseInterpolator.h"
#include "mvTexture.h"
#include "mvTourStop.h"
#include "mvView.h"

#include "vtksys/SystemTools.hxx"

#include "vtkActor.h"
#include "vtkCamera.h"
#include "vtkEquirectangularToCubeMapTexture.h"
#include "vtkFFMPEGVideoSource.h"
#include "vtkFlagpoleLabel.h"
#include "vtkGlobFileNames.h"
#include "vtkImageData.h"
#include "vtkImageResize.h"
#include "vtkJPEGReader.h"
#include "vtkLineSource.h"
#include "vtkLookupTable.h"
#include "vtkMath.h"
#include "vtkMultiThreader.h"
#include "vtkMutexLock.h"
#include "vtkOpenGLMovieSphere.h"
#include "vtkOpenGLPolyDataMapper.h"
#include "vtkOpenGLRenderer.h"
#include "vtkOpenGLRenderWindow.h"
#include "vtkOpenGLTexture.h"
#include "vtkOpenVRPanelRepresentation.h"
#include "vtkPlaneSource.h"
#include "vtkPointData.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"
#include "vtkQuadricClustering.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkSkybox.h"
#include "vtkSphereSource.h"
#include "vtkTextActor3D.h"
#include "vtkTextProperty.h"
#include "vtkTextureObject.h"
#include "vtkTimerLog.h"
#include "vtkXMLDataElement.h"
#include "vtkXMLGenericDataObjectReader.h"
#include "vtkXMLImageDataWriter.h"
#include "vtkXMLPolyDataWriter.h"
#include "vtkXMLUtilities.h"
#include "vtkWindows.h"
#include <mmsystem.h>

#include "vtkOpenVRCamera.h"
#include "vtkOpenVRInteractorStyle.h"
#include "vtkOpenVRModel.h"
#include "vtkOpenVRPanelWidget.h"
#include "vtkOpenVRRenderWindow.h"
#include "vtkOpenVROverlayInternal.h"
#include "vtkOpenVROverlay.h"

namespace
{
const double PRESS_DELAY = 0.350;
}

//----------------------------------------------------------------------------
// class to handle aborts during reading
class ViewLODProgress : public vtkCommand
{
public:
  // generic new method
  static ViewLODProgress *New()
    { return new ViewLODProgress; }

  // the execute
  void Execute(vtkObject *caller,
     unsigned long event, void* vtkNotUsed(v)) override
  {
    this->ThreadInfo->ActiveFlagLock->lock();
    if (*this->ThreadInfo->ActiveFlag == 0)
    {
      vtkAlgorithm *alg = vtkAlgorithm::SafeDownCast(caller);
      alg->AbortExecuteOn();
    }
    this->ThreadInfo->ActiveFlagLock->unlock();
  }

  mvView *Self;
  vtkMultiThreader::ThreadInfo *ThreadInfo;
};

mvView::mvView()
{
  this->InitialTourStop = 0;
  this->CurrentTourStop = -1;
  this->SphereSource->SetThetaResolution(24);
  this->SphereSource->SetPhiResolution(12);
  this->InPhotoSphere = false;
  this->SavedPose = new vtkOpenVRCameraPose();
  this->NextPose = new vtkOpenVRCameraPose();
  this->CurrentPhotoSphere = nullptr;
  this->InLocalPhotoSphere = false;
  this->Animating = false;
  this->Active = true;
  this->ButtonPressTime = 0;
  this->Latitude = 0.0;
  this->Longitude = 0.0;
  this->UseImageBasedLighting = 0;
  this->EnvironmentCubeMap = -1;
}

mvView::~mvView()
{
  this->Flagpoles.clear();
  this->Actors.clear();
  this->PosesXML->Delete();
  delete this->SavedPose;
  delete this->NextPose;
}

vtkActor *mvView::GetPosterActor()
{
  return this->PosterActor;
}

void mvView::LobbyOff()
{
  this->PosterActor->VisibilityOff();
  this->LineActor->VisibilityOff();
  this->MyLobby->GetRenderer()->RemoveActor(this->BackgroundActor);
}

void mvView::LobbyOn()
{
  this->PosterActor->VisibilityOn();
  this->LineActor->VisibilityOn();
}

void mvView::SetPosition(double x1, double y1, double x2, double y2, double height)
{
  this->Plane->SetOrigin(x1, y1, 0.5);
  this->Plane->SetPoint1(x2, y2, 0.5);
  this->Plane->SetPoint2(x1, y1, 0.5 + height);
  this->BackgroundPlane->SetOrigin(x2, y2, 0.5);
  this->BackgroundPlane->SetPoint1(x2 + 0.1*(x2 - x1), y2 + 0.1*(y2 - y1), 0.5);
  this->BackgroundPlane->SetPoint2(x2, y2, 0.5 + height);
  this->Line->SetPoint1((x1 + x2)*0.5, (y1 + y2)*0.5, 0.5);
}

void mvView::LoadAudioClips()
{
  for (auto &ts : this->TourStops)
  {
    std::ostringstream tmps;
      tmps << this->RootDirectory << "/Location" << ts.PoseNumber << ".wav";
    if (ts.AudioFile.size() == 0 &&
        vtksys::SystemTools::FileExists(tmps.str().c_str(),true))
    {
      ts.AudioFile = tmps.str();
    }
  }
}

namespace
{
  bool alessb(mvTourStop &a, mvTourStop &b)
  {
    return (a.PoseNumber < b.PoseNumber);
  }
}

void mvView::LoadExtraData(vtkXMLDataElement *topel)
{
  topel->GetScalarAttribute("Latitude", this->Latitude);
  topel->GetScalarAttribute("Longitude", this->Longitude);

  // get long lat and image
  this->ViewFile = this->RootDirectory;
  this->ViewFile += "/";
  this->ViewFile += topel->GetAttribute("ViewImage");

  // get Photo Spheres
  vtkXMLDataElement *el = topel->FindNestedElementWithName("PhotoSpheres");
  int numSpheres = el ? el->GetNumberOfNestedElements() : 0;
  this->PhotoSpheres.resize(numSpheres);
  for (size_t i = 0; i < numSpheres; i++)
  {
    vtkXMLDataElement *sphereel = el->GetNestedElement(static_cast<int>(i));
    mvPhotoSphere &sphere = this->PhotoSpheres[i];
    sphere.LoadData(sphereel, this->RootDirectory);
  }

  // get Flagpoles
  el = topel->FindNestedElementWithName("Flagpoles");
  int numFlags = el ? el->GetNumberOfNestedElements() : 0;
  this->Flagpoles.resize(numFlags);
  for (size_t i = 0; i < numFlags; i++)
  {
    vtkXMLDataElement *flagel = el->GetNestedElement(static_cast<int>(i));
    mvFlagpole &flagpole = this->Flagpoles[i];
    flagpole.LoadData(flagel);
  }

  // get pose specific data (tourstops)
  el = topel->FindNestedElementWithName("CameraPoses");
  int numPoses = el ? el->GetNumberOfNestedElements() : 0;
  for (size_t i = 0; i < numPoses; i++)
  {
    vtkXMLDataElement *poseel = el->GetNestedElement(static_cast<int>(i));

    float poseNum;
    poseel->GetScalarAttribute("PoseNumber", poseNum);

    bool newTS = true;
    mvTourStop *currTS = nullptr;
    for (auto &ts : this->TourStops)
    {
      if (ts.PoseNumber == poseNum)
      {
        newTS = false;
        currTS = &ts;
        continue;
      }
    }
    if (newTS)
    {
      // insert the pose and resort the vector
      this->TourStops.resize(this->TourStops.size() + 1);
      currTS = &this->TourStops[this->TourStops.size() - 1];
      currTS->PoseNumber = poseNum;

      // resort the vector
      std::sort(this->TourStops.begin(), this->TourStops.end(), alessb);
      for (auto &ts : this->TourStops)
      {
        if (ts.PoseNumber == poseNum)
        {
          currTS = &ts;
          continue;
        }
      }
    }

    currTS->LoadData(poseel, this->RootDirectory, this->CameraPoses);
  }
}

void mvView::LoadIndexDataVersion2(vtkXMLDataElement *topel)
{
  topel->GetScalarAttribute("UseImageBasedLighting",
    this->UseImageBasedLighting);
  topel->GetScalarAttribute("EnvironmentCubeMap",
    this->EnvironmentCubeMap);

  // read in data objects
  {
    vtkXMLDataElement *elements = topel->FindNestedElementWithName("DataObjects");
    int numElements = elements->GetNumberOfNestedElements();
    for (int i = 0; i < numElements; i++)
    {
      vtkXMLDataElement *element = elements->GetNestedElement(i);
      int id;
      element->GetScalarAttribute("DataObjectID", id);
      mvDataObject &dataobject = this->DataObjects[id];
      dataobject.LoadData(element, this->RootDirectory);
    }
  }

  // read in textures
  {
    vtkXMLDataElement *elements = topel->FindNestedElementWithName("Textures");
    int numElements = elements->GetNumberOfNestedElements();
    for (int i = 0; i < numElements; i++)
    {
      vtkXMLDataElement *textureel = elements->GetNestedElement(i);
      int tid;
      textureel->GetScalarAttribute("TextureID", tid);
      mvTexture &texture = this->Textures[tid];
      texture.LoadData(textureel);
    }
  }

  // read in actors
  vtkXMLDataElement *actorsel = topel->FindNestedElementWithName("Actors");
  int numActors = actorsel->GetNumberOfNestedElements();
  for (int i = 0; i < numActors; i++)
  {
    vtkXMLDataElement *actorel = actorsel->GetNestedElement(i);
    int aid;
    actorel->GetScalarAttribute("ActorID", aid);
    mvActor &actor = this->Actors[aid];
    actor.LoadData(actorel);
  }
}

void mvView::LoadIndexData(vtkXMLDataElement *topel)
{
  int indexFileVersion = 0;
  topel->GetScalarAttribute("Version", indexFileVersion);

  // read in poses
  vtkXMLDataElement *el = topel->FindNestedElementWithName("CameraPoses");
  this->PosesXML = el;
  this->PosesXML->Register(nullptr);

  int numPoses = el->GetNumberOfNestedElements();
  this->CameraPoses.resize(numPoses);
  for (size_t i = 0; i < numPoses; i++)
  {
    vtkXMLDataElement *poseel = el->GetNestedElement(static_cast<int>(i));

    mvCameraPose &pose = this->CameraPoses[i];

    poseel->GetScalarAttribute("PoseNumber", pose.PoseNumber);

    int numActors = poseel->GetNumberOfNestedElements();
    pose.ActorIDs.resize(numActors);
    for (size_t j = 0; j < numActors; j++)
    {
      vtkXMLDataElement *actorel = poseel->GetNestedElement(static_cast<int>(j));
      actorel->GetScalarAttribute("ActorID", pose.ActorIDs[j]);
    }

    // add a tour stop for every pose by default
    this->TourStops.resize(this->TourStops.size() + 1);
    auto &currTS = this->TourStops[this->TourStops.size() - 1];
    currTS.CameraPose = &pose;
    currTS.PoseNumber = pose.PoseNumber;

    // resort the vector
    std::sort(this->TourStops.begin(), this->TourStops.end(), alessb);
  }

  if (indexFileVersion >= 2)
  {
    return this->LoadIndexDataVersion2(topel);
  }

  // backwards compatibility
  topel->GetScalarAttribute("Latitude", this->Latitude);
  topel->GetScalarAttribute("Longitude", this->Longitude);

  // read in actors
  vtkXMLDataElement *actorsel = topel->FindNestedElementWithName("Actors");
  int numActors = actorsel->GetNumberOfNestedElements();
  for (int i = 0; i < numActors; i++)
  {
    vtkXMLDataElement *actorel = actorsel->GetNestedElement(i);
    int aid;
    actorel->GetScalarAttribute("ActorID", aid);
    mvActor &actor = this->Actors[aid];
    actor.LoadData(actorel);
    // create data objects refs for polydata
    if (this->DataObjects.find(actor.PolyDataID) == this->DataObjects.end())
    {
      mvDataObject &dataobject = this->DataObjects[actor.PolyDataID];
      dataobject.DataObjectID = actor.PolyDataID;
      dataobject.DataObjectType = VTK_POLY_DATA;
      std::ostringstream tmps;
        tmps << "data/pdata" << dataobject.DataObjectID << ".vtp";
      dataobject.SetFileName(tmps.str(), this->RootDirectory);
    }
  }

  // now generate dataobjects for the textures
  for (int i = 0; i < numActors; i++)
  {
    vtkXMLDataElement *actorel = actorsel->GetNestedElement(i);
    int aid;
    actorel->GetScalarAttribute("ActorID", aid);
    mvActor &actor = this->Actors[aid];
    if (actor.TextureID >= 0
        && this->Textures.find(actor.TextureID) == this->Textures.end())
    {
      mvTexture &texture = this->Textures[actor.TextureID];
      size_t nextDO = this->DataObjects.size();
      mvDataObject &dataobject = this->DataObjects[nextDO];
      dataobject.DataObjectID = static_cast<int>(nextDO);
      dataobject.DataObjectType = VTK_IMAGE_DATA;
      texture.SetImageDataID(static_cast<int>(nextDO));
      std::ostringstream tmps;
        tmps << "data/tdata" << actor.TextureID << ".vti";
      dataobject.SetFileName(tmps.str(), this->RootDirectory);
    }
  }
}

void mvView::Initialize(std::string fname, mvLobby *lobby)
{
  this->MyLobby = lobby;

  this->RootDirectory = vtksys::SystemTools::GetFilenamePath(fname);
  this->Name = vtksys::SystemTools::GetFilenameName(this->RootDirectory);

  vtkNew<vtkXMLUtilities> xml;
  vtkXMLDataElement *topel = xml->ReadElementFromFile(fname.c_str());

  this->LoadIndexData(topel);

  const char *timage = topel->GetAttribute("ViewImage");
  if (timage)
  {
    this->ViewFile = this->RootDirectory;
    this->ViewFile += "/";
    this->ViewFile += timage;
  }
  topel->Delete();

  // also load the extra file if found
  std::string extraname = this->RootDirectory  + "/extra.xml";
  if (vtksys::SystemTools::FileExists(extraname.c_str(), true))
  {
    vtkXMLDataElement *etopel =
      xml->ReadElementFromFile(extraname.c_str());
    this->LoadExtraData(etopel);
    etopel->Delete();
  }

  // draw a line from the view to the map
  this->Line->SetPoint2(2.0*this->Longitude/180.0, 2.0*this->Latitude/180.0, 0.0);
  vtkNew<vtkPolyDataMapper> lineMapper;
  this->LineActor->SetMapper(lineMapper);
  lineMapper->SetInputConnection(this->Line->GetOutputPort());
  this->LineActor->GetProperty()->SetAmbient(1.0);
  this->LineActor->GetProperty()->SetDiffuse(0.0);
  if (this->MyLobby->GetDisplayMap() &&
      (this->Latitude != 0.0 || this->Longitude != 0.0))
  {
    this->MyLobby->GetRenderer()->AddActor(this->LineActor);
  }

  vtkNew<vtkPolyDataMapper> bmapper;
  bmapper->SetInputConnection(this->BackgroundPlane->GetOutputPort());
  this->BackgroundActor->SetMapper(bmapper);
  this->BackgroundActor->GetProperty()->SetColor(0.6,1.0,0.6);
  this->BackgroundActor->GetProperty()->SetAmbient(1.0);
  this->BackgroundActor->GetProperty()->SetDiffuse(0.0);

  // check if the viewfile is present, if not take the first jpeg found
  if (!vtksys::SystemTools::FileExists(this->ViewFile.c_str(), true))
  {
    vtkNew<vtkGlobFileNames> glob;
    glob->SetDirectory(this->RootDirectory.c_str());
    glob->RecurseOff();
    glob->AddFileNames("*.jpg");
    int numjpg = glob->GetNumberOfFileNames();
    if (numjpg > 0)
    {
      this->ViewFile = glob->GetNthFileName(0);
    }
  }

  vtkNew<vtkTexture> tex;
  vtkNew<vtkJPEGReader> reader;
  reader->SetFileName(this->ViewFile.c_str());
  tex->SetInputConnection(reader->GetOutputPort());
  tex->InterpolateOn();
  tex->MipmapOn();
  vtkNew<vtkPolyDataMapper> mapper;
  mapper->SetInputConnection(this->Plane->GetOutputPort());
  this->PosterActor->SetMapper(mapper);
  this->PosterActor->SetTexture(tex);
  this->PosterActor->GetProperty()->SetAmbient(1.0);
  this->PosterActor->GetProperty()->SetDiffuse(0.0);
  this->MyLobby->GetRenderer()->AddActor(this->PosterActor);

  this->LoadAudioClips();

  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetFontFamilyToTimes();
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetFontSize(28);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetFrame(1);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetFrameWidth(4);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetBackgroundOpacity(1.0);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetBackgroundColor(1, 1, 1);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetColor(0, 0, 0);
  this->MessageRepresentation->GetTextActor()->GetTextProperty()->SetFrameColor(0.2,0.2,0.2);

  this->MessageRepresentation->SetCoordinateSystemToWorld();
}

void mvView::SetActive(bool act) {
  this->Active = act;
  this->PosterActor->GetProperty()->SetAmbient(act ? 1.0 : 0.3);
}

void mvView::GenerateLODs()
{
  for (auto & dit : this->DataObjects)
  {
    mvDataObject &dataobject = dit.second;

    std::string const &srcFileName = dataobject.GetFileName();
    vtkNew<vtkXMLGenericDataObjectReader> rdr;
    rdr->SetFileName(srcFileName.c_str());
    rdr->Update();

    // rewrite the full res as it may have been written
    // in an unoptimized format
    {
      vtkXMLWriter *wtr;
      if (dataobject.DataObjectType == VTK_POLY_DATA)
      {
        wtr = vtkXMLPolyDataWriter::New();
      }
      if (dataobject.DataObjectType == VTK_IMAGE_DATA)
      {
        wtr = vtkXMLImageDataWriter::New();
      }
      wtr->SetInputConnection(rdr->GetOutputPort());
      wtr->SetFileName(srcFileName.c_str());
      wtr->SetDataModeToAppended();
      wtr->EncodeAppendedDataOff();
      wtr->SetCompressorTypeToLZ4();
      wtr->Write();
      wtr->Delete();
    }

    // if it is big then generate a LOD. In this case big means
    // more than 10MB
    if (dataobject.LODSizes[0] > 10000)
    {
      std::string pathName =
        vtksys::SystemTools::GetFilenamePath(srcFileName);
      std::string rootName =
        vtksys::SystemTools::GetFilenameWithoutLastExtension(srcFileName);
      std::string typeExt =
        vtksys::SystemTools::GetFilenameLastExtension(srcFileName);

      std::string lodFileName = pathName + "/" + rootName;

      if (dataobject.DataObjectType == VTK_POLY_DATA)
      {
        rdr->Update();
        double bounds[6];
        rdr->GetPolyDataOutput()->GetBounds(bounds);
        double length = rdr->GetPolyDataOutput()->GetLength();
        double factors[3] = {
          (bounds[1] - bounds[0])/length + 0.01,
          (bounds[3] - bounds[2])/length + 0.01,
          (bounds[5] - bounds[4])/length + 0.01
        };
        double factorsCube = factors[0]*factors[1]*factors[2];
        size_t currentSize = dataobject.LODSizes[0];
        int level = 1;
        double targetDiv = 10000.0 *currentSize;
        while (currentSize > 1000)
        {
          targetDiv /= 7.0;
          double B = pow(targetDiv/factorsCube, 0.3333);

          vtkNew<vtkQuadricClustering> cc;
          cc->UseInputPointsOn();
          cc->CopyCellDataOn();
          cc->SetInputConnection(rdr->GetOutputPort());
          cc->AutoAdjustNumberOfDivisionsOff();
          cc->SetNumberOfXDivisions(B*factors[0] + 1);
          cc->SetNumberOfYDivisions(B*factors[1] + 1);
          cc->SetNumberOfZDivisions(B*factors[2] + 1);
          // cerr << "Clustering on "
          //   << cc->GetNumberOfXDivisions() << " "
          //   << cc->GetNumberOfYDivisions() << " "
          //   << cc->GetNumberOfZDivisions() << " "
          //   << level << "\n";

          vtkNew<vtkXMLPolyDataWriter> wtr;
          wtr->SetInputConnection(cc->GetOutputPort());
          std::ostringstream tmps2;
            tmps2 << lodFileName << ".lod" << level << typeExt;
          wtr->SetFileName(tmps2.str().c_str());
          wtr->SetDataModeToAppended();
          wtr->EncodeAppendedDataOff();
          wtr->SetCompressorTypeToLZ4();
          wtr->Write();
          currentSize /= 4;
          level++;
        }
      }

      if (dataobject.DataObjectType == VTK_IMAGE_DATA)
      {
        vtkSmartPointer<vtkImageData> id = rdr->GetImageDataOutput();
        int dims[3];
        id->GetDimensions(dims);

        // write out multiple LODs until we get down to
        // less than 1000, make sure we pick a number that will
        // produce at least 2 more LODs as textures are always setup
        // as mipmaps.
        size_t currentSize = dataobject.LODSizes[0];
        int level = 1;
        while (currentSize > 1000)
        {
          vtkNew<vtkImageResize> shrink;
          shrink->SetInputData(id);
          dims[0] = dims[0] > 1 ? dims[0] / 2 : 1;
          dims[1] = dims[1] > 1 ? dims[1] / 2 : 1;
          shrink->SetOutputDimensions(dims[0],dims[1],1);

          std::ostringstream tmps2;
          tmps2 << lodFileName << ".lod" << level << typeExt;
          vtkNew<vtkXMLImageDataWriter> wtr;
          wtr->SetInputConnection(shrink->GetOutputPort());
          wtr->SetDataModeToAppended();
          wtr->EncodeAppendedDataOff();
          wtr->SetCompressorTypeToLZ4();
          wtr->SetFileName(tmps2.str().c_str());
          wtr->Write();
          id = shrink->GetOutput();
          currentSize /= 4;
          level++;
        }
      }
    }
  }
}

void mvView::ComputeLoadPriority()
{
  // set all DO back to zero
  for (auto & dit : this->DataObjects)
  {
    dit.second.SetLoadPriority(0.0);
  }

  // loop over actors
  for (auto &act : this->Actors)
  {
    act.second.ComputeLoadPriority(this->Textures, this->DataObjects);
  }

  // do we have a env texture?
}

namespace {
VTK_THREAD_RETURN_TYPE LODLoadFunction(void *data)
{
  vtkMultiThreader::ThreadInfo *tinfo =
    static_cast<vtkMultiThreader::ThreadInfo*>(data);

  mvView *self =
    static_cast<mvView *>(tinfo->UserData);
  self->LoadRemainingData(data);
  return VTK_THREAD_RETURN_VALUE;
}
}


void mvView::LoadRemainingData(void *data)
{
  vtkMultiThreader::ThreadInfo *tinfo =
    static_cast<vtkMultiThreader::ThreadInfo*>(data);

  double startTime = vtkTimerLog::GetUniversalTime();

  vtkNew<ViewLODProgress> cb;
  cb->Self = this;
  cb->ThreadInfo = tinfo;

  bool done = false;
  while (!done)
  {
    tinfo->ActiveFlagLock->lock();
    if (*tinfo->ActiveFlag == 0)
    {
      tinfo->ActiveFlagLock->unlock();
      return;
    }
    tinfo->ActiveFlagLock->unlock();

    // compute priorities
    this->ComputeLoadPriority();

    // then find the highest priority
    double highestPriority = 0.0;
    int selectedDO = -1;
    for (auto & dit : this->DataObjects)
    {
      mvDataObject &dobj = dit.second;
      double priority = dobj.GetLoadPriority();
      if (priority > highestPriority)
      {
        selectedDO = static_cast<int>(dit.first);
        highestPriority = priority;
      }
    }

    if (selectedDO >= 0)
    {
      // load the highest priority
      mvDataObject &dobj = this->DataObjects[selectedDO];
      dobj.LoadNextLOD(cb);
      // if aborted just return
      tinfo->ActiveFlagLock->lock();
      if (*tinfo->ActiveFlag == 0)
      {
        tinfo->ActiveFlagLock->unlock();
        return;
      }
      tinfo->ActiveFlagLock->unlock();

      this->MyLobby->RenderLock();

      // if polydata
      if (dobj.DataObjectType == VTK_POLY_DATA)
      {
        for (auto & j : this->Actors)
        {
          if (j.second.PolyDataID == dobj.DataObjectID)
          {
            j.second.VTKActor->GetMapper()->SetInputConnection(
              dobj.GetOutputPort());
            j.second.VTKActor->GetMapper()->Modified();
          }
        }
      }

      // else texture
      if (dobj.DataObjectType == VTK_IMAGE_DATA)
      {
        RenderOperation op;
        op.ImageData = vtkImageData::SafeDownCast(dobj.GetOutputAsDataSet());
        // add all textures that use this image data
        for (auto & t : this->Textures)
        {
          if (t.second.GetImageDataID() == dobj.DataObjectID)
          {
            op.Textures.push_back(static_cast<int>(t.first));
          }
        }
        op.Level = dobj.LastLODLoaded;
        op.SmallestLOD = dobj.GetSmallestLOD() - 1; // the very smallest is not part of the mip map
        this->RenderOperations.push(op);
      }

      this->MyLobby->RenderUnlock();
    }
    else
    {
      done = true;
    }
  }


  double endTime = vtkTimerLog::GetUniversalTime();

  cerr << "LODLoading of " << vtksys::SystemTools::GetFilenameName(this->RootDirectory) << " completed in " << floor(endTime - startTime) << " seconds.\n";
  this->LODLoadThreadId = -1;
}


// handle and prep for loading, but must be fast as it is on the main thread
// leave heavy lifting to the LoadBackgroundThread method
void mvView::LoadMainThread()
{
  // reset current pose to unknown
  this->CurrentTourStop = -1;
  this->InitialTourStop = 0;
  this->LoadProgress = 0;
  this->LODLoadThreadId = -1;
}

// load the data, runs in a background thread so the GUI
// doesn't hang
void mvView::LoadMinimumData(void *data)
{
  vtkMultiThreader::ThreadInfo *tinfo =
    static_cast<vtkMultiThreader::ThreadInfo*>(data);

  // poylydata and textures take the time, the rest is fast
  double totalCount = this->DataObjects.size() + 1.0;
  double accumulatedCount = 0;

  // for each dataobject load the lowest resolution version
  for (auto & dit : this->DataObjects)
  {
    // check for an abort
    tinfo->ActiveFlagLock->lock();
    if (*tinfo->ActiveFlag == 0)
    {
      tinfo->ActiveFlagLock->unlock();
      this->LoadProgress = 0;

      // free already loaded objects
      for (auto & dit2 : this->DataObjects)
      {
        dit2.second.FreeMemory();
      }
      return;
    }
    tinfo->ActiveFlagLock->unlock();

    mvDataObject & data = dit.second;
    data.LoadNextLOD(nullptr);
    // always load two smallest levels of images for mipmapping
    if (data.DataObjectType == VTK_IMAGE_DATA && data.GetNumberOfLODs() > 1)
    {
      data.LoadNextLOD(nullptr);
    }
    accumulatedCount++;
    this->LoadProgress = static_cast<int>(100.0*accumulatedCount/totalCount);
  }

  // and photospheres
  for (auto &sphere : this->PhotoSpheres)
  {
    if (sphere.TextureFile.size())
    {
      vtkNew<vtkPolyDataMapper> smap;
      smap->SetInputConnection(this->SphereSource->GetOutputPort());
      sphere.Actor = vtkActor::New();
      sphere.Actor->SetMapper(smap);
      sphere.Actor->GetProperty()->SetColor(sphere.Color);
      sphere.Actor->GetProperty()->SetOpacity(sphere.Opacity);
      // sphere.Actor->GetProperty()->BackfaceCullingOn();

      sphere.Texture = vtkOpenGLTexture::New();
      sphere.Texture->InterpolateOn();
      // pose.Sphere.Texture->MipmapOn();
      sphere.Texture->RepeatOn();

      // watch for mpeg files
      std::string ext =
        vtksys::SystemTools::GetFilenameLastExtension(sphere.TextureFile);
      if (ext == ".mp4" || ext == ".mkv" || ext == ".webm")
      {
        sphere.IsVideo = true;
        sphere.VideoSource.TakeReference(vtkFFMPEGVideoSource::New());
        sphere.VideoSource->SetFileName(sphere.TextureFile.c_str());
        sphere.VideoSource->SetDecodingThreads(4);
        sphere.Texture->SetInputConnection(sphere.VideoSource->GetOutputPort());
      }
      // otherwise jpeg
      else
      {
        sphere.IsVideo = false;
      }
    }
  }

  this->MyLobby->RenderLock();

  for (auto & act : this->Actors)
  {
    auto &dobj = this->DataObjects[act.second.PolyDataID];
    act.second.VTKActor->GetMapper()->SetInputConnection(
      dobj.GetOutputPort());
    act.second.VTKActor->GetMapper()->StaticOn();
    act.second.SetupTextures(this->Textures);
    this->MyLobby->GetRenderer()->AddActor(act.second.VTKActor);
    act.second.VTKActor->VisibilityOff();
  }

  // add photospheres
  for (auto &sphere : this->PhotoSpheres)
  {
    this->MyLobby->GetRenderer()->AddActor(sphere.Actor);
  }

  // add flags
  for (auto &flag : this->Flagpoles)
  {
    this->MyLobby->GetRenderer()->AddActor(flag.FlagpoleLabel);
  }

  this->MyLobby->RenderUnlock();

  tinfo->ActiveFlagLock->lock();
  if (*tinfo->ActiveFlag == 0)
  {
    tinfo->ActiveFlagLock->unlock();
    this->LoadProgress = 0;
    return;
  }
  tinfo->ActiveFlagLock->unlock();

  while (this->RenderOperations.size())
  {
    this->RenderOperations.pop();
  }

  // 100% done
  this->LoadProgress = 100;
}

void mvView::FinishLoadView()
{
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();

  // for all the textures connect them to a data object
  for (auto & t : this->Textures)
  {
    mvTexture &tex = t.second;
    if (!tex.GetInputTextureObject())
    {
      tex.CreateInputTextureObject(this->DataObjects[t.second.GetImageDataID()], renWin);
      // share the TO with any other textures using that input data
      for (auto & t2 : this->Textures)
      {
        if (t2.second.GetImageDataID() == t.second.GetImageDataID())
        {
          t2.second.SetInputTextureObject(tex.GetInputTextureObject());
        }
      }
    }
  }

  this->MyLobby->GetRenderer()->SetUseImageBasedLighting(
    this->UseImageBasedLighting);
  if (this->EnvironmentCubeMap >= 0)
  {
    this->MyLobby->GetRenderer()->SetEnvironmentCubeMap(this->Textures[this->EnvironmentCubeMap].GetOutputTexture());
  }

  this->LODLoadThreadId =
    this->Threader->SpawnThread( LODLoadFunction, this);

  auto ovl = this->MyLobby->GetDashboardOverlay();
  ovl->ReadCameraPoses(this->PosesXML);
}

void mvView::GoToNextTourStop()
{
  int newStop = (this->CurrentTourStop + 1) % this->TourStops.size();

  // load up the actors for that stop
  if (this->CurrentTourStop >= 0)
  {
    mvTourStop &ts = this->TourStops[this->CurrentTourStop];

    if (ts.AdvanceToLobby)
    {
      this->MyLobby->ReturnFromView();
      return;
    }
  }

  this->GoToTourStop(newStop, true);
}

void mvView::GoToFirstTourStop()
{
  // allows other views to trigger GoToPose before we are loaded.
  int pose = this->InitialTourStop;
  this->InitialTourStop = -1;
  this->GoToTourStop(pose);
}

void mvView::ExitPhotoSphere()
{
  if (this->InPhotoSphere)
  {
    if (this->CurrentPhotoSphere->IsVideo)
    {
      this->CurrentPhotoSphere->VideoSource->Stop();
      this->CurrentPhotoSphere->VideoSource->ReleaseSystemResources();
    }
    this->MyLobby->GetRenderer()->RemoveActor(this->Skybox);
    this->MyLobby->GetRenderer()->RemoveActor(this->MovieSphere);
    this->InPhotoSphere = false;
    this->InLocalPhotoSphere = false;
  }
}

void mvView::UpdatePropVisibilityAndOpacity(mvTourStop &ts, float blendAmount)
{
  vtkRenderer *ren = this->MyLobby->GetRenderer();
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();

  std::set<size_t> usedActors;

  // change visibility of actors
  if (ts.CameraPose)
  {
    for (auto actor : ts.CameraPose->ActorIDs)
    {
      this->Actors[actor].VTKActor->VisibilityOn();
      double targetOpacity = this->Actors[actor].Opacity;
      double opacity = this->Actors[actor].VTKActor->GetProperty()->GetOpacity();
      this->Actors[actor].VTKActor->GetProperty()->SetOpacity(
        blendAmount*targetOpacity > opacity ? blendAmount*targetOpacity : opacity);
      this->Actors[actor].VTKActor->SetForceOpaque(
        (this->Actors[actor].VTKActor->GetProperty()->GetOpacity() >= 1.0));
      usedActors.insert(actor);
    }
  }

  for (auto & act : this->Actors)
  {
    if (usedActors.find(act.first) == usedActors.end())
    {
      act.second.VTKActor->ForceOpaqueOff();
      if (blendAmount >= 1.0)
      {
        act.second.VTKActor->GetProperty()->SetOpacity(0.0);
        act.second.VTKActor->VisibilityOff();
      }
      else
      {
        double opacity = act.second.VTKActor->GetProperty()->GetOpacity();
        act.second.VTKActor->GetProperty()->SetOpacity(
          opacity < (1.0 - blendAmount) ? opacity : 1.0 - blendAmount);
      }
    }
  }

  // display photo spheres
  for (auto &sphere : this->PhotoSpheres)
  {
    if (sphere.Actor)
    {
      if (sphere.TextureFile.size() && (
            sphere.NumberOfLocations == 0 ||
            std::find(sphere.Locations.begin(), sphere.Locations.end(), ts.PoseNumber) != sphere.Locations.end()))
      {
        if (sphere.Size != 0.0)
        {
          sphere.Actor->SetScale(sphere.Size, sphere.Size, sphere.Size);
        }
        else
        {
          double scale = this->MyLobby->GetPhysicalScale();
          sphere.Actor->SetScale(scale*0.2, scale*0.2, scale*0.2);
        }

        sphere.Actor->SetPosition(sphere.Position);
        sphere.Actor->VisibilityOn();
      }
      else
      {
        sphere.Actor->VisibilityOff();
      }
    }
  }

  // display ands update flagpoles
  std::vector<bool> usedFlags;
  usedFlags.resize(this->Flagpoles.size(), false);

  for (int i = 0; i < this->Flagpoles.size(); ++i)
  {
    mvFlagpole &flag = this->Flagpoles[i];
    if (flag.FlagpoleLabel->GetInput() && (
          flag.NumberOfLocations == 0 ||
          std::find(flag.Locations.begin(), flag.Locations.end(), ts.PoseNumber)
          != flag.Locations.end()))
    {
      flag.FlagpoleLabel->VisibilityOn();
      double opacity = flag.FlagpoleLabel->GetProperty()->GetOpacity();
      flag.FlagpoleLabel->GetProperty()->SetOpacity(
        blendAmount > opacity ? blendAmount : opacity);
      flag.FlagpoleLabel->SetForceOpaque(
        (flag.FlagpoleLabel->GetProperty()->GetOpacity() >= 1.0));
      usedFlags[i] = true;
    }
  }

  for (int i = 0; i < this->Flagpoles.size(); ++i)
  {
    if (!usedFlags[i])
    {
      mvFlagpole &flag = this->Flagpoles[i];
      flag.FlagpoleLabel->ForceOpaqueOff();
      if (blendAmount >= 1.0)
      {
        flag.FlagpoleLabel->GetProperty()->SetOpacity(0.0);
        flag.FlagpoleLabel->VisibilityOff();
      }
      else
      {
        double opacity = flag.FlagpoleLabel->GetProperty()->GetOpacity();
        flag.FlagpoleLabel->GetProperty()->SetOpacity(
          opacity < (1.0 - blendAmount) ? opacity : 1.0 - blendAmount);
      }
    }
  }
  this->MyLobby->GetRenderer()->ResetCameraClippingRange();
}

void mvView::HandleAnimation()
{
  if (!this->Animating)
  {
    return;
  }

  // 0 to 1.0
  double percentComplete =
    (vtkTimerLog::GetUniversalTime() - this->AnimationStartTime)/this->AnimationDuration;

  double tickDuration = vtkTimerLog::GetUniversalTime() - this->AnimationLastTick;
  double factor = tickDuration/this->AnimationDuration; // 0 to 1.0
  double total = 0.4 + 0.6*sqrt(vtkMath::Pi())/6.0;
  if (percentComplete < 0.3)
  {
    double eexp = 3.0*percentComplete/0.3 - 3.0;
    double currentValue = exp(-eexp*eexp);
    factor *= currentValue/total;
  }
  else if (percentComplete > 0.7)
  {
    percentComplete = percentComplete > 1.0 ? 1.0 : percentComplete;
    double eexp = 3.0*(1.0 - percentComplete)/0.3 - 3.0;
    double currentValue = exp(-eexp*eexp);
    factor *= currentValue/total;
  }
  else
  {
    factor /= total;
  }

  factor *= 1.01;
  factor += this->AnimationSum;
  if (factor > 1.0)
  {
    factor = 1.0;
  }

  vtkOpenGLRenderer *ren = this->MyLobby->GetRenderer();
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();

  // this->PoseInterpolatorInstance.Fly(this->SavedPose, this->NextPose, ren, factor);
  mvPoseInterpolator::Interpolate(
    this->SavedPose,
    this->NextPose, factor, this->AnimationSum, 0.6,
    this->MyLobby);
  this->MyLobby->GetRenderer()->ResetCameraClippingRange();
  this->AnimationSum = factor;

  if (percentComplete > 0.25)
  {
    mvTourStop &ts = this->TourStops[this->CurrentTourStop];

    double opacityAdjust = 2.0*(percentComplete - 0.25);
    if (opacityAdjust > 1.0)
    {
      opacityAdjust = 1.0;
    }

    this->UpdatePropVisibilityAndOpacity(ts, opacityAdjust);
  }

  this->AnimationLastTick += tickDuration;
  if (this->AnimationLastTick >  this->AnimationStartTime + this->AnimationDuration)
  {
    this->Animating = false;
  }
}

void mvView::SetPose(vtkOpenVRCameraPose *thePose,
  vtkOpenGLRenderer *ren,
  vtkOpenGLRenderWindow *renWin)
{
  auto cam = ren->GetActiveCamera();
  auto ovrcam = vtkOpenVRCamera::SafeDownCast(cam);
  if (ovrcam)
  {
    thePose->Set(ovrcam, static_cast<vtkOpenVRRenderWindow*>(renWin));
  }
  else
  {
    auto *opose = this->MyLobby->GetPhysicalPose();
    std::copy(opose->Translation, opose->Translation + 3, thePose->Translation);
    std::copy(opose->PhysicalViewUp, opose->PhysicalViewUp + 3, thePose->PhysicalViewUp);
    thePose->Distance = opose->Distance;
    std::copy(opose->PhysicalViewDirection, opose->PhysicalViewDirection + 3, thePose->PhysicalViewDirection);

    cam->GetPosition(thePose->Position);
    cam->GetDirectionOfProjection(thePose->ViewDirection);

    thePose->Loaded = true;
  }
}

void mvView::ApplyPose(vtkOpenVRCameraPose *thePose,
  vtkOpenGLRenderer *ren,
  vtkOpenGLRenderWindow *renWin)
{
  auto cam = ren->GetActiveCamera();
  auto ovrcam = vtkOpenVRCamera::SafeDownCast(cam);
  if (ovrcam)
  {
    thePose->Apply(ovrcam, static_cast<vtkOpenVRRenderWindow*>(renWin));
  }
  else
  {
    this->MyLobby->SetPhysicalScale(thePose->Distance);
    this->MyLobby->SetPhysicalTranslation(thePose->Translation);
    this->MyLobby->SetPhysicalViewDirection(thePose->PhysicalViewDirection);
    this->MyLobby->SetPhysicalViewUp(thePose->PhysicalViewUp);

    cam->SetPosition(thePose->Position);
    cam->SetDistance(thePose->Distance);
    cam->SetFocalPoint(
      thePose->Position[0] + thePose->Distance*thePose->ViewDirection[0],
      thePose->Position[1] + thePose->Distance*thePose->ViewDirection[1],
      thePose->Position[2] + thePose->Distance*thePose->ViewDirection[2]
      );
    cam->SetViewUp(thePose->PhysicalViewUp);
    cam->OrthogonalizeViewUp();

  #if 0
    // s = saved values
    vtkVector3d svup(thePose->PhysicalViewUp);
    vtkVector3d svdir(thePose->ViewDirection);
    vtkVector3d strans(thePose->Translation);
    vtkVector3d spos(thePose->Position);
    double sdistance = thePose->Distance;

    // c = current values
    auto *opose = this->MyLobby->GetPhysicalPose();
    vtkVector3d cvup(opose->PhysicalViewUp);
    vtkVector3d cpos;
    cam->GetPosition(cpos.GetData());
    vtkVector3d ctrans(opose->Translation);
    vtkVector3d cvdir;
    cam->GetDirectionOfProjection(cvdir.GetData());
    vtkVector3d civdir(opose->PhysicalViewDirection);
    double cdistance = this->MyLobby->GetPhysicalScale();

    // n = new values
    vtkVector3d nvup = svup;
    std::copy(nvup.GetData(), nvup.GetData() + 3, opose->PhysicalViewUp);

    // sanitize the svdir, must be orthogonal to nvup
    svdir = opose->SanitizeVector(svdir, nvup);

    // make sure cvdir and civdir are orthogonal to our nvup
    cvdir = opose->SanitizeVector(cvdir, nvup);
    civdir = opose->SanitizeVector(civdir, nvup);
    vtkVector3d civright = civdir.Cross(nvup);

    // find the new initialvdir
    vtkVector3d nivdir;
    double theta = acos(svdir.Dot(cvdir));
    if (nvup.Dot(cvdir.Cross(svdir)) < 0.0)
    {
      theta = -theta;
    }
    // rotate civdir by theta
    nivdir = civdir * cos(theta) - civright * sin(theta);
    std::copy(nivdir.GetData(), nivdir.GetData() + 3, opose->PhysicalViewDirection);
    vtkVector3d nivright = nivdir.Cross(nvup);

    // adjust translation so that we are in the same spot
    // as when the camera was saved
    vtkVector3d ntrans;
    vtkVector3d cppwc;
    cppwc = cpos + ctrans;
    double x = cppwc.Dot(civdir) / cdistance;
    double y = cppwc.Dot(civright) / cdistance;

    ntrans = strans * nvup + nivdir * (x * sdistance - spos.Dot(nivdir)) +
      nivright * (y * sdistance - spos.Dot(nivright));

    std::copy(ntrans.GetData(), ntrans.GetData() + 3,  opose->Translation);
    cam->SetPosition(cpos.GetData());

    // this really only sets the distance as the render loop
    // sets focal point and position every frame
    vtkVector3d nfp;
    nfp = cpos + nivdir * sdistance;
    cam->SetFocalPoint(nfp.GetData());
    opose->Distance = sdistance;

    std::copy(thePose->PhysicalViewUp, thePose->PhysicalViewUp +3, opose->PhysicalViewUp);
#endif

#if 0
    // maintain the same translation and orientation from the physical values when done
    vtkVector3d ptrans(this->MyLobby->GetPhysicalTranslation());
    vtkVector3d ctrans(cam->GetPosition());

    cam->SetPosition(thePose->Position);
    cam->SetDistance(thePose->Distance);
    cam->SetFocalPoint(
      thePose->Position[0] + thePose->Distance*thePose->ViewDirection[0],
      thePose->Position[1] + thePose->Distance*thePose->ViewDirection[1],
      thePose->Position[2] + thePose->Distance*thePose->ViewDirection[2]
      );
    cam->SetViewUp(thePose->PhysicalViewUp);
    cam->OrthogonalizeViewUp();
  }
  this->MyLobby->SetPhysicalScale(thePose->Distance);
  this->MyLobby->SetPhysicalTranslation(thePose->Translation);
  this->MyLobby->SetPhysicalViewDirection(thePose->PhysicalViewDirection);
#endif
  }
}

// poseNum is the vector index, which may be different
// from the actual pose number
// fromCollab indicates a message from a collaborator to match their pose.
void mvView::GoToTourStop(int index, bool broadcast, bool fromCollab,
  double *collabTrans, double *collabDir)
{
  if (index < 0 || index >= this->TourStops.size())
  {
    return;
  }
  // if we aren't loaded yet, InitialPose will be >= 0, set it and return
  if (this->InitialTourStop >= 0)
  {
    this->InitialTourStop = index;
    return;
  }

  // are we going from a model view to another model view?
  bool fromPoseToPose = !this->InPhotoSphere;

  // exit any photospheres we are in
  this->ExitPhotoSphere();

  vtkOpenGLRenderer *ren = this->MyLobby->GetRenderer();
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();
  auto ovrrw = vtkOpenVRRenderWindow::SafeDownCast(renWin);

  // load up the actors for that stop
  mvTourStop &ts = this->TourStops[index];

  // if there is an audio clip then play it
  if (ts.AudioFile.size())
  {
    PlaySound(ts.AudioFile.c_str(), nullptr, SND_ASYNC | SND_FILENAME | SND_NODEFAULT);
  }

  // if the tourstop should show a photosphere then do that instead
  if (ts.ShowPhotoSphere.size())
  {
    for (auto &sphere : this->PhotoSpheres)
    {
      if (sphere.TextureFile == ts.ShowPhotoSphere)
      {
        this->CurrentTourStop = index;
        this->Animating = false;
        this->GoToPhotoSphere(sphere);
        if (fromCollab)
        {
          this->MyLobby->SetPhysicalViewDirection(collabDir);
        }
        break;
      }
    }
    if (broadcast)
    {
      if (ovrrw)
      {
        this->MyLobby->GetCollaborationClient()->
          SendMessage("P", index,
            ovrrw->GetPhysicalTranslation(),
            ovrrw->GetPhysicalViewDirection());
      }
      else
      {
        this->MyLobby->GetCollaborationClient()->
          SendMessage("P", index,
            ren->GetActiveCamera()->GetPosition(),
            ren->GetActiveCamera()->GetDirectionOfProjection());
      }
    }
    return;
  }

  this->SetPose(SavedPose, ren, renWin);
  int newCamPose = static_cast<int>(floor(ts.CameraPose->PoseNumber));
  if (ovrrw)
  {
    ovrrw->UpdateHMDMatrixPose();
    ovrrw->GetDashboardOverlay()->LoadCameraPose(newCamPose);
    ovrrw->UpdateHMDMatrixPose();
  }
  else
  {
    this->ApplyPose(this->MyLobby->GetDashboardOverlay()->GetSavedCameraPose(newCamPose), ren, renWin);
  }
  this->SetPose(NextPose, ren, renWin);

  if (fromCollab)
  {
    if (ovrrw)
    {
      ovrrw->SetPhysicalTranslation(collabTrans);
      ovrrw->SetPhysicalViewDirection(collabDir);
      ovrrw->UpdateHMDMatrixPose();
      this->SetPose(NextPose,ren, renWin);
      ovrrw->UpdateHMDMatrixPose();
    }
  }
  else
  {
    if (broadcast)
    {
      auto cp = this->MyLobby->GetDashboardOverlay()->GetSavedCameraPose(newCamPose);
      this->MyLobby->GetCollaborationClient()->SendMessage("P", index,
          cp->Translation,
          cp->PhysicalViewDirection);
      // if (ovrrw)
      // {
      //   this->MyLobby->GetCollaborationClient()->SendMessage("P", index,
      //       ovrrw->GetPhysicalTranslation(),
      //       ovrrw->GetPhysicalViewDirection());
      // }
      // else
      // {
      //   this->MyLobby->GetCollaborationClient()->
      //     SendMessage("P", index,
      //       ren->GetActiveCamera()->GetPosition(),
      //       ren->GetActiveCamera()->GetDistance());
      // }
    }
  }

  if (!fromPoseToPose || this->CurrentTourStop == -1 || ts.AnimationDuration <= 0.0)
  {
    this->CurrentTourStop = index;
    this->UpdatePropVisibilityAndOpacity(ts,1.0);
  }
  else
  {
    this->CurrentTourStop = index;

    // fly between old and new pose
    this->AnimationStartTime = vtkTimerLog::GetUniversalTime();
    this->AnimationLastTick = this->AnimationStartTime;
    this->AnimationDuration = ts.AnimationDuration;
    this->AnimationSum = 0.0;
    this->ApplyPose(SavedPose, ren, renWin);
    if (ovrrw)
    {
      ovrrw->UpdateHMDMatrixPose();
    }

    // enter flying mode
    this->Animating = true;
  }

  if (ovrrw)
  {
    ovrrw->UpdateHMDMatrixPose();
  }

  // Get coordinate system so we can place objects
  // in the user's view easily
  vtkCamera *cam = this->MyLobby->GetRenderer()->GetActiveCamera();
  vtkVector3d physvup(this->MyLobby->GetPhysicalViewUp());
  vtkVector3d camdop(cam->GetDirectionOfProjection());
  vtkVector3d camvright = camdop.Cross(physvup);
  camvright.Normalize();
  double scale = this->MyLobby->GetPhysicalScale();

  // display a message box?
  if (ts.MessageBoxText.size())
  {
    this->MessageWidget->SetInteractor(renWin->GetInteractor());
    this->MessageWidget->SetRepresentation(this->MessageRepresentation.Get());
    this->MessageRepresentation->SetText(ts.MessageBoxText.c_str());

    // have to compute a good placement via
    // vup, normal, bounds, scale
    vtkVector3d messvup;
    double wxyz[4];
    wxyz[0] = -3.1416/6.0;
    wxyz[1] = camvright[0];
    wxyz[2] = camvright[1];
    wxyz[3] = camvright[2];
    vtkMath::RotateVectorByWXYZ(physvup.GetData(), wxyz, messvup.GetData());
    vtkVector3d messnorm;
    wxyz[0] = 3.1416/3.0;
    vtkMath::RotateVectorByWXYZ(physvup.GetData(), wxyz, messnorm.GetData());

    vtkVector3d campos(cam->GetPosition());
    campos = campos + camdop*0.6*scale - camvright*0.4*scale - physvup*0.6*scale;
    double bnds[6] =
    {
      campos[0], campos[0],
      campos[1], campos[1],
      campos[2], campos[2]
    };

    this->MessageRepresentation->PlaceWidgetExtended(
      bnds, messnorm.GetData(), messvup.GetData(), scale*1.5);

    this->MessageWidget->SetEnabled(1);
  }
  else
  {
    this->MessageWidget->SetEnabled(0);
  }

  // after all actors are updated, reset CR
  this->MyLobby->GetRenderer()->ResetCameraClippingRange();
}

void mvView::ReturnFromView()
{
  while (this->RenderOperations.size())
  {
    this->RenderOperations.pop();
  }

  // stop any audio
  PlaySound(nullptr, nullptr, SND_ASYNC);

  if (this->LODLoadThreadId >= 0)
  {
    this->Threader->TerminateThread(this->LODLoadThreadId);
  }

  this->ExitPhotoSphere();

  // free up the memory
  for (auto &sphere : this->PhotoSpheres)
  {
    if (sphere.Texture)
    {
      sphere.Texture->Delete();
      sphere.Texture = nullptr;
    }
    if (sphere.Actor)
    {
      this->MyLobby->GetRenderer()->RemoveActor(sphere.Actor);
      sphere.Actor->Delete();
      sphere.Actor = nullptr;
    }
  }

  for (auto &flag : this->Flagpoles)
  {
    this->MyLobby->GetRenderer()->RemoveActor(flag.FlagpoleLabel);
  }

  // actors
  for (auto &actor : this->Actors)
  {
    this->MyLobby->GetRenderer()->RemoveActor(actor.second.VTKActor);
    actor.second.FreeMemory();
  }

  // and textures
  for (auto &d : this->Textures)
  {
    d.second.FreeMemory();
  }

  // and data objects
  for (auto &d : this->DataObjects)
  {
    d.second.FreeMemory();
  }

  this->MyLobby->GetRenderer()->RemoveActor(this->BackgroundActor);

  vtkOpenGLRenderer *ren = this->MyLobby->GetRenderer();
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();
  ren->ReleaseGraphicsResources(renWin);
  ren->SetEnvironmentCubeMap(nullptr);
  this->MessageWidget->SetEnabled(0);
}

bool mvView::EventCallback(
  unsigned long eventID,
  void* calldata)
{
  if (eventID == vtkCommand::CharEvent)
  {
    auto kc = this->MyLobby->GetInteractor()->GetKeyCode();
    switch (kc)
    {
      case 0x1B: // escape
      case 'Q':
      case 'q':
        this->MyLobby->ReturnFromView();
        return true;
        break;
      case 'r':
      case 'R':
        if (!this->InLocalPhotoSphere)
        {
          int newCamPose = static_cast<int>(floor(this->TourStops[this->CurrentTourStop].CameraPose->PoseNumber));
          this->ApplyPose(this->MyLobby->GetDashboardOverlay()->GetSavedCameraPose(newCamPose),
            this->MyLobby->GetRenderer(), this->MyLobby->GetRenderWindow());
        }
        break;
      case ' ':
        if (this->InLocalPhotoSphere)
        {
          this->ReturnFromPhotoSphere();
        }
        else
        {
          this->GoToNextTourStop();
        }
        break;
    }
  }

  if (eventID == vtkCommand::Button3DEvent)
  {
    vtkEventData* edata = static_cast<vtkEventData*>(calldata);
    vtkEventDataDevice3D* edd = edata->GetAsEventDataDevice3D();
    if (!edd)
    {
      return false;
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::ApplicationMenu &&
      edd->GetAction() == vtkEventDataAction::Release)
    {
      this->MyLobby->ReturnFromView();
      return false;
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::Trigger &&
        edd->GetAction() == vtkEventDataAction::Press)
    {
      // trigger press, record the time. If release is short
      // do a "click", otherwise do hold (show a ray)
      // this->MyLobby->ShowRay(edd->GetDevice());
      this->ButtonPressTime = vtkTimerLog::GetUniversalTime();
      this->ButtonPressDevice = edd->GetDevice();
      vtkRenderer *ren = this->MyLobby->GetRenderer();
      vtkOpenVRRenderWindow *renWin =
        static_cast<vtkOpenVRRenderWindow *>(ren->GetVTKWindow());
      vtkOpenVRModel* mod = renWin->GetTrackedDeviceModel(edd->GetDevice());
      if (mod)
      {
        double scale = this->MyLobby->GetPhysicalScale();
        vtkCamera *cam = ren->GetActiveCamera();
        // Ray may need to be longer than 200 meters for large mine models.
        mod->SetRayLength(RAY_LENGTH * scale);
      }
    }
    if (edd->GetInput() == vtkEventDataDeviceInput::Trigger &&
        edd->GetAction() == vtkEventDataAction::Release)
    {
      static_cast<vtkOpenVRInteractorStyle*>(this->MyLobby->GetStyle())
        ->HideRay(edd->GetDevice());
      this->MyLobby->GetCollaborationClient()->
        SendMessage("HR", static_cast<int>(edd->GetDevice()));
      double releaseTime = vtkTimerLog::GetUniversalTime();
      if (this->ButtonPressTime != 0 &&
        releaseTime - this->ButtonPressTime > PRESS_DELAY)
      {
        // if this wasn't a "click", just hide the ray.
        this->ButtonPressTime = 0;
        return false;
      }
      this->ButtonPressTime = 0;
      if (!this->InPhotoSphere)
      {
        for (auto &sphere : this->PhotoSpheres)
        {
          if (sphere.Actor && sphere.Actor->GetVisibility())
          {
            // if they click in a sphere then go there
            double *pos = sphere.Actor->GetPosition();
            double radius = sphere.Actor->GetScale()[0];
            const double *epos = edd->GetWorldPosition();
            if (sqrt(vtkMath::Distance2BetweenPoints(pos, epos)) < radius)
            {
              this->InLocalPhotoSphere = true;
              this->GoToPhotoSphere(sphere);
              return false;
            }
          }
        }
      }
      if (this->InLocalPhotoSphere)
      {
        this->ReturnFromPhotoSphere();
      }
      else
      {
        this->GoToNextTourStop();
      }
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::TrackPad &&
        edd->GetAction() == vtkEventDataAction::Press)
    {
      if (!this->InPhotoSphere)
      {
        static_cast<vtkOpenVRInteractorStyle*>(this->MyLobby->GetStyle())
          ->StartDolly3D(edd);
      }
    }

    if (edd->GetInput() == vtkEventDataDeviceInput::TrackPad &&
        edd->GetAction() == vtkEventDataAction::Release)
    {
      static_cast<vtkOpenVRInteractorStyle*>(this->MyLobby->GetStyle())->EndDolly3D(edd);
    }
    return false;
  }

  if (eventID == vtkCommand::RenderEvent)
  {
    // process any opengl code we need done in the
    // main thread. Nothing here should take more than
    // 20ms or so. Must be fast
    this->HandleAnimation();
    this->ProcessRenderOperations();

    if (this->ButtonPressTime != 0)
    {
      double holdTime = vtkTimerLog::GetUniversalTime();
      if (holdTime - this->ButtonPressTime > PRESS_DELAY)
      {
        static_cast<vtkOpenVRInteractorStyle*>(this->MyLobby->GetStyle())
          ->ShowRay(this->ButtonPressDevice);
        this->MyLobby->GetCollaborationClient()->
          SendMessage("SR", static_cast<int>(this->ButtonPressDevice));
      }
    }
  }
  return false;
}

// process any opengl code we need done in the
// main thread. Nothing here should take more than
// 20ms or so. Must be fast
void mvView::ProcessRenderOperations()
{
  static unsigned int count = 0;
  count++;
  // reset at 1000
  if (count > 1000)
  {
    count = 0;
  }

  // update progress, now and then
  if (count%10 == 0)
  {
    int prog = this->LoadProgress.load();
    // 0 to 100 percent as an int
    if (prog <= 0 || prog >= 100)
    {
      this->MyLobby->GetRenderer()->RemoveActor(this->BackgroundActor);
    }
    else
    {
      this->MyLobby->GetRenderer()->AddActor(this->BackgroundActor);
      double *p1 = this->Plane->GetPoint1();
      double *p2 = this->Plane->GetPoint2();
      this->BackgroundPlane->SetPoint2(
        p1[0], p1[1], p1[2] + 0.01 * prog * (p2[2] - p1[2]));
    }
  }

  // update the audio direction if needed, also check if the video ended
  if (this->InPhotoSphere && this->CurrentPhotoSphere->IsVideo)
  {
    // if at the end of the movie, possibly autoadvance
    mvTourStop &ts = this->TourStops[this->CurrentTourStop];
    if (this->CurrentPhotoSphere->VideoSource->GetEndOfFile() && ts.AutoAdvance)
    {
      this->GoToNextTourStop();
      return;
    }
    vtkRenderer *ren = this->MyLobby->GetRenderer();
    vtkCamera *cam = ren->GetActiveCamera();
    this->MyLobby->GetAudioHandler()
      ->SetListenerPose(cam->GetViewUp(), cam->GetDirectionOfProjection());
  }

  if (this->RenderOperations.size() && count%4 == 0)
  {
    RenderOperation &op = this->RenderOperations.front();

    vtkImageData *id = op.ImageData;
    int *dims = id->GetDimensions();
    int numComp = id->GetNumberOfScalarComponents();

    vtkOpenGLRenderer *ren = this->MyLobby->GetRenderer();
    vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();

    vtkTextureObject* tobj = this->Textures[op.Textures[0]].GetInputTextureObject();

    if (op.CurrentRow == 0)
    {
      // cerr << "allocating  level " << op.Level << " " << dims[0] << " by " << dims[1] << "\n";
      tobj->SetBaseLevel(op.Level);
      tobj->SetMinLOD(op.Level+1);
      tobj->Bind();
      tobj->SendParameters();
    }

    if (op.CurrentRow == dims[1])
    {
      // cerr << "completed level " << op.Level << "\n";
      if (op.Level < op.SmallestLOD)
      {
        tobj->SetMinLOD(op.Level);
        tobj->Bind();
        tobj->SendParameters();
      }

      this->RenderOperations.pop();
      return;
    }

    // load some data to the texture in chunks of ~4MB RGBA
    int height = 2000000/dims[0];
    if (op.CurrentRow + height > dims[1])
    {
      height = dims[1] - op.CurrentRow;
    }
    tobj->Bind();
    vtkDataArray *da = id->GetPointData()->GetScalars();
    glTexSubImage2D(
      tobj->GetTarget(),
      op.Level, // level
      0, // xoffset
      op.CurrentRow, // yoffset
      dims[0], // width
      height,
      numComp == 4 ? GL_RGBA : GL_RGB,
      GL_UNSIGNED_BYTE,
      da->GetVoidPointer(op.CurrentRow*dims[0]*numComp)
      );
    op.CurrentRow += height;
  }
}

void mvView::GoToPhotoSphere(mvPhotoSphere &sphere)
{
  // turn off other stuff
  for (auto &actor : this->Actors)
  {
    actor.second.VTKActor->VisibilityOff();
  }
  for (auto &sphere : this->PhotoSpheres)
  {
    if (sphere.Actor)
    {
      sphere.Actor->VisibilityOff();
    }
  }
  for (auto &flag : this->Flagpoles)
  {
    flag.FlagpoleLabel->VisibilityOff();
  }
  this->MessageWidget->SetEnabled(0);

  vtkOpenGLRenderer *ren = this->MyLobby->GetRenderer();
  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();

  // save current pose?
  this->SetPose(SavedPose, ren, renWin);

  double plane[4];
  auto pvup = this->MyLobby->GetPhysicalViewUp();
  std::copy(pvup, pvup + 3, plane);

  double right[3];
  // cross to the the right vector
  vtkMath::Cross(sphere.Front, plane, right);

  if (sphere.IsVideo)
  {
    ren->AddActor(this->MovieSphere);
    this->MovieSphere->SetVideoSource(sphere.VideoSource);
    this->MyLobby->GetAudioHandler()->SetupAudioPlayback(sphere.VideoSource);
    this->MyLobby->GetAudioHandler()->SetAudioPose(plane, sphere.Front);
    sphere.VideoSource->Record();
    if (sphere.VideoSource->GetStereo3D() || sphere.Stereo3D)
    {
      this->MovieSphere->SetProjectionToStereoSphere();
    }
    else
    {
      this->MovieSphere->SetProjectionToSphere();
    }
    this->MovieSphere->SetFloorPlane(plane[0], plane[1], plane[2], 0.0);
    this->MovieSphere->SetFloorRight(right[0], right[1], right[2]);
    this->MovieSphere->Reset();
  }
  else
  {
    ren->AddActor(this->Skybox);
    this->Skybox->SetTexture(sphere.Texture);
    vtkNew<vtkJPEGReader> rdr;
    rdr->SetFileName(sphere.TextureFile.c_str());
    rdr->Update();
    sphere.Texture->SetInputConnection(rdr->GetOutputPort());
    if (sphere.Stereo3D)
    {
      this->Skybox->SetProjectionToStereoSphere();
    }
    else
    {
      this->Skybox->SetProjectionToSphere();
    }
    this->Skybox->SetFloorPlane(plane[0], plane[1], plane[2], 0.0);
    this->Skybox->SetFloorRight(right[0], right[1], right[2]);
  }

  double trans[3] = { 0.0, 0.0, 0.0 };
  this->MyLobby->SetPhysicalScale(1.0);
  this->MyLobby->SetPhysicalTranslation(trans);

  ren->GetActiveCamera()->SetPosition(0,0,0);
  ren->GetActiveCamera()->SetFocalPoint(right);
  // Match the length of avatar's ray.
  ren->GetActiveCamera()->SetClippingRange(0.1, RAY_LENGTH);
  this->InPhotoSphere = true;
  this->CurrentPhotoSphere = &sphere;
}

void mvView::ReturnFromPhotoSphere()
{
  this->GoToTourStop(this->CurrentTourStop);

  vtkOpenGLRenderWindow *renWin = this->MyLobby->GetRenderWindow();
  auto ovrrw = vtkOpenVRRenderWindow::SafeDownCast(renWin);

  // restore current pose
  this->ApplyPose(SavedPose, this->MyLobby->GetRenderer(), renWin);
  if (ovrrw)
  {
    ovrrw->UpdateHMDMatrixPose();
  }
  this->MyLobby->GetRenderer()->ResetCameraClippingRange();
}
