PolygonOffset.java Source code

Java tutorial

Introduction

Here is the source code for PolygonOffset.java

Source

/*
 * %Z%%M% %I% %E% %U%
 * 
 * ************************************************************** "Copyright (c)
 * 2001 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * -Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
 * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
 * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
 * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
 * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
 * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
 * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
 * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGES.
 * 
 * You acknowledge that Software is not designed,licensed or intended for use in
 * the design, construction, operation or maintenance of any nuclear facility."
 * 
 * ***************************************************************************
 */

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GraphicsConfiguration;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.text.NumberFormat;
import java.util.Enumeration;
import java.util.EventListener;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.Vector;

import javax.media.j3d.Alpha;
import javax.media.j3d.Appearance;
import javax.media.j3d.Background;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.ImageComponent;
import javax.media.j3d.ImageComponent2D;
import javax.media.j3d.PolygonAttributes;
import javax.media.j3d.RotationInterpolator;
import javax.media.j3d.Screen3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.media.j3d.View;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.vecmath.AxisAngle4f;
import javax.vecmath.Color3f;
import javax.vecmath.Point3d;
import javax.vecmath.Vector3f;

import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGEncodeParam;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import com.sun.j3d.utils.applet.MainFrame;
import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.universe.SimpleUniverse;
import com.sun.j3d.utils.universe.ViewingPlatform;

public class PolygonOffset extends Applet implements Java3DExplorerConstants {

    SimpleUniverse u;

    boolean isApplication;

    Canvas3D canvas;

    OffScreenCanvas3D offScreenCanvas;

    View view;

    PolygonAttributes solidPa;

    PolygonAttributes wirePa;

    float dynamicOffset = 1.0f;

    float staticOffset = 1.0f;

    ViewingPlatform viewingPlatform;

    float innerScale = 0.94f;

    TransformGroup innerTG;

    Transform3D scale;

    float sphereRadius = 0.9f;

    String outFileBase = "offset";

    int outFileSeq = 0;

    float offScreenScale = 1.0f;

    public BranchGroup createSceneGraph() {
        // Create the root of the branch graph
        BranchGroup objRoot = new BranchGroup();

        // Create the transform group node and initialize it to the
        // identity. Enable the TRANSFORM_WRITE capability so that
        // our behavior code can modify it at runtime. Add it to the
        // root of the subgraph.
        TransformGroup objTrans = new TransformGroup();
        objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        objRoot.addChild(objTrans);

        // Create a Sphere. We will display this as both wireframe and
        // solid to make a hidden line display
        // wireframe
        Appearance wireApp = new Appearance();

        ColoringAttributes wireCa = new ColoringAttributes();
        wireCa.setColor(black);
        wireApp.setColoringAttributes(wireCa);
        wirePa = new PolygonAttributes(PolygonAttributes.POLYGON_LINE, PolygonAttributes.CULL_BACK, 0.0f);
        wireApp.setPolygonAttributes(wirePa);
        Sphere outWireSphere = new Sphere(sphereRadius, 0, 15, wireApp);
        objTrans.addChild(outWireSphere);

        // solid
        ColoringAttributes outCa = new ColoringAttributes(red, ColoringAttributes.SHADE_FLAT);
        Appearance outSolid = new Appearance();
        outSolid.setColoringAttributes(outCa);
        solidPa = new PolygonAttributes(PolygonAttributes.POLYGON_FILL, PolygonAttributes.CULL_BACK, 0.0f);
        solidPa.setPolygonOffsetFactor(dynamicOffset);
        solidPa.setPolygonOffset(staticOffset);
        solidPa.setCapability(PolygonAttributes.ALLOW_OFFSET_WRITE);
        outSolid.setPolygonAttributes(solidPa);
        Sphere outSolidSphere = new Sphere(sphereRadius, 0, 15, outSolid);
        objTrans.addChild(outSolidSphere);

        innerTG = new TransformGroup();
        innerTG.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);
        scale = new Transform3D();
        updateInnerScale();
        objTrans.addChild(innerTG);

        // Create a smaller sphere to go inside. This sphere has a different
        // tesselation and color
        Sphere inWireSphere = new Sphere(sphereRadius, 0, 10, wireApp);
        innerTG.addChild(inWireSphere);

        // inside solid
        ColoringAttributes inCa = new ColoringAttributes(blue, ColoringAttributes.SHADE_FLAT);
        Appearance inSolid = new Appearance();
        inSolid.setColoringAttributes(inCa);
        inSolid.setPolygonAttributes(solidPa);
        Sphere inSolidSphere = new Sphere(sphereRadius, 0, 10, inSolid);
        innerTG.addChild(inSolidSphere);

        // Create a new Behavior object that will perform the desired
        // operation on the specified transform object and add it into
        // the scene graph.
        AxisAngle4f axisAngle = new AxisAngle4f(0.0f, 0.0f, 1.0f, -(float) Math.PI / 2.0f);
        Transform3D yAxis = new Transform3D();
        Alpha rotationAlpha = new Alpha(-1, Alpha.INCREASING_ENABLE, 0, 0, 80000, 0, 0, 0, 0, 0);

        RotationInterpolator rotator = new RotationInterpolator(rotationAlpha, objTrans, yAxis, 0.0f,
                (float) Math.PI * 2.0f);
        BoundingSphere bounds = new BoundingSphere(new Point3d(0.0, 0.0, 0.0), 100.0);
        rotator.setSchedulingBounds(bounds);
        objTrans.addChild(rotator);

        // set up a white background
        Background bgWhite = new Background(new Color3f(1.0f, 1.0f, 1.0f));
        bgWhite.setApplicationBounds(bounds);
        objTrans.addChild(bgWhite);

        // Have Java 3D perform optimizations on this scene graph.
        objRoot.compile();

        return objRoot;
    }

    void updateInnerScale() {
        scale.set(innerScale);
        innerTG.setTransform(scale);
    }

    public PolygonOffset() {
        this(false);
    }

    public PolygonOffset(boolean isApplication) {
        this.isApplication = isApplication;
    }

    public void init() {
        setLayout(new BorderLayout());

        GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();

        JPanel canvasPanel = new JPanel();
        GridBagLayout gridbag = new GridBagLayout();
        canvasPanel.setLayout(gridbag);

        canvas = new Canvas3D(config);
        canvas.setSize(600, 600);
        add(canvas, BorderLayout.CENTER);

        u = new SimpleUniverse(canvas);

        if (isApplication) {
            offScreenCanvas = new OffScreenCanvas3D(config, true);
            // set the size of the off-screen canvas based on a scale
            // of the on-screen size
            Screen3D sOn = canvas.getScreen3D();
            Screen3D sOff = offScreenCanvas.getScreen3D();
            Dimension dim = sOn.getSize();
            dim.width *= offScreenScale;
            dim.height *= offScreenScale;
            sOff.setSize(dim);
            sOff.setPhysicalScreenWidth(sOn.getPhysicalScreenWidth() * offScreenScale);
            sOff.setPhysicalScreenHeight(sOn.getPhysicalScreenHeight() * offScreenScale);

            // attach the offscreen canvas to the view
            u.getViewer().getView().addCanvas3D(offScreenCanvas);
        }

        // Create a simple scene and attach it to the virtual universe
        BranchGroup scene = createSceneGraph();

        // set the eye at z = 2.0
        viewingPlatform = u.getViewingPlatform();
        Transform3D vpTrans = new Transform3D();
        vpTrans.set(new Vector3f(0.0f, 0.0f, 2.0f));
        viewingPlatform.getViewPlatformTransform().setTransform(vpTrans);

        // set up a parallel projection with clip limits at 1 and -1
        view = u.getViewer().getView();
        view.setProjectionPolicy(View.PARALLEL_PROJECTION);
        view.setFrontClipPolicy(View.VIRTUAL_EYE);
        view.setBackClipPolicy(View.VIRTUAL_EYE);
        view.setFrontClipDistance(1.0f);
        view.setBackClipDistance(3.0f);

        u.addBranchGraph(scene);

        // set up the sliders
        JPanel guiPanel = new JPanel();
        guiPanel.setLayout(new GridLayout(0, 1));
        FloatLabelJSlider dynamicSlider = new FloatLabelJSlider("Dynamic Offset", 0.1f, 0.0f, 2.0f, dynamicOffset);
        dynamicSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                dynamicOffset = e.getValue();
                solidPa.setPolygonOffsetFactor(dynamicOffset);
            }
        });
        guiPanel.add(dynamicSlider);

        LogFloatLabelJSlider staticSlider = new LogFloatLabelJSlider("Static Offset", 0.1f, 10000.0f, staticOffset);
        staticSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                staticOffset = e.getValue();
                solidPa.setPolygonOffset(staticOffset);
            }
        });
        guiPanel.add(staticSlider);

        FloatLabelJSlider innerSphereSlider = new FloatLabelJSlider("Inner Sphere Scale", 0.001f, 0.90f, 1.0f,
                innerScale);
        innerSphereSlider.addFloatListener(new FloatListener() {
            public void floatChanged(FloatEvent e) {
                innerScale = e.getValue();
                updateInnerScale();
            }
        });
        guiPanel.add(innerSphereSlider);

        if (isApplication) {
            JButton snapButton = new JButton("Snap Image");
            snapButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    Point loc = canvas.getLocationOnScreen();
                    offScreenCanvas.setOffScreenLocation(loc);
                    Dimension dim = canvas.getSize();
                    dim.width *= offScreenScale;
                    dim.height *= offScreenScale;
                    nf.setMinimumIntegerDigits(3);
                    offScreenCanvas.snapImageFile(outFileBase + nf.format(outFileSeq++), dim.width, dim.height);
                    nf.setMinimumIntegerDigits(0);
                }
            });
            guiPanel.add(snapButton);
        }
        add(guiPanel, BorderLayout.EAST);
    }

    public void destroy() {
        u.removeAllLocales();
    }

    //
    // The following allows PolygonOffset to be run as an application
    // as well as an applet
    //
    public static void main(String[] args) {
        new MainFrame(new PolygonOffset(true), 950, 600);
    }
}

interface Java3DExplorerConstants {

    // colors
    static Color3f black = new Color3f(0.0f, 0.0f, 0.0f);

    static Color3f red = new Color3f(1.0f, 0.0f, 0.0f);

    static Color3f green = new Color3f(0.0f, 1.0f, 0.0f);

    static Color3f blue = new Color3f(0.0f, 0.0f, 1.0f);

    static Color3f skyBlue = new Color3f(0.6f, 0.7f, 0.9f);

    static Color3f cyan = new Color3f(0.0f, 1.0f, 1.0f);

    static Color3f magenta = new Color3f(1.0f, 0.0f, 1.0f);

    static Color3f yellow = new Color3f(1.0f, 1.0f, 0.0f);

    static Color3f brightWhite = new Color3f(1.0f, 1.5f, 1.5f);

    static Color3f white = new Color3f(1.0f, 1.0f, 1.0f);

    static Color3f darkGrey = new Color3f(0.15f, 0.15f, 0.15f);

    static Color3f medGrey = new Color3f(0.3f, 0.3f, 0.3f);

    static Color3f grey = new Color3f(0.5f, 0.5f, 0.5f);

    static Color3f lightGrey = new Color3f(0.75f, 0.75f, 0.75f);

    // infinite bounding region, used to make env nodes active everywhere
    BoundingSphere infiniteBounds = new BoundingSphere(new Point3d(), Double.MAX_VALUE);

    // common values
    static final String nicestString = "NICEST";

    static final String fastestString = "FASTEST";

    static final String antiAliasString = "Anti-Aliasing";

    static final String noneString = "NONE";

    // light type constants
    static int LIGHT_AMBIENT = 1;

    static int LIGHT_DIRECTIONAL = 2;

    static int LIGHT_POSITIONAL = 3;

    static int LIGHT_SPOT = 4;

    // screen capture constants
    static final int USE_COLOR = 1;

    static final int USE_BLACK_AND_WHITE = 2;

    // number formatter
    NumberFormat nf = NumberFormat.getInstance();

}

class OffScreenCanvas3D extends Canvas3D {

    OffScreenCanvas3D(GraphicsConfiguration graphicsConfiguration, boolean offScreen) {

        super(graphicsConfiguration, offScreen);
    }

    private BufferedImage doRender(int width, int height) {

        BufferedImage bImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        ImageComponent2D buffer = new ImageComponent2D(ImageComponent.FORMAT_RGB, bImage);
        //buffer.setYUp(true);

        setOffScreenBuffer(buffer);
        renderOffScreenBuffer();
        waitForOffScreenRendering();
        bImage = getOffScreenBuffer().getImage();
        return bImage;
    }

    void snapImageFile(String filename, int width, int height) {
        BufferedImage bImage = doRender(width, height);

        /*
         * JAI: RenderedImage fImage = JAI.create("format", bImage,
         * DataBuffer.TYPE_BYTE); JAI.create("filestore", fImage, filename +
         * ".tif", "tiff", null);
         */

        /* No JAI: */
        try {
            FileOutputStream fos = new FileOutputStream(filename + ".jpg");
            BufferedOutputStream bos = new BufferedOutputStream(fos);

            JPEGImageEncoder jie = JPEGCodec.createJPEGEncoder(bos);
            JPEGEncodeParam param = jie.getDefaultJPEGEncodeParam(bImage);
            param.setQuality(1.0f, true);
            jie.setJPEGEncodeParam(param);
            jie.encode(bImage);

            bos.flush();
            fos.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

class FloatLabelJSlider extends JPanel implements ChangeListener, Java3DExplorerConstants {

    JSlider slider;

    JLabel valueLabel;

    Vector listeners = new Vector();

    float min, max, resolution, current, scale;

    int minInt, maxInt, curInt;;

    int intDigits, fractDigits;

    float minResolution = 0.001f;

    // default slider with name, resolution = 0.1, min = 0.0, max = 1.0 inital
    // 0.5
    FloatLabelJSlider(String name) {
        this(name, 0.1f, 0.0f, 1.0f, 0.5f);
    }

    FloatLabelJSlider(String name, float resolution, float min, float max, float current) {

        this.resolution = resolution;
        this.min = min;
        this.max = max;
        this.current = current;

        if (resolution < minResolution) {
            resolution = minResolution;
        }

        // round scale to nearest integer fraction. i.e. 0.3 => 1/3 = 0.33
        scale = (float) Math.round(1.0f / resolution);
        resolution = 1.0f / scale;

        // get the integer versions of max, min, current
        minInt = Math.round(min * scale);
        maxInt = Math.round(max * scale);
        curInt = Math.round(current * scale);

        // sliders use integers, so scale our floating point value by "scale"
        // to make each slider "notch" be "resolution". We will scale the
        // value down by "scale" when we get the event.
        slider = new JSlider(JSlider.HORIZONTAL, minInt, maxInt, curInt);
        slider.addChangeListener(this);

        valueLabel = new JLabel(" ");

        // set the initial value label
        setLabelString();

        // add min and max labels to the slider
        Hashtable labelTable = new Hashtable();
        labelTable.put(new Integer(minInt), new JLabel(nf.format(min)));
        labelTable.put(new Integer(maxInt), new JLabel(nf.format(max)));
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        /* layout to align left */
        setLayout(new BorderLayout());
        Box box = new Box(BoxLayout.X_AXIS);
        add(box, BorderLayout.WEST);

        box.add(new JLabel(name));
        box.add(slider);
        box.add(valueLabel);
    }

    public void setMinorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMinorTickSpacing(intSpacing);
    }

    public void setMajorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMajorTickSpacing(intSpacing);
    }

    public void setPaintTicks(boolean paint) {
        slider.setPaintTicks(paint);
    }

    public void addFloatListener(FloatListener listener) {
        listeners.add(listener);
    }

    public void removeFloatListener(FloatListener listener) {
        listeners.remove(listener);
    }

    public void stateChanged(ChangeEvent e) {
        JSlider source = (JSlider) e.getSource();
        // get the event type, set the corresponding value.
        // Sliders use integers, handle floating point values by scaling the
        // values by "scale" to allow settings at "resolution" intervals.
        // Divide by "scale" to get back to the real value.
        curInt = source.getValue();
        current = curInt / scale;

        valueChanged();
    }

    public void setValue(float newValue) {
        boolean changed = (newValue != current);
        current = newValue;
        if (changed) {
            valueChanged();
        }
    }

    private void valueChanged() {
        // update the label
        setLabelString();

        // notify the listeners
        FloatEvent event = new FloatEvent(this, current);
        for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
            FloatListener listener = (FloatListener) e.nextElement();
            listener.floatChanged(event);
        }
    }

    void setLabelString() {
        // Need to muck around to try to make sure that the width of the label
        // is wide enough for the largest value. Pad the string
        // be large enough to hold the largest value.
        int pad = 5; // fudge to make up for variable width fonts
        float maxVal = Math.max(Math.abs(min), Math.abs(max));
        intDigits = Math.round((float) (Math.log(maxVal) / Math.log(10))) + pad;
        if (min < 0) {
            intDigits++; // add one for the '-'
        }
        // fractDigits is num digits of resolution for fraction. Use base 10 log
        // of scale, rounded up, + 2.
        fractDigits = (int) Math.ceil((Math.log(scale) / Math.log(10)));
        nf.setMinimumFractionDigits(fractDigits);
        nf.setMaximumFractionDigits(fractDigits);
        String value = nf.format(current);
        while (value.length() < (intDigits + fractDigits)) {
            value = value + "  ";
        }
        valueLabel.setText(value);
    }

}

class FloatEvent extends EventObject {

    float value;

    FloatEvent(Object source, float newValue) {
        super(source);
        value = newValue;
    }

    float getValue() {
        return value;
    }
}

interface FloatListener extends EventListener {
    void floatChanged(FloatEvent e);
}

class LogFloatLabelJSlider extends JPanel implements ChangeListener, Java3DExplorerConstants {

    JSlider slider;

    JLabel valueLabel;

    Vector listeners = new Vector();

    float min, max, resolution, current, scale;

    double minLog, maxLog, curLog;

    int minInt, maxInt, curInt;;

    int intDigits, fractDigits;

    NumberFormat nf = NumberFormat.getInstance();

    float minResolution = 0.001f;

    double logBase = Math.log(10);

    // default slider with name, resolution = 0.1, min = 0.0, max = 1.0 inital
    // 0.5
    LogFloatLabelJSlider(String name) {
        this(name, 0.1f, 100.0f, 10.0f);
    }

    LogFloatLabelJSlider(String name, float min, float max, float current) {

        this.resolution = resolution;
        this.min = min;
        this.max = max;
        this.current = current;

        if (resolution < minResolution) {
            resolution = minResolution;
        }

        minLog = log10(min);
        maxLog = log10(max);
        curLog = log10(current);

        // resolution is 100 steps from min to max
        scale = 100.0f;
        resolution = 1.0f / scale;

        // get the integer versions of max, min, current
        minInt = (int) Math.round(minLog * scale);
        maxInt = (int) Math.round(maxLog * scale);
        curInt = (int) Math.round(curLog * scale);

        slider = new JSlider(JSlider.HORIZONTAL, minInt, maxInt, curInt);
        slider.addChangeListener(this);

        valueLabel = new JLabel(" ");

        // Need to muck around to make sure that the width of the label
        // is wide enough for the largest value. Pad the initial string
        // be large enough to hold the largest value.
        int pad = 5; // fudge to make up for variable width fonts
        intDigits = (int) Math.ceil(maxLog) + pad;
        if (min < 0) {
            intDigits++; // add one for the '-'
        }
        if (minLog < 0) {
            fractDigits = (int) Math.ceil(-minLog);
        } else {
            fractDigits = 0;
        }
        nf.setMinimumFractionDigits(fractDigits);
        nf.setMaximumFractionDigits(fractDigits);
        String value = nf.format(current);
        while (value.length() < (intDigits + fractDigits)) {
            value = value + " ";
        }
        valueLabel.setText(value);

        // add min and max labels to the slider
        Hashtable labelTable = new Hashtable();
        labelTable.put(new Integer(minInt), new JLabel(nf.format(min)));
        labelTable.put(new Integer(maxInt), new JLabel(nf.format(max)));
        slider.setLabelTable(labelTable);
        slider.setPaintLabels(true);

        // layout to align left
        setLayout(new BorderLayout());
        Box box = new Box(BoxLayout.X_AXIS);
        add(box, BorderLayout.WEST);

        box.add(new JLabel(name));
        box.add(slider);
        box.add(valueLabel);
    }

    public void setMinorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMinorTickSpacing(intSpacing);
    }

    public void setMajorTickSpacing(float spacing) {
        int intSpacing = Math.round(spacing * scale);
        slider.setMajorTickSpacing(intSpacing);
    }

    public void setPaintTicks(boolean paint) {
        slider.setPaintTicks(paint);
    }

    public void addFloatListener(FloatListener listener) {
        listeners.add(listener);
    }

    public void removeFloatListener(FloatListener listener) {
        listeners.remove(listener);
    }

    public void stateChanged(ChangeEvent e) {
        JSlider source = (JSlider) e.getSource();
        curInt = source.getValue();
        curLog = curInt / scale;
        current = (float) exp10(curLog);

        valueChanged();
    }

    public void setValue(float newValue) {
        boolean changed = (newValue != current);
        current = newValue;
        if (changed) {
            valueChanged();
        }
    }

    private void valueChanged() {
        String value = nf.format(current);
        valueLabel.setText(value);

        // notify the listeners
        FloatEvent event = new FloatEvent(this, current);
        for (Enumeration e = listeners.elements(); e.hasMoreElements();) {
            FloatListener listener = (FloatListener) e.nextElement();
            listener.floatChanged(event);
        }
    }

    double log10(double value) {
        return Math.log(value) / logBase;
    }

    double exp10(double value) {
        return Math.exp(value * logBase);
    }

}