Draw force elements
DrawForceElementsWindow.cpp
// Copyright 2010 ESRI
// 
// All rights reserved under the copyright laws of the United States
// and applicable international laws, treaties, and conventions.
// 
// You may freely redistribute and use this sample code, with or
// without modification, provided you include the original copyright
// notice and use restrictions.
// 
// See the use restrictions.
// 


#include <math.h>

#include "DrawForceElementsWindow.h"
#include "DrawForceElements.h"

using namespace std;


#define FE_TYPE_CODES "UFNH"


// construct a table of frame height factors based on Table VIII in 2525B
// these are used to "normalize" the size of the exported images somewhat
struct FrameHeight2525B
{
  QString affiliations;
  QString dimensions;
  double  heightRatio;
};
static const int c_numFrameHeights = 13;
static const int c_unknownSurface = 1;
static const int c_friendlySeaSurface = 5;
static const FrameHeight2525B c_frameHeights[c_numFrameHeights] =
{
  // unknown frames
  { "PUGW",   "PA",  1.3  }, // air and space
  { "PUGW",   "GFS", 1.44 }, // surface
  { "PUGW",   "U",   1.3  }, // subsurface

  // friendly frames
  { "FADMJK", "PA",  1.2  }, // air and space
  { "FADMJK", "GF",  1.0  }, // ground
  { "FADMJK", "S",   1.2  }, // sea surface and ground equipment
  { "FADMJK", "U",   1.2  }, // subsurface

  // neutral frames
  { "NL",     "PA",  1.2  }, // air and space
  { "NL",     "GFS", 1.1  }, // surface
  { "NL",     "U",   1.2  }, // subsurface

  // hostile frames
  { "HS",     "PA",  1.3  }, // air and space
  { "HS",     "GFS", 1.44 }, // surface
  { "HS",     "U",   1.3  }  // subsurface

  // there are two special cases:
  // 1.  battle dimension Z always uses an unknown surface dimension frame (1.44)
  // 2.  friendly ground equipment (dimension "G") uses a sea surface frame (1.2)
  //     so we check for code "E" at index 4 and anything other than "-" at index 5
};


//////////////////////////////////////////////////////////
// private member functions for DrawForceElementsWindow //
//////////////////////////////////////////////////////////


// compute the union of the graphic's bounds with the refresh envelope
// if the refresh envelope is null, create a new one and populate it with the FE's bounds
void DrawForceElementsWindow::addRefresh (ICachedGraphicPtr ipFE)
{
  IEnvelopePtr ipFEEnvelope (CLSID_Envelope);
  ipFE->QueryEnvelope (getScreenDisplay(), ipFEEnvelope);
  if ( ! m_ipRefreshEnvelope )
    m_ipRefreshEnvelope = ipFEEnvelope;
  else
    m_ipRefreshEnvelope->Union (ipFEEnvelope);
}


// add the named tool to the toolbar control
void DrawForceElementsWindow::addTool (const char *name)
{
  CComBSTR toolID ("esriControlCommands.");
  toolID.Append (name);
  long itemIndex;
  m_ipToolbarControl->AddItem (CComVariant(toolID), 0, -1, VARIANT_FALSE, 0, esriCommandStyleIconOnly, &itemIndex);
}


// convenience method to extricate this much-used interface
IActiveViewPtr DrawForceElementsWindow::getActiveView()
{
  IActiveViewPtr ipActiveView;
  m_ipMapControl->get_ActiveView (&ipActiveView);
  return ipActiveView;
}


// compute the size of a force element in geographic units based on the current value of m_sldSize
double DrawForceElementsWindow::computeSize()
{
  // calculate the new symbol size as a fraction of the visible extent
  IEnvelopePtr ipExtent;
  getActiveView()->get_Extent (&ipExtent);
  double height;
  ipExtent->get_Height (&height);
  return ( 0.002 * height * double(m_sldSize->value()) );
}


// get the text of the selected property from the selected FE and put it in the line editor
void DrawForceElementsWindow::copyPropToText()
{
  if ( m_ipSelectedFE )
  {
    USES_CONVERSION;

    IFEGraphicPtr    ipFEGraphic (m_ipSelectedFE);
    IForceElementPtr ipForceElement;
    IPropertySetPtr  ipPropertySet;
    CComVariant      variant;
    CComBSTR         bstrValue;

    // update the label editor to reflect the attributes of the newly selected element
    ipFEGraphic->get_ForceElement (&ipForceElement);
    ipForceElement->get_PropertySet (&ipPropertySet);
    ipPropertySet->GetProperty (CComBSTR((const char*)m_cmbPropName->currentText().toAscii()), &variant);
    if ( variant.vt == VT_BSTR )
      bstrValue = variant.bstrVal;
    if ( bstrValue.Length() > 0 )
      m_txtPropValue->setText (OLE2A(bstrValue));
    else
      m_txtPropValue->setText ("");
  }
}


// if UI is in edit mode, get the SIC of the selected FE and put it in the line editor
void DrawForceElementsWindow::copySICToText()
{
  if ( m_ipSelectedFE && m_uiMode == c_uiModeEditFE )
  {
    USES_CONVERSION;
    static const QString feTypeCodes (FE_TYPE_CODES);

    // put the selected graphic's SIC in the editor widget and check the selected radio button
    IFEGraphicPtr ipFEGraphic (m_ipSelectedFE);
    CComBSTR bstrSIC;
    ipFEGraphic->get_SymbolID (&bstrSIC);
    m_txtSIC->setText (OLE2A(bstrSIC));
    int feType = feTypeCodes.indexOf (m_txtSIC->text()[1]);
    if ( feType >= 0 && feType < NUM_FE_TYPES && feTypeCodes[feType] != QChar(m_forceElementType) )
    {
      // change the selected radio button to the one matching the selected graphic's type
      m_suppressChangeFEType = true;
      m_rbFEType[feType]->setChecked (true);
      m_suppressChangeFEType = false;
      m_forceElementType = feTypeCodes[feType].toAscii();
    }
  }
}


// get the line editor's text and put it in the selected property of the selected FE
void DrawForceElementsWindow::copyTextToProp()
{
  if ( m_ipSelectedFE )
  {
    IFEGraphicPtr    ipFEGraphic (m_ipSelectedFE);
    IForceElementPtr ipForceElement;
    IPropertySetPtr  ipPropertySet;
    QString          propValue (m_txtPropValue->text());

    // set the selected annotation property on the force element
    ipFEGraphic->get_ForceElement (&ipForceElement);
    ipForceElement->get_PropertySet (&ipPropertySet);
    if ( propValue.length() > 0 )
      ipPropertySet->SetProperty (
        CComBSTR((const char*)m_cmbPropName->currentText().toAscii()),
        CComVariant(CComBSTR((const char*)propValue.toAscii()))
      );
    else
      ipPropertySet->SetProperty (
        CComBSTR((const char*)m_cmbPropName->currentText().toAscii()),
        CComVariant(CComBSTR(""))
      );
    // tell the graphic to rebuild its labels
    m_ipSelectedFE->Refresh (getScreenDisplay());
  }
}


// if UI is in edit mode, get the line editor's text and put it in the SIC of the selected FE
void DrawForceElementsWindow::copyTextToSIC()
{
  if ( m_ipSelectedFE && m_uiMode == c_uiModeEditFE )
  {
    IFEGraphicPtr     ipFEGraphic     (m_ipSelectedFE);
    IForceElementPtr  ipForceElement;
    IActiveViewPtr    ipActiveView    (getActiveView());
    IScreenDisplayPtr ipScreenDisplay (getScreenDisplay(ipActiveView));

    // change the selected element to the new type
    resetRefresh (m_ipSelectedFE);
    ipFEGraphic->get_ForceElement (&ipForceElement);
    ipForceElement->put_MessageString (CComBSTR((const char*)m_txtSIC->text().toAscii()));
    m_ipSelectedFE->Refresh (ipScreenDisplay);
    addRefresh (m_ipSelectedFE);
    ipActiveView->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
  }
}


// enable and disable the delete and export buttons based on the application state
void DrawForceElementsWindow::enableDisable()
{
  m_btnDelete->setEnabled ( m_ipSelectedFE != 0 );
  bool haveAnyForceElements = ( m_forceElements.size() > 0 );
  m_btnClear->setEnabled ( haveAnyForceElements );
  m_btnExport->setEnabled ( haveAnyForceElements );
}


// convenience method to extricate this much-used interface
IScreenDisplayPtr DrawForceElementsWindow::getScreenDisplay (IActiveViewPtr ipActiveView)
{
  if ( ! ipActiveView )
    ipActiveView = getActiveView();
  IScreenDisplayPtr ipScreenDisplay;
  ipActiveView->get_ScreenDisplay (&ipScreenDisplay);
  return ipScreenDisplay;
}


// reset the refresh extent to null or that of the specified graphic
void DrawForceElementsWindow::resetRefresh (ICachedGraphicPtr ipFE)
{
  m_ipRefreshEnvelope = 0;
  if ( ipFE )
    addRefresh (ipFE);
}


//////////////////////////////////////////////////////////////////////
// event handlers (Qt slots, IMapControlEvents2, IActiveViewEvents) //
//////////////////////////////////////////////////////////////////////


void DrawForceElementsWindow::OnAfterDraw (VARIANT display, long viewDrawPhase)
{
  if ( viewDrawPhase == esriViewForeground )
  {
    // draw the force element symbols directly on the display - no caching
    // you can see the extent of m_ipRefreshEnvelope in the part of the map that flickers
    IScreenDisplayPtr ipScreenDisplay (getScreenDisplay());
    ipScreenDisplay->StartDrawing (0, esriNoScreenCache);
    for ( list<ICachedGraphic*>::iterator itr = m_forceElements.begin(); itr != m_forceElements.end(); ++itr )
      (*itr)->Draw (ipScreenDisplay, NULL);

    // if a force element is selected, highlight it using the default symbol
    if ( m_ipSelectedFE )
      m_ipSelectedFE->Highlight (ipScreenDisplay, m_ipHighlightColor, NULL);

    ipScreenDisplay->FinishDrawing();
  }
}


void DrawForceElementsWindow::OnMouseDown (long button, long shift, long x, long y, double mapX, double mapY)
{
  // we only work on the LMB here
  if ( button != 1 ) return;
  if ( m_uiMode == c_uiModePlaceFE )
  {
    // do some basic validation on the SIC
    QByteArray qsic  (m_txtSIC->text().toAscii());
    bool       valid (true);
    const char *sic  (qsic.data());

    // there are many other ways to make an invalid FE SIC, but these are the most basic
    if ( qsic.length() != 15 )
    {
      QMessageBox::critical (
        this,
        "Invalid Symbol ID Code",
        "The symbol ID code must be 15 characters in length."
      );
      valid = false;
    }
    else if ( qsic[0] != 'S' )
    {
      QMessageBox::critical (
        this,
        "Non-FE Symbol ID Code",
        "Force element symbol ID codes all begin with 'S'."
      );
      valid = false;
    }
    // generate the next SIC, if selected
    if ( m_chkRandomizeSIC->isChecked() )
      randomizeSIC();
    if ( ! valid )
      return;
    else
      printf ("Placing %s at (%f, %f).\n", sic, mapX, mapY);

    IForceElementPtr   ipForceElement   (CLSID_ForceElement);
    IPointPtr          ipPoint          (CLSID_Point);
    IFEGraphicPtr      ipFEGraphic;
    IFEGraphicStylePtr ipFEGraphicStyle;
    ICachedGraphicPtr  ipCachedGraphic;

    // construct a new force element graphic using the current settings
    ipForceElement->put_MessageString (CComBSTR(sic));
    ipPoint->PutCoords (mapX, mapY);
    ipForceElement->putref_Shape (ipPoint);
    m_ipRenderer->GraphicByForceElement (ipForceElement, &ipCachedGraphic);
    ipCachedGraphic->put_Angle (m_angle);
    ipCachedGraphic->put_Size (computeSize());

    // tell it to use polygon shapes instead of fonts to render text (because it's faster)
    ipFEGraphic = ipCachedGraphic;
    ipFEGraphic->get_Style (&ipFEGraphicStyle);
    ipFEGraphicStyle->put_UseFonts (VARIANT_FALSE);
    ipFEGraphic->putref_Style (ipFEGraphicStyle);

    // make the new force element the selected object and set its first label property, if any
    resetRefresh (m_ipSelectedFE);
    m_ipSelectedFE = ipCachedGraphic;
    m_forceElements.push_back (ipCachedGraphic.Detach());
    copyTextToProp();

    // refresh the display around where the new symbol was added
    addRefresh (m_ipSelectedFE);
    getActiveView()->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
    enableDisable();
  }
  else if ( m_uiMode == c_uiModeEditFE )
  {
    // we only want to perform dragging when in edit mode
    m_mouseDown = true;
    m_prevX = mapX;
    m_prevY = mapY;

    // pick an existing force element from the map to replace the existing selection, if any
    resetRefresh (m_ipSelectedFE);
    m_ipSelectedFE = 0;

    // find the graphic under the mouse click that is closest to the end of the list, if any
    // use a forward search because we want to use the iterator to rearrange the list afterward
    list<ICachedGraphic*>::iterator found = m_forceElements.end();
    for ( list<ICachedGraphic*>::iterator itr = m_forceElements.begin(); itr != m_forceElements.end(); ++itr )
    {
      // loop through the pointers and perform a hit test on each one using the mouse location
      VARIANT_BOOL isHit (VARIANT_FALSE);
      (*itr)->HitTest (mapX, mapY, 0.0, &isHit);
      if ( isHit == VARIANT_TRUE )
        found = itr;
    }
    if ( found != m_forceElements.end() )
    {
      // there is a hit - replace the selected graphic and halt the search
      // move the new selection to the back of the list so it will render on top (no AddRef or Release needed)
      m_ipSelectedFE = *found;
      m_forceElements.erase (found);
      m_forceElements.push_back (m_ipSelectedFE);
      addRefresh (m_ipSelectedFE);
    }
    if ( m_ipSelectedFE )
    {
      // update the control panel to reflect the attributes of the newly selected graphic
      copySICToText();
      copyPropToText();
    }
    // refresh the display - OnAfterDraw draws the symbols
    getActiveView()->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
    enableDisable();
  }
}


void DrawForceElementsWindow::OnMouseMove (long button, long shift, long x, long y, double mapX, double mapY)
{
  if ( m_mouseDown && m_ipSelectedFE != 0 )
  {
    IFEGraphicPtr    ipFEGraphic    (m_ipSelectedFE);
    IForceElementPtr ipForceElement;
    IPointPtr        ipPoint;
    IActiveViewPtr   ipActiveView   (getActiveView());
    double           deltaX         (mapX - m_prevX);
    double           deltaY         (mapY - m_prevY);
    double           prevX;
    double           prevY;

    // store the current mouse coordinates as the basis of the next delta
    m_prevX = mapX;
    m_prevY = mapY;

    // drag the selected FE along with the mouse
    resetRefresh (m_ipSelectedFE);
    ipFEGraphic->get_ForceElement (&ipForceElement);
    ipForceElement->get_Shape (&ipPoint);
    ipPoint->QueryCoords (&prevX, &prevY);
    ipPoint->PutCoords (prevX + deltaX, prevY + deltaY);

    // refresh the force element and the display
    m_ipSelectedFE->Refresh (getScreenDisplay(ipActiveView));
    addRefresh (m_ipSelectedFE);
    ipActiveView->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
  }
}


void DrawForceElementsWindow::OnMouseUp (long button, long shift, long x, long y, double mapX, double mapY)
{
  m_mouseDown = false;
}


void DrawForceElementsWindow::changeAngle (int sliderValue)
{
  // convert the slider value (degrees) into radians
  m_angle = double(sliderValue) * 0.0174532925199432957692369;
  if ( m_forceElements.size() > 0 )
  {
    // update the graphics to the new angle and redraw everything
    for ( list<ICachedGraphic*>::iterator itr = m_forceElements.begin(); itr != m_forceElements.end(); ++itr )
      (*itr)->put_Angle (m_angle);
    getActiveView()->PartialRefresh (esriViewForeground, NULL, NULL);
  }
}


// called when one of the Force Element Type radio buttons is clicked
// alter the contents of the SIC line editor and change the SIC in the selected FE
void DrawForceElementsWindow::changeFEType (int radioButtonID)
{
  static const char *feTypeCodes (FE_TYPE_CODES);

  if ( m_suppressChangeFEType ) return;

  if ( radioButtonID < NUM_FE_TYPES && feTypeCodes[radioButtonID] != m_forceElementType )
  {
    m_forceElementType = feTypeCodes[radioButtonID];
    QString sic (m_txtSIC->text());
    sic[1] = m_forceElementType;
    m_txtSIC->setText (sic);
    copyTextToSIC();
  }
}


// called when one of the Mode radio buttons is clicked - change the UI mode
void DrawForceElementsWindow::changeMode (int radioButtonID)
{
  static const char *hints[] = {
    "Use the toolbar or change the mode.",
    "Click on the map to place a <b>Force Element</b>.",
    "Drag a <b>Force Element</b> or change its properties."
  };
  UIMode newMode ((UIMode)radioButtonID);
  if ( newMode != m_uiMode )
  {
    // switch the toolbar on and off depending on the mode
    if ( newMode == c_uiModeArcGIS )
    {
      m_ipToolbarControl->put_Enabled (VARIANT_TRUE);
      m_ipToolbarControl->SetBuddyControl (m_ipMapControl);
      m_controlPanel->setEnabled (false);
    }
    else
    {
      m_ipToolbarControl->put_Enabled (VARIANT_FALSE);
      m_ipToolbarControl->SetBuddyControl (NULL);
      m_controlPanel->setEnabled (true);
    }
    // update the control panel
    m_uiMode = newMode;
    m_lblHint->setText (hints[m_uiMode]);

    // make sure the SIC is properly randomized before placement, if appropriate
    // otherwise make sure the SIC being edited is that of the selected FE
    if ( m_uiMode == c_uiModePlaceFE && m_chkRandomizeSIC->isChecked() )
      randomizeSIC();
    else
      copySICToText();
    enableDisable();
  }
}


// called when the label name combo box is changed
// get the label text from the selected FE and put it in the line editor
void DrawForceElementsWindow::changePropName (int newPropIndex)
{
  m_lblPlacement->setText (m_labelPlacements[newPropIndex]);
  copyPropToText();
}


// called when the current label text is edited - apply the new text to the selected FE
void DrawForceElementsWindow::changePropText (const QString&)
{
  if ( m_ipSelectedFE )
  {
    // if the text gets smaller, we need to refresh the extent from _before_ the text is changed
    // if the text gets larger, we need to refresh the extent from _after_ the text is changed
    resetRefresh (m_ipSelectedFE);
    copyTextToProp();
    addRefresh (m_ipSelectedFE);
    getActiveView()->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
  }
}


// called when the SIC text is edited - apply the new SIC to the selected FE
void DrawForceElementsWindow::changeSICText (const QString&)
{
  copyTextToSIC();
}


// called when the size slider is moved - resize all the force elements
void DrawForceElementsWindow::changeSize (int)
{
  if ( m_forceElements.size() > 0 )
  {
    // update the graphics to the new size and redraw everything
    double newSize = computeSize();
    for ( list<ICachedGraphic*>::iterator itr = m_forceElements.begin(); itr != m_forceElements.end(); ++itr )
      (*itr)->put_Size (newSize);
    getActiveView()->PartialRefresh (esriViewForeground, NULL, NULL);
  }
}


// called when the clear button is clicked
void DrawForceElementsWindow::clearFE()
{
  // confirm this action with the user
  QMessageBox::StandardButton response = QMessageBox::question (
    this,
    "Delete All Force Elements?",
    "Please confirm that you want to delete all <b>Force Elements</b> from the map.",
    QMessageBox::Yes | QMessageBox::No
  );
  if ( response == QMessageBox::No ) return;

  // remove all the FE graphics and refresh the display
  m_ipSelectedFE = 0;
  while ( m_forceElements.size() > 0 )
  {
    // be sure to dereference these pointers; otherwise they will leak
    m_forceElements.back()->Release();
    m_forceElements.pop_back();
  }
  getActiveView()->PartialRefresh (esriViewForeground, NULL, NULL);
}


// called when the delete button is clicked
void DrawForceElementsWindow::deleteFE()
{
  // take advantage of condition established by OnMouseDown - selected FE is at the end of the list
  m_forceElements.back()->Release();
  m_forceElements.pop_back();
  resetRefresh (m_ipSelectedFE);
  m_ipSelectedFE = 0;

  // there will be no selected element after this call
  getActiveView()->PartialRefresh (esriViewForeground, NULL, m_ipRefreshEnvelope);
  enableDisable();
}


// called when the export button is clicked
void DrawForceElementsWindow::exportFE()
{
  USES_CONVERSION;
  static const double unitFrameHeight = 200; // unit frame height in pixels

  IFEGraphicPtr     ipFEGraphic;
  IExportGraphicPtr ipExportGraphic;
  IScreenDisplayPtr ipScreenDisplay  (getScreenDisplay());
  IPolygonPtr       ipFrameArea;
  IEnvelopePtr      ipFrameBounds;
  IEnvelopePtr      ipElementBounds  (CLSID_Envelope);
  CComBSTR          bstrSIC;
  VARIANT_BOOL      succeeded;
  double            frameHeight;
  double            frameHeightRatio;
  double            elementHeight;
  long              imageHeight;
  char              *sic;
  char              fileName[128];
  int               sequenceNumber   (0);

  printf ("exporting force elements\n");
  for ( list<ICachedGraphic*>::iterator itr = m_forceElements.begin(); itr != m_forceElements.end(); ++itr )
  {
    // build the file name from the FE properties
    bstrSIC.Empty();
    ipFEGraphic = *itr;
    ipFEGraphic->get_SymbolID (&bstrSIC);
    sic = OLE2A (bstrSIC);
    sprintf (fileName, "fe_%03d_%s.png", ++sequenceNumber, sic);

    // look up the symbol's frame height ratio
    frameHeightRatio = 0.0;
    for ( int i = 0; i < c_numFrameHeights; ++i )
    {
      // it's primarily determined by affiliation and battle space dimension
      if ( c_frameHeights[i].affiliations.contains(sic[1]) && c_frameHeights[i].dimensions.contains(sic[2]) )
      {
        // in most cases, this will be the result
        frameHeightRatio = c_frameHeights[i].heightRatio;
        break;
      }
    }
    // check for a couple of degenerate cases
    if ( frameHeightRatio == 0.0 )
    {
      // unknown battle space dimension ("Z")
      if ( sic[2] == 'Z' )
        frameHeightRatio = c_frameHeights[c_unknownSurface].heightRatio;

      // friendly ground equipment ("**G*E*")
      else if (
        c_frameHeights[c_friendlySeaSurface].affiliations.contains(sic[1]) &&
        sic[2] == 'G' &&
        sic[4] == 'E' &&
        sic[5] != '-'
      )
        frameHeightRatio = c_frameHeights[c_friendlySeaSurface].heightRatio;

      // sanity check for any other case we didn't account for
      else
        frameHeightRatio = 1.0;
    }
    // compute the height of the image to account for the varying text shapes
    ipFEGraphic->get_FrameArea (&ipFrameArea);
    ipFrameArea->get_Envelope (&ipFrameBounds);
    (*itr)->QueryEnvelope (ipScreenDisplay, ipElementBounds);
    ipFrameBounds->get_Height (&frameHeight);
    ipElementBounds->get_Height (&elementHeight);

#if defined(ESRI_UNIX)
    imageHeight = long (round(unitFrameHeight * frameHeightRatio * elementHeight / frameHeight));
#elif defined(ESRI_WINDOWS)
    imageHeight = long (unitFrameHeight * frameHeightRatio * elementHeight / frameHeight);
#endif

    // perform the export operation
    ipExportGraphic = (*itr);
    ipExportGraphic->ExportToFile (
      ipScreenDisplay,
      CComBSTR(fileName),
      NULL,
      imageHeight,
      0,
      0,
      0.0,
      0.0,
      VARIANT_FALSE,
      &succeeded
    );
    printf ("\texport %s %s\n", fileName, ((succeeded==VARIANT_TRUE) ? "succeeded" : "failed"));
  }
  printf ("export complete\n");
}


// randomize the SIC that is currently displayed in the line editor
void DrawForceElementsWindow::randomizeSIC()
{
  QString sic (getSIC());
  if ( m_forceElementType != 'F' )
    sic[1] = m_forceElementType;
  m_txtSIC->setText (sic);
  copyTextToSIC();
}


////////////////////////////////
// constructor and destructor //
////////////////////////////////


DrawForceElementsWindow::DrawForceElementsWindow() :
  m_angle                    (0.0),
  m_forceElementType         ('F'),
  m_ipHighlightColor         (CLSID_RgbColor),
  m_ipMapControlEventsHelper (CLSID_MapControlEvents2Listener),
  //m_ipRenderer               (CLSID_ForceElement2525BRenderer),
  m_mouseDown                (false),
  m_suppressChangeFEType     (false),
  m_uiMode                   (c_uiModeArcGIS)
{
  USES_CONVERSION;

  printf ("creating widgets\n");

  // create the child widgets of this window
  QSplitter    *splitter         = new QSplitter    (this);
  QWidget      *qtPanel          = new QWidget      (splitter);
               m_lblHint         = new QLabel       ("Use the toolbar or change the mode.", qtPanel);
  QGroupBox    *gbMode           = new QGroupBox    ("Mode", qtPanel);
  QRadioButton *rbGIS            = new QRadioButton ("ArcGIS Toolbar", gbMode);
  QRadioButton *rbPlace          = new QRadioButton ("Place Force Element", gbMode);
  QRadioButton *rbEdit           = new QRadioButton ("Edit Force Element", gbMode);
               m_controlPanel    = new QWidget      (qtPanel);
  QGroupBox    *gbFEType         = new QGroupBox    ("Force Element Affiliation", m_controlPanel);
               m_rbFEType[0]     = new QRadioButton ("Unknown", gbFEType);
               m_rbFEType[1]     = new QRadioButton ("Friendly", gbFEType);
               m_rbFEType[2]     = new QRadioButton ("Neutral", gbFEType);
               m_rbFEType[3]     = new QRadioButton ("Hostile", gbFEType);
               m_chkRandomizeSIC = new QCheckBox    ("Randomize symbol ID code after placement", m_controlPanel);
  QLabel       *lblSIC           = new QLabel       ("Symbol ID Code:", m_controlPanel);
               m_txtSIC          = new QLineEdit    (getSIC(), m_controlPanel);
               m_cmbPropName     = new QComboBox    (m_controlPanel);
               m_txtPropValue    = new QLineEdit    (m_controlPanel);
  QLabel       *lblPlacement     = new QLabel       ("Label Placement:", m_controlPanel);
               m_lblPlacement    = new QLabel       (m_controlPanel);
  QLabel       *lblSize          = new QLabel       ("Symbol Size (% of display):", m_controlPanel);
               m_sldSize         = new QSlider      (Qt::Horizontal, m_controlPanel);
  QLabel       *lblAngle         = new QLabel       ("Symbol Rotation:", m_controlPanel);
  QSlider      *sldAngle         = new QSlider      (Qt::Horizontal, m_controlPanel);
  QPushButton  *btnRandomizeSIC  = new QPushButton  ("Randomize Symbol ID", m_controlPanel);
               m_btnDelete       = new QPushButton  ("Delete Selected", m_controlPanel);
               m_btnClear        = new QPushButton  ("Clear All", m_controlPanel);
               m_btnExport       = new QPushButton  ("Export to PNG", m_controlPanel);
  QWidget      *gisPanel         = new QWidget      (splitter);
  QAxCtl       *toolbarControl   = new QAxCtl       (AoPROGID_ToolbarControl, gisPanel, "Toolbar Control");
  QAxCtl       *mapControl       = new QAxCtl       (AoPROGID_MapControl, gisPanel, "Map Control");

  // set properties on some of the widgets
  toolbarControl->setMinimumHeight (28);
  toolbarControl->setMaximumHeight (28);
  splitter->setOpaqueResize (false);
  QList<int> sizes (splitter->sizes());
  sizes[0] = 300;
  sizes[1] = 600;
  splitter->setSizes (sizes);
  m_sldSize->setRange (1, 100);
  m_sldSize->setSliderPosition (50);
  sldAngle->setRange (0, 360);
  sldAngle->setSliderPosition (0);
  m_chkRandomizeSIC->setChecked (true);
  m_btnDelete->setEnabled (false);
  m_btnExport->setEnabled (false);
  m_controlPanel->setEnabled (false);

  printf("Initilizing MOLE Renderer\n");

  IMoleCoreHelperPtr ipMoleCoreHelper (CLSID_MoleCoreHelper);
  ICacheRendererPtr ipCR;
  ipMoleCoreHelper->get_ForceElementRenderer(&ipCR);
  m_ipRenderer = ipCR;

  // initialize the labeling controls from the FE graphic factory
  IFEGraphicFactoryPtr ipFEGraphicFactory;
  m_ipRenderer->get_GraphicFactory (&ipFEGraphicFactory);
  IEnumAttributeLabelPtr ipEnumAttributeLabel (ipFEGraphicFactory);
  ipEnumAttributeLabel->Reset();
  IAttributeLabelPtr ipAttributeLabel;
  ipEnumAttributeLabel->NextLabel (&ipAttributeLabel);
  while ( ipAttributeLabel )
  {
    // get the semicolon-separated list of field names and the label placement name
    CComBSTR bstrPlacementName, bstrFields;
    ipAttributeLabel->get_Name (&bstrPlacementName);
    ipAttributeLabel->get_Field (&bstrFields);

    // parse out the field names to populate the combo box
    QString qsPlacementName (OLE2A(bstrPlacementName));
    QString qsFields (OLE2A(bstrFields));
    QStringList qslFields (qsFields.split(QChar(';'), QString::SkipEmptyParts));
    for ( int i = 0; i < qslFields.size(); ++i )
    {
      m_cmbPropName->addItem (qslFields[i]);
      m_labelPlacements.append (qsPlacementName);
    }
    // make sure that all labels can be displayed
    ipAttributeLabel->put_IsVisible (VARIANT_TRUE);
    ipEnumAttributeLabel->NextLabel (&ipAttributeLabel);
  }
  // select the first attribute in the list
  m_cmbPropName->setCurrentIndex (0);
  m_lblPlacement->setText (m_labelPlacements[0]);

  // set up the mode radio button logic
  QButtonGroup *bgMode = new QButtonGroup (gbMode);
  bgMode->addButton (rbGIS,   c_uiModeArcGIS);
  bgMode->addButton (rbPlace, c_uiModePlaceFE);
  bgMode->addButton (rbEdit,  c_uiModeEditFE);
  rbGIS->setChecked (true);

  // set up the mode radio button layout
  QVBoxLayout *modeLayout = new QVBoxLayout (gbMode);
  modeLayout->addWidget (rbGIS);
  modeLayout->addWidget (rbPlace);
  modeLayout->addWidget (rbEdit);

  // set up the FE type radio button logic and layout; initially select friendly type
  QButtonGroup *bgFEType = new QButtonGroup (gbFEType);
  QVBoxLayout *feTypeLayout = new QVBoxLayout (gbFEType);
  for ( int i = 0; i < NUM_FE_TYPES; ++i )
  {
    bgFEType->addButton (m_rbFEType[i], i);
    feTypeLayout->addWidget (m_rbFEType[i]);
  }
  m_rbFEType[1]->setChecked (true);

  // establish the control panel layout
  int row = 0;
  QGridLayout *controlPanelLayout = new QGridLayout (m_controlPanel);
  controlPanelLayout->setMargin (0);
  controlPanelLayout->addWidget (gbFEType,          row++, 0, 1, 2);
  controlPanelLayout->addWidget (m_chkRandomizeSIC, row++, 0, 1, 2);
  controlPanelLayout->addWidget (lblSIC,            row,   0, Qt::AlignRight);
  controlPanelLayout->addWidget (m_txtSIC,          row++, 1);
  controlPanelLayout->addWidget (m_cmbPropName,     row,   0);
  controlPanelLayout->addWidget (m_txtPropValue,    row++, 1);
  controlPanelLayout->addWidget (lblPlacement,      row,   0, Qt::AlignRight);
  controlPanelLayout->addWidget (m_lblPlacement,    row++, 1);
  controlPanelLayout->addWidget (lblSize,           row,   0, Qt::AlignRight);
  controlPanelLayout->addWidget (m_sldSize,         row++, 1);
  controlPanelLayout->addWidget (lblAngle,          row,   0, Qt::AlignRight);
  controlPanelLayout->addWidget (sldAngle,          row++, 1);
  controlPanelLayout->addWidget (btnRandomizeSIC,   row++, 1);
  controlPanelLayout->addWidget (m_btnDelete,       row++, 1);
  controlPanelLayout->addWidget (m_btnClear,        row++, 1);
  controlPanelLayout->addWidget (m_btnExport,       row++, 1);
  controlPanelLayout->setRowStretch (row, 1);

  // establish the Qt panel layout
  QBoxLayout *qtLayout = new QVBoxLayout (qtPanel);
  qtLayout->addWidget (m_lblHint);
  qtLayout->addWidget (gbMode);
  qtLayout->addWidget (m_controlPanel);

  // establish the GIS layout
  QVBoxLayout *gisLayout = new QVBoxLayout (gisPanel);
  gisLayout->setMargin (0);
  gisLayout->setSpacing (0);
  gisLayout->addWidget (toolbarControl);
  gisLayout->addWidget (mapControl, 1);

  // establish the main layout
  QVBoxLayout *rootLayout = new QVBoxLayout (this);
  rootLayout->setMargin (0);
  rootLayout->setSpacing (0);
  rootLayout->addWidget (splitter);

  // set properties on this widget and connect event handlers
  resize (900, 600);
  setWindowTitle ("Draw Force Elements");
  connect (bgMode,          SIGNAL(buttonClicked(int)),         SLOT(changeMode(int)));
  connect (bgFEType,        SIGNAL(buttonClicked(int)),         SLOT(changeFEType(int)));
  connect (m_sldSize,       SIGNAL(valueChanged(int)),          SLOT(changeSize(int)));
  connect (sldAngle,        SIGNAL(valueChanged(int)),          SLOT(changeAngle(int)));
  connect (m_cmbPropName,   SIGNAL(currentIndexChanged(int)),   SLOT(changePropName(int)));
  connect (m_txtPropValue,  SIGNAL(textEdited(const QString&)), SLOT(changePropText(const QString&)));
  connect (m_txtSIC,        SIGNAL(textEdited(const QString&)), SLOT(changeSICText(const QString&)));
  connect (btnRandomizeSIC, SIGNAL(clicked()),                  SLOT(randomizeSIC()));
  connect (m_btnDelete,     SIGNAL(clicked()),                  SLOT(deleteFE()));
  connect (m_btnClear,      SIGNAL(clicked()),                  SLOT(clearFE()));
  connect (m_btnExport,     SIGNAL(clicked()),                  SLOT(exportFE()));

  printf ("setting up the controls\n");

  // extricate the COM interfaces of the ArcGIS controls
  IUnknownPtr ipUnknown;
  mapControl->getInterface (&ipUnknown);
  m_ipMapControl = ipUnknown;
  if ( ! m_ipMapControl ) ABORT;
  toolbarControl->getInterface (&ipUnknown);
  m_ipToolbarControl = ipUnknown;
  if ( ! m_ipToolbarControl ) ABORT;

  // buddy up and load the toolbar
  m_ipToolbarControl->SetBuddyControl (m_ipMapControl);
  addTool ("ControlsOpenDocCommand");
  addTool ("ControlsAddDataCommand");
  addTool ("ControlsSaveAsDocCommand");
  addTool ("ControlsMapZoomPanTool");
  addTool ("ControlsMapZoomInFixedCommand");
  addTool ("ControlsMapZoomOutFixedCommand");
  addTool ("ControlsMapZoomInTool");
  addTool ("ControlsMapZoomOutTool");
  addTool ("ControlsMapFullExtentCommand");
  addTool ("ControlsMapZoomToLastExtentBackCommand");
  addTool ("ControlsMapZoomToLastExtentForwardCommand");
  addTool ("ControlsMapZoomToolControl");
  addTool ("ControlsSelectTool");
  addTool ("ControlsMapGoToCommand");
  addTool ("ControlsMapIdentifyTool");
  addTool ("ControlsMapMeasureTool");
  m_ipToolbarControl->put_ToolTips (VARIANT_TRUE);

  // register for map control events - use static cast because argument type != interface
  m_ipMapControlEventsHelper->Startup (static_cast<IMapControlEvents2Helper*>(this));
  m_ipMapControlEventsHelper->AdviseEvents (m_ipMapControl, NULL);

  printf ("loading the map\n");

  // load a default map file
  CComVariant vUndefined ((long)DISP_E_PARAMNOTFOUND, VT_ERROR);
  CComBSTR bstmp;
  m_ipMapControl->LoadMxFile ((bstmp = dsDataPath("Continents/Continents.mxd")), vUndefined, vUndefined);
  SysFreeString(bstmp);

  // initialize the highlight color
  m_ipHighlightColor->put_Red (0);
  m_ipHighlightColor->put_Green (255);
  m_ipHighlightColor->put_Blue (255);
}


DrawForceElementsWindow::~DrawForceElementsWindow()
{
  printf ("deregistering event handler\n");

  m_ipMapControlEventsHelper->UnadviseEvents();
  m_ipMapControlEventsHelper->Shutdown();

  printf ("releasing MOLE resources\n");

  IMoleCoreHelperPtr ipMoleCoreHelper (CLSID_MoleCoreHelper);
  ipMoleCoreHelper->ReleaseForceElementRenderer();
  while ( m_forceElements.size() > 0 )
  {
    // be sure to dereference these pointers; otherwise they will leak
    m_forceElements.back()->Release();
    m_forceElements.pop_back();
  }
  // the controls are in the Qt tree, but dereference them for completeness
  m_ipMapControl             = 0;
  m_ipMapControlEventsHelper = 0;
}