package ifs;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Random;
import java.util.Scanner;

import javax.imageio.ImageIO;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFileChooser;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

public class IFSCanvas extends JPanel {

	private int iterations = 100;
	private int iterationsForFastBatch = 50;
	
	private final static int FAST_BATCH_SIZE = 2500;
	private Point2D.Double[] fastbatch;
	private Random random = new Random();

	private IFSRunner runner;

	private BufferedImage image;
	private int leftgap, topgap, unitSquareSize;
	private ArrayList<AffineMap> maps = new ArrayList<AffineMap>();
	private int selectedMapIndex = -1;
	private SelectionInfo si;
	private final static Color DARK_BLUE = new Color(0,0,150);
	private final static Color DARK_MAGENTA = new Color(155,0,155);
	private MenuHandler menuHandler = new MenuHandler();

	private boolean showMaps = true;
	private volatile boolean colorCode = false;

	private JFileChooser fileDialog;  // File dialog for open and save commands.
	
	private Color[] palette = { new Color(150,0,0), new Color(0,150,0), new Color(0,0,150),
			new Color(0,150,150), new Color(150,0,150), new Color(150,150,0),
			Color.RED, Color.GREEN, Color.BLUE, Color.CYAN, Color.MAGENTA, Color.GRAY,
			new Color(255,150,150), new Color(150,255,150), new Color(150,255,255), 
			new Color(255,150,255), new Color(255,255,150), Color.BLACK
	};



	public IFSCanvas() {
		MouseHandler mh = new MouseHandler();
		addMouseListener(mh);
		addMouseMotionListener(mh);
		addComponentListener(new ComponentAdapter() {
			public void componentResized(ComponentEvent e) {
				synchronized (IFSCanvas.this) {
					setShowMaps(true);
					if (maps.size() > 0 && selectedMapIndex == -1)
						select(0);
					incJobnum();
					image = null;
					int w = getWidth();
					int h = getHeight();
					if (w >= h) {
						unitSquareSize = h - 20;
						topgap = 10;
						leftgap = (w - unitSquareSize) / 2;
					}
					else {
						unitSquareSize = w - 20;
						topgap = (h - unitSquareSize) / 2;
						leftgap = 10;
					}
				}
			}
		});
		fastbatch = new Point2D.Double[FAST_BATCH_SIZE];
		for (int i = 0; i < FAST_BATCH_SIZE; i++)
			fastbatch[i] = new Point2D.Double();
		runner = new IFSRunner();
		runner.start();
	}

	int getIterationCount() {
		return iterations;
	}

	synchronized void setIterationCount(int iterations) {
		if (iterations >= 10 && iterations != this.iterations) {
			this.iterations = iterations;
			if (maps.size() > 1)
				clearImage();
		}
	}

	int getIterationsForPreview() {
		return iterationsForFastBatch;
	}

	synchronized void setIterationsForPreview(int iterationsForFastBatch) {
		if (iterationsForFastBatch >= 10 && iterationsForFastBatch != this.iterationsForFastBatch) {
			this.iterationsForFastBatch = iterationsForFastBatch;
			if (maps.size() > 1 && selectedMapIndex >= 0)
				repaint();
		}
	}

	public JMenuBar getMenuBar(boolean forApplet) {
		return menuHandler.makeMenus(forApplet);
	}
	
	protected void paintComponent(Graphics g) {
		checkImage();
		if (image != null && selectedMapIndex == -1 && maps.size() > 1) {
			g.drawImage(image,0,0,null);
		}
		else {
			g.setColor(Color.WHITE);
			g.fillRect(0, 0, getWidth(), getHeight());
		}
		if (! showMaps)
			return;
		Graphics2D g2 = (Graphics2D)g;
		g2.translate(leftgap,topgap);
		if (selectedMapIndex >= 0 && maps.size() > 1) {
			makeFastBatch();
			g2.setColor(DARK_MAGENTA);
			for (Point2D.Double pt : fastbatch) {
				int a = (int)(pt.x*unitSquareSize);
				int b = (int)((1-pt.y)*unitSquareSize);
				g2.fillRect(a-1,b-1,3,3);
			}
		}
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		for (int i = 0; i < maps.size(); i++) {
			if (i != selectedMapIndex) {
				AffineMap map = maps.get(i);
				drawAffineMap(map, g2, unitSquareSize, unitSquareSize);
			}
		}
		if (selectedMapIndex >= 0) {
			AffineMap map = maps.get(selectedMapIndex);
			if (si == null)
				si = new SelectionInfo(map,unitSquareSize,unitSquareSize);
			drawSelectedAffineMap(map, g2, unitSquareSize, unitSquareSize);
		}
		g2.translate(-leftgap,-topgap);
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		g2.setColor(new Color(0,0,0,60));
		if (leftgap > 3) {
			g2.fillRect(0, 0, leftgap-3, getHeight());
			g2.fillRect(leftgap+unitSquareSize+3, 0, leftgap+1, getHeight());
		}
		if (topgap > 3) {
			g2.fillRect(leftgap-3, 0, unitSquareSize+6, topgap-3);
			g2.fillRect(leftgap-3, topgap+unitSquareSize+3, unitSquareSize+6, topgap+1);
		}
		Graphics g1 = g.create(leftgap-3, topgap-3, unitSquareSize+6, unitSquareSize+6);
		for (int i = 2; i >= 0; i--) {
			int s = unitSquareSize + 6;
			g1.setColor(Color.RED);
			g1.drawLine(0,i,s,i);
			g1.setColor(Color.GREEN);
			g1.drawLine(s-i-1,0,s-i-1,s);
			g1.setColor(Color.BLUE);
			g1.drawLine(0,s-i-1,s,s-i-1);
			g1.setColor(Color.CYAN);
			g1.drawLine(i,0,i,s);
		}
		g1.dispose();
	}

	synchronized private void checkImage() {
		if ((image == null || image.getWidth() != getWidth() || image.getHeight() != getHeight()) 
				&& maps.size() > 1 && selectedMapIndex == -1) {
			image = null;
			image = new BufferedImage(getWidth(),getHeight(),BufferedImage.TYPE_INT_RGB);
			Graphics ig = image.getGraphics();
			ig.setColor(Color.WHITE);
			ig.fillRect(0,0,image.getWidth(),image.getHeight());
			ig.dispose();
		}
	}

	private synchronized void select(int selectedIndex) {
		selectedMapIndex = selectedIndex;
		menuHandler.flipSelected.setEnabled(selectedMapIndex >= 0);
		menuHandler.deleteSelected.setEnabled(selectedMapIndex >= 0);
		si = null;
		repaint();
		incJobnum();
	}
	
	private synchronized void deleteSelectedMap() {
		if (selectedMapIndex == -1)
			return;
		incJobnum();
		clearImage();
		maps.remove(selectedMapIndex);
		select(-1);
	}
	
	private synchronized void flipSelectedMap() {
		if (selectedMapIndex == -1)
			return;
		AffineMap oldMap = maps.get(selectedMapIndex);
		AffineMap map = new AffineMap(oldMap.x0,oldMap.y0,oldMap.x2,oldMap.y2,oldMap.x1,oldMap.y1);
		setMapAt(selectedMapIndex,map);
	}

	private synchronized void setShowMaps(boolean show) {
		if (show == showMaps)
			return;
		showMaps =  show;
		menuHandler.showMapsToggle.setState(show);
		select(-1);
	}

	private void clearImage() {
		if (image == null)
			return;
		Graphics g = image.getGraphics();
		g.setColor(Color.WHITE);
		g.fillRect(0,0,image.getWidth(),image.getHeight());
		g.dispose();
	}

	private synchronized void addMap(AffineMap map, boolean select) {
		incJobnum();
		clearImage();
		maps.add(map);
		setShowMaps(true);
		if (select)
			select(maps.size()-1);
		else
			select(-1);
	}

	private synchronized void clear() {
		clearImage();
		incJobnum();
		maps.clear();
		select(-1);
		notify();
	}

	private synchronized void setMapAt(int index, AffineMap map) {
		clearImage();
		incJobnum();
		maps.set(index,map);
		si = null;
		repaint();
		notify();
	}
	
	private synchronized void setColorCode(boolean show) {
		if (show == colorCode)
			return;
		colorCode = show;
		menuHandler.colorCodeCommand.setState(colorCode);
		clearImage();
		incJobnum();
		repaint();
	}

	private void makeFastBatch() {
		double[] cumulativeAreas = new double[maps.size()];
		cumulativeAreas[0] = maps.get(0).area;
		for (int i = 1; i < cumulativeAreas.length; i++)
			cumulativeAreas[i] = cumulativeAreas[i-1] + maps.get(i).area;
		double totalArea = cumulativeAreas[cumulativeAreas.length-1];
		random.setSeed(17);
		for (Point2D.Double pt : fastbatch) {
			pt.setLocation(random.nextDouble(), random.nextDouble());
			for (int i = 0; i < iterationsForFastBatch; i++) {
				double r = random.nextDouble() * totalArea;
				int j = 0;
				while (j < cumulativeAreas.length-1 && r > cumulativeAreas[j])
					j++;
				maps.get(j).apply(pt);
			}
		}
	}

	private void drawLine(Graphics2D g, int width, int height, double x1, double y1, double x2, double y2) {
		int a,b,c,d;
		a = (int)(x1*width);
		b = (int)((1-y1)*height);
		c = (int)(x2*width);
		d = (int)((1-y2)*height);
		g.drawLine(a,b,c,d);
	}

	private void drawAffineMap(AffineMap map, Graphics2D g, int width, int height) {
		g.setColor(Color.RED);
		drawLine(g,width,height,map.x2,map.y2,map.x3,map.y3);
		g.setColor(Color.GREEN);
		drawLine(g,width,height,map.x3,map.y3,map.x1,map.y1);
		g.setColor(Color.BLUE);
		drawLine(g,width,height,map.x1,map.y1,map.x0,map.y0);
		g.setColor(Color.CYAN);
		drawLine(g,width,height,map.x0,map.y0,map.x2,map.y2);
	}

	private void drawSelectedAffineMap(AffineMap map, Graphics2D g, int width, int height) {
		Polygon quad = new Polygon();
		quad.addPoint((int)(map.x0*width), (int)((1-map.y0)*height));
		quad.addPoint((int)(map.x1*width), (int)((1-map.y1)*height));
		quad.addPoint((int)(map.x3*width), (int)((1-map.y3)*height));
		quad.addPoint((int)(map.x2*width), (int)((1-map.y2)*height));
		g.setColor(new Color(0,0,255,50));
		g.fillPolygon(quad);
		Stroke saveStroke = g.getStroke();
		g.setStroke(new BasicStroke((float)2.5));
		drawAffineMap(map,g,width,height);
		si.draw(g,width,height);
		g.setStroke(saveStroke);
	}
	
	synchronized void incJobnum() {
		runner.currentJobnum++;
		notify();
	}

	private class IFSRunner extends Thread {
		int jobnum;
		final int batchsize = 500;
		Point2D.Double[] points;
		int[] colorInfo;
		int currentJobnum;
		IFSRunner() {
			try {
				setPriority(getPriority() - 1);
				setDaemon(true);
			}
			catch (Exception e) {
			}
		}
		private void makeBatch() {
			double[] cumulativeAreas = new double[maps.size()];
			cumulativeAreas[0] = maps.get(0).area;
			for (int i = 1; i < cumulativeAreas.length; i++)
				cumulativeAreas[i] = cumulativeAreas[i-1] + maps.get(i).area;
			double totalArea = cumulativeAreas[cumulativeAreas.length-1];
			for (int p = 0; p < batchsize; p++) {
				Point2D.Double pt = points[p];
				if (jobnum != currentJobnum)
					return;
				pt.setLocation(Math.random(), Math.random());
				for (int i = 0; i < iterations; i++) {
					double r = Math.random() * totalArea;
					colorInfo[p] = 0;
					while (colorInfo[p] < cumulativeAreas.length-1 && r > cumulativeAreas[colorInfo[p]])
						colorInfo[p]++;
					maps.get(colorInfo[p]).apply(pt);
				}
			}
		}
		public void run() {
			points =  new Point2D.Double[batchsize];
			colorInfo = new int[batchsize];
			int[] intPalette;
			for (int i = 0; i < batchsize; i++)
				points[i] = new Point2D.Double();
			while (true) {
				try {
					synchronized(IFSCanvas.this) {
						while (selectedMapIndex > -1 || maps.size() < 2) {
							try {
								IFSCanvas.this.wait();
							}
							catch (InterruptedException e) {
							}
						}
						jobnum = currentJobnum;
						intPalette = new int[maps.size()];
						for (int i = 0; i < intPalette.length; i++) {
							intPalette[i] = palette[ i % intPalette.length ].getRGB();
						}
					}
					while(true) {
						if (jobnum != currentJobnum)
							break;
						makeBatch(); 
						boolean newpoint = false;
						boolean pointinrange = false;
						synchronized(IFSCanvas.this) {
							if (jobnum != currentJobnum)
								break;
							checkImage();
							for (int i = 0; i < batchsize; i++) {
								Point2D.Double pt = points[i];
								int x = leftgap + (int)(pt.x*unitSquareSize);
								int y = topgap + (int)((1-pt.y)*unitSquareSize);
								try {
									int rgb = image.getRGB(x,y);
									if ((rgb & 0xFFFFFF) == 0xFFFFFF) {
										newpoint = true;
										if (colorCode)
											image.setRGB(x,y,intPalette[colorInfo[i]]);
										else
											image.setRGB(x,y,0xFF000000);
									}
									pointinrange = true;
								}
								catch (Exception e) {
								}
							}
						}
						if (!newpoint && jobnum == currentJobnum) {
							System.out.println("No new points");
							synchronized(IFSCanvas.this) {
								while (jobnum == currentJobnum) { 
									try {
										IFSCanvas.this.wait();
									}
									catch (InterruptedException e) {
									}
								}
							}
							break;
						}
						if (!pointinrange && jobnum == currentJobnum) {
							System.out.println("All points out of range");
							synchronized(IFSCanvas.this) {
								while (jobnum == currentJobnum) {
									try {
										wait();
									}
									catch (InterruptedException e) {
									}
								}
							}
							break;
						}
						repaint();
					}
				}
				catch (Exception e) {
				}
			}
		}
	}

	private static class SelectionInfo {
		double centerX, centerY;
		double rotateHandleX, rotateHandleY;
		double[] cornerX = new double[4];
		double[] cornerY = new double[4];
		double[] arrowX = new double[5];  // centers of arrows; last spot is for arrow at head of rotate handle
		double[] arrowY = new double[5];
		double[] arrowDX = new double[5]; // arrow directions, unit vectors
		double[] arrowDY = new double[5];
		SelectionInfo(AffineMap map, int w, int h) {
			make(map,w,h);
		}
		void make(AffineMap map, int width, int height) {
			cornerX[0] = map.x0;
			cornerY[0] = map.y0;
			cornerX[1] = map.x1;
			cornerY[1] = map.y1;
			cornerX[2] = map.x2;
			cornerY[2] = map.y2;
			cornerX[3] = map.x3;
			cornerY[3] = map.y3;
			centerX = centerY = 0;
			for (int i = 0; i < 4; i++) {
				centerX += cornerX[i];
				centerY += cornerY[i];
			}
			centerX /= 4;
			centerY /= 4;
			double dx = map.x1 - map.x0;
			double dy = map.y1 - map.y0;
			double length = Math.sqrt(dx*dx + dy*dy);
			dx = dx/length;
			dy = dy/length;
			double temp = dx*Math.cos(Math.PI/6) - dy*Math.sin(Math.PI/6);
			dy= dx*Math.sin(Math.PI/6) + dy*Math.cos(Math.PI/6);
			dx = temp;
			rotateHandleX = centerX + (80.0/width) * dx;
			rotateHandleY = centerY + (80.0/width) * dy;
			arrowX[0] = (map.x0 + map.x1)/2;
			arrowY[0] = (map.y0 + map.y1)/2;
			arrowX[1] = (map.x2 + map.x3)/2;
			arrowY[1] = (map.y2 + map.y3)/2;
			arrowX[2] = (map.x0 + map.x2)/2;
			arrowY[2] = (map.y0 + map.y2)/2;
			arrowX[3] = (map.x1 + map.x3)/2;
			arrowY[3] = (map.y1 + map.y3)/2;
			arrowX[4] = rotateHandleX;
			arrowY[4] = rotateHandleY;
			arrowDX[0] = map.x1 - map.x0;
			arrowDY[0] = map.y1 - map.y0;
			arrowDX[1] = map.x2 - map.x3;
			arrowDY[1] = map.y2 - map.y3;
			arrowDX[2] = map.x0 - map.x2;
			arrowDY[2] = map.y0 - map.y2;
			arrowDX[3] = map.x3 - map.x1;
			arrowDY[3] = map.y3 - map.y1;
			arrowDX[4] = -dy;
			arrowDY[4] = dx;
			for (int i = 0; i < 4; i++) {
				length = Math.sqrt( arrowDX[i]*arrowDX[i] + arrowDY[i]*arrowDY[i] );
				arrowDX[i] /= length;
				arrowDY[i] /= length;
			}
		}
		void draw(Graphics2D g, int width, int height) {
			double pw = 1.0/width;
			double ph = 1.0/height;
			g.setColor(Color.BLACK);
			for (int i = 0; i < 4; i++)
				g.fillRect((int)(cornerX[i]*width) - 4, (int)((1-cornerY[i])*height) - 4, 9, 9);
			g.setColor(DARK_BLUE);
			g.setStroke(new BasicStroke((float)2.5));
			g.draw(new Line2D.Double(centerX*width, (1-centerY)*height, rotateHandleX*width, (1-rotateHandleY)*height));
			g.fillOval((int)(centerX*width) - 4, (int)((1-centerY)*width) - 4, 9, 9);
			g.setStroke(new BasicStroke((float)5.0));
			for (int i = 0; i < 5; i++) {
				g.setColor(i < 4 ? Color.BLACK : DARK_BLUE);
				double a = 20;  // half of arrow length
				double x1 = arrowX[i] + a*pw*arrowDX[i];
				double y1 = arrowY[i] + a*ph*arrowDY[i];
				double x2 = arrowX[i] - a*pw*arrowDX[i];
				double y2 = arrowY[i] - a*ph*arrowDY[i];
				g.draw(new Line2D.Double(x1*width,(1-y1)*height,x2*width,(1-y2)*height));
				double x3 = x1 - a*pw*(arrowDX[i] - arrowDY[i])/6;
				double y3 = y1 - a*pw*(arrowDY[i] + arrowDX[i])/6;
				double x4 = x1 - a*pw*(arrowDX[i] + arrowDY[i])/6;
				double y4 = y1 - a*pw*(arrowDY[i] - arrowDX[i])/6;
				g.draw(new Line2D.Double(x1*width,(1-y1)*height,x3*width,(1-y3)*height));
				g.draw(new Line2D.Double(x1*width,(1-y1)*height,x4*width,(1-y4)*height));
				x3 = x2 + a*pw*(arrowDX[i] - arrowDY[i])/6;
				y3 = y2 + a*pw*(arrowDY[i] + arrowDX[i])/6;
				x4 = x2 + a*pw*(arrowDX[i] + arrowDY[i])/6;
				y4 = y2 + a*pw*(arrowDY[i] - arrowDX[i])/6;
				g.draw(new Line2D.Double(x2*width,(1-y2)*height,x3*width,(1-y3)*height));
				g.draw(new Line2D.Double(x2*width,(1-y2)*height,x4*width,(1-y4)*height));
			}
		}
	}

	private class MouseHandler implements MouseListener, MouseMotionListener {

		boolean dragging;  // information for dragging
		int whatIsBeingDragged;
		AffineMap dragMap;
		final static int
		CORNER0 = 0, CORNER1 = 1, CORNER2 = 2, CORNER3 = 3,
		SKEW0 = 4, SKEW1 = 5, SKEW2 = 6, SKEW3 = 7,
		SIDE0 = 8, SIDE1 = 9, SIDE2 = 10, SIDE3 = 11,
		ROTATE = 12, INSIDE = 13, NONE = -1;
		int startX, startY;
		int prevX, prevY;
		double dragLineDX, dragLineDY;
		boolean shifted;
		int insideDragWhileShifted;  // 0 = not dragging yet; 1 = horizontal drag; 2 = vertical drag

		boolean hitPoint(int mouseX, int mouseY, double x, double y) {
			int xint = leftgap + (int)(x*unitSquareSize);
			int yint = topgap + (int)((1-y)*unitSquareSize);
			int xdiff = xint - mouseX;
			int ydiff = yint - mouseY;
			return xdiff*xdiff + ydiff*ydiff < 36;
		}

		boolean hitLine(int mouseX, int mouseY, double x1, double y1, double x2, double y2, int epsilon) {
			double dx = x2 - x1;
			double dy = y2 - y1;
			double length = Math.sqrt(dx*dx+dy*dy);
			dx /= length;
			dy /= length;
			double ptx = (double)(mouseX - leftgap) / unitSquareSize;
			double pty = 1.0 - (double)(mouseY - topgap) / unitSquareSize;
			double distance = (ptx - x1)*(-dy) + (pty - y1)*dx;
			double distanceInPixels = distance*unitSquareSize;
			if (Math.abs(distanceInPixels) > 4)
				return false;
			double projlen = (ptx - x1)*(-dy) + (pty - y1)*dx;
			double projx = ptx - projlen*(-dy);
			double projy = pty - projlen*dx;
			double eps = 1.0/unitSquareSize * epsilon;
			return ( (x1 - eps <= projx && projx <= x2 + eps || x2 - eps <= projx && projx <= x1 + eps)
					&& (y1 - eps <= projy && projy <= y2 + eps || y2 - eps <= projy && projy <= y1 + eps) );
		}

		boolean inside(int mouseX, int mouseY, AffineMap map) {
			double ptx = (double)(mouseX - leftgap) / unitSquareSize;
			double pty = 1.0 - (double)(mouseY - topgap) / unitSquareSize;
			return map.containsPoint(ptx, pty);
		}

		boolean hitArrow(int mouseX, int mouseY, double centerX, double centerY, double dx, double dy) {
			double pw = 1.0/unitSquareSize;
			double x1 = centerX + dx*pw*20;
			double y1 = centerY + dy*pw*20;
			double x2 = centerX - dx*pw*20;
			double y2 = centerY - dy*pw*20;
			return hitLine(mouseX, mouseY, x1, y1, x2, y2, 7);
		}

		public void mousePressed(MouseEvent e) {
			if (!showMaps)
				return;
			if (dragging)
				return;
			shifted = e.isShiftDown() || e.isMetaDown();
			insideDragWhileShifted = 0;
			int x = e.getX();
			int y = e.getY();
			int hit = NONE;
			if (selectedMapIndex > -1) {
				AffineMap map = maps.get(selectedMapIndex);
				if (si == null)
					si = new SelectionInfo(map,unitSquareSize,unitSquareSize);
				dragLineDX = Double.NaN;
				if (hitPoint(x,y,si.cornerX[0],si.cornerY[0])) {
					hit = CORNER0;
					dragLineDX = si.cornerX[3] - si.cornerX[0];
					dragLineDY = si.cornerY[3] - si.cornerY[0];
					double length = Math.sqrt(dragLineDX*dragLineDX + dragLineDY*dragLineDY);
					dragLineDX /= length;
					dragLineDY /= length;
				}
				else if (hitPoint(x,y,si.cornerX[1],si.cornerY[1])) {
					hit = CORNER1;
					dragLineDX = si.cornerX[2] - si.cornerX[1];
					dragLineDY = si.cornerY[2] - si.cornerY[1];
					double length = Math.sqrt(dragLineDX*dragLineDX + dragLineDY*dragLineDY);
					dragLineDX /= length;
					dragLineDY /= length;
				}
				else if (hitPoint(x,y,si.cornerX[2],si.cornerY[2])) {
					hit = CORNER2;
					dragLineDX = si.cornerX[1] - si.cornerX[2];
					dragLineDY = si.cornerY[1] - si.cornerY[2];
					double length = Math.sqrt(dragLineDX*dragLineDX + dragLineDY*dragLineDY);
					dragLineDX /= length;
					dragLineDY /= length;
				}
				else if (hitPoint(x,y,si.cornerX[3],si.cornerY[3])) {
					hit = CORNER3;
					dragLineDX = si.cornerX[0] - si.cornerX[3];
					dragLineDY = si.cornerY[0] - si.cornerY[3];
					double length = Math.sqrt(dragLineDX*dragLineDX + dragLineDY*dragLineDY);
					dragLineDX /= length;
					dragLineDY /= length;
				}
				else if (hitArrow(x,y,si.arrowX[0],si.arrowY[0],si.arrowDX[0],si.arrowDY[0])) {
					hit = SKEW0;
					dragLineDX = si.arrowDX[0];
					dragLineDY = si.arrowDY[0];
				}
				else if (hitArrow(x,y,si.arrowX[1],si.arrowY[1],si.arrowDX[1],si.arrowDY[1])) {
					hit = SKEW1;
					dragLineDX = si.arrowDX[1];
					dragLineDY = si.arrowDY[1];
				}
				else if (hitArrow(x,y,si.arrowX[2],si.arrowY[2],si.arrowDX[2],si.arrowDY[2])) {
					hit = SKEW2;
					dragLineDX = si.arrowDX[2];
					dragLineDY = si.arrowDY[2];
				}
				else if (hitArrow(x,y,si.arrowX[3],si.arrowY[3],si.arrowDX[3],si.arrowDY[3])) {
					hit = SKEW3;
					dragLineDX = si.arrowDX[3];
					dragLineDY = si.arrowDY[3];
				}
				else if (hitArrow(x,y,si.arrowX[4],si.arrowY[4],si.arrowDX[4],si.arrowDY[4]))
					hit = ROTATE;
				else if (hitLine(x,y,map.x0,map.y0,map.x1,map.y1,5)) {
					hit = SIDE0;
					dragLineDX = si.arrowDX[2];
					dragLineDY = si.arrowDY[2];
				}
				else if (hitLine(x,y,map.x3,map.y3,map.x2,map.y2,5)) {
					hit = SIDE1;
					dragLineDX = si.arrowDX[2];
					dragLineDY = si.arrowDY[2];
				}
				else if (hitLine(x,y,map.x0,map.y0,map.x2,map.y2,5)) {
					hit = SIDE2;
					dragLineDX = si.arrowDX[0];
					dragLineDY = si.arrowDY[0];
				}
				else if (hitLine(x,y,map.x3,map.y3,map.x1,map.y1,5)) {
					hit = SIDE3;
					dragLineDX = si.arrowDX[0];
					dragLineDY = si.arrowDY[0];
				}
				else if (hitLine(x,y,si.centerX,si.centerY,si.rotateHandleX,si.rotateHandleY,5))
					return;
				else if (inside(x,y,map))
					hit = INSIDE;
				if (hit != NONE) {
					dragging = true;
					dragMap = map;
					startX = prevX = x;
					startY = prevY = y;
					whatIsBeingDragged = hit;
					repaint();
					return;
				}
			}
			for (int i = maps.size()-1; i >= 0; i--) {
				if (i != selectedMapIndex) {
					AffineMap map = maps.get(i);
					if (hitLine(x,y,map.x0,map.y0,map.x1,map.y1,4) || hitLine(x,y,map.x1,map.y1,map.x3,map.y3,4) ||
							hitLine(x,y,map.x3,map.y3,map.x2,map.y2,4) || hitLine(x,y,map.x2,map.y2,map.x0,map.y0,4)) {
						select(i);
						return;
					}
				}
			}
			double ptx = (double)(x - leftgap) / unitSquareSize;
			double pty = 1.0 - (double)(y - topgap) / unitSquareSize;
			for (int i = maps.size()-1; i >= 0; i--)
				if (i != selectedMapIndex && maps.get(i).containsPoint(ptx, pty)) {
					select(i);
					return;
				}
			select(-1);
		}

		public void mouseReleased(MouseEvent e) {
			dragging = false;
			dragMap = null;
		}

		public void mouseDragged(MouseEvent e) {
			if (!dragging)
				return;
			int x = e.getX();
			int y = e.getY();
			double offsetFromStartX = (double)(x - startX)/unitSquareSize;
			double offsetFromStartY = (double)(-(y - startY))/unitSquareSize;
			if ( ! Double.isNaN(dragLineDX) )  {
				double dot = dragLineDX*offsetFromStartX + dragLineDY*offsetFromStartY;
				offsetFromStartX = dragLineDX * dot;
				offsetFromStartY = dragLineDY * dot;
			}
			AffineMap map = dragMap;
			AffineMap newMap = null;
			double startX_real = (double)(startX-leftgap)/unitSquareSize;
			double startY_real = 1 - (double)(startY-topgap)/unitSquareSize;
			double centerX = (map.x1 + map.x2)/2;
			double centerY = (map.y1 + map.y2)/2;
			try {
				switch (whatIsBeingDragged) {
				case INSIDE:
					if (shifted) {
						if (insideDragWhileShifted == 0) {
							if (Math.abs(offsetFromStartX) + Math.abs(offsetFromStartY) < 10.0/unitSquareSize)
								break;
							if (Math.abs(offsetFromStartX) >= Math.abs(offsetFromStartY))
								insideDragWhileShifted = 1;
							else
								insideDragWhileShifted = 2;
						}
						if (insideDragWhileShifted == 1)
							offsetFromStartY = 0;
						else
							offsetFromStartX = 0;
					}
					newMap = new AffineMap(map.x0+offsetFromStartX,map.y0+offsetFromStartY,map.x1+offsetFromStartX,
							map.y1+offsetFromStartY,map.x2+offsetFromStartX,map.y2+offsetFromStartY);
					break;
				case CORNER0:
				case CORNER1:
				case CORNER2:
				case CORNER3:
					double startDist = Math.sqrt((centerX-startX_real)*(centerX-startX_real) + (centerY-startY_real)*(centerY-startY_real));
					double newx, newy, newDist,ratio;
					double newCenterX = centerX;
					double newCenterY = centerY;
					if (!shifted) {
						newCenterX = centerX + offsetFromStartX/2;
						newCenterY = centerY + offsetFromStartY/2;
					}
					if (whatIsBeingDragged == CORNER0) {
						newx = map.x0+offsetFromStartX;
						newy = map.y0+offsetFromStartY;
					}
					else if (whatIsBeingDragged == CORNER1) {
						newx = map.x1+offsetFromStartX;
						newy = map.y1+offsetFromStartY;
					}
					else if (whatIsBeingDragged == CORNER2) {
						newx = map.x2+offsetFromStartX;
						newy = map.y2+offsetFromStartY;
					}
					else {
						newx = map.x3+offsetFromStartX;
						newy = map.y3+offsetFromStartY;
					}
					newDist = Math.sqrt((newCenterX-newx)*(newCenterX-newx) + (newCenterY-newy)*(newCenterY-newy));
					if (newDist < 1e-10)
						break;
					ratio = newDist/startDist;
					if ( (newCenterX - newx > 0) != (centerX - startX_real > 0) ||
							(newCenterY - newy > 0) != (centerY - startY_real > 0) )
						ratio = -ratio;
					newMap = new AffineMap(newCenterX+ratio*(map.x0-centerX),newCenterY+ratio*(map.y0-centerY),
							newCenterX+ratio*(map.x1-centerX),newCenterY+ratio*(map.y1-centerY),
							newCenterX+ratio*(map.x2-centerX),newCenterY+ratio*(map.y2-centerY));
					break;
				case SKEW0:
				case SIDE0:
					if (shifted)
						newMap = new AffineMap(map.x0+offsetFromStartX,map.y0+offsetFromStartY,map.x1+offsetFromStartX,
								map.y1+offsetFromStartY,map.x2-offsetFromStartX,map.y2-offsetFromStartY);
					else
						newMap = new AffineMap(map.x0+offsetFromStartX,map.y0+offsetFromStartY,map.x1+offsetFromStartX,
							map.y1+offsetFromStartY,map.x2,map.y2);
					break;
				case SKEW1:
				case SIDE1:
					if (shifted)
						newMap = new AffineMap(map.x0-offsetFromStartX,map.y0-offsetFromStartY,map.x1-offsetFromStartX,
								map.y1-offsetFromStartY,map.x2+offsetFromStartX,map.y2+offsetFromStartY);
					else
						newMap = new AffineMap(map.x0,map.y0,map.x1,
								map.y1,map.x2+offsetFromStartX,map.y2+offsetFromStartY);
					break;
				case SKEW2:
				case SIDE2:
					if (shifted)
						newMap = new AffineMap(map.x0+offsetFromStartX,map.y0+offsetFromStartY,map.x1-offsetFromStartX,
								map.y1-offsetFromStartY,map.x2+offsetFromStartX,map.y2+offsetFromStartY);
					else
						newMap = new AffineMap(map.x0+offsetFromStartX,map.y0+offsetFromStartY,map.x1,
								map.y1,map.x2+offsetFromStartX,map.y2+offsetFromStartY);
					break;
				case SKEW3:
				case SIDE3:
					if (shifted)
						newMap = new AffineMap(map.x0-offsetFromStartX,map.y0-offsetFromStartY,map.x1+offsetFromStartX,
								map.y1+offsetFromStartY,map.x2-offsetFromStartX,map.y2-offsetFromStartY);
					else
						newMap = new AffineMap(map.x0,map.y0,map.x1+offsetFromStartX,
								map.y1+offsetFromStartY,map.x2,map.y2);
					break;
				case ROTATE:
					double y_real = 1 - (double)(y-topgap)/unitSquareSize;
					double x_real = (double)(x-leftgap)/unitSquareSize;
					double original_angle = Math.atan2(startY_real - centerY, startX_real - centerX);
					double angle = Math.atan2(y_real - centerY, x_real - centerX);
					double change = angle - original_angle;
					if (shifted)
						change = Math.PI/12*((int)(change/(Math.PI/12)));
					double s = Math.sin(change);
					double c = Math.cos(change);
					double a0,b0,a1,b1,a2,b2;
					a0 = centerX + (c*(map.x0-centerX)-s*(map.y0-centerY));
					b0 = centerY + (s*(map.x0-centerX)+c*(map.y0-centerY));
					a1 = centerX + (c*(map.x1-centerX)-s*(map.y1-centerY));
					b1 = centerY + (s*(map.x1-centerX)+c*(map.y1-centerY));
					a2 = centerX + (c*(map.x2-centerX)-s*(map.y2-centerY));
					b2 = centerY + (s*(map.x2-centerX)+c*(map.y2-centerY));
					newMap = new AffineMap(a0,b0,a1,b1,a2,b2);
					break;
				}
				if (newMap != null) {
					setMapAt(selectedMapIndex,newMap);
				}
			}
			catch (IllegalArgumentException z) {
			}
			prevX = x;
			prevY = y;
		}

		public void mouseMoved(MouseEvent e) { }
		public void mouseClicked(MouseEvent e) { }
		public void mouseEntered(MouseEvent e) { }
		public void mouseExited(MouseEvent e) { }

	}

	private class MenuHandler implements ActionListener {
		String commandKey;
		JCheckBoxMenuItem showMapsToggle;
		JCheckBoxMenuItem colorCodeCommand;
		JMenuItem deleteSelected, flipSelected;
		JMenuBar makeMenus(boolean forApplet) {
			JMenuBar menuBar = new JMenuBar();
			if ( ! forApplet ) {
				JMenu fileMenu = new JMenu("File");
				addItem(fileMenu,"Save Map Data...","S");
				addItem(fileMenu,"Open Map Data File...","O");
				fileMenu.addSeparator();
				addItem(fileMenu,"Save PNG Image...", "P");
				addItem(fileMenu,"Save PNG Image Including Maps...", "shift P");
				fileMenu.addSeparator();
				addItem(fileMenu, "Clear All Maps");
				fileMenu.addSeparator();
				addItem(fileMenu,"Quit", "Q");
				menuBar.add(fileMenu);
			}
			JMenu controlMenu = new JMenu("Control");
			menuBar.add(controlMenu);
			showMapsToggle = new JCheckBoxMenuItem("Show Maps",true);
			if (!forApplet)
				showMapsToggle.setAccelerator(makeAccel("M"));
			showMapsToggle.addActionListener(this);
			controlMenu.add(showMapsToggle);
			colorCodeCommand = new JCheckBoxMenuItem("Color Code Maps",false);
			if (!forApplet)
				colorCodeCommand.setAccelerator(makeAccel("K"));
			colorCodeCommand.addActionListener(this);
			controlMenu.add(colorCodeCommand);
			if (forApplet) {
				controlMenu.addSeparator();
				addItem(controlMenu, "Clear All Maps");
			}
			controlMenu.addSeparator();
			addItem(controlMenu,"Select No Map");
			addItem(controlMenu,"Select Next Map");
			controlMenu.addSeparator();
			deleteSelected = addItem(controlMenu,"Delete Selected Map", forApplet? null : "X");
			flipSelected = addItem(controlMenu,"Flip Selected Map", forApplet? null : "F");
			deleteSelected.setEnabled(false);
			flipSelected.setEnabled(false);
			controlMenu.addSeparator();
			addItem(controlMenu,"Set Iteration Count");
			JMenu addMenu = new JMenu("Add Map");
			addItem(addMenu,"Add 1/2 Size Map", forApplet? null : "1");
			addItem(addMenu,"Add 1/3 Size Map", forApplet? null : "2");
			addItem(addMenu,"Add 1/4 Size Map", forApplet? null : "3");
			addItem(addMenu,"Add 1/5 Size Map", forApplet? null : "4");
			addItem(addMenu,"Add 1/6 Size Map", forApplet? null : "5");
			addItem(addMenu,"Add 1/8 Size Map", forApplet? null : "6");
			addItem(addMenu,"Add 2/3 Size Map", forApplet? null : "7");
			addItem(addMenu,"Add 3/4 Size Map", forApplet? null : "8");
			addItem(addMenu,"Add 7/8 Size Map", forApplet? null : "9");
			menuBar.add(addMenu);
			JMenu exampleMenu = new JMenu("Examples");
			addItem(exampleMenu,"Sierpinski Triangle");
			addItem(exampleMenu,"Triangle with Inversion");
			addItem(exampleMenu,"Sierpinski Carpet");
			addItem(exampleMenu,"Koch Curve");
			addItem(exampleMenu,"Antenna");
			addItem(exampleMenu,"Spiral");
			menuBar.add(exampleMenu);
			return menuBar;
		}
		KeyStroke makeAccel(String keyStroke) {
			if (commandKey == null) {
				commandKey = "control ";
				try {
					if (System.getProperty("mrj.version") != null)
						commandKey = "meta ";
				}
				catch (SecurityException e) {
				}
			}
			return KeyStroke.getKeyStroke(commandKey + keyStroke);
		}
		JMenuItem addItem(JMenu menu, String text, String keyStroke) {
			JMenuItem item = new JMenuItem(text);
			menu.add(item);
			item.addActionListener(this);
			if (keyStroke != null)
				item.setAccelerator(makeAccel(keyStroke));
			return item;
		}
		JMenuItem addItem(JMenu menu, String text) {
			return addItem(menu,text,null);
		}
		void addMap(double sizeFactor) {
			double x = 0.5 - sizeFactor/2;
			double y = 0.5 - sizeFactor/2;
			IFSCanvas.this.addMap(new AffineMap(x,y,x+sizeFactor,y,x,y+sizeFactor),true);
		}
		public void actionPerformed(ActionEvent evt) {
			String command = evt.getActionCommand();
			if (command.equals("Quit"))
				System.exit(0);
			else if (command.equals("Save Map Data..."))
				saveMapsData();
			else if (command.equals("Open Map Data File..."))
				openMapsFile();
			else if (command.equals("Save PNG Image Including Maps..."))
				saveImage(true);
			else if (command.equals("Save PNG Image..."))
				saveImage(false);
			else if (command.equals("Clear All Maps")) {
				clear();
			}
			else if (command.equals("Select No Map")) {
				if (selectedMapIndex >= 0)
					select(-1);
			}
			else if (command.equals("Select Next Map")) {
				if (maps.size() == 0)
					return;
				setShowMaps(true);
				if (selectedMapIndex >= 0)
					select( selectedMapIndex == maps.size()-1 ? 0 : selectedMapIndex + 1);
				else
					select(0);
			}
			else if (command.equals("Delete Selected Map"))
				deleteSelectedMap();
			else if (command.equals("Flip Selected Map"))
				flipSelectedMap();
			else if (command.equals("Set Iteration Count"))
				new IterationCountDialog(IFSCanvas.this).setVisible(true);
			else if (command.equals("Show Maps"))
				setShowMaps(showMapsToggle.getState());
			else if (command.equals("Color Code Maps"))
				setColorCode(colorCodeCommand.getState());
			else if (command.equals("Add 1/2 Size Map"))
				addMap(0.5);
			else if (command.equals("Add 1/3 Size Map"))
				addMap(1.0/3.0);
			else if (command.equals("Add 1/4 Size Map"))
				addMap(0.25);
			else if (command.equals("Add 1/5 Size Map"))
				addMap(0.2);
			else if (command.equals("Add 1/6 Size Map"))
				addMap(1.0/6.0);
			else if (command.equals("Add 1/8 Size Map"))
				addMap(0.125);
			else if (command.equals("Add 2/3 Size Map"))
				addMap(2.0/3.0);
			else if (command.equals("Add 3/4 Size Map"))
				addMap(0.75);
			else if (command.equals("Add 7/8 Size Map"))
				addMap(0.875);
			else if (command.equals("Sierpinski Triangle")) {
				double t = 0.25;
				installExample(new double[][] { 
						{ 0, 0, 0.5, 0, 0, 0.5 },
						{ 0.5, 0, 1, 0, 0.5, 0.5 },
						{ t, 0.5, t+0.5, 0.5, t, 1 }
				});
			}
			else if (command.equals("Triangle with Inversion")) {
				double t = 0.25;
				installExample(new double[][] { 
						{ 0, 0, 0.5, 0, 0, 0.5 },
						{ 0.5, 0, 1, 0, 0.5, 0.5 },
						{ t, 1, t+0.5, 1, t, 0.5 }
				});
			}
			else if (command.equals("Sierpinski Carpet")) {
				double t = 1.0/3.0;
				double s = 2*t;
				installExample(new double[][] {
						{ 0,0, t,0, 0,t },
						{ t,0, s,0, t,t },
						{ s,0, 1,0, s,t },
						
						{ 0,t, t,t, 0,s },
						{ s,t, 1,t, s,s },
						
						{ 0,s, t,s, 0,1 },
						{ t,s, s,s, t,1 },
						{ s,s, 1,s, s,1 },
				});
			}
			else if (command.equals("Koch Curve")) {
				double t = 1.0/3.0;
				double s30 = Math.sin(Math.PI/6);
				double c30 = Math.cos(Math.PI/6);
				installExample(new double[][] {
						{ 0,t, t,t, 0,2*t },
						{2*t,t, 1,t, 2*t,2*t }, 
						{ t+t/2*c30,0.5-t/2*s30, t+t/2*c30+t*s30,0.5-t/2*s30+t*c30,  t-t/2*c30,0.5+t/2*s30 },
						{ 2*t+-t/2*c30-t*s30,0.5-t/2*s30+t*c30, 2*t+-t/2*c30,0.5-t/2*s30, 2*t+-t/2*c30-t*s30+t*c30,0.5-t/2*s30+t*c30+t*s30 }
				});
			}
			else if (command.equals("Antenna")) {
				double t = 1.0/3.0;
				installExample(new double[][] {
						{ 0,2*t, t,2*t, 0,1 },
						{ 2*t,0, 1,0, 2*t,t },
						{ t/2,1-t/2, 1-t/2,1-t/2, t/2,t/2 }
				});
			}
			else if (command.equals("Spiral")) {
				installExample(new double[][] {
						{0.28905566795944176, 0.2513366656842541, 0.09036898139052062, 0.2508887640207258, 0.2895035696229701, 0.05264997911533296, },
						{-0.1143641581399009, 0.20908397337302598, 0.7921786528896009, -0.11562678440252727, 0.21034659963565214, 1.1156267844025272, }

				});
			}
		}
	}
	
	private synchronized void installExample(double[][] data) {
    	clear();
    	incJobnum();
    	for (double[] d : data) 
    		maps.add(new AffineMap(d[0],d[1],d[2],d[3],d[4],d[5]));
    	setShowMaps(true);
    	repaint();
	}

	private synchronized void saveImage(boolean includeMaps) {
		if (maps.size() < 2) {
			JOptionPane.showMessageDialog(this, "You should have at least two maps\nbefore you think about saving.");
			return;
		}
		if (fileDialog == null)      
			fileDialog = new JFileChooser();
		BufferedImage saveImage;
		fileDialog.setSelectedFile(new File("ChaosGameImage.png")); 
		if (includeMaps)
			fileDialog.setDialogTitle("Save Image with Maps");
		else
			fileDialog.setDialogTitle("Save Image");
		int option = fileDialog.showSaveDialog(this);
		if (option != JFileChooser.APPROVE_OPTION)
			return;  // User canceled or clicked the dialog's close box.
		File selectedFile = fileDialog.getSelectedFile();
		if (selectedFile.exists()) {  // Ask the user whether to replace the file.
			int response = JOptionPane.showConfirmDialog( this,
					"The file \"" + selectedFile.getName() + "\" already exists.\nDo you want to replace it?",
					"Confirm Replace File",
					JOptionPane.YES_NO_OPTION, 
					JOptionPane.WARNING_MESSAGE );
			if (response == JOptionPane.NO_OPTION)
				return;  // User does not want to replace the file.
		}
		saveImage = image;
		try {
			if (includeMaps) {
				saveImage = new BufferedImage(getWidth(),getHeight(), BufferedImage.TYPE_INT_ARGB);
				Graphics g = saveImage.getGraphics();
				int saveSelectedIndex = selectedMapIndex;
				boolean saveShow = showMaps;
				selectedMapIndex = -1;
				showMaps = true;
				paintComponent(g);
				selectedMapIndex = saveSelectedIndex;
				showMaps = saveShow;
				g.dispose();
			}
		}
		catch (OutOfMemoryError e) {
			saveImage = image;
		}
		try {
			boolean hasPNG = ImageIO.write(saveImage,"PNG",selectedFile);
			if ( ! hasPNG )
				throw new Exception("Sorry, the PNG image format\ndoesn't seem to be available!");
		}
		catch (Exception e) {
			JOptionPane.showMessageDialog(this,
					"Sorry, an error occurred while trying to\n"
					+ "write the file \"" + selectedFile.getName() + "\".\n"
					+ "Error: " + e.getMessage());
		}   
	}

	private void saveMapsData() {
		if (maps.size() < 2) {
			JOptionPane.showMessageDialog(this, "You should have at least two maps\nbefore you think about saving.");
			return;
		}
		if (fileDialog == null)      
			fileDialog = new JFileChooser();
		fileDialog.setDialogTitle("Save Maps Data");
		fileDialog.setSelectedFile(new File("ChaosGameMaps.txt"));
		int option = fileDialog.showSaveDialog(this);
		if (option != JFileChooser.APPROVE_OPTION)
			return;  // User canceled or clicked the dialog's close box.
		File selectedFile = fileDialog.getSelectedFile();
		if (selectedFile.exists()) {  // Ask the user whether to replace the file.
			int response = JOptionPane.showConfirmDialog( this,
					"The file \"" + selectedFile.getName() + "\" already exists.\nDo you want to replace it?",
					"Confirm Replace File",
					JOptionPane.YES_NO_OPTION, 
					JOptionPane.WARNING_MESSAGE );
			if (response == JOptionPane.NO_OPTION)
				return;  // User does not want to replace the file.
		}
		try {
			PrintWriter out = new PrintWriter(selectedFile);
			out.println("Affine Maps for Chaos Game");
			out.println(maps.size());
			for (AffineMap map : maps) {
				out.print(map.x0 + " ");
				out.print(map.y0 + " ");
				out.print(map.x1 + " ");
				out.print(map.y1 + " ");
				out.print(map.x2 + " ");
				out.println(map.y2);
			}
			out.flush();
			out.close();
		}
		catch (Exception e) {
			JOptionPane.showMessageDialog(this,
					"Sorry, an error occurred while trying to\n"
					+ "write the file \"" + selectedFile.getName() + "\".\n"
					+ "Error: " + e.getMessage());
		}   
	}

    private void openMapsFile() {
        if (fileDialog == null)
           fileDialog = new JFileChooser();
        fileDialog.setDialogTitle("Open Chaos Game Map Data File");
        fileDialog.setSelectedFile(null);  // No file is initially selected.
        int option = fileDialog.showOpenDialog(this);
        if (option != JFileChooser.APPROVE_OPTION)
           return;  // User canceled or clicked the dialog's close box.
        File selectedFile = fileDialog.getSelectedFile();
        try {
        	Scanner in = new Scanner(selectedFile);
        	String firstLine = in.nextLine();
        	if ( ! firstLine.startsWith("Affine Maps") )
        		throw new Exception("This does not seem to be a Chaos Game data file.");
        	int ct = in.nextInt();
        	if (ct < 2)
        		throw new Exception("This does not seem to be a Chaos Game data file.");
        	double[][] data = new double[ct][6];
        	for (int i = 0; i < ct; i++) {
        		for (int j = 0; j < 6; j++)
        			data[i][j] = in.nextDouble();
        	}
        	in.close();
        	installExample(data);
        }
        catch (Exception e) {
        	JOptionPane.showMessageDialog(this,
					"Sorry, an error occurred while trying to\n"
					+ "read the file \"" + selectedFile.getName() + "\".\n"
					+ "Error: " + e.getMessage());
        }   
    }

}

