Source code for FractalPathPanel.java

What follows is the highlighted source code with comments. You can also download this file directly: FractalPathPanel.java.

/*
 * Copyright (C) 2012 W. Patrick Hooper <wphooper@gmail.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package graphics;

import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import number.Complex;
import path.*;

/**
 * A FractalPathPanel displays a fractal curve and allows the user to manipulate
 * the curve and the display of this curve.
 *
 * @author W. Patrick Hooper <wphooper@gmail.com>
 */
public class FractalPathPanel extends JPanel implements MouseListener, MouseMotionListener {

    /**
     * Construct a FractalPathPanel which by default displays part of the Koch
     * Snowflake.
     */
    public FractalPathPanel() {
        setGamma(new VertexPath(
                new Complex(0.0),
                new Complex(1.0 / 3.0),
                new Complex(0.5, Math.sqrt(3) / 6),
                new Complex(2.0 / 3.0),
                new Complex(1.0)));
        // Define the bounding box to fit the fractal path:
        fit();
        setBackground(Color.WHITE); // Make the background color white

        // Necessary for mouse interaction:
        addMouseListener(this);
        addMouseMotionListener(this);
    }
    
    //// CURVE MANIPULATION FUNCTIONS AND VARIABLES
    // Curve used to "seed" the substitution fractal
    private VertexPath gamma;
    // The fractal curve we display
    private SubstitutionFractal fractal;
    // Depth to look at for the approximation
    private int depth = 2;
    // The approximation to the fractal curve
    private PolygonalPath approx_path;

    /**
     * Set a different seed curve for the fractal.
     */
    public final void setGamma(PolygonalPath gamma) {
        this.gamma = new VertexPath(gamma);
        fractal = new SubstitutionFractal(gamma, gamma);
        approx_path = new PolygonalApproximation(fractal, depth);
        repaint();
    }

    /** Change the quality of the polygonal approximation of the fractal curve.
     * 
     * @param new_depth Depth to descend in the tree of edges defining the
     * fractal curve.
     * @see PolygonalApproximation
     */
    public final void setDepth(int new_depth) {
        // Prevent depth from becoming negative, and ignore false changes.
        if ((new_depth >= 0) && (new_depth != depth)) {
            depth = new_depth;
            approx_path = new PolygonalApproximation(fractal, depth);
            postStatus("Fractal approximation depth set to " + depth + ".");
            repaint();
        }
    }

    /** Decrease the quality of the polygonal approximation. */
    public final void decreaseDepth() {
        setDepth(depth - 1);
    }

    /** Increase the quality of the polygonal approximation. */
    public final void increaseDepth() {
        setDepth(depth + 1);
    }
    
    //// FUNCTIONS RELATED TO THE STATUSBAR
    private JLabel status_bar = null;

    /**
     * Set the label to print status updates to.
     */
    public void setStatusBar(JLabel new_bar) {
        status_bar = new_bar;
        // This will update the status bar with the current mode of interaction:
        setInteractionMode(current_mode);
    }

    private void postStatus(String status) {
        if (status_bar != null) {
            status_bar.setText(status);
        }
    }
    
    //// FUNCTIONS RELATED TO THE BOUNDING BOX AND COORDINATE CHANGES
    // We guarantee that this box will be displayed.
    private Rectangle2D display_box;
    // This affine transformation converts from math coordinates to screen coordinates.
    private AffineTransform current_transform = new AffineTransform();

    /**
     * This function returns an affine transform which sends display_box into
     * the rectangle representing screen coordinates for this panel.
     *
     * The affine transformation we return has the following properties. It
     * sends the bounding_box into the panel rectangle, so that the center of
     * the bounding box is sent the center of the panel. The scaling preserves
     * the aspect ratio.
     */
    private AffineTransform getTransform() {
        /*
         * The component has pixels whose x-coordinates are numbered 0 to
         * getWidth()-1 as we move rightward. The y-coordinates of these pixels
         * increases from 0 to getHeight()-1 as we move downward.
         *
         * We will return the transformation which takes the bounding box for
         * our curve into the rectangle representing coordinates for our
         * component on the screen. To move this box into the screen we use the
         * following steps: 1. We translate our box, moving the center of our
         * box to the origin. 2. We scale the box by a constant so that the
         * image of our box has width and height less than or equal to the width
         * and height of this component. 3. We negate the y-coordinate, because
         * in mathematics the y-coordinate increases as we move upward. 4. We
         * translate the origin so that it is moved to the center of the
         * component.
         *
         * These steps are carried out below.
         */

        // Construct a transformation which translates the plane moving the center of 
        // our box to the origin.
        AffineTransform transform =
                AffineTransform.getTranslateInstance(-display_box.getCenterX(),
                -display_box.getCenterY());

        // The number scale is the minimal ratio of screen dimensions to bounding box dimensions.
        double scale = Math.min(getWidth() / display_box.getWidth(),
                getHeight() / display_box.getHeight());

        // The following line post-composes by scaling the plane by the number "scale".
        // Because we use the same constant in each coordinate, we preserve the aspect ratio.
        transform.preConcatenate(AffineTransform.getScaleInstance(scale, scale));

        // This has the effect of negating the y-coordinate:
        transform.preConcatenate(AffineTransform.getScaleInstance(1, -1));

        // Now translate the origin until it is centered in the component.
        transform.preConcatenate(
                AffineTransform.getTranslateInstance(getWidth() / 2, getHeight() / 2));

        return transform;
    }

    /**
     * Convert the Complex number z into screen coordinates.
     */
    private Point2D toScreenCoordinates(Complex z) {
        // Convert z to a Point2D, apply the affine transform and return the result.
        return current_transform.transform(new Point2D.Double(z.re(), z.im()), null);
    }

    /**
     * Convert the point from screen coordinates into math coordinates.
     */
    private Complex toMathCoordinates(Point2D p) {
        try {
            Point2D math_point = current_transform.inverseTransform(p, null);
            return new Complex(math_point.getX(), math_point.getY());
        } catch (NoninvertibleTransformException e) {
            // We would enter here if the current_transform represents a matrix
            // which is not invertible. This should never happen, because
            // we constructed our transformation as a composition of
            // invertible transformations.
            System.err.println("FractalPathPanel failed to invert the "
                    + "current_transform in the toMathCoordinates() method.");
            return new Complex(0);
        }
    }

    /**
     * Guarantee the whole curve is displayed in the window.
     */
    public final void fit() {
        display_box = PathUtil.boundingBox(approx_path);
        postStatus("Zooming to fit the fractal curve in the window.");
        repaint();
    }

    /**
     * Rescale the box we are guaranteed to display by a multiplicative constant
     * via a dilation fixing the provided complex number.
     *
     * All calculations are done in math coordinates.
     *
     * @param scaling_constant A positive constant.
     * @param fixed_point Point fixed by the dilation.
     */
    public void scale(double scaling_constant, Complex fixed_point) {
        if (scaling_constant > 0) {
            Complex min = new Complex(display_box.getMinX(), display_box.getMinY()),
                    max = new Complex(display_box.getMaxX(), display_box.getMaxY());
            min = min.minus(fixed_point).mult(scaling_constant).add(fixed_point);
            max = max.minus(fixed_point).mult(scaling_constant).add(fixed_point);
            Complex diff = max.minus(min);
            display_box = new Rectangle2D.Double(min.re(), min.im(), diff.re(), diff.im());
            repaint();
        }
    }

    //// INTERACTION MODES
    public enum InteractionMode {
        DO_NOTHING, ZOOM_IN, ZOOM_OUT, MOVE_POINT, DELETE_POINT, ADD_POINT;
    }
    private InteractionMode current_mode = InteractionMode.DO_NOTHING;

    public void setInteractionMode(InteractionMode new_mode) {
        current_mode = new_mode;
        switch (current_mode) {
            case DO_NOTHING:
                postStatus("Welcome to the Fractal Path Explorer.");
                break;
            case ZOOM_IN:
                postStatus("Click to zoom in to a point.");
                break;
            case ZOOM_OUT:
                postStatus("Click to zoom out from a point.");
                break;
            case MOVE_POINT:
                postStatus("Drag a point to change the path.");
                dragging_point = false;
                break;
            case DELETE_POINT:
                postStatus("Click a point to delete.");
                break;
            case ADD_POINT:
                postStatus("Click a point to add to the path.");
                break;
        }
        repaint();
    }

    // MOUSE INTERACTION:
    int point_dragged;
    boolean dragging_point = false;

    @Override
    public void mouseClicked(MouseEvent me) {
        Complex z = this.toMathCoordinates(me.getPoint());
        switch (current_mode) {
            case ZOOM_IN:
                scale(0.8, z);
                break;
            case ZOOM_OUT:
                scale(1.25, z);
                break;
            case DELETE_POINT:
                EdgeIterator it = gamma.iterator();
                if (it.hasNext()) {
                    it.next(); // skip first edge.
                }
                int vertex = 1;
                while (it.hasNext()) {
                    // Get the starting point of the segment, and convert it to 
                    // screen coordinates:
                    Point2D pt = this.toScreenCoordinates(it.next().startingPoint());
                    if (pt.distance(me.getPoint()) < 5) {
                        setGamma(gamma.removePoint(vertex));
                        break;
                    }
                    vertex = vertex + 1;
                }
                break;
            case ADD_POINT:
                it = gamma.iterator();
                vertex = 1;
                while (it.hasNext()) {
                    // Get the midpoint of the segment, and convert it to screen coordinates:
                    Complex midpoint=it.next().midpoint();
                    Point2D pt = this.toScreenCoordinates(midpoint);
                    if (pt.distance(me.getPoint()) < 5) {
                        setGamma(gamma.addPoint(vertex, midpoint));
                        break;
                    }
                    vertex = vertex + 1;
                }
                break;
        }
    }

    @Override
    public void mouseEntered(MouseEvent me) {
    }

    @Override
    public void mouseExited(MouseEvent me) {
    }

    @Override
    public void mousePressed(MouseEvent me) {
        Complex z = this.toMathCoordinates(me.getPoint());
        switch (current_mode) {
            case MOVE_POINT:
                EdgeIterator it = gamma.iterator();
                if (it.hasNext()) {
                    it.next(); // skip first edge.
                }
                int vertex = 1;
                while (it.hasNext()) {
                    // Get the starting point of the segment, and convert it to 
                    // screen coordinates:
                    Point2D pt = this.toScreenCoordinates(it.next().startingPoint());
                    if (pt.distance(me.getPoint()) < 5) {
                        point_dragged = vertex;
                        dragging_point = true;
                        break; // leave the while loop.
                    }
                    vertex = vertex + 1;
                }
                break;
        }
    }

    @Override
    public void mouseReleased(MouseEvent me) {
        Complex z = this.toMathCoordinates(me.getPoint());
        switch (current_mode) {
            case MOVE_POINT:
                dragging_point = false;
                break;
        }
    }

    @Override
    public void mouseDragged(MouseEvent me) {
        Complex z = this.toMathCoordinates(me.getPoint());
        switch (current_mode) {
            case MOVE_POINT:
                if (dragging_point) {
                    setGamma(gamma.movePoint(point_dragged, z));
                }
                break;
        }
    }

    @Override
    public void mouseMoved(MouseEvent me) {
    }

    // DRAWING ROUTINE:
    /**
     * This is called when we need to draw the polygonal path.
     */
    @Override
    public void paintComponent(Graphics gfx) {
        // This stuff is standard, and should be in any paintComponent method. 
        super.paintComponent(gfx);
        Graphics2D g = (Graphics2D) gfx;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        // Reset the transformation which converts from math coordinates to screen coordinates.
        current_transform = getTransform();

        // Draw in black, with lines of width equal to one pixel. 
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1));
        PathDrawing.drawPolygonalPath(g, current_transform, approx_path);

        switch (current_mode) {
            case MOVE_POINT:
                drawGamma(g);
                break;
            case DELETE_POINT:
                drawGamma(g);
                break;
            case ADD_POINT:
                g.setColor(Color.BLUE);
                PathDrawing.drawPolygonalPath(g, current_transform, gamma);
                g.setColor(Color.RED);
                EdgeIterator it = gamma.iterator();
                while (it.hasNext()) {
                    // Get the midpoint of the segment, and convert it to screen coordinates:
                    Point2D pt = this.toScreenCoordinates(it.next().midpoint());
                    // Draw a circle with screen-radius 5 about the point.
                    g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10));
                }
        }
    }

    /** Draw the curve gamma and its vertices. */
    private void drawGamma(Graphics2D g) {
        g.setColor(Color.BLUE);
        PathDrawing.drawPolygonalPath(g, current_transform, gamma);
        g.setColor(Color.RED);
        EdgeIterator it = gamma.iterator();
        if (it.hasNext()) {
            it.next(); // skip first edge.
        }
        while (it.hasNext()) {
            // Get the starting point of the segment, and convert it to 
            // screen coordinates:
            Point2D pt = this.toScreenCoordinates(it.next().startingPoint());
            // Draw a circle with screen-radius 5 about the point.
            g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10));
        }
    }

}
HOOPER >>>>> JAVA TUTORIAL
Last modified on August 25, 2021.
[check html] [check css]