//
// VisADSlider.java
//
/*
VisAD system for interactive analysis and visualization of numerical
data. Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom
Rink, Dave Glowacki, Steve Emmerson, Tom Whittaker, Don Murray, and
Tommy Jasmin.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
MA 02111-1307, USA
*/
package visad.util;
// JFC packages
import javax.swing.*;
import javax.swing.event.*;
// AWT class
import java.awt.Dimension;
// RMI class
import java.rmi.RemoteException;
// VisAD package
import visad.*;
/** VisADSlider combines a JSlider and a JLabel and links them to either a
* Real (via a DataReference) or a ScalarMap that maps to
* Display.SelectValue. Changes in the slider will reflect the Real or
* ScalarMap linked to it. If no bounds are specified, they will be
* detected from the ScalarMap and the slider will auto-scale. Note that
* a slider linked to a Real cannot auto-scale, because it has no way to
* detect the bounds.<br>
* <br>
* {@link javax.swing.BoxLayout BoxLayout} doesn't handle a mixture
* of the standard center-aligned widgets and VisADSliders, which
* are left-aligned by default. If you have problems with widgets
* being too wide, you may want to change the other widgets in
* the {@link javax.swing.JPanel JPanel} to align on the left
* (e.g. <tt>widget.setAlignmentX(BoxLayout.LEFT_ALIGNMENT)</tt>)
*/
public class VisADSlider extends JPanel implements ChangeListener,
ControlListener, ScalarMapListener
{
/** The default number of ticks the slider should have */
private static final int D_TICKS = 1000;
/** Default width of the slider in pixels */
private static final int SLIDER_WIDTH = 150;
/** Default width of the label in pixels */
private static final int LABEL_WIDTH = 200;
/** The JSlider that forms part of the VisADSlider's UI */
private JSlider slider;
/** The JLabel that forms part of the VisADSlider's UI */
private JLabel label;
/** The ScalarMap that is linked to this VisADSlider (null if none) */
private ScalarMap map;
/** The ValueControl that this VisADSlider utilizes (null if none) */
private ValueControl control;
/** The DataReference that is linked to this VisADSlider (null if none) */
private DataReference sRef;
/** The type of the linked Real (null if none) */
private RealType realType;
/** The name of the variable being modified by this VisADSlider */
private String sName;
/** The minimum allowed slider value */
private double sMinimum;
/** The maximum allowed slider value */
private double sMaximum;
/** The current slider value */
private double sCurrent;
/** The number of ticks in the slider */
private int sTicks;
/** <CODE>true</CODE> if the widget will auto-scale */
private boolean autoScale;
/** <CODE>true</CODE> if the slider ticks should be integers */
private boolean integralValues;
/** <CODE>true</CODE> if the label width should be dynamically scaled */
private boolean dynamicLabelWidth;
/** JSlider values range between <tt>low</tt> and <tt>hi</tt>
* (with initial value <tt>st</tt>) and are multiplied by
* <tt>scale</tt> to create Real values of RealType <tt>rt</tt>
* referenced by <tt>ref</tt>.
*/
public VisADSlider(String n, int lo, int hi, int st, double scale,
DataReference ref, RealType rt) throws VisADException,
RemoteException {
this(ref, null, (float) (lo * scale), (float) (hi * scale),
(float) (st * scale), hi - lo,
(ref == null || ref.getData() instanceof Real) ? null : rt,
n, false, false);
}
/** JSlider values range between <tt>low</tt> and <tt>hi</tt>
* (with initial value <tt>st</tt>) and are multiplied by
* <tt>scale</tt> to create Real values of RealType <tt>rt</tt>
* referenced by <tt>ref</tt>. The slider label has a
* dynamically sized width if <tt>dynamicLabelWidth</tt> is <tt>true</tt>.
*/
public VisADSlider(String n, int lo, int hi, int st, double scale,
DataReference ref, RealType rt,
boolean dynamicLabelWidth)
throws VisADException, RemoteException
{
this(ref, null, (float) (lo * scale), (float) (hi * scale),
(float) (st * scale), hi - lo,
(ref == null || ref.getData() instanceof Real) ? null : rt,
n, false, dynamicLabelWidth);
}
/** construct a VisADSlider from a ScalarMap that maps to
* Display.SelectValue, with auto-scaling minimum and maximum bounds,
* non-integral values, and a statically sized label.
*/
public VisADSlider(ScalarMap smap) throws VisADException, RemoteException {
// CASE ONE
this(null, smap, Float.NaN, Float.NaN, Float.NaN, D_TICKS, null, null,
false, false);
}
/** construct a VisADSlider from a ScalarMap that maps to
* Display.SelectValue, with auto-scaling minimum and maximum bounds,
* either integer or floating-point values, depending on the setting
* of <tt>integralTicks</tt>, and a statically sized label.
*/
public VisADSlider(ScalarMap smap, boolean integralTicks)
throws VisADException, RemoteException {
// CASE ONE
this(null, smap, Float.NaN, Float.NaN, Float.NaN, D_TICKS, null, null,
integralTicks, false);
}
/** construct a VisADSlider from a ScalarMap that maps to
* Display.SelectValue, with auto-scaling minimum and maximum bounds,
* either integer or floating-point values (depending on the setting
* of <tt>integralTicks</tt>, and either a static or dynamically
* sized label (depending on the setting of <tt>dynamicLabelWidth</tt>.
*/
public VisADSlider(ScalarMap smap, boolean integralTicks,
boolean dynamicLabelWidth)
throws VisADException, RemoteException {
// CASE ONE
this(null, smap, Float.NaN, Float.NaN, Float.NaN, D_TICKS, null, null,
integralTicks, dynamicLabelWidth);
}
/** construct a VisADSlider from a ScalarMap that maps to
* Display.SelectValue, with minimum and maximum bounds min and max,
* no auto-scaling, non-integer values, and a static label width.
*/
public VisADSlider(ScalarMap smap, float min, float max)
throws VisADException, RemoteException {
// CASE TWO
this(null, smap, min, max, Float.NaN, D_TICKS, null, null, false, false);
}
/** construct a VisADSlider by creating a Real and linking it to r,
using RealType rt and name n, with minimum and maximum bounds
min and max, and starting value start */
public VisADSlider(DataReference ref, float min, float max, float start,
RealType rt, String n) throws VisADException,
RemoteException {
// CASE THREE
this(ref, null, min, max, start, D_TICKS, rt, n, false, false);
}
/** construct a VisADSlider from an existing Real pointed to by r,
with minimum and maximum bounds min and max */
public VisADSlider(DataReference ref, float min, float max)
throws VisADException, RemoteException {
// CASE FOUR
this(ref, null, min, max, Float.NaN, D_TICKS, null, null, false, false);
}
/** complete constructor */
private VisADSlider(DataReference ref, ScalarMap smap, float min, float max,
float start, int sliderTicks, RealType rt, String n,
boolean integralValues, boolean dynamicLabelWidth)
throws VisADException, RemoteException {
this.integralValues = integralValues;
this.dynamicLabelWidth = dynamicLabelWidth;
// set up some UI components
setAlignmentX(LEFT_ALIGNMENT); // VisADSliders default to LEFT_ALIGNMENT
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
sTicks = sliderTicks;
Dimension d;
slider = new JSlider(0, sTicks, sTicks / 2);
d = slider.getMinimumSize();
slider.setMinimumSize(new Dimension(SLIDER_WIDTH, d.height));
d = slider.getPreferredSize();
slider.setPreferredSize(new Dimension(SLIDER_WIDTH, d.height));
d = slider.getMaximumSize();
slider.setMaximumSize(new Dimension(SLIDER_WIDTH, d.height));
// by default, don't auto-scale
autoScale = false;
// set up internal components
if (ref == null) {
// this VisADSlider should link to a ScalarMap
if (smap == null) {
throw new VisADException("VisADSlider: must specify either a " +
"DataReference or a ScalarMap!");
}
if (smap.getDisplayScalar() != Display.SelectValue) {
throw new VisADException("VisADSlider: ScalarMap must be to " +
"Display.SelectValue!");
}
if (!(smap.getScalar() instanceof RealType)) {
throw new VisADException("VisADSlider: ScalarMap must be from " +
"a RealType!");
}
map = smap;
control = (ValueControl) smap.getControl();
if (control == null) {
throw new VisADException("VisADSlider: ScalarMap must be addMap'ed " +
"to a Display");
}
sRef = null;
sName = smap.getScalarName();
start = (float) control.getValue();
if (min == min && max == max && start == start) {
// do not use auto-scaling (CASE TWO)
sMinimum = min;
sMaximum = max;
if (integralValues) {
int tmp = (int )(sMaximum - sMinimum);
if (tmp != sTicks) {
sTicks = tmp;
slider.setMaximum(sTicks);
}
}
sCurrent = start;
initLabel();
smap.setRange(min, max);
if (start < min || start > max) {
start = (min + max) / 2;
control.setValue(start);
}
}
else {
// enable auto-scaling (CASE ONE)
autoScale = true;
initLabel();
}
control.addControlListener(this);
smap.addScalarMapListener(this);
}
else {
// this VisADSlider should link to a Real
map = null;
control = null;
if (ref == null) {
throw new VisADException("VisADSlider: DataReference " +
"cannot be null!");
}
sRef = ref;
Data data = ref.getData();
if (data == null) {
// the Real must be created (CASE THREE)
if (rt == null) {
throw new VisADException("VisADSlider: RealType cannot be null!");
}
if (n == null) {
throw new VisADException("VisADSlider: name cannot be null!");
}
realType = rt;
if (min != min || max != max || start != start) {
throw new VisADException("VisADSlider: min, max, and start " +
"cannot be NaN!");
}
sMinimum = min;
sMaximum = max;
sCurrent = (start < min || start > max) ? (min + max) / 2 : start;
sRef.setData(new Real(realType, sCurrent));
}
else {
// the Real already exists (CASE FOUR)
if (!(data instanceof Real)) {
throw new VisADException("VisADSlider: DataReference " +
"must point to a Real!");
}
Real real = (Real) data;
realType = (RealType) real.getType();
sCurrent = (float) real.getValue();
if (min != min || max != max) {
throw new VisADException("VisADSlider: minimum and maximum " +
"cannot be NaN!");
}
sMinimum = min;
sMaximum = max;
if (sCurrent < min || sCurrent > max) sCurrent = (min + max) / 2;
}
sName = (n != null) ? n : realType.getName();
initLabel();
// watch for changes in Real, and update slider when necessary
CellImpl cell = new CellImpl() {
public void doAction() throws VisADException, RemoteException {
// update slider when value of linked Real changes
if (sRef != null) {
double val;
try {
val = ((Real) sRef.getData()).getValue();
if (!Util.isApproximatelyEqual(sCurrent, val)) updateSlider(val);
} catch (RemoteException re) {
if (visad.collab.CollabUtil.isDisconnectException(re)) {
// Remote data server went away
sRef = null;
}
throw re;
}
}
}
};
if (ref instanceof RemoteDataReference) {
RemoteCell remoteCell = new RemoteCellImpl(cell);
remoteCell.addReference(ref);
}
else cell.addReference(ref);
}
// add UI components
add(slider);
add(label);
// add listeners
slider.addChangeListener(this);
// do initial update of the slider
updateSlider(start);
}
/** sets up the JLabel */
private void initLabel() {
String str = sName + " = " + PlotText.shortString(sCurrent);
Dimension d;
if (dynamicLabelWidth) {
label = new JLabel(str);
} else {
// add a bit of whitespace to the string to avoid value truncation
label = new JLabel(str + " ");
d = label.getMinimumSize();
label.setMinimumSize(new Dimension(LABEL_WIDTH, d.height));
d = label.getPreferredSize();
label.setPreferredSize(new Dimension(LABEL_WIDTH, d.height));
d = label.getMaximumSize();
label.setMaximumSize(new Dimension(LABEL_WIDTH, d.height));
}
label.setAlignmentX(JLabel.CENTER_ALIGNMENT);
}
/**
* Hardcode the preferred size of the slider after increasing
* the current width by the specified percentage (or decreasing
* it if <tt>percent</tt> is negative.)<br>
* <br>
* This method is primarily useful to keep changes in the
* label (of a VisADSlider with <tt>dynamicLabelWidth</tt>
* set to <tt>true</t>)
* from causing the rest of the window to be redrawn.
*
* @param percent percent of current size to use for hardcoded
* size. (e.g. to keep the current size, specify
* <tt>100</tt>; to increase the size a bit,
* specify <tt>115</tt>, to decrease it a bit,
* specify <tt>85</tt>, etc.)
*/
public void hardcodeSizePercent(int percent)
{
Dimension d = getPreferredSize();
int newWidth = d.width + (d.width * (percent - 100)) / 100;
setPreferredSize(new Dimension(newWidth, d.height));
}
/** called when slider is adjusted */
public synchronized void stateChanged(ChangeEvent e) {
try {
double val = slider.getValue();
double cur = (sMaximum - sMinimum) * (val / sTicks) + sMinimum;
if (integralValues) {
cur = Math.floor(cur + 0.5);
}
if (!Util.isApproximatelyEqual(sCurrent, cur)) {
if (control != null) control.setValue(cur);
else if (sRef != null) {
try {
sRef.setData(new Real(realType, cur));
} catch (RemoteException re) {
if (visad.collab.CollabUtil.isDisconnectException(re)) {
// Remote data server went away
sRef = null;
}
throw re;
}
}
}
}
catch (VisADException exc) {
exc.printStackTrace();
}
catch (RemoteException exc) {
exc.printStackTrace();
}
}
/** used for auto-scaling the minimum and maximum */
public void mapChanged(ScalarMapEvent e) {
if (!autoScale) return;
double[] range = map.getRange();
sMinimum = (float) range[0];
sMaximum = (float) range[1];
if (integralValues) {
int tmp = (int )(sMaximum - sMinimum);
if (tmp != sTicks) {
sTicks = tmp;
slider.setMaximum(sTicks);
}
}
sCurrent = (float) control.getValue();
if (sCurrent < sMinimum || sCurrent > sMaximum) {
sCurrent = (sMinimum + sMaximum) / 2;
}
updateSlider(sCurrent);
}
/**
* ScalarMapListener method used to detect new control.
*/
public void controlChanged(ScalarMapControlEvent evt) {
int id = evt.getId();
if (id == ScalarMapEvent.CONTROL_REMOVED ||
id == ScalarMapEvent.CONTROL_REPLACED)
{
control = null;
}
if (id == ScalarMapEvent.CONTROL_REPLACED ||
id == ScalarMapEvent.CONTROL_ADDED)
{
control = (ValueControl) evt.getScalarMap().getControl();
}
}
/** Update slider when value of linked ValueControl changes */
public void controlChanged(ControlEvent e)
throws VisADException, RemoteException
{
double cur = control.getValue();
if (!Util.isApproximatelyEqual(sCurrent, cur)) {
updateSlider(control.getValue());
}
}
/** Update the slider's value */
private synchronized void updateSlider(double value) {
if (integralValues) {
value = Math.floor(value + 0.5);
}
int ival = (int) (sTicks * ((value - sMinimum) / (sMaximum - sMinimum)));
if (Math.abs(slider.getValue() - ival) > 1) {
slider.removeChangeListener(this);
slider.setValue(ival);
slider.addChangeListener(this);
}
sCurrent = value;
label.setText(sName + " = " + PlotText.shortString(sCurrent));
invalidate();
}
}