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 geometry.LineSegment;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
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 {

    private final TransformManager transform_manager;

    /**
     * Construct a FractalPathPanel which by default displays part of the Koch
     * Snowflake.
     */
    public FractalPathPanel() {
        assignGamma(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:
        transform_manager = new TransformManager(this, PathUtil.boundingBox(approx_path));
        transform_manager.setListener(new TransformChangedListener() {

            @Override
            public void transformChanged() {
                redrawTheCurve();
            }
        });
        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 double max_step = 1.0 / 4.0;
    // The approximation to the fractal curve
    private PolygonalPath approx_path;

    /**
     * Set a different seed curve for the fractal.
     */
    private final void assignGamma(PolygonalPath gamma) {
        this.gamma = new VertexPath(gamma);
        fractal = new SubstitutionFractal(gamma, gamma);
        approx_path = new MaxStepApproximation(fractal, max_step);
    }

    /**
     * Set a different seed curve for the fractal.
     */
    public final void setGamma(PolygonalPath gamma) {
        assignGamma(gamma);
        redrawTheCurve();
    }

    /**
     * Change the quality of the polygonal approximation of the fractal curve.
     *
     * @param new_max_step Maximum edge length acceptable for a polygonal
     * approximation. This number should be larger than 0 and less than or equal
     * to 1.
     * @see PolygonalApproximation
     */
    public final void setMaxStep(double new_max_step) {
        // Prevent max_step from becoming negative, and ignore false changes.
        if ((new_max_step > 0) && (new_max_step <= 1) && (new_max_step != max_step)) {
            max_step = new_max_step;
            approx_path = new MaxStepApproximation(fractal, max_step);
            postStatus("Max edge length of polygonal approximate set to "
                    + max_step * transform_manager.getTransform().getScaleX() + " pixels.");
            redrawTheCurve();
        }
    }

    /**
     * Decrease the quality of the polygonal approximation.
     */
    public final void decreaseMaxStep() {
        setMaxStep(max_step / 2);
    }

    /**
     * Increase the quality of the polygonal approximation.
     */
    public final void increaseMaxStep() {
        setMaxStep(2 * max_step);
    }
    //// 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);
        }
    }

    /**
     * Guarantee the whole curve is displayed in the window.
     */
    public final void fit() {
        if (isRedrawing()) {
            postStatus("In order to Zoom Fit, you must wait until the whole curve is drawn.");
        } else {
            transform_manager.setDisplayBox(draw_it.getBoundingBox());
            postStatus("Zooming to fit the fractal curve in the window.");
        }
    }

    //// INTERACTION MODES
    public enum InteractionMode {

        DO_NOTHING, ZOOM_IN, ZOOM_OUT, ZOOM_BOX, 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 ZOOM_BOX:
                postStatus("Drag your mouse to draw a rectangle to zoom to.");
                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:
    // For MOVE_POINT mode
    int point_dragged;
    boolean dragging_point = false;
    // For ZOOM_BOX mode
    boolean dragging_box = false;
    Complex dragging_box_start = new Complex(0, 0),
            dragging_box_end = new Complex(0, 0);

    private Rectangle2D.Double getZoomRectangle() {
        double minx = Math.min(dragging_box_start.re(), dragging_box_end.re()),
                maxx = Math.max(dragging_box_start.re(), dragging_box_end.re()),
                miny = Math.min(dragging_box_start.im(), dragging_box_end.im()),
                maxy = Math.max(dragging_box_start.im(), dragging_box_end.im());
        return new Rectangle2D.Double(minx, miny, maxx - minx, maxy - miny);
    }

    @Override
    public void mouseClicked(MouseEvent me) {
        Complex z = new Complex(transform_manager.toMathCoordinates(me.getPoint()));
        switch (current_mode) {
            case ZOOM_IN:
                transform_manager.scale(0.8, z.toPoint());
                break;
            case ZOOM_OUT:
                transform_manager.scale(1.25, z.toPoint());
                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 = transform_manager.toScreenCoordinates(
                            it.next().startingPoint().toPoint());
                    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 = transform_manager.toScreenCoordinates(midpoint.toPoint());
                    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 = new Complex(transform_manager.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 = transform_manager.toScreenCoordinates(it.next().startingPoint().toPoint());
                    if (pt.distance(me.getPoint()) < 5) {
                        point_dragged = vertex;
                        dragging_point = true;
                        break; // leave the while loop.
                    }
                    vertex = vertex + 1;
                }
                break;
            case ZOOM_BOX:
                dragging_box = true;
                dragging_box_end = dragging_box_start = z;
                break;
        }
    }

    @Override
    public void mouseReleased(MouseEvent me) {
        Complex z = new Complex(transform_manager.toMathCoordinates(me.getPoint()));
        switch (current_mode) {
            case MOVE_POINT:
                dragging_point = false;
                break;
            case ZOOM_BOX:
                if (dragging_box) {
                    dragging_box_end = z;
                    Rectangle2D r = getZoomRectangle();
                    if ((r.getWidth() > 0) && (r.getHeight() > 0)) {
                        transform_manager.setDisplayBox(r);
                    }
                    dragging_box = false;
                }
                break;
        }
    }

    @Override
    public void mouseDragged(MouseEvent me) {
        Complex z = new Complex(transform_manager.toMathCoordinates(me.getPoint()));
        switch (current_mode) {
            case MOVE_POINT:
                if (dragging_point) {
                    setGamma(gamma.movePoint(point_dragged, z));
                    postStatus("Point moved to " + z.cartesianString() + ".");
                }
                break;
            case ZOOM_BOX:
                dragging_box_end = z;
                repaint();
                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);

        if (draw_it == null) {
            redrawTheCurve();
        }
        g.drawImage(draw_it.img, 0, 0, null);

        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, transform_manager.getTransform(), 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 = transform_manager.toScreenCoordinates(
                            it.next().midpoint().toPoint());
                    // Draw a circle with screen-radius 5 about the point.
                    g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10));
                }
                break;
            case ZOOM_BOX:
                if (dragging_box) {
                    g.setColor(Color.ORANGE);
                    g.setStroke(new BasicStroke(2));
                    g.draw(transform_manager.getTransform().createTransformedShape(getZoomRectangle()));
                }
                break;

        }
    }

    /**
     * Called for printing, and image output through FreeHEP's VectorGraphics.
     */
    public void printComponent(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);

        if (draw_it == null) {
            redrawTheCurve();
        }
        g.drawImage(draw_it.img, 0, 0, null);

        switch (current_mode) {
            case MOVE_POINT:
                drawGamma(g);
                break;
            case DELETE_POINT:
                drawGamma(g);
                break;
            case ADD_POINT:
                g.setStroke(new BasicStroke(2));
                g.setColor(Color.BLUE);
                PathDrawing.drawPolygonalPath(g, transform_manager.getTransform(), 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 = transform_manager.toScreenCoordinates(
                            it.next().midpoint().toPoint());
                    // 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);
        g.setStroke(new BasicStroke(2));
        PathDrawing.drawPolygonalPath(g, transform_manager.getTransform(), 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 = transform_manager.toScreenCoordinates(
                    it.next().startingPoint().toPoint());
            // Draw a circle with screen-radius 5 about the point.
            g.fill(new Ellipse2D.Double(pt.getX() - 5, pt.getY() - 5, 10, 10));
        }
    }
    DrawTheCurve draw_it;
    Thread drawing_thread;

    public void redrawTheCurve() {
        if ((drawing_thread != null) && (drawing_thread.isAlive())) {
            drawing_thread.interrupt();
        }
        draw_it = new DrawTheCurve();
        drawing_thread = (new Thread(draw_it));
        drawing_thread.start();
    }

    public boolean isRedrawing() {
        return ((drawing_thread != null) && (drawing_thread.isAlive()));
    }

    private class DrawTheCurve implements Runnable {

        private final int width, height;
        private final Image img;
        private final AffineTransform t;
        private final PolygonalPath p;
        Timer repainter;
        // We will be computing a bounding box:
        double box_minx, box_maxx, box_miny, box_maxy;

        public DrawTheCurve() {
            width = getWidth();
            height = getHeight();
            img = createImage(width, height);
            t = new AffineTransform(transform_manager.getTransform());
            p = approx_path;

            ActionListener repaint_action = new ActionListener() {

                @Override
                public void actionPerformed(ActionEvent evt) {
                    repaint();
                }
            };
            repainter = new Timer(100, repaint_action);
        }

        public Image getImage() {
            return img;
        }

        @Override
        public void run() {
            if (img == null) {
                return;
            }

            // Repaint every tenth of a second until the drawing has finished.
            repainter.start();

            Graphics2D g = (Graphics2D) img.getGraphics();
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
            g.setColor(Color.BLACK);
            g.setStroke(new BasicStroke(1));

            EdgeIterator it = p.iterator();
            LineSegment s = it.next();

            box_minx = box_maxx = s.startingPoint().re();
            box_miny = box_maxy = s.startingPoint().re();

            Line2D line_segment = new Line2D.Double(s.startingPoint().re(),
                    s.startingPoint().im(),
                    s.endingPoint().re(),
                    s.endingPoint().im());

            g.draw(t.createTransformedShape(line_segment));

            while (it.hasNext()) {
                s = it.next();
                if (s.startingPoint().re() < box_minx) {
                    box_minx = s.startingPoint().re();
                } else if (s.startingPoint().re() > box_maxx) {
                    box_maxx = s.startingPoint().re();
                }
                if (s.startingPoint().im() < box_miny) {
                    box_miny = s.startingPoint().im();
                } else if (s.startingPoint().im() > box_maxy) {
                    box_maxy = s.startingPoint().im();
                }


                // Constuct a Line2D with the same start and end points.
                // The advantage of this object is that it can be drawn.
                line_segment = new Line2D.Double(s.startingPoint().re(),
                        s.startingPoint().im(),
                        s.endingPoint().re(),
                        s.endingPoint().im());
                // Convert the line segment into screen coordinates, then draw it.
                g.draw(t.createTransformedShape(line_segment));

                if (Thread.interrupted()) {
                    // We've been interrupted: no more crunching.
                    break;
                }
            }

            repainter.stop();

            repaint();
        }

        public Rectangle2D getBoundingBox() {
            return new Rectangle2D.Double(box_minx, box_miny, box_maxx - box_minx, box_maxy - box_miny);
        }
    }
}
HOOPER >>>>> JAVA TUTORIAL
Last modified on August 17, 2018.
[check html] [check css]