sim.display.Display2D Maven / Gradle / Ivy
Show all versions of mason Show documentation
/*
Copyright 2006 by Sean Luke and George Mason University
Licensed under the Academic Free License version 3.0
See the file "LICENSE" for more information
*/
package sim.display;
import sim.portrayal.*;
import sim.engine.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
import java.awt.event.*;
import java.util.*;
import sim.util.Bag;
import java.io.*;
import sim.util.gui.*;
import sim.util.media.*;
import sim.util.*;
import java.util.prefs.*;
/**
Display2D holds, displays, and manipulates 2D Portrayal objects, allowing the user to scale them,
scroll them, change how often they're updated, take snapshots, and generate Quicktime movies.
Display2D is Steppable, and each time it is stepped it redraws itself. Display2D also handles
double-click events, routing them to the underlying portrayals as inspector requests.
In addition to various GUI widgets, Display2D holds a JScrollView which in turn holds a
Display2D.InnerDisplay2D (a JComponent responsible for doing the actual drawing). Display2D can be placed
in a JFrame; indeed it provides a convenience function to sprout its own JFrame with the method
createFrame(). You can put Display2D in your own JFrame if you like, but you should try to call
Display2D.quit() when the frame is disposed.
Display2D's constructor takes a height and a width; this will be the "expected" height and
width of the underlying portrayal region when the Display2D is scaled to 1.0 (the default).
The portrayals will also have an origin at (0,0) -- the top left corner. Display2D will automatically
clip the portrayals to the area (0,0) to (width * scale, height * scale).
Display2D's step() method is typically called from the underlying schedule thread; this means
that it has to be careful about painting as Swing widgets expect to be painted in the event loop thread.
Display2D handles this in two ways. First, on MacOS X, the step() method calls repaint(), which will
in turn call paintComponent() from the event loop thread at a time when the underlying schedule thread
is doing nothing -- see Console. Second, on Windows and XWindows, the step() method immediately calls
paintComponent(). Different OSes do it differently because MacOS X is far more efficient using standard
repaint() calls, which get routed through Quartz. The step() method also updates various widgets using
SwingUtilities.invokeLater().
*/
public class Display2D extends JComponent implements Steppable, Manipulating2D
{
boolean forcePrecise = false; // PDF sets this
boolean precise = false;
/** Returns true if this display has been set to always draw precisely. Note that even if this
function returns false, the display may draw precisely in certain circumstances, such as
when outputting to a PDF. */
public boolean getPrecise() { return precise; }
/** Sets this display to always draw precisely (or not). Note that even if this display has
been set to not display precisely, it may still draw precisely in certain circumstances, such as
when outputting to a PDF. */
public void setPrecise(boolean precise) { this.precise = precise; optionPane.preciseDrawing.setSelected(precise); }
public String DEFAULT_PREFERENCES_KEY = "Display2D";
String preferencesKey = DEFAULT_PREFERENCES_KEY; // default
/** If you have more than one Display2D in your simulation and you want them to have
different preferences, set each to a different key value. The default value is DEFAULT_PREFERENCES_KEY.
You may not have a key which ends in a forward slash (/) when trimmed
Key may be set to null (the default). */
public void setPreferencesKey(String s)
{
if (s.trim().endsWith("/"))
throw new RuntimeException("Key ends with '/', which is not allowed");
else preferencesKey = s;
}
public String getPreferencesKey() { return preferencesKey; }
/** Option pane */
public class OptionPane extends JFrame
{
// buffer stuff
int buffering;
JRadioButton useNoBuffer = new JRadioButton("By Drawing Separate Rectangles");
JRadioButton useBuffer = new JRadioButton("Using a Stretched Image");
JRadioButton useDefault = new JRadioButton("Let the Program Decide How");
ButtonGroup usageGroup = new ButtonGroup();
JCheckBox antialias = new JCheckBox("Antialias Graphics");
JCheckBox alphaInterpolation = new JCheckBox("Better Transparency");
JCheckBox interpolation = new JCheckBox("Bilinear Interpolation of Images");
JCheckBox tooltips = new JCheckBox("Tool Tips");
JCheckBox preciseDrawing = new JCheckBox("Precise Drawing");
JButton systemPreferences = new JButton("MASON");
JButton appPreferences = new JButton("Simulation");
NumberTextField xOffsetField = new NumberTextField(0,1,50)
{
public double newValue(final double val)
{
double scale = getScale();
insideDisplay.xOffset = val / scale;
Display2D.this.repaint(); // redraw the inside display
return insideDisplay.xOffset * scale;
}
};
NumberTextField yOffsetField = new NumberTextField(0,1,50)
{
public double newValue(final double val)
{
double scale = getScale();
insideDisplay.yOffset = val / scale;
Display2D.this.repaint(); // redraw the inside display
return insideDisplay.yOffset * scale;
}
};
ActionListener listener = null;
OptionPane(String title)
{
super(title);
useDefault.setSelected(true);
useNoBuffer.setToolTipText("When not using transparency on Windows/XWindows,
this method is often (but not always) faster");
usageGroup.add(useNoBuffer);
usageGroup.add(useBuffer);
useBuffer.setToolTipText("When using transparency, or when on a Mac,
this method is usually faster, but may require more
memory (especially on Windows/XWindows) --
increasing heap size can help performance.");
usageGroup.add(useDefault);
JPanel p2 = new JPanel();
Box b = new Box(BoxLayout.Y_AXIS);
b.add(useNoBuffer);
b.add(useBuffer);
b.add(useDefault);
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.setBorder(new javax.swing.border.TitledBorder("Draw Grids of Rectangles..."));
p.add(b,BorderLayout.CENTER);
p2.setLayout(new BorderLayout());
p2.add(p,BorderLayout.NORTH);
LabelledList l = new LabelledList("Origin Offset in Pixels");
l.addLabelled("X Offset", xOffsetField);
l.addLabelled("Y Offset", yOffsetField);
p2.add(l,BorderLayout.CENTER);
getContentPane().add(p2,BorderLayout.NORTH);
String text = "Sets the offset of the origin of the display. This is independent of the scrollbars." +
"
If the simulation has enabled it, you can also change the offset by dragging with the" +
"
right mouse button down (or on the Mac, a two finger tap-drag or Command-drag)." +
"
Additionally, you can reset the origin to (0,0) with a right-mouse button double-click.";
l.setToolTipText(text);
xOffsetField.setToolTipText(text);
yOffsetField.setToolTipText(text);
b = new Box(BoxLayout.Y_AXIS);
b.add(antialias);
b.add(interpolation);
b.add(alphaInterpolation);
b.add(tooltips);
b.add(preciseDrawing);
p = new JPanel();
p.setLayout(new BorderLayout());
p.setBorder(new javax.swing.border.TitledBorder("Graphics Features"));
p.add(b,BorderLayout.CENTER);
getContentPane().add(p,BorderLayout.CENTER);
listener = new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
useTooltips = tooltips.isSelected();
precise = preciseDrawing.isSelected();
if (useDefault.isSelected())
buffering = FieldPortrayal2D.DEFAULT;
else if (useBuffer.isSelected())
buffering = FieldPortrayal2D.USE_BUFFER;
else buffering = FieldPortrayal2D.DONT_USE_BUFFER;
insideDisplay.setupHints(antialias.isSelected(), alphaInterpolation.isSelected(), interpolation.isSelected());
Display2D.this.repaint(); // redraw the inside display
}
};
useNoBuffer.addActionListener(listener);
useBuffer.addActionListener(listener);
useDefault.addActionListener(listener);
antialias.addActionListener(listener);
alphaInterpolation.addActionListener(listener);
interpolation.addActionListener(listener);
tooltips.addActionListener(listener);
preciseDrawing.addActionListener(listener);
// add preferences
b = new Box(BoxLayout.X_AXIS);
b.add(new JLabel(" Save as Defaults for "));
b.add(appPreferences);
b.add(systemPreferences);
getContentPane().add(b, BorderLayout.SOUTH);
systemPreferences.putClientProperty( "JComponent.sizeVariant", "mini" );
systemPreferences.putClientProperty( "JButton.buttonType", "bevel" );
systemPreferences.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
String key = getPreferencesKey();
savePreferences(Prefs.getGlobalPreferences(key));
// if we're setting the system preferences, remove the local preferences to avoid confusion
Prefs.removeAppPreferences(simulation, key);
}
});
appPreferences.putClientProperty( "JComponent.sizeVariant", "mini" );
appPreferences.putClientProperty( "JButton.buttonType", "bevel" );
appPreferences.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
String key = getPreferencesKey();
savePreferences(Prefs.getAppPreferences(simulation, key));
}
});
setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);
setResizable(false);
pack();
}
/** Saves the Option Pane Preferences to a given Preferences Node */
void savePreferences(Preferences prefs)
{
try
{
prefs.putInt(DRAW_GRIDS_KEY,
useNoBuffer.isSelected() ? 0 :
useBuffer.isSelected() ? 1 : 2);
prefs.putDouble(X_OFFSET_KEY, xOffsetField.getValue());
prefs.putDouble(Y_OFFSET_KEY, yOffsetField.getValue());
prefs.putBoolean(ANTIALIAS_KEY, antialias.isSelected());
prefs.putBoolean(BETTER_TRANSPARENCY_KEY, alphaInterpolation.isSelected());
prefs.putBoolean(INTERPOLATION_KEY, interpolation.isSelected());
prefs.putBoolean(TOOLTIPS_KEY, tooltips.isSelected());
prefs.putBoolean(PRECISE_KEY, preciseDrawing.isSelected());
if (!Prefs.save(prefs))
Utilities.inform ("Preferences Cannot be Saved", "Your Java system can't save preferences. Perhaps this is an applet?", this);
}
catch (java.security.AccessControlException e) { } // it must be an applet
}
static final String DRAW_GRIDS_KEY = "Draw Grids";
static final String X_OFFSET_KEY = "X Offset";
static final String Y_OFFSET_KEY = "Y Offset";
static final String ANTIALIAS_KEY = "Antialias";
static final String BETTER_TRANSPARENCY_KEY = "Better Transparency";
static final String INTERPOLATION_KEY = "Bilinear Interpolation";
static final String TOOLTIPS_KEY = "Tool Tips";
static final String PRECISE_KEY = "Precise Drawing";
/** Resets the Option Pane Preferences by loading from the preference database */
void resetToPreferences()
{
try
{
Preferences systemPrefs = Prefs.getGlobalPreferences(getPreferencesKey());
Preferences appPrefs = Prefs.getAppPreferences(simulation, getPreferencesKey());
int val = appPrefs.getInt(DRAW_GRIDS_KEY,
systemPrefs.getInt(DRAW_GRIDS_KEY,
useNoBuffer.isSelected() ? 0 :
useBuffer.isSelected() ? 1 : 2));
if (val == 0) useNoBuffer.setSelected(true);
else if (val == 1) useBuffer.setSelected(true);
else // (val == 0)
useDefault.setSelected(true);
xOffsetField.setValue(xOffsetField.newValue(appPrefs.getDouble(X_OFFSET_KEY,
systemPrefs.getDouble(X_OFFSET_KEY, 0))));
yOffsetField.setValue(yOffsetField.newValue(appPrefs.getDouble(Y_OFFSET_KEY,
systemPrefs.getDouble(Y_OFFSET_KEY, 0))));
antialias.setSelected(appPrefs.getBoolean(ANTIALIAS_KEY,
systemPrefs.getBoolean(ANTIALIAS_KEY, false)));
alphaInterpolation.setSelected(appPrefs.getBoolean(BETTER_TRANSPARENCY_KEY,
systemPrefs.getBoolean(BETTER_TRANSPARENCY_KEY, false)));
interpolation.setSelected(appPrefs.getBoolean(INTERPOLATION_KEY,
systemPrefs.getBoolean(INTERPOLATION_KEY, false)));
tooltips.setSelected(appPrefs.getBoolean(TOOLTIPS_KEY,
systemPrefs.getBoolean(TOOLTIPS_KEY, false)));
preciseDrawing.setSelected(appPrefs.getBoolean(PRECISE_KEY,
systemPrefs.getBoolean(PRECISE_KEY, false)));
// trigger resets by calling the listener. Don't bother with an event
listener.actionPerformed(null);
}
catch (java.security.AccessControlException e) { } // it must be an applet
}
}
/** Removes all mouse listeners, mouse motion listeners, and Key listeners from this component. Mostly used for kiosk mode stuff -- see the Howto */
public void removeListeners()
{
// moved to the Display2D ot be at the same level as handleEvent
insideDisplay.removeListeners();
}
/** The object which actually does all the drawing. Perhaps we should move this out. */
public class InnerDisplay2D extends JComponent
{
/** Image buffer for doing buffered draws, mostly for screenshots etc. */
BufferedImage buffer = null;
/** The width of the display when the scale is 1.0 */
public double width;
/** The height of the display when the scale is 1.0 */
public double height;
/** x offset */
public double xOffset;
/** y offset */
public double yOffset;
/** @deprecated Use Display2D.removeListeners instead. */
public void removeListeners()
{
MouseListener[] mls = (MouseListener[])(getListeners(MouseListener.class));
for(int x = 0 ; x < mls.length; x++)
{ removeMouseListener(mls[x]); }
MouseMotionListener[] mmls = (MouseMotionListener[])(getListeners(MouseMotionListener.class));
for(int x = 0 ; x < mmls.length; x++)
{ removeMouseMotionListener(mmls[x]); }
KeyListener[] kls = (KeyListener[])(getListeners(KeyListener.class));
for(int x = 0 ; x < kls.length; x++)
{ removeKeyListener(kls[x]); }
}
/** Creates an InnerDisplay2D with the provided width and height. */
InnerDisplay2D(double width, double height)
{
this.width = width;
this.height = height;
setupHints(false,false,false); // go for speed
}
/** Overloaded to return (width * scale, height * scale) */
public Dimension getPreferredSize()
{ return new Dimension((int)(width*getScale()),(int)(height*getScale())); }
/** Overloaded to return (width * scale, height * scale) */
public Dimension getMinimumSize()
{ return getPreferredSize(); }
/** Paints a movie, by drawing to a buffer, then
encoding the buffer to disk, then optionally
writing the buffer to the provided Graphics2D.
If the Graphics2D is null, it's just written out to disk.
This method will only write to disk when "appropriate", that is,
if the current schedule has advanced to the point that a new
frame is supposed to be outputted (given the frame rate of the
movie). In any rate, it'll write to the Graphic2D if
provided. */
public void paintToMovie(Graphics g)
{
// although presently paintToMovie is called solely from paintComponent,
// which already has synchronized on the schedule, we do this anyway for
// good measure. See stopMovie() and startMovie() for why synchronization
// is important.
synchronized(Display2D.this.simulation.state.schedule)
{
// only paint if it's appropriate
long steps = Display2D.this.simulation.state.schedule.getSteps();
if (steps > lastEncodedSteps &&
shouldUpdate() &&
Display2D.this.simulation.state.schedule.getTime() < Schedule.AFTER_SIMULATION)
{
Display2D.this.movieMaker.add(paint(g,true,false));
lastEncodedSteps = steps;
}
else paint(g,false,false);
}
}
/** Hints used to draw objects to the screen or to a buffer */
public RenderingHints unbufferedHints;
/** Hints used to draw the buffered image to the screen */
public RenderingHints bufferedHints;
/** The default method for setting up the given hints.
By default they suggest that Java2D emphasize efficiency over prettiness.*/
public void setupHints(boolean antialias, boolean niceAlphaInterpolation, boolean niceInterpolation)
{
unbufferedHints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); // in general
unbufferedHints.put(RenderingHints.KEY_INTERPOLATION,
niceInterpolation ? RenderingHints.VALUE_INTERPOLATION_BILINEAR :
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
// dunno what to do here about antialiasing on MacOS X
// -- if it's on, then circles can get drawn as squares (see woims demo at 1.5 scale)
// -- but if it's off, then stuff gets antialiased in pictures but not on a screenshot.
// My inclination is to leave it off.
unbufferedHints.put(RenderingHints.KEY_ANTIALIASING,
antialias ? RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
unbufferedHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
antialias ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
unbufferedHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
niceAlphaInterpolation ?
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY :
RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
bufferedHints = new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); // in general
bufferedHints.put(RenderingHints.KEY_INTERPOLATION,
niceInterpolation ? RenderingHints.VALUE_INTERPOLATION_BILINEAR :
RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
// similarly
bufferedHints.put(RenderingHints.KEY_ANTIALIASING,
antialias ? RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
bufferedHints.put(RenderingHints.KEY_TEXT_ANTIALIASING,
antialias ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON :
RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
bufferedHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
niceAlphaInterpolation ?
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY :
RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);
}
java.lang.ref.WeakReference toolTip = new java.lang.ref.WeakReference(null);
public JToolTip createToolTip()
{
JToolTip tip = super.createToolTip();
toolTip = new java.lang.ref.WeakReference(tip);
return tip;
}
protected MouseEvent lastToolTipEvent = null;
// could be called from the model thread OR the swing thread -- must be careful
public String getToolTipText(MouseEvent event)
{
if (useTooltips)
{
lastToolTipEvent = event;
final Point point = event.getPoint();
return createToolTipText( new Rectangle2D.Double(point.x,point.y,1,1), Display2D.this.simulation);
}
else return null;
}
String lastToolTipText = null;
// this will be called from the model thread most likely, so we need
// to make sure that we actually do the updating in an invokeLater...
public void updateToolTips()
{
if (useTooltips && lastToolTipEvent != null)
{
final String s = getToolTipText(lastToolTipEvent);
if (s==null || !s.equals(lastToolTipText))
{
// ah, we need to update.
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
setToolTipText(lastToolTipText = s);
JToolTip tip = (JToolTip)(toolTip.get());
if (tip!=null && tip.getComponent()==InnerDisplay2D.this) tip.setTipText(s);
}
});
}
}
}
static final int MAX_TOOLTIP_LINES = 10;
public String createToolTipText( Rectangle2D.Double rect, final GUIState simulation )
{
String s = "";
Bag[] hitObjects = objectsHitBy(rect);
int count = 0;
for(int x=0;x 0) s += "
";
if (count >= MAX_TOOLTIP_LINES) { return s + "...etc. "; }
count++;
String status = p.portrayal.getStatus((LocationWrapper) (hitObjects[x].objs[i]));
if (status != null) s += status; // might return null, sort of meaning "leave me alone"
}
}
if (count==0) return null;
s += "