/* 

SOMapplet2 - a simple Self Organizing Map demo

Copyright (C) 2003-2004  Matti Pöllä

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.
*/


import java.util.*;
import javax.swing.*;         
import java.awt.*;
import java.awt.event.*;
import java.awt.Graphics;
import javax.swing.event.*;
import java.awt.geom.*;

/** 
 * Class Neuron represents a pixel in a 2-by-2 SOM.
 *
 * @author Matti Pöllä
 * @version %I%, %G%
 */
class Neuron {

    double[] w = new double[Params.INPUT_DIMENSION];

    /**
     * Constructor for a Neuron.
     */
    public Neuron() {
	for(int i = 0; i < this.w.length; i++)
	    this.w[i] = 160 + 20 * Math.random();
    }

    public Neuron(int r, int g, int b) {
	this.w[1] = r;
	this.w[1] = g;
	this.w[1] = b;
    }
    
    /**
     * Constructor for a neuron with defined weights.
     */
    public Neuron(double r, double g, double b) {

	this.w[0] = r;
	this.w[1] = g;
	this.w[2] = b;
    }

    /**
     * Gets the neuron weight vector.
     *
     * @return weight vector
     */
    public double[] getWeights() {
	return this.w;
    }

    /**
     * Adjusts the weight vector of a neuron
     * defined by the sample vector and the distance.
     *
     * @param ref reference neuron for adjusting.
     * @param dist distance to the reference neuron.
     */
    public void adjustTo(Neuron ref, double dist) {

	for(int i = 0; i < 3; i++) {
	    double bias = ref.getWeights()[i] - this.w[i];
	    if (dist < 1)
		this.w[i] = ref.getWeights()[i];
	    else
		this.w[i] += 1 * bias * Math.exp(-0.3*dist); // <-- update function
	    
	}

	for(int i = 0; i < 3; i++) {
	    if(this.w[i] < 0)
		this.w[i] = 0;
	    if(this.w[i] > 255)
		this.w[i] = 255;
	}
    }
}

/**
 * Class SOM represents the neuron grid forming a 2-by-2
 * map.
 *
 * @author Matti Pöllä
 * @version %I%, %G%
 */
class SOM {

    private Neuron[][] neuronGrid = new Neuron[Params.MAP_SIZE][Params.MAP_SIZE];

    /**
     * Constructor for a SOM object.
     */
    public SOM() {

	neuronGrid = new Neuron[Params.MAP_SIZE][Params.MAP_SIZE];

	for(int i=0; i < Params.MAP_SIZE; i++)
	    for(int j=0; j < Params.MAP_SIZE; j++)
		neuronGrid[i][j] = new Neuron();

    }

    /**
     * Gets the size of the map.
     *
     * @return map size.
     */
    public int getMapSize() {
	return Params.MAP_SIZE;
    }

    /**
     * Initalizes a SOM.
     */
    public void initSOM() {

	neuronGrid = new Neuron[Params.MAP_SIZE][Params.MAP_SIZE];

	for(int i=0; i < Params.MAP_SIZE; i++)
	    for(int j=0; j < Params.MAP_SIZE; j++)
		neuronGrid[i][j] = new Neuron();
    }

    /** 
     * Executes a sigle round of training for each training
     * sample.
     */
    public void trainSOM() {

	int[] match = new int[2];
	
	Neuron t = new Neuron((int)255*Math.random(), (int)255*Math.random(), (int)255*Math.random());
	match = searchBMU(t.getWeights());
	//bmuCoords[0] = match;
	for (int k = 0; k < Params.MAP_SIZE; k++) {
	    for (int j = 0; j < Params.MAP_SIZE; j++) {
		double distance = Math.sqrt(Math.pow(match[0] - k, 2) + 
					    Math.pow(match[1] - j, 2));
		neuronGrid[k][j].adjustTo(t, distance);
	    }
	}
    }

    /**
     * Returns coordinates of the best matching unit for a 
     * neuron.
     *
     * @param x weight vector of a neuron
     * @return coordinates of the best matcing unit neuron
     */
    public int[] searchBMU(double[] x) {

	int best_x, best_y = 0;
	double distance = 900;
	int[] location = new int[2];
	double new_distance = 990;

	for (int i = 0; i < Params.MAP_SIZE; i++) {
	    for (int j = 0; j < Params.MAP_SIZE; j++) {
	
		new_distance = getEucleidianDistance(x, neuronGrid[i][j].getWeights());

		if (new_distance < distance) {
		    location[0] = i;
		    location[1] = j;
		    distance = new_distance;
		}
	    }    
	}
	return location;
    }

    public double[] getWeights(int i, int j) {

	return neuronGrid[i][j].getWeights();
    }

    /**
     * Computes the Eucleidian distance between two vectors.
     *
     * @param w1 vector #1
     * @param w2 vector #2
     * @return Eucleidian distance between the two vectors
     */
    public double getEucleidianDistance(double[] w1, double[] w2) {

	return Math.sqrt(Math.pow(w1[0] - w2[0], 2) + 
			 Math.pow(w1[1] - w2[1], 2) + 
			 Math.pow(w1[2] - w2[2], 2));	
    }

}

/**
 * Self-Organizing Map demo applet.
 *
 * @author Matti Pöllä
 * @version %I%, %G%
 */
public class SOMapplet2 extends JApplet implements Runnable, ActionListener {

    Thread animatorThread;
    PaintBoard mapScreen;
    JButton button_initalize, button_train, button_stop;

    SOM som = new SOM();

    public String getAppletInfo() {
	return "SOMapplet v0.1 | Matti Pöllä <matti.polla@hut.fi>";
    }

    public void init() {

	mapScreen = new PaintBoard();
	button_initalize = new JButton("Initalize map");
	button_initalize.setActionCommand("init");
	button_train = new JButton("Train map");
	button_train.setActionCommand("train");
	button_stop = new JButton("Stop");
	button_stop.setActionCommand("stop");
	button_initalize.addActionListener(this);
	button_train.addActionListener(this);
	button_stop.addActionListener(this);

	JPanel basePaneeli = new JPanel();
	basePaneeli.setLayout(new BorderLayout());
	JPanel controlPaneeli = new JPanel();

	JPanel piirtoPaneeli = new JPanel();
	piirtoPaneeli.setLayout(new GridLayout(1, 1));

	controlPaneeli.add(button_initalize);
	controlPaneeli.add(button_train);
	controlPaneeli.add(button_stop);

	piirtoPaneeli.add(mapScreen);

	basePaneeli.add("Center", piirtoPaneeli);
	basePaneeli.add("South", controlPaneeli);
	getContentPane().add(basePaneeli);

	som.initSOM();
    }

    public void start() {
	animatorThread = new Thread(this);
    }

    public void run() {

	int rounds = 0;

	while (Thread.currentThread() == animatorThread && rounds < Params.TRAINING_ROUNDS) {

	    mapScreen.repaint();
	    rounds++;
	    try {
		// A small delay...
		animatorThread .sleep(200);
	    } catch (Exception e) {}
	    som.trainSOM();
	    mapScreen.repaint();
	}
    }
    public void stop() {
	animatorThread = null;
    }

    /**
     * Handles events caused by the buttons.
     */
    public void actionPerformed(ActionEvent e) {
	Object source = e.getSource();
	JButton actionButton = (JButton)source;

	if(actionButton.getActionCommand().equals("init")) {
	    animatorThread = null;
	    som.initSOM();
	    mapScreen.repaint();
	}
	else if(actionButton.getActionCommand().equals("train")) {
	    animatorThread = new Thread(this);
	    animatorThread.start();
	}
	else if(actionButton.getActionCommand().equals("stop")) {
	    animatorThread = null;
	}      
    }
    

    /** 
     * Class PaintBoard is used to draw the SOM.
     *
     * @author Matti Pöllä
     * @version %I%, %G%
     */
    class PaintBoard extends JPanel {

	public PaintBoard() {
	    super();
	}

	public void paint(Graphics g) {
	    
	    Graphics2D g2 = (Graphics2D)g;
	    g2.setColor(new Color(250, 250, 250)); // Draws background.

	    for (int i = 0; i < som.getMapSize(); i++) {
		for (int j = 0; j < som.getMapSize(); j++) {
		    double[] rgb = som.getWeights(i, j);
		    g2.setColor(new Color((int)rgb[0], (int)rgb[1], (int)rgb[2]));
		    g2.fillRect(10 * i, 10 * j, 10, 10);
		}
	    }
	}
    }
}

class Params {

    public static final int TRAINING_ROUNDS = 2000;
    public static final int INPUT_DIMENSION = 3;
    public static final int MAP_SIZE = 45;
}