import java.applet.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;

// Class which holds needed constants:
class C {
	public static final double c = 2.997925e8;	// light velocity - ms^-1
	public static final double G = 6.67e-11; 		// Gravity const.
	public static final double b = 0.00289;			// Wienuv zakon
	public static final double sigma = 5.67e-8;	// Stefan-Boltzmann
	// optimalizations only:
	public static final double c2 = c*c; 
	public static final double G2 = G*G;
	public static final double b4 = b*b*b*b;
	// other useful consts:
	public static final double SecPerYear = 365.0*24.0*3600.0;	
	public static final double Me = 6e24; // Mass of Earth
	public static final double Ms = 2e30; // Mass of Sun
}

// Holds some settings - fonts etc...
class Settings {
	public static String VersionInfo = "Verze 1.11 - 11.2000.AD, TaZ";
	// Graph area settings:
	public static String Font = "Verdana";
	public static int FontSize = 11; // pixels :-((
	public static int Spacing1 = 3;  // px
	  // to detect needed space to left from y-axis
	static String testStr1 = "0.";
	static String testStr2 = "E-000";
	public static String testStr; 
	  // Count of value labels on the X-axis (time)
	public static int labelsX = 5; 
		//      ----- || -----          Y-axis (temp, mass, intensity,...)	
	public static int labelsY = 4; 
	public static int labelLen = 8; 
	  // length of the label lines on axises [px]
	public static Color axisCol = Color.black; 
		// Color used to paint axises
	
	// korekcni konstanty pro demonstrativnejshi nastaveni grafu
	// ovlivnuji maxima na ose y pro teplotu a intenzitu ( -> infinity)
	// viz. fce BH.changeMass
	
	  // replaced by timeCorr: public static double tempCorr = 0.5;
	public static double intCorr = 100; // intensity
	public static double timeCorr = 0.9999; // for temp GraphLine Y-scale
	public static double timeCorr2 = 0.98; // for color maping
	
	// strings & conversion coeficients for different units
	public static String strKg = "kg";
	public static String strMs = "Mass of Sun";
  public static String strMe = "Mass of Earth";	
	public static double[] ccMass = {1, C.Ms, C.Me};
	
	public static String strS = "s"; // seconds
	public static String strYears = "Years";
	public static double[] ccTime = {1, C.SecPerYear};
	
	public static String strM = "m";
	public static String strKm = "km";
	public static String strMm = "mm";
	public static double[] ccRadius = {1, 1000, 0.001};
	
	public static String strSet = "Set:";
	public static String strAnimate = "Animate!";
	public static String strStop = "Stop!";
	
	public static String striMass = "initial mass";
	public static String strtPos = "data inspector (time position)";
	
	public static int animDelay = 20; // [ms]
	public static int animLength = 15000; // [ms]
	
	// method of painting graph lines:
	/* 1 - faster (?) - paints all lines at a time (=> safes time on 
				 computing values (Mc,Tc,Ic,Rc)
		 0 - slower (?) - paints entire line at a time, then next line...*/
	/* oh no, it seems, that the "0" method is faster ??? but why!?!
		 the variables are quite quick to compute, but there's too many
		 workaround operations in the "1" method :-(( */
	public static int paintMethod = 0;
	  // color of the x-pointing line:
	public static Color ptrLnCol = Color.black;
	
	  // legend colors:
	public static Color colMass = Color.blue.darker();
	public static Color colRad = Color.red;
	public static Color colTemp = Color.green.darker().darker();
	public static Color colInt = Color.yellow.darker();
	
	  // used to format doubles:
	public static int fdigs = 4; // wanted number of digits after a dot
	public static String aDot = ".";
	public static String aExp = "E";
	
	public static int holeSize = 150; // px
	
	// used by ColMap:
	public static int mapWidth = 30; // [px] width of color map area
	
	// used by AHole:
	public static Color holeBack = Color.gray.brighter();
	public static Color holeFront = Color.black;
	
  // used to convert double to "nice" string :-)
	public static String dTos(double d) {
		String s1, s2, s3;
		int dotI, expI, stopI; // dot and exponent char. indexes in s1
		
		// convert to loooong string
		s1 = Double.toString(d);
		
		if (Double.isNaN(d) || Double.isInfinite(d)) return s1;
		
		// get indexes of searched chars.
		  // dot is always present...
		dotI = s1.indexOf(aDot)+1;  
		  // remove dot, if no (or less :-)) fract. digits wanted
		if (fdigs < 1) dotI--; 			
		// E is not always there...
		expI = s1.indexOf(aExp);
		if (expI == -1) expI = s1.length();
		stopI = Math.min(dotI+Math.max(fdigs, 0), expI);
		
		// get first part:
		s2 = s1.substring(0, stopI);
		s3 = s1.substring(expI, s1.length());
		
		return (s2+s3);
	}
}

public class BH extends Applet implements ActionListener,
  ItemListener, Runnable {
	Engine e;
	// Applet Controls Tree & support vars:
	Panel wall, sett, bottom, data;
		// wall - where to place paint controls
		Graph g; // Graph canvas
		// sett - for controls with settings
		TextField tfMs; 		// StartMass text field
		Choice chMassUnit;	// setMass unit selector
		Button btnSetM;			// setMass button
		Choice chWhat;			// setWhat choice
		Button btnAnim; 		// animate button
		
		// data panel - data from graph (ptrLn pos info)
		// data labels: time, Mass, Radius, Temp, Int.
		Label lbt, lbM, lbR, lbT, lbI;
		// conversion constants for data display:
		// data value is divided by this constant to get number to display
		// enables easy selection of simple display units
		double ccT, ccM, ccR; 
		// unit selectors:
		Choice chtU, chMU, chRU;
		
		AHole hole; // black hole picture
		ColMap colormap; // legend for colors
		
		// animation vars:
		Thread animTh;
		double animCurX, animMaxX, animXStep;
		int animDelay = 50; // ms
		
	// Used graph lines:
	GraphLine lRad, lTemp, lMass, lInt;
	  
  public BH() { super();
  	e = new Engine();
  }
  
  public void fetchParams() {
  	ParamHelper ph = new ParamHelper(this);
  	boolean Params=true;
  	int i = 1;
  	StringBuffer buf = new StringBuffer(Settings.testStr1);
  	
  	// configure digit count after dot in doubles
  	try {	ph.setParamName("Digits"); }
  	catch (NullPointerException e) {
  		System.out.println("Cannot fetch parameters: "+e.toString());
  		Params = false;
  	}
  	
  	if (Params) {
  		if (ph.hasValue())	
	  		Settings.fdigs = ph.asIntBounds(Settings.fdigs, 1, Integer.MAX_VALUE);
	  }
  	
  	for (; i <= Settings.fdigs; i++) buf.append("0");
  		
  	Settings.testStr = buf.append(Settings.testStr2).toString();
  	
  if (Params) {	
  	// configure strings (easy to do :-))
  	ph.setParamName("str-Kg");
  	if (ph.hasValue()) Settings.strKg = ph.asString();
  	
  	ph.setParamName("str-Ms");
  	if (ph.hasValue()) Settings.strMs = ph.asString();

  	ph.setParamName("str-Me");
  	if (ph.hasValue()) Settings.strMe = ph.asString();

  	ph.setParamName("str-S");
  	if (ph.hasValue()) Settings.strS = ph.asString();

  	ph.setParamName("str-Years");
  	if (ph.hasValue()) Settings.strYears = ph.asString();

  	ph.setParamName("str-M");
  	if (ph.hasValue()) Settings.strM = ph.asString();

  	ph.setParamName("str-Km");
  	if (ph.hasValue()) Settings.strKm = ph.asString();

  	ph.setParamName("str-Mm");
  	if (ph.hasValue()) Settings.strMm = ph.asString();
  	
		ph.setParamName("str-SetCmd");
  	if (ph.hasValue()) Settings.strSet = ph.asString();

		ph.setParamName("str-AnimCmd");
  	if (ph.hasValue()) Settings.strAnimate = ph.asString();
  	
  	ph.setParamName("str-StopCmd");
  	if (ph.hasValue()) Settings.strStop = ph.asString();
  	
  	ph.setParamName("str-InitMass");
  	if (ph.hasValue()) Settings.striMass = ph.asString();

  	ph.setParamName("str-TimePos");
  	if (ph.hasValue()) Settings.strtPos = ph.asString();

  	// font config:
  	ph.setParamName("Font-Name");
  	if (ph.hasValue()) Settings.Font = ph.asString();
  	
  	ph.setParamName("Font-Size");
  	if (ph.hasValue()) Settings.FontSize = ph.asIntBounds(Settings.FontSize,
  		3, Integer.MAX_VALUE);

		// labels:
  	ph.setParamName("Labels-X");
  	if (ph.hasValue()) Settings.labelsX = ph.asIntBounds(Settings.labelsX,
  		2, Integer.MAX_VALUE);

  	ph.setParamName("Labels-Y");
  	if (ph.hasValue()) Settings.labelsY = ph.asIntBounds(Settings.labelsY,
  		2, Integer.MAX_VALUE);
  		
  	ph.setParamName("Labels-Length");
  	if (ph.hasValue()) Settings.labelLen = ph.asIntBounds(Settings.labelLen,
  		0, Integer.MAX_VALUE);
  		
  	// color map, hole size:
  	ph.setParamName("Hole-Size");
  	if (ph.hasValue()) Settings.holeSize = ph.asIntBounds(Settings.holeSize,
  		1, Integer.MAX_VALUE);
  		
		ph.setParamName("ColorMap-Width");
  	if (ph.hasValue()) Settings.mapWidth = ph.asIntBounds(Settings.mapWidth,
  		1, Integer.MAX_VALUE);
  		
		ph.setParamName("Corr-Time");
  	if (ph.hasValue()) Settings.timeCorr = ph.asDoubleBounds(Settings.timeCorr,
  		0.8, 0.999999999999);

		ph.setParamName("Corr-Time2");
  	if (ph.hasValue()) Settings.timeCorr2 = ph.asDoubleBounds(Settings.timeCorr2,
  		0.7, 1);
  	
  	ph.setParamName("Corr-Intensity");
  	if (ph.hasValue()) Settings.intCorr = ph.asDoubleBounds(Settings.intCorr,
  		1.1, 100000);
  		
  	// colors:
  	ph.setParamName("Color-Axis");
  	if (ph.hasValue()) Settings.axisCol = ph.asColor(Settings.axisCol);
  	
  	ph.setParamName("Color-Mass");
  	if (ph.hasValue()) Settings.colMass = ph.asColor(Settings.colMass);

  	ph.setParamName("Color-Radius");
  	if (ph.hasValue()) Settings.colRad = ph.asColor(Settings.colRad);
  	
  	ph.setParamName("Color-Temperature");
  	if (ph.hasValue()) Settings.colTemp = ph.asColor(Settings.colTemp);
  	
  	ph.setParamName("Color-Intensity");
  	if (ph.hasValue()) Settings.colInt = ph.asColor(Settings.colInt);
  	
  	// animation control:
		ph.setParamName("Anim-Delay");
  	if (ph.hasValue()) Settings.animDelay = ph.asIntBounds(Settings.animDelay,
  		1, 1000);
  		
  	ph.setParamName("Anim-Length");
  	if (ph.hasValue()) Settings.animLength = ph.asIntBounds(Settings.animLength,
  		Settings.animDelay, Integer.MAX_VALUE);
  } // if Params...
  }
  
  public String[][] getParameterInfo() {
  	String pinfo[][] = {
	 		{"Digits",    "1-18",    "digits in fractional part of numbers"},
	 		{"str-Kg", "string", "'Kilograms' measurement unit name"},
	 		{"str-Ms", "string", "Mass of Sun"},
	 		{"str-Me", "string", "Mass of Earth"},
	 		{"str-S", "string", "seconds"},
	 		{"str-Years", "string", "years"},
	 		{"str-M", "string", "meter"},
	 		{"str-Km", "string", "kilometer"},
	 		{"str-Mm", "string", "milimeter"},
	 		{"str-SetCmd", "string", "'Set' string"},
	 		{"str-AnimCmd", "string", "'Animate' string"},
	 		{"str-StopCmd", "string", "'Stop' string"},
	 		{"str-InitMass", "string", "'initial mass' string"},
	 		{"str-TimePos", "string", "'time position' string"},
	 			
	 		{"Font-Name", "font face name", "font face used to render texts"},
	 		{"Font-Size", "3 - MaxInt", "font size in pixels"},
	 			
	 		{"Labels-X", "2 - MaxInt", "count of value ticks on X-axis"},
	 		{"Labels-Y", "2 - MaxInt", "count of value ticks on Y-axis"},
	 		{"Labels-Length", "0 - MaxInt", "length of value ticks in pixels"},
	 			
	 		{"Hole-Size", "1 - MaxInt", "hole image dimensions (square) in pixels"},
	 		{"ColorMap-Width", "1 - MaxInt", "color ramp width in pixels"},
	 			
	 		{"Corr-Time", "0.8 - 0.999999999", "used to shorten time when counting peek value of Temperature"},
	 		{"Corr-Time2", "0.7 - 1.0", "used to shorten time when counting peek value of Temperature for color ramp"},
	 		{"Corr-Intensity", "1.1 - 1.0e5", "peek value of Intensity=Intensity(0)*'Corr-Intensity'"},
	 			
	 		{"Anim-Delay", "1-1000", "delay between frames"},
	 		{"Anim-Length", "'Anim-Delay'-MaxInt", "requested animation length in seconds"},
	 			
	 		{"Color-Axis", "HTML-hexa-color", "color used to paint axises"},
			{"Color-Mass", "HTML-hexa-color", "color used to paint Mass line"},
			{"Color-Radius", "HTML-hexa-color", "color used to paint Radius line"},
			{"Color-Temperature", "HTML-hexa-color", "color used to paint Temperature line"},
			{"Color-Intensity", "HTML-hexa-color", "color used to paint Intensity line"},
 		};
 		
 		return pinfo;
  }
  
  public void init() {
  	// load params from HTML
  	fetchParams();
  	
   	// The size of the applet/window should be known this time
  	initControls();
  	
  	changeMass(2.0e30); // initial mass settings
  	
  	System.out.println("BHVap Initialized!");
		System.out.println(Settings.VersionInfo);
  	
		// #debug
  	System.out.println("Error-less MATH: ((1.0e-5)/5.0)*3="+Double.toString(((1.0e-5)/5.0)*3)+
  		" insted of 6.0e-6 :-(((.");
  }
  public void start() {
  	// #debug
  	System.out.println("BHVap Started!");
  }
  public void stop() {
  	// #debug
    System.out.println("BHVap Stopped!");
  }
  
  public String getAppletInfo ()	{ 
  	return Settings.VersionInfo;
  }
  
  public void run() {
  	// init animation
  	animCurX = 0;
  	animMaxX = e.Tt;
  	animDelay = Settings.animDelay;
  	animXStep = (animMaxX-animCurX)/(Settings.animLength/animDelay);
  	
  	// run animation
  	while ((animTh != null) && (animCurX <= animMaxX)) {
    	try { Thread.sleep(animDelay);
    	} catch (InterruptedException e) {}
  		
    	setPtrLine(animCurX);
    	animCurX += animXStep;
		}
		
		if (animTh != null) setPtrLine(animMaxX);
		
		// cleanup animation
		animStop();
  }
  
  public void animStart() {
  	// already running
  	if (animTh != null) return;
  	
  	animTh = new Thread(this);
  	btnAnim.setLabel(Settings.strStop);
  	animTh.start();
  }
  
  public void animStop() {
  	animTh = null;
  	btnAnim.setLabel(Settings.strAnimate);
  }
  
  public void actionPerformed(ActionEvent a) {
  	Object src = a.getSource();

  	if (src == btnSetM) {
  		try {
  			
  			switch (chWhat.getSelectedIndex()) {
  				case 0: {
						double k = 1;
						
						switch (chMassUnit.getSelectedIndex()) {
							case 1: { k = C.Ms; break; }
							case 2: { k = C.Me; break; }
						}
						
  					changeMass(k*Double.valueOf(tfMs.getText()).doubleValue());
  					break;
  				}
  				case 1: {
  					setPtrLine(Double.valueOf(tfMs.getText()).doubleValue());
  					break;
  				}
  			}
  		} catch (NumberFormatException nfe) {
  				System.out.println(nfe);
  			}
  	}
  	
  	if (src == btnAnim) {
  		if (animTh == null) { 
  			animStart();
  		}	else { 
  			animStop();
  		}
  	}
  }
  
  public void itemStateChanged(ItemEvent e) {
    Object src = e.getSource();
  	Choice tmp = (Choice)(src);
  	
  	if (src == chtU) { ccT = Settings.ccTime[tmp.getSelectedIndex()]; }
  	if (src == chMU) { ccM = Settings.ccMass[tmp.getSelectedIndex()]; }
  	if (src == chRU) { ccR = Settings.ccRadius[tmp.getSelectedIndex()]; }
  	
  	// show changes:
  	showValues(g.ptrLn);
  }
  
  public void setPtrLine(double xValue) {
  	g.setPtrLn(xValue);
  	showValues(xValue);
  }
  
  public void setPtrLine(int xcoord) {
  	setPtrLine(g.getXcoordXvalue(xcoord));
  }
  
  public void showValues(double xValue) {
  	// prepare data:
  	e.setTime(xValue);
  	
  	// real values:
  	lbt.setText(Settings.dTos(e.Tm/ccT));
  	lbM.setText(Settings.dTos(e.Mc/ccM));
  	lbR.setText(Settings.dTos(e.Rc/ccR));
  	lbI.setText(Settings.dTos(e.Ic));
  	lbT.setText(Settings.dTos(e.Tc));
  	// update hole image:
  	hole.setColor(ColMap.colorIndexToColor(colormap.getColorIndex(e.Tc)));
  	
  	hole.setCurM(e.Mc);  	
  }
  
  public void changeMass(double m) {
  	double newPtrLn;
  	double tempS, tempE;
  	
  	// make sure anim. is not running
  	animStop();
  	
  	// change Engines value:
  	e.setMass(m);
  	// setup x-axis
  	g.xMax = e.Tt;
  	
  	// setup color range for given range of temps
  	e.setTime(0.0);
  	tempS = e.Tc;
  	e.setTime(e.Ttcorr);
  	tempE = e.Tc;
  	colormap.setTempRange(tempS, tempE);
  	
  	// hole image setup
  	hole.setOrigM(m);
  	
  	// graph lines setup:
  	
  	/* Ty drobne upravy yMin a yMax jsou quli tomu, aby na grafech 
  	   bylo videt to, co na nich videt byt ma. Jinak by totiz napr.
  	   graf intenzity byla lomena cara, celou dobu by sla nulou a 
  	   na konci by vyletela na maximum... Klinicky testovano.      */
		// Radius:
	  lRad.yMin = 0; lRad.yMax = lRad.getFx(g.xMin)*1.001;
		// Mass:
	  lMass.yMin = 0; lMass.yMax = lMass.getFx(g.xMin);
	  // Temperature:
	  lTemp.yMin = lTemp.getFx(g.xMin); lTemp.yMax = lTemp.getFx(g.xMax*Settings.timeCorr);//*Settings.tempCorr;
	  // Intensity:
	  lInt.yMin = lInt.getFx(g.xMin); lInt.yMax = lInt.getFx(g.xMin)*Settings.intCorr;
	  
	  // get ptrLine within bounds
	  newPtrLn = Math.min(Math.max(g.ptrLn, g.xMin), g.xMax);
	  	  
	  g.setPtrLn(newPtrLn);
	  g.dataChanged();
	  showValues(g.ptrLn);
	  
	  // #debug
	  System.out.println("Mass changed to Ms = "+m+" kg.");
  }
  
  public Label createLabel(String lTitle, int align, Color col) {
  	Label tempL = new Label(lTitle, align);
  	tempL.setForeground(col);
  	return tempL;
  }
  
  public void initControls() {
  	Dimension d = getSize();
  	String space = " ";
  	
  	setLayout(new BorderLayout());

  	wall = new Panel(new BorderLayout());
  	add(wall, "Center");

  	g = new Graph();
  	g.xMin = 0;
  	g.xMax = e.Tt;
  	g.addMouseListener(new MouseAdapter() {
  		public void mouseClicked(MouseEvent me) {
	 			// Clicked left button?
  			if ((me.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
  				// Yes, move ptrLn to that position:
  				setPtrLine(me.getX());
  			}
  		}
  	});
  	// init GraphLines:

  	// Radius:
	  lRad = new BHLines(e, BHLines.RAD, g.xMin, g.xMax);
	  lRad.col = Settings.colRad;
	  g.lines.addElement(lRad);
	  
		// Mass:
	  lMass = new BHLines(e, BHLines.MASS, g.xMin, g.xMax);
	  lMass.col = Settings.colMass;
	  g.lines.addElement(lMass);
	  
	  // Temperature:
	  lTemp = new BHLines(e, BHLines.TEMP, g.xMin, g.xMax);
	  lTemp.col = Settings.colTemp;
	  g.lines.addElement(lTemp);
	  
	  // Intensity:
	  lInt = new BHLines(e, BHLines.INT, g.xMin, g.xMax);	  
	  lInt.col = Settings.colInt;
	  g.lines.addElement(lInt);

	  wall.add(g, "Center");
  	
  	sett = new Panel(new GridLayout(1, 1));
  	add(sett, "North");
  	
  	tfMs = new TextField(20);
  	sett.add(tfMs);
  	
  	chMassUnit = new Choice();
  	chMassUnit.addItem(Settings.strKg);
  	chMassUnit.addItem(Settings.strMs);
  	chMassUnit.addItem(Settings.strMe);
  	sett.add(chMassUnit);
  	
  	btnSetM = new Button(Settings.strSet);
  	btnSetM.addActionListener(this);
  	sett.add(btnSetM);
  	
  	chWhat = new Choice();
  	chWhat.addItem(Settings.striMass);
  	chWhat.addItem(Settings.strtPos);
  	// listen to changes:  	
  	chWhat.addItemListener(this);
  	sett.add(chWhat);
  	
  	btnAnim = new Button(Settings.strAnimate);
  	btnAnim.addActionListener(this);
  	sett.add(btnAnim);
  	
  	data = new Panel(new GridLayout(6, 3));
  	data.setBackground(Color.white);
  	
  	// time line:
  	data.add(new Label("t = ", Label.RIGHT));
  	data.add(lbt = new Label(space, Label.CENTER));
  	data.add(chtU = new Choice());
  	// time conversion = none (seconds is default)
  	ccT = Settings.ccTime[0];
  	chtU.addItem(Settings.strS);
  	chtU.addItem(Settings.strYears);
  	// listen to changes:
  	chtU.addItemListener(this);
  	
  	// Mass line:
  	data.add(createLabel("m = ", Label.RIGHT, Settings.colMass));
  	data.add(lbM = createLabel(space, Label.CENTER, Settings.colMass));
  	data.add(chMU = new Choice());
  	chMU.setForeground(Settings.colMass);
  	// no conversion by default
  	ccM = Settings.ccMass[0];
  	chMU.addItem(Settings.strKg);
  	chMU.addItem(Settings.strMs);
  	chMU.addItem(Settings.strMe);
  	// listen to changes:
  	chMU.addItemListener(this);
  	
  	data.add(createLabel("R = ", Label.RIGHT, Settings.colRad));
  	data.add(lbR = createLabel(space, Label.CENTER, Settings.colRad));
  	data.add(chRU = new Choice());
  	chRU.setForeground(Settings.colRad);
 		// radius conversion = none (meters is default)
  	ccR = Settings.ccRadius[0];
  	chRU.addItem(Settings.strM);
  	chRU.addItem(Settings.strKm);
  	chRU.addItem(Settings.strMm);
  	// listen to changes:
  	chRU.addItemListener(this);
  	
  	// Temp line:
  	data.add(createLabel("T = ", Label.RIGHT, Settings.colTemp));
  	data.add(lbT = createLabel(space, Label.CENTER, Settings.colTemp));
  	data.add(createLabel("K", Label.LEFT, Settings.colTemp));
  	
  	// Int line:
  	data.add(createLabel("I = ", Label.RIGHT, Settings.colInt));
  	data.add(lbI = createLabel(space, Label.CENTER, Settings.colInt));
  	data.add(createLabel("W/m^2", Label.LEFT, Settings.colInt));
  	
  	bottom = new Panel(new BorderLayout());
  	
  	hole = new AHole();
  	colormap = new ColMap(0, 273.0);

  	bottom.add(data, "West");
  	bottom.add(hole, "Center");
  	bottom.add(colormap, "East");
  	
  	add(bottom, "South");
  }
}

// A line in the Graph area, universal
// to implement your own func. just override getFx() func.
class GraphLine {
	public double yMin,yMax;
	public Color col = Color.black;
	
	// constructor with autoscale:
	public GraphLine(double xMin, double xMax) { super();
		// auto scale:
		col = Color.black;
	  autoScale(xMin, xMax);
	}
	
	/* area defines where to paint on g
	   x, x+width defines pixel range, where the x variable goes from
	     startX to stopX
	   y, y+height defines where are yMin and yMax values
	*/
	public void paintLine(Graphics g, Rectangle area, 
		double startX, double stopX) {
	
		int x=area.x, xMax=area.x+area.width, ybase=area.y+area.height;
		double xv=startX, xpp=(double)(stopX-startX)/(double)(area.width); // x value increment per pixel
		double yscale=(area.height)/(Math.abs(yMax-yMin)); // scaling factor of y axis
		double tmp;
		int yp,yc;
		
		
		g.setColor(col);
		// walk thru pixels to go and paint appropriate lines
		tmp=getFx(xv)*yscale;
		if (Double.isNaN(tmp) || (tmp > (ybase + 10))) {
			yp = ybase+10;
		} else yp = (int)(tmp);
		for (x++; x <= xMax; x++) {
			xv += xpp;

			tmp=getFx(xv)*yscale;
			if (Double.isNaN(tmp) || (tmp > (ybase + 10))) {
				yc = ybase+10;
			} else yc = (int)(tmp);
			
			g.drawLine(x-1, ybase-yp, x, ybase-yc);
			// next point:			
			yp=yc;
		}
	}
	
	// returns function value for specified x; defines line's shape
	// must be overriden in subclasses	
	public double getFx(double x) {
		return(x*x+yMin);
	}
	
	public void autoScale(double xMin, double xMax) {
		yMin = getFx(xMin); yMax = getFx(xMax);
	}
}

// BlackHole GraphLines:
class BHLines extends GraphLine {
	// function IDs:
	public static final int NONE = -1;
	public static final int MASS = 0; // hmotnost
	public static final int TEMP = 1; // teplota
	public static final int INT	 = 2; // intenzita
	public static final int RAD	 = 3; // polomer
	
	public int whichfc = BHLines.NONE; // function id
	public Engine eng; // used to produce values
	
	public BHLines(Engine e, int fcid, double xMin, double xMax) {
		super(xMin, xMax);
		eng = e;
		whichfc = fcid;
		autoScale(xMin, xMax);		
	}
	
	public double getFx(double x) {
		try {
			eng.setTime(x);
			switch (whichfc) {
				case BHLines.MASS: return eng.Mc;
				case BHLines.TEMP: return eng.Tc;
				case BHLines.INT: return eng.Ic;
				case BHLines.RAD: return eng.Rc;
				default: return 0;
			}
		} catch (NullPointerException e) { return 0; }
	}
}

// Container (paper) for GraphLines
class Graph extends Canvas {
		// holds list of GraphLines to paint
	protected Vector lines;
		// offScreen image painted @ dimensions oldDim (image cache)
	public Image offImg;
	protected Dimension oldDim;
	// Coordinates on Canvas of:
		// LeftBottom, RightTop bounds of the area for lines
	protected Dimension lb, rt;
	public double xMin = 0; 		// Minimum on x axis
	public double xMax = 1000;	// Maximum on x axis
		// "pointer" line - shows position in graph
		// ptrLn < xMin, > xMax - no line painted else painted :-)
	public double ptrLn = 1e50;
		
	public Graph() { super();
	  lines = new Vector(10, 5);
	  
	  lb = new Dimension(); rt = new Dimension();
	  
		setBackground(Color.white);
		setForeground(Color.black);
		setFont(new Font(Settings.Font, Font.PLAIN, Settings.FontSize));
	}
	
	public void update(Graphics g) {
		paint(g);		// erases the background itself (if needed)
	}
	
	// called to flush image cache & render new
	public void dataChanged() {
		// causes new image rendering in paint func.
		oldDim = null;
		repaint();
	}
	
	public int getPtrLnX() {
		return (int)(((rt.width-lb.width)/(xMax-xMin))*ptrLn+lb.width+1);
	}
	
	// converts x coordinate in graph canvas into x value on the axis (time)
	public double getXcoordXvalue(int xcoord) {
		// bounds check:
		xcoord = Math.max(xcoord, lb.width);
		xcoord = Math.min(xcoord, rt.width);
		xcoord -= lb.width;
		return((xMax-xMin)*(double)xcoord/(rt.width-lb.width)+xMin);
	}
	
	public void setPtrLn(double newX) {
		int prevX = getPtrLnX(), from, w;
		if ((offImg != null) && (oldDim != null)) {
			Graphics g = offImg.getGraphics();
		
			// erases the old one:			
			if ((ptrLn >= xMin) && (ptrLn <= xMax)) {
				paintPtrLn(g);
				// debug
				//repaint((long)0, (int)(getPtrLnX()-1), 0, 3, (int)oldDim.height);
			}
			
			ptrLn = Math.min(Math.max(newX, xMin), xMax);
			// paints the new one:
			paintPtrLn(g);
			// debug
			//repaint((long)0, (int)(getPtrLnX()-1), 0, 3, (int)oldDim.height);
			
			from = Math.min(prevX, getPtrLnX())-2;
			w = Math.abs(getPtrLnX()-prevX)+3;
			
			// debug
			//System.out.println("G: Repaint cmd: "+from+", "+w);
			repaint((long)0, from, 0, w, (int)oldDim.height);
		}
	}
	
	public void paint(Graphics g) {
		Graphics offG;
		Dimension d = getSize();
		
		if (oldDim != null) {
			if (d.equals(oldDim)) {
				// buffer exists, size not changed -> use cached image
				g.drawImage(offImg, 0, 0, this);
				return;
			}
		}
		// cached image	does not exists, or resized...
		offImg = createImage(d.width, d.height);
		offG = offImg.getGraphics();
		prepareGraph(offG);
		paintLines(offG);
		if ((ptrLn >= xMin) && (ptrLn <= xMax)) { paintPtrLn(offG); }
		g.drawImage(offImg, 0, 0, this);
		oldDim = d; 	// save rendered image's dims.
	}
	
	public void paintPtrLn(Graphics g) {
		int x=getPtrLnX();
				
		g.setColor(getBackground());
		g.setXORMode(Settings.ptrLnCol);
		g.drawLine(x-1, 0, x-1, lb.height+1);
	}
	
	public void paintLines(Graphics g) {
		int i, iMax=lines.size();
		GraphLine ln;
		// full graph area:
		// r.width set later
		Rectangle r = new Rectangle(lb.width, rt.height, 
			0, lb.height-rt.height);
		// "faster" version vars:
		int c,cMax;
		double x, dx;
		
		if (Settings.paintMethod == 1) {
			r.width = 1;
			if (iMax > 0) {
				x = xMin;
				dx = (xMax-x)/(rt.width-lb.width);
				for (c=lb.width, r.x=lb.width, cMax=rt.width; c < cMax; c++) {
					r.x = c;
					for (i=0; i < iMax; i++) {
						ln = (GraphLine)(lines.elementAt(i));
						ln.paintLine(g, r, x, x+dx);
					}
					x += dx;
				}
			}
		} else {
			// This is (I think) the slower version:
			r.width = rt.width-lb.width;
			if (iMax > 0) {
				for (i=0; i < iMax; i++) {
					ln = (GraphLine)(lines.elementAt(i));
					ln.paintLine(g, r, xMin, xMax);
				}
			}
		}
	}

	// paints background, axises etc.
	public void prepareGraph(Graphics g) {
	  Dimension d = getSize();
		String uff = "\u00A9 TaZ";
		String zero = Settings.dTos(xMin);
		String label;
		FontMetrics fm = g.getFontMetrics();
		// counters & temporary variables:
		int tmp, i, j, c1;
		double tmp2, tmp3, tmp4, ofs;
		// used when painting Y labels:
			// starting Y and Y delta, label height
		int ylabels, ylabeld, ylblx;
			// temp when painting...
		GraphLine ln;
		
		g.clearRect(0, 0, d.width, d.height);
		
		// #debug - sign, that I painted it :-)
		g.setColor(Color.blue.darker());
		g.drawString(uff, d.width - fm.stringWidth(uff)-1, 
			fm.getAscent()+1);
		
		// Axises:
		tmp = Settings.Spacing1; // optimalization?
		lb.setSize(fm.stringWidth(Settings.testStr)+2+2*tmp + Settings.labelLen,
			d.height-2-2*tmp-fm.getHeight() - Settings.labelLen);
		rt.setSize(d.width-(fm.stringWidth(Settings.dTos(xMax))/2+tmp),
			fm.getHeight()*lines.size()/2+tmp);
		
		g.setColor(Settings.axisCol);

		tmp = lb.width-2;
		g.drawLine(tmp, 0, tmp, d.height); // left axis #1
		tmp++;
		g.drawLine(tmp, 0, tmp, d.height); // left axis #2
		// the description will be under the axis,
		tmp = lb.height+1;
		g.drawLine(0, tmp, d.width, tmp); // bottom axis #1
		tmp++;
		g.drawLine(0, tmp, d.width, tmp); // bottom axis #2

		// Describe time axis
		c1 = lb.height+2; 					// 2 pixels wide are the horizontal lines
		tmp = c1+Settings.labelLen; // end of the short label line
			// delta between labels
		tmp2 = (double)(rt.width-lb.width)/(double)Settings.labelsX;
			// tmp3: increment of label value; tmp4: label value
		tmp4 = tmp3 = xMax/(double)Settings.labelsX; 
		
		// Write zero to the zero ;-)
		g.drawString(zero, (int)(-fm.stringWidth(zero))-Settings.Spacing1+lb.width,
			c1+Settings.Spacing1+fm.getAscent() + Settings.labelLen);
		
		for (i=1, ofs=lb.width-1; i <= Settings.labelsX; i++) {
			ofs += tmp2;
			label = Settings.dTos(tmp4);
			g.drawLine((int)ofs, c1, (int)ofs, tmp);
			g.drawString(label, (int)(ofs - fm.stringWidth(label)/2),
				c1+Settings.Spacing1+fm.getAscent() + Settings.labelLen);
			tmp4 += tmp3;
		}
		
		// Write correct Y-axis value labels for current GraphLines (lines)
		// x-coord of the short label line
		tmp = lb.height;
		// distance between lines
		tmp2 = (double)(lb.height-rt.height)/(double)Settings.labelsY; 
		// label markers:
		// y-distance between two value labels for the same point
		ylabeld = fm.getHeight();
		// x coordinate of the end of label value
		ylblx = lb.width-2-Settings.Spacing1 - Settings.labelLen;
		
		for (i=1; i <= Settings.labelsY; i++) {
			// paint the short line
			tmp -= tmp2;
			g.setColor(Settings.axisCol);
			g.drawLine(lb.width-2, tmp, lb.width-2-Settings.labelLen, tmp);
		
			// Y value labels:
			ylabels = tmp-((ylabeld*lines.size()) >> 1)+fm.getAscent()+(Settings.Spacing1 >> 1);
			for (j=0; j < lines.size(); j++) {
				ln = (GraphLine)lines.elementAt(j);
				label = Settings.dTos((double)(ln.yMin+((ln.yMax-ln.yMin)/Settings.labelsY))*i);
				g.setColor(ln.col);
				c1 = ylblx - fm.stringWidth(label);
				if (c1 < 0) c1 = 0;
				g.drawString(label, c1, ylabels+ylabeld*j);
			}
		}
	}
}

class Engine {
	static double alphax3 = 3.0*((Math.PI*C.sigma*C.b4*C.c2)/(C.G2));
	
	public double Ms = Double.NaN; // Starting Mass
	// Currents:
	public double Mc = 0; // Mass [kg]
	public double Tc = 0; // Temperature [K]
	public double Ts = 0, Te = 0; // start, end temperatures
	public double Rc = 0; // Radius (Rg - Schwarzschild) [m]
	public double Ic = 0; // Intensity of radiation [W/m^2]
	public double Tm = 0; // Time from Start (0) [seconds]
	// Totals:
	public double Tt = 0;
	public double Ttcorr = 0; // Total time needed (-> Mc=0); Ttcorr = Tt*Settings.timeCorr2;
	  
  // Sets initial mass; recalculates Tt; calls reCalculate
  void setMass(double aMass) {
    Ms = aMass;
    Tt = Math.pow(Ms, 3.0)/alphax3;
    
    Ttcorr = Tt*Settings.timeCorr2;
    //Ts = 
    
    reCalculate();
  }
  
  // Changes time to aTime; calls reCalculate to update variables
  void setTime(double aTime) {
  	// do not recalc if prev value was the same
  	if (Tm != aTime) {
  		Tm = aTime;
  		reCalculate();
  	}
  }
  
  void reCalculate() {
  	// Vztahy pro veliciny:
  	try {  		
      Mc = Math.pow((Math.pow(Ms, 3.0) - alphax3*Tm), 1.0/3.0);
      // vzhledem k nepresnostem muze byt Ms^4 < 3xalpha*t :-(
      if (!Double.isNaN(Mc)) {
        Rc = (2.0*C.G*Mc)/(C.c2);
        Tc = C.b/Rc;
        Ic = C.sigma*Math.pow(Tc, 4.0);
      } else {
      	Mc = 0;
      	Rc = 0;
      	Tc = Double.POSITIVE_INFINITY;
      	Ic = Double.POSITIVE_INFINITY;
      }
    } catch (ArithmeticException ae) { 
    	  System.out.println(ae);
      };
  }  
  
  double secondsToYears(double Secs) {
  	return(Secs/C.SecPerYear);
  }
}

class AHole extends Canvas {
	double origM, xM;
	int oldw, oldw2;
	Image offImg;
	Color curColor = Color.red;
		
	public AHole() { super();
		// inits
		setBackground(Settings.holeBack);
		setForeground(Settings.holeFront);
		setVisible(true);
		setSize(Settings.holeSize, Settings.holeSize);
	}
	
	public void setColor(Color col) {
		offImg = null; // force repaint :-)
		curColor = col;
	}
	
	public void setCurM(double curM) {
		if (origM <= 0) { xM = 0; }
		else { xM = curM/origM; }
		repaint();
	}
	
	public void setOrigM(double M) {
		origM = Math.max(M, 0);
		setCurM(M);
	}
	
	public void update(Graphics g) {
		paint(g);
	}
	
	public void paint(Graphics g) {
		Graphics o;
		Dimension d = getSize();
		// radius of the circle
		int w = Math.max(0, Math.min(d.width-2, d.height-2)), 
			w2 = (int)(w * xM);
			
		if ((oldw != w) || (oldw2 != w2) || (offImg == null)) {
		 	if (w > 0) { 
				offImg = createImage(d.width, d.height);
				oldw = w; oldw2 = w2;
				o = offImg.getGraphics();
								
				// draw current size hole:
				// set color according to temperature: N/A now :-(
				o.setColor(curColor);			
				o.fillOval((d.width/2)-w2/2, (d.height/2)-(w2/2), w2, w2);
				
				// draw the outside (original size) black hole line
				o.setColor(Color.black);
				o.drawOval((d.width/2)-w/2, (d.height/2)-(w/2), w, w);
			}
		}
		g.drawImage(offImg, 0, 0, this);
	}
}

class ColMap extends Canvas {
	double tempMin = 0, tempMax = 0, 
		tempRange = 0; // tempMax-tempMin
	// image cache
	Dimension oldDim;
	Image offImg;
	
	public ColMap(double tempMinimum, double tempMaximum) { super();
		setBackground(Color.white);
		setSize(Settings.mapWidth, Settings.holeSize);
		setTempRange(tempMinimum, tempMaximum);
	}
	
	public void setTempRange(double tempMinimum, double tempMaximum) {
		tempMinimum = Math.max(tempMinimum, 0.0);
		tempMaximum = Math.max(tempMaximum, 0.0);
		
		if (tempMinimum < tempMaximum) {
			// correct values on input
			tempMin = tempMinimum; tempMax = tempMaximum;
		} else { // invert values on input
			tempMin = tempMaximum; tempMax = tempMinimum;
		}
		
		tempRange = tempMax - tempMin;

//		oldDim = null; // forces new painting, bypass of image cache
//		repaint(); // show changes
	}
	
	// returns colorindex in the range <0; 1>; colors are relative! 
	public float getColorIndex(double tempCurrent) {
		double tmp = tempCurrent-tempMin, res;
					
		if (tmp <= 0) return 0;
		else return (float)Math.min((double)1, tmp/tempRange);
	}
	
	public static Color colorIndexToColor(float ci) {
		float r = 0, g = 0, b = 0;
		
		ci *= (float)3.0;
		r = Math.min((float)1, ci);
		if (ci >= (float)1) g = Math.min((float)1, ci-(float)1);
		if (ci >= (float)2) b = Math.min((float)1, ci-(float)2);
		
		return new Color(r, g, b);
	}
	
	public void paint (Graphics g) {
		Graphics offG;
		Dimension d = getSize();
		int textH = g.getFontMetrics().getHeight();
		int line = 0, lnmax = d.height-1,
			lnSx = d.width-Settings.mapWidth, lnEx = d.width-1;
		float coli = 1, cold = (float)1.0/d.height;
		
		if (oldDim != null) {
			if (d.equals(oldDim)) {
				// buffer exists, size not changed -> use cached image
				g.drawImage(offImg, 0, 0, this);
				return;
			}
		}
		// cached image	does not exists, or resized...
		offImg = createImage(d.width, d.height);
		offG = offImg.getGraphics();
		
		// paint color ramp
		for (; line < lnmax; line++, coli -= cold) {
			offG.setColor(colorIndexToColor(coli));
			offG.drawLine(lnSx, line, lnEx, line);
		}		
		
		// #debug
		System.out.println("ColorMap rendered.");
		
		// paint description (3 point legend)
//		offG.setColor(Settings.colTemp);
//		offG.drawString(Settings.dTos(tempMin), 1, textH+1);
//		offG.drawString(Settings.dTos((tempMin+tempMax)/2), 1, (d.height+textH)/2);
//		offG.drawString(Settings.dTos(tempMax), 1, d.height-1);

		
		g.drawImage(offImg, 0, 0, this);
		oldDim = d; 	// save rendered image's dims.	
	}
}

class App {
	static BH Vap;
  public static void main(String[] args) {
    System.out.println("Loading the Black Hole Vaporisator...");    
    
    // Create container for App:
    Frame F = new Frame("Applet Container");
        
    // Simulate applet's life cycle in an app
    Vap = new BH();
    Vap.setSize(300, 400);
    F.add(Vap);    
		Vap.init();
		F.setSize(300+10, 400+20);
    F.setVisible(true);
    Vap.start();
    F.addWindowListener(new WindowAdapter()
		{
		  public void windowClosing(WindowEvent e)	{ 
		  	Vap.stop(); System.exit(0); }
		  public void windowOpened(WindowEvent e)	{ 
		  	Vap.start(); }
		  public void windowDeiconified(WindowEvent e)	{ 
		  	Vap.start(); }
		  public void windowIconified(WindowEvent e)	{ 
		  	Vap.stop(); }
		});
  }
}

// used to fetch params from HTML <param> code
class ParamHelper {
	String Name, Value; // cache
	Applet a;
	
	public ParamHelper(Applet app) { super();
		a = app;
	}
	
	public void setParamName(String pName) {
		Name = pName;
		Value = a.getParameter(Name);
	}
	
	public boolean hasValue() {
		return (Value != null);
	}
	
	public String asString() {
		return Value;
	}
	
	public String asStr(String onErr) {
		if (hasValue())	return Value;
		else return onErr;
	}
	
	public int asInt(int onErr) {
		try {
			return Integer.parseInt(Value);
		} catch (NumberFormatException nfe) { return onErr; }
	}
	
	public int asIntBounds(int onErr, int min, int max) {
		int i = asInt(onErr);
		return Math.min(max, Math.max(i, min));
	}
	
	public double asDouble(double onErr) {
		try {
			return Double.valueOf(Value).doubleValue();
		} catch (NumberFormatException nfe) { return onErr; }
	}
	
	public double asDoubleBounds(double onErr, double min, double max) {
		double d = asDouble(onErr);
		return Math.min(max, Math.max(d, min));
	}
	
	// Parses the Value as HTML color in the hex format: "#RRGGBB"
	public Color asColor(Color onErr) {
		int	col;
		
		try {
			if (Value.charAt(0) != '#') return onErr;
			try {
				col = Integer.parseInt(Value.substring(1), 16);
			} catch (NumberFormatException e) { return onErr;}
			return new Color(col);
		} catch (IndexOutOfBoundsException e) {	return onErr;}
	}
}