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); } } }