/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package ganttviewer.pert;

import ganttviewer.pert.model.GanttToSDMAdapter;
import ganttviewer.util.GanttViewerLogger;
import ganttviewer.util.PERTDiagram;
import ilog.views.IlvApplyObject;
import ilog.views.IlvGrapher;
import ilog.views.IlvGraphic;
import ilog.views.IlvManager;
import ilog.views.IlvManagerViewInteractor;
import ilog.views.IlvPoint;
import ilog.views.IlvRect;
import ilog.views.IlvTransformer;
import ilog.views.event.InteractorListener;
import ilog.views.event.TransformerListener;
import ilog.views.gantt.IlvActivity;
import ilog.views.gantt.IlvConstraint;
import ilog.views.gantt.IlvGanttModel;
import ilog.views.gantt.IlvGanttModelUtil;
import ilog.views.gantt.event.SelectionEvent;
import ilog.views.gantt.event.SelectionListener;
import ilog.views.gantt.util.event.IlvHashedEventService;
import ilog.views.interactor.IlvPanInteractor;
import ilog.views.interactor.IlvSelectInteractor;
import ilog.views.sdm.IlvSDMEngine;
import ilog.views.sdm.IlvSDMView;
import ilog.views.sdm.event.SDMEngineSelectionEvent;
import ilog.views.sdm.event.SDMEngineSelectionListener;
import ilog.views.sdm.event.SDMGraphLayoutRendererEvent;
import ilog.views.sdm.event.SDMGraphLayoutRendererListener;
import ilog.views.swing.IlvJScrollManagerView;
import ilog.views.swing.IlvToolTipManager;
import ilog.views.symbol.util.IlvSelectableSymbol;
import ilog.views.util.event.IlvEventListenerCollection;
import ilog.views.util.event.IlvEventListenerSet;

import java.awt.AWTEvent;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;
import javax.swing.plaf.basic.BasicArrowButton;

/**
 * Implementation of the PERT diagram.
 */
SuppressWarnings("serial")
public class PERTDiagramImpl extends JRootPane implements PERTDiagram,
    SDMEngineSelectionListener {

  private static final Rectangle OVERVIEW_BOUNDS = new Rectangle(10, 10, 200,
      100);

  /**
   * The overview.
   */
  private Overview overview;

  /**
   * The manager view.
   */
  private IlvJScrollManagerView scrollView;

  /**
   * The event service.
   */
  private IlvHashedEventService eventService;

  /**
   * The selection listener(s).
   */
  private IlvEventListenerCollection<SelectionListener> selectionListeners;

  /**
   * Selection listening enabled or disabled.
   */
  private boolean listenToSelection = true;

  /**
   * Layout listener.
   */
  private SDMGraphLayoutRendererListener layoutListener;

  /**
   * Apply collapse action.
   */
  private IlvApplyObject applyCollapse;

  /**
   * Builds a <code>PERTDiagramImpl</code>.
   */
  public PERTDiagramImpl() {
    initEventService();
    // create the PERT diagram
    createPERTDiagram();
  }

  /**
   * Creates a PERT diagram.
   */
  protected void createPERTDiagram() {
    scrollView = new IlvJScrollManagerView(new IlvSDMView());
    setContentPane(scrollView);
    // overview
    overview = new Overview((IlvSDMView) scrollView.getView());
    // collapse action
    applyCollapse = new IlvApplyObject() {
      Override
      public void apply(IlvGraphic g, Object arg) {
        ((IlvGrapher) g).setCollapsed(true);
      }
    };
    // layout listener
    layoutListener = new SDMGraphLayoutRendererListener() {
      Override
      public void performLayoutEnded(SDMGraphLayoutRendererEvent event) {
        overview.updateContent();
      }

      Override
      public void performLayoutStarted(SDMGraphLayoutRendererEvent event) {
      }
    };
    // glass pane layer customization
    initGlassPane();
  }

  /**
   * Custom glass pane.
   *
   */
  class CustomGlassPane extends JPanel implements AWTEventListener {

    /**
     * Builds a <code>CustomGlassPane</code>.
     */
    public CustomGlassPane() {
      try {
        Toolkit.getDefaultToolkit().addAWTEventListener(this,
            AWTEvent.MOUSE_EVENT_MASK);
      } catch (Exception e) {
        // doing nothing, the overview will not be displayed
      }
    }

    /**
     * @see javax.swing.JComponent.contains(int, int)
     */
    Override
    public boolean contains(int x, int y) {
      // AWT system shows the cursor of the topmost visible component
      // In our case, the topmost visible component is this glass pane
      // and we want to display the current cursor of the IlvSDMView
      // This fix makes the glass pane completely transparent
      return false;
    }

    /**
     *  @see java.awt.event.AWTEventListener.eventDispatched(java.awt.AWTEvent)
     */
    Override
    public void eventDispatched(AWTEvent event) {
      if (event instanceof MouseEvent) {
        MouseEvent me = (MouseEvent) event;
        if (!SwingUtilities.isDescendingFrom(me.getComponent(),
            PERTDiagramImpl.this)) {
          return;
        }
        // make the overview visible or not depending on the current interaction
        if (isTranslation(me)) {
          overview.setVisible(true);
        } else if (stopTranslation(me)) {
          overview.setVisible(false);
        }
      }
    }

  }

  /**
   * Indicates whether the mouse event is considered as a translation interaction.
   * @param me The mouse event.
   * @return Whether the mouse event is considered as a translation interaction.
   */
  protected boolean isTranslation(MouseEvent me) {
    if (me.getSource() instanceof IlvSDMView) {
      return (((IlvSDMView) me.getSource()).getInteractor() instanceof IlvPanInteractor)
          && (MouseEvent.MOUSE_PRESSED == me.getID());
    }
    if ((me.getSource() instanceof JScrollBar)
        || (me.getSource() instanceof BasicArrowButton)) {
      return MouseEvent.MOUSE_PRESSED == me.getID();
    }
    return false;
  }

  /**
   * Indicates whether the mouse event is considered as a stop-translation interaction.
   * @param me The mouse event.
   * @return Whether the mouse event is considered as a stop-translation interaction.
   */
  protected boolean stopTranslation(MouseEvent me) {
    return MouseEvent.MOUSE_RELEASED == me.getID();
  }

  /**
   * Customizes the glass pane to display an overview.
   */
  protected void initGlassPane() {
    JPanel glassPane = new CustomGlassPane();
    glassPane.setLayout(null);
    glassPane.setOpaque(false);
    glassPane.add(overview);
    overview.setBounds(PERTDiagramImpl.OVERVIEW_BOUNDS);
    overview.setVisible(false);
    setGlassPane(glassPane);
    glassPane.setVisible(true);
  }

  /**
   * Initializes the event services.
   */
  private void initEventService() {
    eventService = new IlvHashedEventService();
    selectionListeners = new IlvEventListenerSet<SelectionListener>();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#initialize()
   */
  Override
  public void initialize() {
    // activate the mouse wheel support
    scrollView.setWheelScrollingEnabled(true);
    IlvSDMView view = (IlvSDMView) scrollView.getView();
    // activate tooltips
    IlvToolTipManager.registerView(view);
    // activate wheel zooming
    view.setWheelZoomingEnabled(true);
    // selection
    IlvSelectInteractor selectInteractor = new IlvSelectInteractor();
    selectInteractor.setMoveAllowed(false);
    selectInteractor.setMultipleSelectionMode(true);
    view.setInteractor(selectInteractor);
    // listen to selection
    view.getSDMEngine().addSDMEngineSelectionListener(this);
    // a way to improve the performances:
    // no rendering done on selection or property change
    // selection is more reactive
    view.getSDMEngine().setRenderingDoneMode(IlvSDMEngine.NEVER);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#attachGanttModel(ilog.views.gantt.IlvGanttModel)
   */
  Override
  public void attachGanttModel(IlvGanttModel ganttModel) {
    IlvSDMEngine engine = getSDMEngine();
    engine.setAdjusting(true);
    try {
      // create the model adapter
      GanttToSDMAdapter sdmModel = new GanttToSDMAdapter(ganttModel);
      engine.setModel(sdmModel, true);
      engine.performNodeLayout();
    } catch (Exception e) {
      GanttViewerLogger.error(e);
    } finally {
      engine.setAdjusting(false);
    }
  }

  /**
   * Returns the collapsed activities.
   * @return The collapsed activities.
   */
  protected Set<IlvActivity> getCollapsedNodes() {
    IlvSDMEngine engine = getSDMEngine();
    Set<IlvActivity> collapsedNodes = new HashSet<IlvActivity>();
    // retrieve collapsed nodes
    Enumeration<?> objects = engine.getAllObjects();
    while (objects.hasMoreElements()) {
      Object obj = objects.nextElement();
      if (obj instanceof IlvActivity) {
        IlvGraphic graphic = engine.getGraphic(obj, false);
        if (graphic instanceof IlvGrapher) {
          IlvGrapher grapher = (IlvGrapher) graphic;
          if (grapher.isCollapsed()) {
            collapsedNodes.add((IlvActivity) obj);
          }
        }
      }
    }
    return collapsedNodes;
  }

  /**
   * Collapses the nodes corresponding to the given activities.
   * @param activities The activities.
   */
  protected void setCollapsedNodes(Set<IlvActivity> activities) {
    if ((activities == null) || (activities.size() == 0)) {
      return;
    }
    IlvSDMEngine engine = getSDMEngine();
    Iterator<IlvActivity> it = activities.iterator();
    while (it.hasNext()) {
      IlvGrapher grapher = (IlvGrapher) engine.getGraphic(it.next(), false);
      grapher.getGraphicBag().applyToObject(grapher, applyCollapse, grapher,
          false);
    }
    // perform layout
    engine.performNodeLayout();
  }

  /**
   * Gets the selected objects.
   * @return The selected objects.
   */
  protected Set<Object> getSelectedObjects() {
    IlvSDMEngine engine = getSDMEngine();
    Set<Object> selectedObjects = new HashSet<Object>();
    // retrieve collapsed nodes
    Enumeration<?> objects = engine.getSelectedObjects();
    while (objects.hasMoreElements()) {
      selectedObjects.add(objects.nextElement());
    }
    return selectedObjects;
  }

  /**
   * Internal selection of the given objects.
   * @param selectedObjects The objects to select.
   */
  protected void setSelectedObjects(Set<Object> selectedObjects) {
    if ((selectedObjects == null) || (selectedObjects.size() == 0)) {
      return;
    }
    listenToSelection = false;
    Iterator<Object> objects = selectedObjects.iterator();
    while (objects.hasNext()) {
      Object obj = objects.next();
      if (obj instanceof IlvActivity) {
        setSelectedImpl((IlvActivity) obj, true);
      } else if (obj instanceof IlvConstraint) {
        setSelectedImpl((IlvConstraint) obj, true);
      }
    }
    listenToSelection = true;
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#applyStyle(java.lang.String)
   * @proofread
   */
  Override
  public void applyStyle(String css) throws Exception {
    Set<IlvActivity> collapsedNodes = null;
    IlvSDMEngine engine = getSDMEngine();
    // if this is not the first stylesheet:
    if (engine.getNodeLayoutRenderer() != null) {
      // remove the previous layout listener
      engine.getNodeLayoutRenderer().removeGraphLayoutRendererListener(
          layoutListener);
      // retrieve the collapsed nodes to set them collapsed after applying the new stylesheet
      collapsedNodes = getCollapsedNodes();
    }
    // get selection
    Set<Object> selectedObjects = getSelectedObjects();
    // apply stylesheet
    getSDMView().setStyleSheets(0, css);
    // restaure collapsed nodes
    setCollapsedNodes(collapsedNodes);
    // restaure selection
    setSelectedObjects(selectedObjects);
    // add the layout listener again
    engine.getNodeLayoutRenderer().addGraphLayoutRendererListener(
        layoutListener);
    engine.performNodeLayout();
  }

  /**
   * Returns the SDM view.
   * @return The SDM view.
   */
  public IlvSDMView getSDMView() {
    return (IlvSDMView) scrollView.getView();
  }

  /**
   * Returns the SDM engine.
   * @return The SDM engine.
   */
  public IlvSDMEngine getSDMEngine() {
    return getSDMView().getSDMEngine();
  }

  /**
   * Returns the loaded Gantt data model.
   * @return The Gantt data model.
   */
  Override
  public IlvGanttModel getGanttModel() {
    GanttToSDMAdapter sdmModel = (GanttToSDMAdapter) this.getSDMEngine()
        .getModel();
    return sdmModel.getGanttModel();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#isSelected(ilog.views.gantt.IlvActivity)
   */
  Override
  public boolean isSelected(IlvActivity activity) {
    return getSDMEngine().isSelected(activity);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#deselectAll()
   */
  Override
  public void deSelectAll() {
    getSDMEngine().deselectAllObjects();
  }

  /**
   * Selects or deselects the given activity.
   * @param activity The activity.
   * @param selected The desired selection status.
   */
  protected void setSelectedImpl(IlvActivity activity, boolean selected) {
    GanttToSDMAdapter model = (GanttToSDMAdapter) getSDMEngine().getModel();
    if (model.isNode(activity) && getGanttModel().contains(activity)
        && !IlvGanttModelUtil.hasChildren(getGanttModel(), activity)) {
      getSDMEngine().setSelected(activity, selected);
    }
    // select or deselect the activity
    IlvGraphic activityGraphic = getSDMEngine().getGraphic(activity, false);
    if (activityGraphic instanceof IlvSelectableSymbol) {
      ((IlvSelectableSymbol) activityGraphic).selectionStateChanged();
    }
  }

  /**
   * Selects or deselects the given constraint.
   * @param constraint The constraint.
   * @param selected The desired selection status.
   */
  protected void setSelectedImpl(IlvConstraint constraint, boolean selected) {
    GanttToSDMAdapter model = (GanttToSDMAdapter) getSDMEngine().getModel();
    if (model.isLink(constraint) && getGanttModel().contains(constraint)) {
      getSDMEngine().setSelected(constraint, selected);
    }
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#select(ilog.views.gantt.IlvActivity, boolean)
   */
  Override
  public void setSelected(IlvActivity activity, boolean selected) {
    setSelectedImpl(activity, selected);
  }

  /**
   * Listens to selection changes.
   * @param event The selection event.
   * @proofread
   */
  Override
  public void selectionChanged(SDMEngineSelectionEvent event) {
    if (!listenToSelection) {
      return;
    }
    if (event.getDataObject() instanceof IlvActivity) {
      IlvActivity activity = (IlvActivity) event.getDataObject();
      boolean selected = event.getEngine().isSelected(activity);
      // select or deselect the activity
      IlvGraphic activityGraphic = event.getEngine()
          .getGraphic(activity, false);
      if (activityGraphic instanceof IlvSelectableSymbol) {
        ((IlvSelectableSymbol) activityGraphic).selectionStateChanged();
      }
      // forward the event only if this is an activity
      fireSelectionEvent(event);
      // get selected constraints
      Enumeration<?> e = event.getEngine().getSelectedObjects();
      Set<Object> selectedConstraints = new HashSet<Object>();
      while (e.hasMoreElements()) {
        Object obj = e.nextElement();
        if (((GanttToSDMAdapter) getSDMEngine().getModel()).isLink(obj)) {
          selectedConstraints.add(obj);
        }
      }
      if (!selected) {
        // we only deselect constraints that are not connected to a selected activity
        Iterator<?> constraints = selectedConstraints.iterator();
        while (constraints.hasNext()) {
          IlvConstraint constraint = (IlvConstraint) constraints.next();
          if ((constraint.getFromActivity().equals(activity) && !event
              .getEngine().isSelected(constraint.getToActivity()))
              || (constraint.getToActivity().equals(activity) && !event
                  .getEngine().isSelected(constraint.getFromActivity()))) {
            setSelectedImpl(constraint, false);
          }
        }
      } else {
        // if an activity has been selected, highlight or unhighlight related constraints
        IlvGanttModel model = getGanttModel();
        Iterator<?> constraints = model.constraintIteratorFromActivity(activity);
        while (constraints.hasNext()) {
          IlvConstraint constraint = (IlvConstraint) constraints.next();
          setSelectedImpl(constraint, true);
        }
        constraints = model.constraintIteratorToActivity(activity);
        while (constraints.hasNext()) {
          IlvConstraint constraint = (IlvConstraint) constraints.next();
          setSelectedImpl(constraint, true);
        }
      }
    }
  }

  /**
   * Fires a <code>SelectionEvent</code> event.
   * @param event An SDM engine event that will be transformed into a Gantt selection event.
   */
  private void fireSelectionEvent(SDMEngineSelectionEvent event) {
    // Create a Gantt selection event.
    SelectionEvent e = new SelectionEvent(event.getDataObject(), getSDMEngine()
        .isSelected(event.getDataObject()));
    eventService.publish(e);
    for (Iterator<SelectionListener> i = selectionListeners.getListeners(); i.hasNext();) {
      SelectionListener listener = i.next();
      listener.selectionChanged(e);
    }
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#addInteractorListener(ilog.views.InteractorListener)
   */
  Override
  public void addInteractorListener(InteractorListener listener) {
    scrollView.getView().addInteractorListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#addSelectionListener(ilog.views.gantt.event.SelectionListener)
   */
  Override
  public void addSelectionListener(SelectionListener listener) {
    selectionListeners.addListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#addTransformerListener(ilog.views.TransformerListener)
   */
  Override
  public void addTransformerListener(TransformerListener listener) {
    scrollView.getView().addTransformerListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#removeInteractorListener(ilog.views.event.InteractorListener)
   */
  Override
  public void removeInteractorListener(InteractorListener listener) {
    scrollView.getView().removeInteractorListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#removeSelectionListener(ilog.views.gantt.event.SelectionListener)
   */
  Override
  public void removeSelectionListener(SelectionListener listener) {
    selectionListeners.removeListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#removeTransformerListener(ilog.views.event.TransformerListener)
   */
  Override
  public void removeTransformerListener(TransformerListener listener) {
    scrollView.getView().removeTransformerListener(listener);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#zoom(ilog.views.IlvPoint, double, double)
   */
  Override
  public void zoom(IlvPoint point, double sx, double sy) {
    scrollView.getView().zoom(point, sx, sy, true);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#zoomToFit()
   */
  Override
  public void zoomToFit() {
    scrollView.getView().fitTransformerToContent();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#getInteractor()
   */
  Override
  public IlvManagerViewInteractor getInteractor() {
    return scrollView.getView().getInteractor();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#setInteractor(ilog.views.IlvManagerViewInteractor)
   */
  Override
  public void setInteractor(IlvManagerViewInteractor interactor) {
    scrollView.getView().setInteractor(interactor);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#getTransformer()
   */
  Override
  public IlvTransformer getTransformer() {
    return scrollView.getView().getTransformer();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#setTransformer(ilog.views.IlvTransformer)
   */
  Override
  public void setTransformer(IlvTransformer t) {
    scrollView.getView().setTransformer(t);
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#boundingBox()
   */
  Override
  public IlvRect boundingBox() {
    return getSDMView().getSDMEngine().getGrapher().boundingBox();
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#boundingBox(IlvActivity)
   */
  Override
  public IlvRect boundingBox(IlvActivity activity) {
    IlvGraphic graphic = getSDMEngine().getGraphic(activity, false);
    if (graphic == null) {
      return new IlvRect();
    }
    return graphic.boundingBox(((IlvManager) graphic.getGraphicBag())
        .getTopLevelTransformer());
  }

  /* (non-Javadoc)
   * @see ganttviewer.pert.PERTDiagram#visibleRect()
   */
  Override
  public IlvRect visibleRect() {
    Rectangle visibleRect = getSDMView().visibleRect();
    return new IlvRect(visibleRect.x, visibleRect.y, visibleRect.width,
        visibleRect.height);
  }

  /**
   * Overview panel.
   */
  class Overview extends JPanel {

    /**
     * Cached image to render.
     */
    private BufferedImage cachedImage = null;

    /**
     * Transparency.
     */
    private AlphaComposite alpha;

    /**
     * The associated view.
     */
    private IlvSDMView view;

    /**
     * Visible area color
     */
    private final Color visibleAreaColor = new Color(100, 100, 255, 100);

    /**
     * Internal transformer.
     */
    private IlvTransformer transformer;

    /**
     * Builds an <code>Overview</code>
     * @param view The view from which we want to create an overview.
     */
    public Overview(IlvSDMView view) {
      this.view = view;
      // alpha transparency management
      alpha = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f);
      // internal transformer
      transformer = new IlvTransformer();
      // Transparent panel
      setOpaque(false);
    }

    /**
     * Recreates the cached image.
     */
    protected void rebuildCachedImage() {
      Rectangle bounds = getBounds();
      // create a new background image
      BufferedImage snapshot = new BufferedImage(bounds.width, bounds.height,
          BufferedImage.TYPE_INT_ARGB);
      Graphics2D g2d = snapshot.createGraphics();
      g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
          RenderingHints.VALUE_ANTIALIAS_ON);
      // source and destination rectangles
      IlvRect srcRect = boundingBox();
      IlvRect dstRect = new IlvRect(0, 0, bounds.width, bounds.height);
      // fit to content
      double sx = dstRect.width / srcRect.width;
      double sy = dstRect.height / srcRect.height;
      double scale = Math.min(sx, sy);
      double x0 = -scale * srcRect.x
          + ((sx > sy) ? (dstRect.width - scale * srcRect.width) / 2 : 0);
      double y0 = -scale * srcRect.y
          + ((sx <= sy) ? (dstRect.height - scale * srcRect.height) / 2 : 0);
      transformer.setValues(scale, 0., 0., scale, x0, y0);
      // draw
      getSDMView().getSDMEngine().getGrapher().print(g2d, srcRect, view,
          transformer, false);
      g2d.dispose();
      // create the image to be rendered
      cachedImage = new BufferedImage(bounds.width, bounds.height,
          BufferedImage.TYPE_INT_ARGB);
      g2d = cachedImage.createGraphics();
      g2d.setComposite(alpha);
      g2d.setColor(Color.LIGHT_GRAY);
      g2d.fillRect(0, 0, bounds.width, bounds.height);
      g2d.drawImage(snapshot, 0, 0, bounds.width, bounds.height, null);
      // dispose resources
      g2d.dispose();
      snapshot.flush();
    }

    /**
     * Refreshes the overview.
     */
    public void updateContent() {
      Rectangle bounds = getBounds();
      if ((bounds.width > 0) && (bounds.height > 0)) {
        rebuildCachedImage();
      }
    }

    /**
     * Rendering.
     * @param g Graphic context.
     */
    Override
    public void paint(Graphics g) {
      Graphics2D g2d = (Graphics2D) g;
      Rectangle bounds = getBounds();
      super.paint(g2d);
      if (cachedImage == null) {
        return;
      }
      // draw
      g2d.drawImage(cachedImage, 0, 0, bounds.width, bounds.height, null);
      // highlight visible area
      displayVisibleArea(g2d);
    }

    /**
     * Displays the visible area.
     * @param g Graphic context.
     */
    protected void displayVisibleArea(Graphics g) {
      // apply transformer on visible area
      IlvRect visibleRect = visibleRect();
      // transformation
      view.getTransformer().inverse(visibleRect);
      transformer.applyFloor(visibleRect);
      // draw the rectangle
      g.setColor(visibleAreaColor);
      g.fillRect((int) visibleRect.x, (int) visibleRect.y,
          (int) visibleRect.width + 1, (int) visibleRect.height + 1);
    }

  }

}