/* --------------------------------------------------------------------

Copyright (c) 1996 by Christopher R. Waterson. All rights reserved.

Permission to use, copy, modify, and distribute this software
and its documentation for NON-COMMERCIAL purposes and without
fee is hereby granted provided that this copyright notice
appears in all copies.

$Id: GraphApplet.java 1.2 1996/06/19 03:46:37 waterson Exp $

File:		GraphApplet.java
Synopsis:	An applet that animates a graph
Created:	1996/06/18 waterson
Modified:	$Date: 1996/06/19 03:46:37 $ $Author: waterson $

-------------------------------------------------------------------- */

import java.util.*;
import java.awt.*;
import java.applet.*;



/**
 * An applet that displays and animates a graph with little electrically
 * charged balls connected by springs.
 */

public class GraphApplet
extends Applet
implements Runnable {
	/**
	 * The default number of nodes if no count is specified
	 */

	public static final int DEFAULT_NODE_COUNT = 20;


	/**
	 * The number of milliseconds between refreshed
	 */

	public static final int REFRESH_INTERVAL = 100;


	/**
	 * The graph
	 */

	SparseGraph m_graph;


	/**
	 * A thread for animation
	 */

	Thread m_animationThread;


	/**
	 * A double buffer for painting
	 */

	Image m_paintBuffer;


	/**
	 * The double buffer's graphics context
	 */

	Graphics m_paintBufferGraphics;


	/**
	 * A random number generator
	 */

	Random m_random = new Random();


	/**
	 * The node that's "grabbed" by the mouse, if any
	 */

	VisibleGraphNode m_grabbedNode;



	/**
	 * Initialize the applet
	 */

	public void init() {
	try {
		// The size of the applet window
		Dimension d = size();

		// Get the image to display for each node
		String imageName = getParameter("ImageFilename");
		Image nodeImage = getImage(getDocumentBase(), imageName);

		// Create the graph
		m_graph = new SparseGraph();

		// Get the NodeCount parameter
		Integer nodeCountInteger = new Integer(getParameter("NodeCount"));
		int nodeCount = nodeCountInteger.intValue();
		
		if (nodeCount <= 0) nodeCount = DEFAULT_NODE_COUNT;

		// Create the nodes in the graph
		VisibleGraphNode[] nodes = new VisibleGraphNode[nodeCount];
		for (int i = 0; i < nodeCount; ++i) {
			int x = Math.abs(m_random.nextInt() % d.width);
			int y = Math.abs(m_random.nextInt() % d.height);
			nodes[i] = new VisibleGraphNode(nodeImage, x, y);

			m_graph.addNode(nodes[i]);
		}


		// Create edges between consecutive nodes in the graph
		for (int i = 0; i < nodeCount; ++i) {
			WeightedGraphArc arc = new WeightedGraphArc(nodes[i], nodes[(i+1)%nodeCount], 50.0);
			m_graph.addArc(arc);
		}


		// Create additional random arcs as well
		Integer arcCountInteger = new Integer(getParameter("ArcCount"));
		int arcCount = arcCountInteger.intValue();

		if (arcCount > nodeCount) arcCount = nodeCount;

		while (arcCount > 0) {
			// Add edges randomly
			int i = Math.abs(m_random.nextInt() % nodeCount);
			int j = Math.abs(m_random.nextInt() % nodeCount);
	
			WeightedGraphArc arc = new WeightedGraphArc(nodes[i], nodes[j], 50.0);

			if (m_graph.containsArc(arc)) continue;

			m_graph.addArc(arc);
			arcCount--;
		}

		// Create the animation thread
		m_animationThread = new Thread(this);
	} catch (GraphException e) {
		// This shouldn't happen but...
		e.printStackTrace();
	} }


	/**
	 * Start the animation running, or resume it if it was running
	 * already.
	 */

	public void start() {
		if (m_animationThread.isAlive()) {
			m_animationThread.resume();
		} else {
			m_animationThread.start();
		}
	}


	/**
	 * Suspend the animation.
	 */

	public void stop() {
		if (m_animationThread.isAlive()) {
			m_animationThread.suspend();
		}
	}


	/**
	 * Kill the applet
	 */

	public void destroy() {
		m_animationThread.stop();
	}




	/**
	 * Animate the applet
	 */

	public void run() {
	try {
		for (;;) {
			Thread.currentThread().sleep(REFRESH_INTERVAL);

			// Compute the repelling force between each node. Use something akin
			// to Coulomb's law
			Enumeration outer = m_graph.nodes();
			while (outer.hasMoreElements()) {
				VisibleGraphNode node1 = (VisibleGraphNode) outer.nextElement();

				Enumeration inner = m_graph.nodes();
				while (inner.hasMoreElements()) {
					VisibleGraphNode node2 = (VisibleGraphNode) inner.nextElement();
					if (node2 == node1) continue;

					// Compute the straight-line distance between the points
					double dx = node1.x - node2.x;
					double dy = node1.y - node2.y;

					// Make sure that it's never zero
					if (dx == 0) dx = (m_random.nextDouble() - 0.5);
					if (dy == 0) dy = (m_random.nextDouble() - 0.5);

					double rSquared = dx * dx + dy * dy;

					double f = (node1.getCharge() * node2.getCharge()) / rSquared;

					double r = Math.sqrt(rSquared);
					double fx = f * dx / r;
					double fy = f * dy / r;

					node1.addForce(fx, fy);
				}
			}



			// Compute the attractive force between nodes connected by arcs using
			// something akin to the spring law
			Enumeration arcs = m_graph.arcs();
			while (arcs.hasMoreElements()) {
				WeightedGraphArc arc = (WeightedGraphArc) arcs.nextElement();

				VisibleGraphNode node1 = (VisibleGraphNode) arc.from();
				VisibleGraphNode node2 = (VisibleGraphNode) arc.to();

				// Compute the straight-line distance between the points
				double dx = node1.x - node2.x;
				double dy = node1.y - node2.y;

				// Make sure that it's never zero
				if (dx == 0) dx = (m_random.nextDouble() - 0.5);
				if (dy == 0) dy = (m_random.nextDouble() - 0.5);

				double r = Math.sqrt(dx * dx + dy * dy);

				// Compute the force exerted by the arc
				double f = r / arc.getWeight();

				// Break it into x- and y-components
				double fx = f * dx / r;
				double fy = f * dy / r;

				// And apply equal but opposite attractive forces to each node
				node1.addForce(-fx, -fy);
				node2.addForce(fx, fy);
			}


			// Move each node according to the forces we've just applied
			Dimension d = size();

			Enumeration nodes = m_graph.nodes();
			while (nodes.hasMoreElements()) {
				VisibleGraphNode node = (VisibleGraphNode) nodes.nextElement();

				if (node != m_grabbedNode) node.reactToAppliedForces(d);
			}


			// Force a re-paint
			repaint(REFRESH_INTERVAL);
		}
	} catch (InterruptedException e) {
		// Just exit quietly...
	} }



	/**
	 * Repaint the applet's entire graphics context
	 */

	public void paint(Graphics g) {
		Dimension d = size();

		if (m_paintBuffer == null
		||  m_paintBuffer.getWidth(this) != d.width
		||  m_paintBuffer.getHeight(this) != d.height) {
			// Create a new image for double-buffered painting
			m_paintBuffer = createImage(d.width, d.height);

			// Dispose of the graphics buffer if one exists
			if (m_paintBufferGraphics != null) m_paintBufferGraphics.dispose();

			// ...and cache the new one
			m_paintBufferGraphics = m_paintBuffer.getGraphics();
		}

		// Paint into the double buffer
		paintGraph(m_paintBufferGraphics);

		// ...and BLT it to the screen
		g.drawImage(m_paintBuffer, 0, 0, this);
	}




	/**
	 * Update some of the applet's graphics context
	 */

	public void update(Graphics g) {
		Dimension d = size();

		if (m_paintBuffer == null
		||  m_paintBuffer.getWidth(this) != d.width
		||  m_paintBuffer.getHeight(this) != d.height) {
			// If things have changed significantly, do a full-blown repaint
			super.update(g);
			return;
		}

		// Paint into the double buffer...
		paintGraph(m_paintBufferGraphics);

		// ...and BLT it to the screen
		g.drawImage(m_paintBuffer, 0, 0, this);
	}



	/**
	 * Draw the graph into the specified graphics context
	 */

	public void paintGraph(Graphics g) {
		Dimension d = size();

		g.setColor(getBackground());
		g.fillRect(0, 0, d.width, d.height);

		g.setColor(Color.black);
		Enumeration arcs = m_graph.arcs();
		while (arcs.hasMoreElements()) {
			GraphArc arc = (GraphArc) arcs.nextElement();

			VisibleGraphNode from = (VisibleGraphNode) arc.from();
			VisibleGraphNode to   = (VisibleGraphNode) arc.to();

			g.drawLine(from.x, from.y, to.x, to.y);
		}


		Enumeration nodes = m_graph.nodes();
		while (nodes.hasMoreElements()) {
			VisibleGraphNode node = (VisibleGraphNode) nodes.nextElement();

			Image nodeImage = node.getImage();

			int width  = nodeImage.getWidth(this);
			int height = nodeImage.getHeight(this);

			g.drawImage(nodeImage, node.x - width / 2, node.y - width / 2, this);
		}
	}



	/**
	 * Handle user events
	 */

	public boolean handleEvent(Event event) {
		if (event.id == Event.MOUSE_DOWN) {
			// Grab the node that's under the mouse
			m_grabbedNode = findNode(event.x, event.y);
			return true;

		} else if (event.id == Event.MOUSE_DRAG) {
			// If there's a node that's grabbed, then drag it around the screen.
			if (m_grabbedNode != null) {
				m_grabbedNode.dragTo(event.x, event.y);
				repaint(REFRESH_INTERVAL);
			}
			return true;

		} else if (event.id == Event.MOUSE_UP) {
			// Release the node that's currently grabbed (if any)
			m_grabbedNode = null;
			return true;

		}

		return super.handleEvent(event);
	}



	/**
	 * Try to locate the visible graph node that contains the
	 * specified point by iterating through all the nodes.
	 */

	public VisibleGraphNode findNode(int x, int y) {
		Enumeration nodes = m_graph.nodes();
		while (nodes.hasMoreElements()) {
			VisibleGraphNode node = (VisibleGraphNode) nodes.nextElement();

			// Compute the node's visible rectangle
			Image nodeImage = node.getImage();

			int dx = nodeImage.getWidth(this);
			int dy = nodeImage.getHeight(this);

			Rectangle rect = new Rectangle(node.x - dx / 2, node.y - dy / 2, dx, dy);

			// If (x, y) is inside the rectangle, we've found our man
			if (rect.inside(x, y)) return node;
		}

		return null;
	}
}




