/*
* 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 interactor;
import java.awt.AWTEvent;
import java.awt.Cursor;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.*;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.*;
import java.util.logging.Logger;
import java.util.logging.Level;
import ilog.views.chart.IlvAxis;
import ilog.views.chart.IlvChart;
import ilog.views.chart.IlvChartDecoration;
import ilog.views.chart.IlvChartInteractor;
import ilog.views.chart.IlvCoordinateSystem;
import ilog.views.chart.IlvDataInterval;
import ilog.views.chart.IlvDataWindow;
import ilog.views.chart.IlvDoublePoint;
import ilog.views.chart.IlvStyle;
import ilog.views.chart.graphic.IlvDataIndicator;
import ilog.views.util.collections.IlvCollections;
import ilog.views.util.IlvResourceUtil;
/**
* A class to interact on an <code>IlvDataIndicator</code>.
* The <code>IlvChartDataIndicatorInteractor</code> class provides the
* following facilities to graphically modify the definition of a data
* indicator:
* <ul><li>Drag a border of the indicator to modify the value of the
* corresponding bound.</li>
* <li>Drag the indicator itself to modify the corresponding range
* definition.</li>
* </ul>.
* <p>The <code>IlvChartDataIndicatorInteractor</code> defines two modes of
* interaction whether its <code>opaqueEdit</code> property is enabled or not.
* If the property is enabled, the indicator modification is "real time": each
* <code>mouseMotion</code> event immediately modifies the indicator values
* and hence cannot be canceled. If the mode is disabled, the indicator
* modification will only be performed when the interaction ends (on a
* <code>MOUSE_RELEASED</code> event).
* <p>The registered name of this interactor is "DataIndicator".
*/
public class IlvChartDataIndicatorInteractor extends IlvChartInteractor
{
// ============================ Metainformation ============================
static {
IlvChartInteractor.register("DataIndicator",
IlvChartDataIndicatorInteractor.class);
}
/**
* Returns a localized name for this interactor class.
*/
public static String getLocalizedName(Locale locale)
{
return IlvResourceUtil.getBundle("messages",
IlvChartDataIndicatorInteractor.class,
locale)
.getString("IlvChartDataIndicatorInteractor");
}
// ============================ Logging Support ============================
static Logger logger;
static Logger getLogger() {
if (logger == null)
logger =
Logger.getLogger(IlvChartDataIndicatorInteractor.class.getName());
return logger;
}
// ===================== Customization, Bean Properties =====================
private transient IlvDataIndicator _indicator;
/**
* Returns the indicator that is being dragged.
*/
protected final IlvDataIndicator getIndicator()
{
return _indicator;
}
//--------------------------------------------------------------------------
private boolean _opaqueEdit;
/**
* Returns whether the <code>opaqueEdit</code> mode is enabled.
* @see #setOpaqueEdit
*/
public final boolean isOpaqueEdit()
{
return _opaqueEdit;
}
/**
* Sets whether the <code>opaqueEdit</code> mode is enabled. This property
* defines how the modifications are propagated to the indicator. If
* <code>true</code>, each mouse event corresponds to an immediate indicator
* modification. If <code>false</code>, the modification occurs when the
* interaction ends. The default value is <code>false</code>.
*/
public void setOpaqueEdit(boolean opaque)
{
_opaqueEdit = opaque;
}
// ============================ State Variables ============================
private transient Object _currentValue;
//--------------------------------------------------------------------------
/**
* This interface implements the detail behavior, as a reaction to
* mouse events.
*/
private interface InteractionHandler extends Serializable
{
public Object computeValue(double dx, double dy, boolean apply);
public Cursor getCursor();
}
/**
* This class implements the detail behavior when an indicator is
* translated.
*/
private final class Translater implements InteractionHandler
{
Override
public Object computeValue(double dx, double dy, boolean apply)
{
Object value;
int type = getIndicator().getType();
switch (type) {
case IlvDataIndicator.X_VALUE:
case IlvDataIndicator.Y_VALUE:
double v = (_currentValue != null
? ((Double)_currentValue).doubleValue()
: getIndicator().getValue());
getLogger().log(Level.FINE, "v:" + v +" dx:" + dx + " dy:" + dy);
v += type == IlvDataIndicator.X_VALUE ? dx : dy;
if (apply)
getIndicator().setValue(v);
value = Double.valueOf(v);
getLogger().log(Level.FINE, "value:" + value);
break;
default:
IlvDataWindow window = (_currentValue != null
? (IlvDataWindow)_currentValue
: getIndicator().getDataWindow());
if (type != IlvDataIndicator.Y_RANGE) {
window.xRange.translate(dx);
}
if (type != IlvDataIndicator.X_RANGE) {
window.yRange.translate(dy);
}
if (apply) {
if (type == IlvDataIndicator.X_RANGE)
getIndicator().setRange(window.xRange);
else if (type == IlvDataIndicator.Y_RANGE)
getIndicator().setRange(window.yRange);
else
getIndicator().setDataWindow(window);
}
value = window;
}
return value;
}
Override
public Cursor getCursor()
{
return Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR);
}
}
private InteractionHandler _translater = new Translater();
/**
* This class implements the detail behavior when an indicator is
* resized.
*/
private final class Reshaper implements InteractionHandler
{
static final int NONE = 0;
static final int XMIN = 1;
static final int XMAX = 2;
static final int YMIN = 3;
static final int YMAX = 4;
private int bound;
public Reshaper(int bound)
{
this.bound = bound;
}
Override
public Cursor getCursor()
{
return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
}
Override
public Object computeValue(double dx, double dy, boolean apply)
{
IlvDataWindow window = (_currentValue != null
? (IlvDataWindow)_currentValue
: getIndicator().getDataWindow());
if(window == null)
return null;
IlvDataInterval range = null;
int type = getIndicator().getType();
switch (bound) {
case XMIN:
window.xRange.min += dx;
range = window.xRange;
break;
case XMAX:
window.xRange.max += dx;
range = window.xRange;
break;
case YMIN:
window.yRange.min += dy;
range = window.yRange;
break;
case YMAX:
window.yRange.max += dy;
range = window.yRange;
break;
}
if (apply) {
if (type != IlvDataIndicator.WINDOW)
getIndicator().setRange(range);
else
getIndicator().setDataWindow(window);
}
return window;
}
}
// The detail interaction delegate.
private InteractionHandler _handler;
//--------------------------------------------------------------------------
private transient IlvDoublePoint _dragPoint;
// ============================ Ghost handling ============================
private IlvStyle getGhostStyle()
{
IlvStyle style = _indicator.getStyle();
if (style == null)
style = new IlvStyle(1, getGhostColor());
return style;
}
/**
* Draws the ghost.
*/
Override
protected void drawGhost(Graphics g)
{
IlvChart chart = getChart();
int type = _indicator.getType();
IlvStyle style = getGhostStyle();
IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex());
Shape shp;
switch (type) {
case IlvDataIndicator.X_VALUE:
case IlvDataIndicator.Y_VALUE:
double value = ((Double)_currentValue).doubleValue();
int axisType =
(_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS);
shp = chart.getProjector().getShape(value,
axisType,
chart.getChartArea().getPlotRect(),
sys);
break;
default:
IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue);
w.intersection(sys.getVisibleWindow());
if (w.isEmpty())
return;
shp = chart.getProjector().getShape(w,
chart.getChartArea().getPlotRect(),
sys);
break;
}
style.renderShape(g, shp);
}
/**
* Returns the ghost bounds.
*/
Override
protected Rectangle getGhostBounds()
{
IlvChart chart = getChart();
int type = _indicator.getType();
IlvStyle style = getGhostStyle();
IlvCoordinateSystem sys = getChart().getCoordinateSystem(getYAxisIndex());
Shape shp;
switch (type) {
case IlvDataIndicator.X_VALUE:
case IlvDataIndicator.Y_VALUE:
double value = ((Double)_currentValue).doubleValue();
int axisType =
(_indicator.getAxisIndex() == -1 ? IlvAxis.X_AXIS : IlvAxis.Y_AXIS);
shp = chart.getProjector().getShape(value,
axisType,
chart.getChartArea().getPlotRect(),
sys);
break;
default:
IlvDataWindow w = new IlvDataWindow((IlvDataWindow)_currentValue);
w.intersection(sys.getVisibleWindow());
if (w.isEmpty())
return new Rectangle();
shp = chart.getProjector().getShape(w,
chart.getChartArea().getPlotRect(),
sys);
break;
}
return style.getShapeBounds(shp).getBounds();
}
// ========================= Mouse event handling =========================
/**
* Determines which indicator the user meant by clicking at (x,y).
* Sets _indicator and returns true if one was found; returns false otherwise.
*/
private boolean findIndicator(int x, int y)
{
List<IlvChartDecoration> decos = getChart().getDecorations();
Iterator<IlvChartDecoration> ite = IlvCollections.reversedIterator(decos);
while (ite.hasNext()) {
IlvChartDecoration deco = ite.next();
if (deco instanceof IlvDataIndicator) {
if (((IlvDataIndicator)deco).contains(x, y)) {
_indicator = (IlvDataIndicator)deco;
return true;
}
}
}
return false;
}
/**
* After findIndicator returned true, determine which kind of interaction
* to use with _indicator.
*/
private InteractionHandler getInteractionHandler(MouseEvent event)
{
IlvDoublePoint start = getData(event);
InteractionHandler handler;
IlvDataWindow window = _indicator.getDataWindow();
int type = _indicator.getType();
switch (type) {
case IlvDataIndicator.X_VALUE:
case IlvDataIndicator.Y_VALUE:
// All the user can do with such an indicator is to move it.
// No need to test the event's coordinates - findIndicator already did
// it.
handler = _translater;
break;
case IlvDataIndicator.X_RANGE:
case IlvDataIndicator.Y_RANGE:
// When clicking near one of the two borders, the user wants to move
// this border; otherwise he wants to move the entire indicator.
{
boolean onX = type == IlvDataIndicator.X_RANGE;
IlvDataInterval range;
double value;
if (onX) {
range = window.xRange;
value = start.x;
} else {
range = window.yRange;
value = start.y;
}
double l = range.getLength();
double delta = l*0.10;
if (value < range.min + delta)
handler = new Reshaper(onX ? Reshaper.XMIN : Reshaper.YMIN);
else if (value > range.max - delta)
handler = new Reshaper(onX ? Reshaper.XMAX : Reshaper.YMAX);
else // (value >= range.min + delta && value <= range.max - delta)
handler = _translater;
}
break;
case IlvDataIndicator.WINDOW:
// When clicking near one of the four borders, the user wants to move
// this border; otherwise he wants to move the entire indicator.
{
int nearXBorder;
double distToXBorder;
{
double l = window.xRange.getLength();
double delta = l*0.10;
if (start.x < window.xRange.min + delta) {
nearXBorder = Reshaper.XMIN;
IlvDoublePoint pt =
new IlvDoublePoint(window.xRange.min,
start.y > window.yRange.max ? window.yRange.max :
start.y < window.yRange.min ? window.yRange.min :
start.y);
pt = toDisplay(pt);
distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
} else if (start.x > window.xRange.max - delta) {
nearXBorder = Reshaper.XMAX;
IlvDoublePoint pt =
new IlvDoublePoint(window.xRange.max,
start.y > window.yRange.max ? window.yRange.max :
start.y < window.yRange.min ? window.yRange.min :
start.y);
pt = toDisplay(pt);
distToXBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
} else {
nearXBorder = Reshaper.NONE;
distToXBorder = 0;
}
}
int nearYBorder;
double distToYBorder;
{
double l = window.yRange.getLength();
double delta = l*0.10;
if (start.y < window.yRange.min + delta) {
nearYBorder = Reshaper.YMIN;
IlvDoublePoint pt =
new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max :
start.x < window.xRange.min ? window.xRange.min :
start.x,
window.yRange.min);
pt = toDisplay(pt);
distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
} else if (start.y > window.yRange.max - delta) {
nearYBorder = Reshaper.YMAX;
IlvDoublePoint pt =
new IlvDoublePoint(start.x > window.xRange.max ? window.xRange.max :
start.x < window.xRange.min ? window.xRange.min :
start.x,
window.yRange.max);
pt = toDisplay(pt);
distToYBorder = Math.hypot(event.getX()-pt.x, event.getY()-pt.y);
} else {
nearYBorder = Reshaper.NONE;
distToYBorder = 0;
}
}
if (nearXBorder != Reshaper.NONE && nearYBorder != Reshaper.NONE) {
if (distToXBorder < distToYBorder) {
nearYBorder = Reshaper.NONE;
} else {
nearXBorder = Reshaper.NONE;
}
}
if (nearXBorder != Reshaper.NONE)
handler = new Reshaper(nearXBorder);
else if (nearYBorder != Reshaper.NONE)
handler = new Reshaper(nearYBorder);
else
handler = _translater;
}
break;
default:
throw new AssertionError();
}
return handler;
}
/**
* Called to validate the new position. You can override this
* method to perform additional checks. The default implementation clip the
* point to the coordinate system data window.
* @param pt The new position to validate (in data coordinate system).
*/
protected void validate(IlvDoublePoint pt)
{
IlvDataInterval range = getChart().getXAxis().getDataRange();
pt.x = Math.max(range.min, Math.min(range.max, pt.x));
range = getChart().getYAxis(_indicator.getAxisIndex()).getDataRange();
pt.y = Math.max(range.min, Math.min(range.max, pt.y));
}
/**
* Handles the mouse events.
*/
Override
public void processMouseEvent(MouseEvent event)
{
switch (event.getID()) {
case MouseEvent.MOUSE_PRESSED:
if (((event.getModifiersEx() & getEventMaskEx()) == getEventMaskEx())
&& ((event.getModifiersEx() & ~getEventMaskEx()) == 0)) {
if (findIndicator(event.getX(), event.getY())) {
_dragPoint = getData(event);
_handler = getInteractionHandler(event);
startOperation(event);
_currentValue = _handler.computeValue(0, 0, _opaqueEdit);
if (!_opaqueEdit)
drawGhost();
if (isConsumeEvents())
event.consume();
}
}
break;
case MouseEvent.MOUSE_RELEASED:
if ((event.getModifiersEx() & getEventMaskEx()) != getEventMaskEx()) {
if (isInOperation()) {
IlvDoublePoint pt = getData(event);
validate(pt);
if (!_opaqueEdit)
drawGhost();
double dx = pt.x - _dragPoint.x;
double dy = pt.y - _dragPoint.y;
_handler.computeValue(dx, dy, true);
endOperation(event);
if (isConsumeEvents())
event.consume();
}
}
break;
}
}
/**
* Handles the mouse motion events.
*/
Override
public void processMouseMotionEvent(MouseEvent event)
{
switch (event.getID()) {
case MouseEvent.MOUSE_DRAGGED:
if (isInOperation()) {
IlvDoublePoint pt = getData(event);
validate(pt);
double dx = pt.x - _dragPoint.x;
double dy = pt.y - _dragPoint.y;
if (_opaqueEdit) {
_currentValue = _handler.computeValue(dx, dy, true);
} else {
drawGhost();
_currentValue = _handler.computeValue(dx, dy, false);
drawGhost();
}
_dragPoint = pt;
if (isConsumeEvents())
event.consume();
}
break;
}
}
/**
* Handles the key event.
*/
Override
public void processKeyEvent(KeyEvent event)
{
if (event.getID() == KeyEvent.KEY_PRESSED
&& event.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (!_opaqueEdit && _dragPoint != null)
drawGhost();
abort();
if (isConsumeEvents())
event.consume();
}
}
// =================== Overrides from IlvChartInteractor ===================
/**
* Indicates whether the interactor can be used with a 3D chart.
* @return <code>false</code>.
*/
Override
public boolean has3DSupport()
{
return false;
}
/**
* Called when the interaction starts.
* You may override this method to add your own initialization
* code.
* @param event The event that starts the interaction.
*/
Override
protected void startOperation(MouseEvent event)
{
super.startOperation(event);
setAllowDrawGhost(!_opaqueEdit);
if (_handler != null)
setCursor(_handler.getCursor());
}
/**
* Called when the interaction ends.
* You may override this method to add your own initialization
* code.
* @param event The event that ends the interaction.
*/
Override
protected void endOperation(MouseEvent event)
{
super.endOperation(event);
setAllowDrawGhost(false);
setCursor(null);
_indicator = null;
_dragPoint = null;
_currentValue = null;
}
/**
* Called when the interaction has been aborted.
* You may override this method to add your own initialization
* code.
*/
Override
protected void abort()
{
super.abort();
setAllowDrawGhost(false);
setCursor(null);
_indicator = null;
_dragPoint = null;
_currentValue = null;
}
// ============================= Constructors =============================
private void initTransientFields()
{
_dragPoint = null;
_indicator = null;
_currentValue = null;
}
/**
* Creates a new <code>IlvChartDataIndicatorInteractor</code> object
* associated to the BUTTON1_DOWN_MASK mouse button with the
* <code>opaqueEdit</code> mode disabled.
*/
public IlvChartDataIndicatorInteractor()
{
this(0, MouseEvent.BUTTON1_DOWN_MASK, false);
}
/**
* Creates a new <code>IlvChartEditPointInteractor</code> object associated
* with the given mouse button linked to the given Y-axis.
*/
public IlvChartDataIndicatorInteractor(int yAxisIdx,
int buttonOrKeyEventMask,
boolean opaqueEdit)
{
super(yAxisIdx, buttonOrKeyEventMask);
enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK |
AWTEvent.MOUSE_EVENT_MASK |
AWTEvent.KEY_EVENT_MASK);
_opaqueEdit = opaqueEdit;
initTransientFields();
}
// ============================= Serialization =============================
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
initTransientFields();
}
}