/* * The MIT License (MIT) * * Copyright (c) 2015 Camil Staps * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.camilstaps.mandelbrot; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.event.InputEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Observable; import java.util.Observer; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.SwingWorker; /** * The Grid is both a View and a Controller: it shows a graphical representation * of the FractalModel, and allows the user to control that FractalModel by * clicking (zoom in), shift-clicking (zoom out) and dragging (box zoom). * @author Camil Staps */ public class Grid extends JPanel implements Observer, MouseListener, MouseMotionListener { /** * Hard maximum amount of repetitions and the amount of steps to take to get * to that maximum */ private static final int REPETITIONS_MAX = 1000, STEPS = 50; /** * Width and height of the grid */ private static final int WIDTH = 400, HEIGHT = 400; /** * List of colours for visualisation */ private final int[] colors; /** * The FractalModel to visualise */ private final FractalModel fractalModel; /** * Used for visualisation */ private final BufferedImage image; private final WritableRaster raster; /** * Count pixels changed to check if we need to repaint */ private int pixelCounter; /** * Updating the view is done by SwingWorkers. The solution provides support * for multiple SwingWorkers; Updaters; which are held in this list. */ private final List updaters = new ArrayList<>(); /** * Used for handling mouse events and zooming */ private int start_x, start_y, old_x, old_y; private boolean dragging = false; Graphics graphics; /** * A progress bar */ private ProgressView progressView; /** * Whether or not to use multiple SwingWorkers. * Using multiple SwingWorkers is *not* faster. The application is already * faster by using memory in the MandelbrotFractal class. Using multiple * SwingWorkers does not aid in speed. */ private boolean useMultipleSwingWorkers = false; /** * Create a new Grid to view and control a FractalModel * @param fractalModel */ public Grid(FractalModel fractalModel) { this.fractalModel = fractalModel; fractalModel.addObserver(this); image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB); raster = image.getRaster(); setPreferredSize(new Dimension(WIDTH, HEIGHT)); colors = new int[REPETITIONS_MAX + 1]; for (int i = 0; i <=REPETITIONS_MAX; i++) { colors[i] = Color.HSBtoRGB( 0.07f, 0.5f + 0.5f * (float) i / (float) REPETITIONS_MAX, 1); } addMouseListener(this); addMouseMotionListener(this); update(fractalModel, null); } /** * Get the progress bar (create it if it doesn't exist yet) * @return */ public ProgressView getProgressView() { if (progressView == null) { progressView = new ProgressView(); } return progressView; } @Override public int getWidth() { return WIDTH; } @Override public int getHeight() { return HEIGHT; } /** * Change whether we use multiple SwingWorkers or not. This change will * take effect when the FractalModel is updated. * @param value */ public void setUseMultipleSwingWorkers(boolean value) { useMultipleSwingWorkers = value; } /** * Stop the previous update and start rendering all over again. * @param o * @param o1 */ @Override public final synchronized void update(Observable o, Object o1) { if (updaters != null) { synchronized (updaters) { for (Updater updater : updaters) { updater.stop(); updater.cancel(true); } } } synchronized (updaters) { updaters.clear(); if (useMultipleSwingWorkers) { Updater u1 = new Updater(0, WIDTH / 2, 0, HEIGHT / 2); u1.execute(); updaters.add(u1); Updater u2 = new Updater(WIDTH / 2, WIDTH, 0, HEIGHT / 2); u2.execute(); updaters.add(u2); Updater u3 = new Updater(0, WIDTH / 2, HEIGHT / 2, HEIGHT); u3.execute(); updaters.add(u3); Updater u4 = new Updater(WIDTH / 2, WIDTH, HEIGHT / 2, HEIGHT); u4.execute(); updaters.add(u4); } else { Updater u = new Updater(0, WIDTH, 0, HEIGHT); u.execute(); updaters.add(u); } } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); g.drawImage(image, 0, 0, null); } /** * Set a pixel on the grid * @param x * @param y * @param rgb */ public synchronized void setPixel(int x, int y, int[] rgb) { raster.setPixel(x, y, rgb); pixelCounter++; if (pixelCounter == WIDTH * HEIGHT) { pixelCounter = 0; repaint(); } } /** * Get the 'mathematical' x coordinate that belongs to a pixel's x coordinate * @param pixel_x * @return */ protected double getRealX(int pixel_x) { return ((double) pixel_x * (fractalModel.getEndX() - fractalModel.getStartX()) / (double) WIDTH) + fractalModel.getStartX(); } /** * Get the 'mathematical' y coordinate that belongs to a pixel's y coordinate * @param pixel_y * @return */ protected double getRealY(int pixel_y) { return ((double) pixel_y * (fractalModel.getEndY() - fractalModel.getStartY()) / (double) HEIGHT) + fractalModel.getStartY(); } /** * Click to zoom in; Shift-click to zoom out * @param me */ @Override public void mouseClicked(MouseEvent me) { if ((me.getModifiers() & InputEvent.SHIFT_MASK) != 0) { zoomOut(me); } else { zoomIn(me); } } /** * Keep track of mouse data for the box zoom * @param me */ @Override public void mousePressed(MouseEvent me) { old_x = start_x = me.getX(); old_y = start_y = me.getY(); } /** * If this was a box zoom, ... well, perform a box zoom. * @param me */ @Override public synchronized void mouseReleased(MouseEvent me) { if (me.getX() != start_x || me.getY() != start_y) { zoomBox(me); } dragging = false; } /** * Zoom in with factor 2 * @param me */ private void zoomIn(MouseEvent me) { double offset_x = (fractalModel.getEndX() - fractalModel.getStartX()) / 4; double offset_y = (fractalModel.getEndY() - fractalModel.getStartY()) / 4; double newCenterX = getRealX(me.getX()); fractalModel.setStartX(newCenterX - offset_x); fractalModel.setEndX(newCenterX + offset_x); double newCenterY = getRealY(me.getY()); fractalModel.setStartY(newCenterY - offset_y); fractalModel.setEndY(newCenterY + offset_y); } /** * Zoom out with factor 2 * @param me */ private void zoomOut(MouseEvent me) { double offset_x = fractalModel.getEndX() - fractalModel.getStartX(); double offset_y = fractalModel.getEndY() - fractalModel.getStartY(); double newCenterX = getRealX(me.getX()); fractalModel.setStartX(newCenterX - offset_x); fractalModel.setEndX(newCenterX + offset_x); double newCenterY = getRealY(me.getY()); fractalModel.setStartY(newCenterY - offset_y); fractalModel.setEndY(newCenterY + offset_y); } /** * Zoom to the selected box. * Intentionally chose to let the user only select squares, otherwise it's too easy to mess up the scale * @param me */ private void zoomBox(MouseEvent me) { double newStartX = getRealX(start_x), newEndX = getRealX(me.getX()), newStartY = getRealY(start_y), newEndY = getRealY(start_y + me.getX() - start_x); fractalModel.setStartX(newStartX); fractalModel.setEndX(newEndX); fractalModel.setStartY(newStartY); fractalModel.setEndY(newEndY); } @Override public void mouseEntered(MouseEvent me) {} @Override public void mouseExited(MouseEvent me) {} /** * Draw the zoombox on dragging * @param me */ @Override public void mouseDragged(MouseEvent me) { Graphics g = getSafeGraphics(); if (g != null) { if (dragging) { eraseZoombox(); } g.drawRect(start_x, start_y, me.getX() - start_x, me.getX() - start_x); old_y = old_x = me.getX(); dragging = true; } } /** * Semi-singleton construction for graphics * @return */ private Graphics getSafeGraphics() { if (graphics == null) { graphics = getGraphics(); graphics.setXORMode(Color.white); } return graphics; } /** * Erase the old zoombox if it exists */ private void eraseZoombox() { if (start_x < 0 || start_y < 0 || old_x < 0 || old_y < 0) return; getSafeGraphics().drawRect(start_x, start_y, old_x - start_x, old_y - start_x); } @Override public void mouseMoved(MouseEvent me) {} /** * A SwingWorker to update the graphics */ protected class Updater extends SwingWorker, Map> { private boolean doneProcessing = true, stop = false; private final int start_x, end_x, start_y, end_y; /** * Update only the given rectangle * @param start_x * @param end_x * @param start_y * @param end_y */ public Updater(int start_x, int end_x, int start_y, int end_y) { super(); this.start_x = start_x; this.end_x = end_x; this.start_y = start_y; this.end_y = end_y; } /** * Calculate all points in the rectangle using the FractalModel * @return the resulting points */ @Override protected Map doInBackground() { Map results = new HashMap<>(); for (int repetitions = REPETITIONS_MAX / STEPS; repetitions <= REPETITIONS_MAX && !stop; repetitions += REPETITIONS_MAX / STEPS) { for (int x = start_x; x < end_x && !stop; x++) { for (int y = start_y; y < end_y && !stop; y++) { Point p = new Point(x, y); results.put(p, fractalModel.getMandelNumber(getRealX(x), getRealY(y), repetitions)); } } // Since we're always publishing the same object, there's no point in calling publish if process() wasn't called yet if (doneProcessing && !stop) { doneProcessing = false; publish(results); } setProgress(repetitions * 100 / REPETITIONS_MAX); } return results; } /** * Process a list of points, and update the progress bar * @param results */ @Override protected void process(List> results) { if (progressView != null) { progressView.setValue(); } for (Map resultMap : results) { if (stop || dragging) break; for (Entry result : resultMap.entrySet()) { int rgbValue = colors[result.getValue()]; int[] rgb = { (rgbValue >> 16) & 0xff, (rgbValue >> 8) & 0xff, rgbValue & 0xff }; setPixel(result.getKey().x, result.getKey().y, rgb); } } doneProcessing = true; } /** * Tell the updater to stop */ public void stop() { stop = true; } } /** * A point on the grid */ protected class Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object another) { if (another == null || another.getClass() != Point.class) { return false; } Point that = (Point) another; return that.x == x && that.y == y; } @Override public int hashCode() { return (x << 8) | y; } } /** * A special ProgressBar which is hidden when full, and takes the average * of the progresses of all Updaters as its value */ protected class ProgressView extends JProgressBar { public ProgressView() { setMaximum(100); setMinimum(0); setValue(0); } @Override public final void setValue(int n) { setVisible(n != 100); super.setValue(n); } /** * Calculate the value as the average of the progresses of all Updaters */ public void setValue() { int sum = 0, count = 0; for (Updater updater : updaters) { sum += updater.getProgress(); count++; } setValue(sum / count); } } }