/*
* VectorDisplay.java
* (FScape)
*
* Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 09-Jan-05 copied from Meloncillo and simplified (non-editable)
*/
package de.sciss.fscape.gui;
import javax.swing.*;
import java.awt.*;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Vector;
/**
* A <code>VectorDisplay</code> is a two dimensional
* panel which plots a sampled function (32bit float) and allows
* the user to edit this one dimensional data vector
* (or table, simply speaking). It implements
* the <code>EventManager.Processor</code> interface
* to handle custom <code>VectorDisplayEvent</code>s
* and implements the <code>VirtualSurface</code>
* interface to allow transitions between screen
* space and normalized virtual space.
* <p>
* Often it is convenient to attach a popup menu
* created by the static method in <code>VectorTransformer</code>.
* <p>
* Examples of using <code>VectorDisplay</code>s aer
* the <code>SigmaReceiverEditor</code> and the
* <code>SimpleTransmitterEditor</code>.
*/
public class VectorDisplay
extends JComponent {
private float[] vector;
private final GeneralPath path = new GeneralPath();
private Shape pathTrns;
private TextLayout txtLay = null;
private Rectangle2D txtBounds;
private Dimension recentSize;
private Image image = null;
private static final Stroke strkLine = new BasicStroke(0.5f);
private static final Paint pntArea = new Color(0x42, 0x5E, 0x9D, 0x7F);
// private static final Paint pntLine = Color.black;
// private static final Paint pntLabel = new Color( 0x00, 0x00, 0x00, 0x3F );
private final Paint pntBg;
private final Paint pntLine;
private final Paint pntLabel;
private final AffineTransform trnsScreenToVirtual = new AffineTransform();
private final AffineTransform trnsVirtualToScreen = new AffineTransform();
private float min = 0.0f; // minimum vector value
private float max = 1.0f; // maximum vector value
private boolean fillArea = true; // fill area under the vector polyline
private String label = null; // optional text label
// --- top painter ---
private final Vector<TopPainter> collTopPainters = new Vector<TopPainter>();
/**
* Creates a new VectorDisplay with an empty vector.
* The defaults are wrapX = false, wrapY = false,
* min = 0, max = 1.0, fillArea = true, no label
*/
public VectorDisplay() {
this(new float[0]);
}
public VectorDisplay(float[] vector) {
this(vector, UIManager.getBoolean("dark-skin"));
}
public VectorDisplay(boolean dark) {
this(new float[0], dark);
}
/**
* Creates a new VectorDisplay with an given initial vector.
* The defaults are wrapX = false, wrapY = false,
* min = 0, max = 1.0, fillArea = true, no label
*
* @param vector the initial vector data
*/
public VectorDisplay( float[] vector, boolean dark )
{
setOpaque( false );
setMinimumSize( new Dimension( 64, 16 ));
// setPreferredSize( new Dimension( 288, 144 )); // XXX
recentSize = getMinimumSize();
setVector( null, vector );
// addComponentListener( this );
pntBg = dark ? Color.darkGray : Color.lightGray;
pntLine = dark ? Color.lightGray : Color.black;
pntLabel = dark ? new Color(0xFF, 0xFF, 0xFF, 0x3F) : new Color(0x00, 0x00, 0x00, 0x3F);
}
/**
* Replaces the existing vector by another one.
* This dispatches a <code>VectorDisplayEvent</code>
* to registered listeners.
*
* @param source the source in the <code>VectorDisplayEvent</code>.
* use <code>null</code> to prevent event dispatching.
* @param vector the new vector data
*/
public void setVector( Object source, float[] vector )
{
this.vector = vector;
recalculatePath();
repaint();
}
/**
* Gets the current data array.
*
* Warning: the returned array is not a copy and therefore
* any modifications are forbidden. this also implies
* that relevant data be copied by the listener
* immediately upon receiving the vector.
* Synchronization: should only be called in the event thread
*
* @return the current vector data of the editor. valid data
* is from index 0 to the end of the array.
*/
public float[] getVector()
{
return vector;
}
/**
* Changes the allowed range for vector values.
* Influences the graphics display such that
* the top margin of the panel corresponds to max
* and the bottom margin corresponds to min. Also
* user drawings are limited to these values unless
* wrapY is set to true (not yet implemented).
*
* Warning: the current vector is left untouched,
* even if values lie outside the new
* allowed range.
*
* @param min new minimum y value
* @param max new maximum y value
*/
public void setMinMax( float min, float max )
{
if( this.min != min || this.max != max ) {
this.min = min;
this.max = max;
repaint();
}
}
/**
* Gets the minimum allowed y value
*
* @return the minimum specified function value
*/
public float getMin()
{
return min;
}
/**
* Gets the maximum allowed y value
*
* @return the maximum specified function value
*/
public float getMax()
{
return max;
}
/**
* Set the graph display mode
*
* @param fillArea if <code>false</code>, a hairline is
* drawn to connect the sample values. if
* <code>true</code>, the area below the
* curve is additionally filled with a
* translucent colour.
*/
public void setFillArea( boolean fillArea )
{
if( this.fillArea != fillArea ) {
this.fillArea = fillArea;
repaint();
}
}
/**
* Select the allowed range for vector values.
* Influences the graphics display.
*/
public void setLabel( String label )
{
if( this.label == null || label == null || !this.label.equals( label )) {
txtLay = null;
this.label = label;
repaint();
}
}
public void paintComponent( Graphics g )
{
super.paintComponent( g );
Dimension d = getSize();
if( d.width != recentSize.width || d.height != recentSize.height ) {
recentSize = d;
recalculateTransforms();
recreateImage();
redrawImage();
} else if( pathTrns == null ) {
recalculateTransforms();
recreateImage(); // XXX since we don't clear the background any more to preserve Aqua LAF
redrawImage();
}
if( image != null ) {
g.drawImage( image, 0, 0, this );
}
// --- invoke top painters ---
if( !collTopPainters.isEmpty() ) {
Graphics2D g2 = (Graphics2D) g;
// AffineTransform trnsOrig = g2.getTransform();
int i;
// g2.transform( trnsVirtualToScreen );
g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
for( i = 0; i < collTopPainters.size(); i++ ) {
collTopPainters.get( i ).paintOnTop( g2 );
}
// g2.setTransform( trnsOrig );
}
}
/**
* Registers a new top painter.
* If the top painter wants to paint
* a specific portion of the surface,
* it must make an appropriate repaint call!
*
* Synchronization: this method must be called in the event thread
*
* @param p the painter to be added to the paint queue
*/
public void addTopPainter( TopPainter p )
{
if( !collTopPainters.contains( p )) {
collTopPainters.add( p );
}
}
/**
* Removes a registered top painter.
*
* Synchronization: this method must be called in the event thread
*
* @param p the painter to be removed from the paint queue
*/
public void removeTopPainter( TopPainter p )
{
collTopPainters.remove( p );
}
private void recreateImage()
{
if( image != null ) image.flush();
image = createImage( recentSize.width, recentSize.height );
}
private void redrawImage()
{
if( image == null ) return;
Graphics2D g2 = (Graphics2D) image.getGraphics();
g2.setPaint( pntBg );
g2.fillRect( 0, 0, image.getWidth ( this ), image.getHeight ( this ));
g2.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
g2.setRenderingHint( RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE );
// g2.setRenderingHint( RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY );
if (fillArea) {
g2.setPaint(pntArea);
g2.fill(pathTrns);
}
g2.setStroke(strkLine);
g2.setPaint(pntLine);
g2.draw( pathTrns );
if( label != null ) {
g2.setPaint( pntLabel );
if( txtLay == null ) {
txtLay = new TextLayout( label, getFont(), g2.getFontRenderContext() );
txtBounds = txtLay.getBounds();
}
txtLay.draw( g2, recentSize.width - (float) txtBounds.getWidth() - 4,
recentSize.height - (float) txtBounds.getHeight() );
}
g2.dispose();
}
/*
* Recalculates a Java2D path from the vector
* that will be used for painting operations
*/
private void recalculatePath() {
int i;
float f1;
float f2 = (min - max) / recentSize.height + min;
path.reset();
if( vector.length > 0 ) {
f1 = 1.0f / vector.length;
path.moveTo( -0.01f, f2 );
path.lineTo( -0.01f, vector[0] );
for( i = 0; i < vector.length; i++ ) {
path.lineTo( i * f1, vector[i] );
}
path.lineTo( 1.01f, vector[vector.length-1] );
path.lineTo( 1.01f, f2 );
path.closePath();
// System.out.println( "recalculated path" );
}
pathTrns = null;
}
// ---------------- VirtualSurface interface ----------------
/*
* Recalculates the transforms between
* screen and virtual space
*/
private void recalculateTransforms() {
trnsVirtualToScreen.setToTranslation(0.0, recentSize.height);
trnsVirtualToScreen.scale(recentSize.width, recentSize.height / (min - max));
trnsVirtualToScreen.translate(0.0, -min);
trnsScreenToVirtual.setToTranslation(0.0, min);
trnsScreenToVirtual.scale(1.0 / recentSize.width, (min - max) / recentSize.height);
trnsScreenToVirtual.translate(0.0, -recentSize.height);
pathTrns = path.createTransformedShape(trnsVirtualToScreen);
}
/**
* Converts a location on the screen
* into a point the virtual space.
* Neither input nor output point need to
* be limited to particular bounds
*
* @param screenPt point in screen space
* @return the input point transformed to virtual space
*/
public Point2D screenToVirtual(Point2D screenPt) {
return trnsScreenToVirtual.transform(screenPt, null);
}
/**
* Converts a shape in the screen space
* into a shape in the virtual space.
*
* @param screenShape arbitrary shape in screen space
* @return the input shape transformed to virtual space
*/
public Shape screenToVirtual(Shape screenShape) {
return trnsScreenToVirtual.createTransformedShape(screenShape);
}
/**
* Converts a point in the virtual space
* into a location on the screen.
*
* @param virtualPt point in the virtual space whose
* visible bounds are (0, 0 ... 1, 1)
* @return point in the display space
*/
public Point2D virtualToScreen(Point2D virtualPt) {
return trnsVirtualToScreen.transform(virtualPt, null);
}
/**
* Converts a shape in the virtual space
* into a shape on the screen.
*
* @param virtualShape arbitrary shape in virtual space
* @return the input shape transformed to screen space
*/
public Shape virtualToScreen(Shape virtualShape) {
return trnsVirtualToScreen.createTransformedShape(virtualShape);
}
/**
* Converts a rectangle in the virtual space
* into a rectangle suitable for Graphics clipping
*
* @param virtualClip a rectangle in virtual space
* @return the input rectangle transformed to screen space,
* suitable for graphics clipping operations
*/
public Rectangle virtualToScreenClip(Rectangle2D virtualClip) {
Point2D screenPt1 = trnsVirtualToScreen.transform(new Point2D.Double(virtualClip.getMinX(),
virtualClip.getMinY()), null);
Point2D screenPt2 = trnsVirtualToScreen.transform(new Point2D.Double(virtualClip.getMaxX(),
virtualClip.getMaxY()), null);
return new Rectangle((int) Math.floor(screenPt1.getX()), (int) Math.floor(screenPt1.getY()),
(int) Math.ceil(screenPt2.getX()), (int) Math.ceil(screenPt2.getY()));
}
}