Das Apfelmännchen/ FraktaleMengeAusgabe

/*
  Source file: FraktaleMengeAusgabe.java
  Author: Markus Bautsch
  Licence: public domain
  Date: 3 November 2022
  Version: 1.1 / using method java.awt.Graphics.drawImage
  Revision: 27 May 2023
  Version: 2.1 (implementing key listener, PNG file compression, variable complex values)
  Programming language: Java
 */

/*
  Graphische Ausgabe von Julia- und Mandelbrot-Mengen.
  Mandelbrot-Mengen werden mit dem konstanten komplexwertigen Parameter c = (0, 0) gekennzeichnet.
 */

public final class FraktaleMengeAusgabe extends javax.swing.JFrame implements java.awt.event.KeyListener
{
	// serialVersionUID fuer die Serialisation, die in der Klasse javax.swing.JFrame implementiert ist.
	private final static long serialVersionUID = 1;

	// Klassenkonstanten
	private final static int offset = 10;
	private final static int offsetTop = 30;

	// Instanzkonstanten
	private KomplexeZahl c; 
	private boolean isMandelbrot;
	private double minX;
	private double maxX;
	private double minY;
	private double maxY;
	final private int pixelZahlX;
	final private int pixelZahlY;
	private double shiftFactor = 0.05; // Zum Zoomen und Verschieben des Bildes

	// Instanzvariablen
	private int keyCode;
	private java.awt.image.BufferedImage bufferedImage;
	private java.awt.image.WritableRaster imageRaster;

	/* Konstruktor fuer die Initialisierung von Instanzen der Klasse FraktaleMengeAusgabe.
	 * @param c: Komplexwertiger Koeffizient der quadratischen Polynome
	 * @param minX: Kleinster x-Wert der rechteckigen Darstellung der fraktalen Menge
	 * @param maxX: Groesster x-Wert der rechteckigen Darstellung der fraktalen Menge
	 * @param minY: Kleinster y-Wert der rechteckigen Darstellung der fraktalen Menge
	 * @param maxY: Groesster y-Wert der rechteckigen Darstellung der fraktalen Menge
	 * @param bildhoehe: Die Anzahl der Bildpunkte in der Bildhoehe
	 * @param shiftFactor: Anteil des verschobenen oder gezoomten Bildausschnitts
	 */
	public FraktaleMengeAusgabe (KomplexeZahl c,
			double minX, double maxX, double minY, double maxY, int bildhoehe,
			double shiftFactor)
	{
		super ("Fraktale Menge");
		this.setComplexParameter (c);
		this.minX = minX;
		this.maxX = maxX;
		this.minY = minY;
		this.maxY = maxY;
		this.pixelZahlY = bildhoehe;
		this.pixelZahlX = (int) (bildhoehe / (maxY - minY) * (maxX - minX));
		this.shiftFactor = shiftFactor;
		this.keyCode = 0;
		this.bufferedImage = new java.awt.image.BufferedImage (this.pixelZahlX, this.pixelZahlY, java.awt.image.BufferedImage.TYPE_INT_RGB);
		this.imageRaster = bufferedImage.getWritableTile (0, 0);
		this.setDefaultCloseOperation (javax.swing.WindowConstants.EXIT_ON_CLOSE);
		this.addKeyListener (this);
	}

	// Methode setName fuer die Bestimmung des aktuellen Titels der Instanz von FraktaleMengeAusgabe.
	private void setName ()
	{
		java.lang.String fractalSetName;
		if (this.isMandelbrot)
		{
			fractalSetName = "Mandelbrot-Menge";
		}
		else
		{
			fractalSetName = "Julia-Menge" + "_c.re_" + this.c.re + "_c.im_" + this.c.im;
		}
		fractalSetName = fractalSetName + "_re_" + this.minX + "_" + this.maxX + "_im_" + this.minY + "_" + this.maxY;
		this.setTitle (fractalSetName);
	}

	// Methode saveFile für das Speichern einer fraktalen Graphik in einer graphischen PNG-Datei
	public void saveFile ()
	{
		final java.lang.String fileExtension = "png";
		java.util.Iterator <javax.imageio.ImageWriter> iter = javax.imageio.ImageIO.getImageWritersByFormatName (fileExtension);
		javax.imageio.ImageWriter imageWriter = (javax.imageio.ImageWriter) iter.next ();
		javax.imageio.ImageWriteParam imageWriteParam = imageWriter.getDefaultWriteParam ();
		imageWriteParam.setCompressionMode (javax.imageio.ImageWriteParam.MODE_DEFAULT); // for compressed file
		try
		{
			java.lang.String fileName = this.getTitle () + "." + fileExtension;
			javax.imageio.stream.FileImageOutputStream fileImageOutputStream = new javax.imageio.stream.FileImageOutputStream (new java.io.File (fileName));
			imageWriter.setOutput (fileImageOutputStream);
			javax.imageio.IIOImage iIOimg = new javax.imageio.IIOImage ((java.awt.image.RenderedImage) bufferedImage, null, null);
			java.lang.System.out.println ("Save " + fileName);
			imageWriter.write (null, iIOimg, imageWriteParam);
		}
		catch (java.lang.Exception exception)
		{
			exception.printStackTrace ();
		}
	}

	/*
	 * Bestimmt die Farbe eines darzustellenden Punktes aus der Zahl der durchgefuehrten Iterationen in der Polynomfolge
	 * @param zahlZurFarbbestimmung: Zahl der durchgefuehrten Iterationen, für die eine Farbbestimmung durchgeführt werden aoll
	 * @return: Farbe vom Datentyp java.awt.Color
	 */
	private static int [] bestimmeAusgabefarbe (int zahlZurFarbbestimmung)
	{
		final int hell = 255;
		final int schwellfaktor1 = 8;
		final int schwellfaktor2 = 16;
		final int schwelle1 = hell / schwellfaktor1;
		final int schwelle2 = hell / schwellfaktor2;

		int helligkeit = hell * zahlZurFarbbestimmung / FraktaleMenge.maximaleAnzahlDerIterationen; // liegt im Intervall [0 .. HELL]

		int [] farbe = {0, 0, 0}; // Vorgabe = schwarz fuer Hintergrund, wenn Polynomfolge als nicht divergent angesehen wird

		if (zahlZurFarbbestimmung < FraktaleMenge.maximaleAnzahlDerIterationen)
		{
			if (helligkeit > schwelle1) // Polynomfolge divergiert langsam
			{
				farbe [0] = hell; // rot
				farbe [1] = hell; // gruen
			}
			else if (helligkeit > schwelle2) // Polynomfolge divergiert schneller
			{
				farbe [0] = schwellfaktor1 * helligkeit; // rot
				farbe [1] = helligkeit; // gruen
			}
			else // Polynomfolge divergiert schnell
			{
				farbe [0] = schwellfaktor2 * helligkeit; // rot
				farbe [1] = helligkeit; // gruen
			}
			farbe [2] = helligkeit; // blau
		}
		return farbe;
	}

	/*
	 * Zeichnet einen farbigen Punkt in das Bild
	 * @param frame: Frame von Datentyp FraktaleMengeAusgabe
	 * @param z: Variable z des Polynoms
	 * @param c: Variable c des Polynoms
	 * @param xWert Der X-Wert an dem der Punkt gemalt werden soll
	 * @param yWert Der Y-Wert an dem der Punkt gemalt werden soll
	 */
	private static void zeichnePunkt (FraktaleMengeAusgabe frame, KomplexeZahl z, KomplexeZahl c, int xWert, int yWert)
	{
		int zahlZurFarbbestimmung = FraktaleMenge.iterationszahlBisSchranke (z, c);
		int [] farbe = bestimmeAusgabefarbe (zahlZurFarbbestimmung);
		frame.imageRaster.setPixel (xWert, yWert, farbe);
	}

	/*
	 * Berechnet den Skalierungsfaktor auf einer Koordinatenachse
	 * @param min: Der minimale Wert auf der Koordinatenachse
	 * @param max: Der maximale Wert auf der Koordinatenachse
	 * @param pixelZahl: Die gewuenschte Anzahl der Pixel auf der Koordinatenachse
	 * @return: Skalierungsfaktor auf der Koordinatenachse
	 */
	private static double berechneSkalierungsfaktor (double min, double max, long pixelZahl)
	{
		double skalierungsfaktor = (max - min) / pixelZahl;
		return skalierungsfaktor;
	}

	/*
	 * Transformiert eine x-Koordinate aus der Pixel-Ebene in die mathematische Ebene
	 * @param minX: Der minimale x-Wert in der mathematischen Ebene
	 * @param skalierungsfaktorX: Skalierungsfaktor von der Pixel-Ebene in die mathematische Ebene in x-Richtung
	 * @param pixelKoordinateX: Die x-Koordinate in der Pixel-Ebene
	 * @return: Die x-Koordinate in der mathematischen Ebene
	 */
	private static double koordinatentransformationX (double minX, double skalierungsfaktorX, long pixelKoordinateX)
	{
		double xKoordinate = minX + (pixelKoordinateX * skalierungsfaktorX);
		return xKoordinate;
	}

	/*
	 * Transformiert eine y-Koordinate aus der Pixel-Ebene in die mathematische Ebene
	 * @param maxY: Der maximale y-Wert auf der mathematischen Ebene
	 * @param skalierungsfaktorY: Skalierungsfaktor von der Pixel-Ebene in die mathematische Ebene in y-Richtung
	 * @param pixelKoordinateY: Die y-Koordinate in der Pixel-Ebene
	 * @return: Die y-Koordinate in der mathematischen Ebene
	 */
	private static double koordinatentransformationY (double maxY, double skalierungsfaktorY, long pixelKoordinateY)
	{
		double yKoordinate = maxY - (pixelKoordinateY * skalierungsfaktorY);
		return yKoordinate;
	}

	/*
	 * Zeichnet eine fraktale Menge (Julia-Menge oder Mandelbrot-Menge)
	 * @param fraktakeMenge: fraktale Menge
	 * @param graphics: Graphikelement
	 */
	private static void fraktaleMenge (FraktaleMengeAusgabe fraktaleMenge)
	{
		final double skalierungX = berechneSkalierungsfaktor (fraktaleMenge.minX, fraktaleMenge.maxX, fraktaleMenge.pixelZahlX);
		final double skalierungY = berechneSkalierungsfaktor (fraktaleMenge.minY, fraktaleMenge.maxY, fraktaleMenge.pixelZahlY);

		KomplexeZahl c = fraktaleMenge.c; // Parameter c der fraktalen Menge
		KomplexeZahl z = new KomplexeZahl (0,0); // Parameter z der fraktalen Menge

		int ix; // Zaehler für Spalten in x-Richtung nach rechts
		int iy; // Zaehler für Zeilen in y-Richtung nach unten

		iy = 0; // oben starten
		while (iy < fraktaleMenge.pixelZahlY)
		{			
			ix = 0; // links starten
			while (ix < fraktaleMenge.pixelZahlX)
			{
				double re = koordinatentransformationX (fraktaleMenge.minX, skalierungX, ix);
				double im = koordinatentransformationY (fraktaleMenge.maxY, skalierungY, iy);
				if (fraktaleMenge.isMandelbrot) // Mandelbrot-Menge
				{
					z.re = 0; // Realteil von z0 ist immer gleich 0
					z.im = 0; // Imaginaerteil von z0 ist immer gleich 0
					c.re = re; // Realteil von c ist immer gleich der x-Koordinate
					c.im = im; // Imaginaerteil von c ist immer gleich der y-Koordinate
				}
				else // Julia-Menge
				{
					z.re = re; // Realteil von z ist immer gleich der x-Koordinate
					z.im = im; // Imaginaerteil von z ist immer gleich der y-Koordinate	
				}
				zeichnePunkt (fraktaleMenge, z, c, ix, iy);
				ix++;
			}
			iy++;
		}
	}

	/*
	 * Zeichnet eine fraktale Menge
	 * @param graphics Graphisches Element
	 */
	public void paint (java.awt.Graphics graphics)
	{
		// Rahmengröße, Hintergrund und das Layout werden gesetzt.
		this.setName ();
		fraktaleMenge (this);
		graphics.drawImage (bufferedImage, offset, offset + offsetTop, null);
	}

	// Methode init fuer die Initialisierung der graphischen Ausgabe einer Instanz von FraktaleMengeAusgabe.
	public void init ()
	{
		// Rahmengroesse, Hintergrund und das Layout werden gesetzt
		this.setSize (this.pixelZahlX, this.pixelZahlY);
		this.setBackground (java.awt.Color.WHITE);
		java.awt.image.BufferedImage bufferedImage = new java.awt.image.BufferedImage (this.pixelZahlX, this.pixelZahlY, java.awt.image.BufferedImage.TYPE_INT_RGB);
		java.awt.Graphics2D graphics2D = bufferedImage.createGraphics ();
		this.setVisible (true);
		this.setAlwaysOnTop (true);
		this.paint (graphics2D);
	}

	@Override
	public void keyPressed (java.awt.event.KeyEvent event)
	{
		this.keyCode = event.getKeyCode ();
	}

	@Override
	public void keyReleased(java.awt.event.KeyEvent event)
	{
	}

	@Override
	public void keyTyped (java.awt.event.KeyEvent event)
	{
	}

	public int getKeyCode ()
	{
		int returnKeyCode = this.keyCode;
		this.keyCode = 0;
		return returnKeyCode;
	}

	public double getMinX ()
	{
		return this.minX;
	}

	public double getMaxX ()
	{
		return this.maxX;
	}

	public double getMinY ()
	{
		return this.minY;
	}

	public double getMaxY ()
	{
		return this.maxY;
	}

	public void setBorders (double minX, double maxX, double minY, double maxY)
	{
		this.minX = minX;
		this.maxX = maxX;
		this.minY = minY;
		this.maxY = maxY;
	}

	public KomplexeZahl getComplexParameter ()
	{
		KomplexeZahl c = this.c;
		return c;
	}

	public void setComplexParameter (KomplexeZahl c)
	{
		this.c = c;
		this.isMandelbrot = (KomplexeZahl.betragsquadrat (c) == 0);
	}

	private double getXShift ()
	{
		double minX = this.getMinX ();
		double maxX = this.getMaxX ();
		double shiftX = (maxX - minX) * shiftFactor;
		return shiftX;
	}

	private double getYShift ()
	{
		double minY = this.getMinY ();
		double maxY = this.getMaxY ();
		double shiftY = (maxY - minY) * shiftFactor;
		return shiftY;
	}

	public double getShiftFactor ()
	{
		double shiftFactor = this.shiftFactor;
		return shiftFactor;
	}

	public void setShiftFactor (double shiftFactor)
	{
		this.shiftFactor = shiftFactor;
	}

	public void center ()
	{
		double minX = this.getMinX ();
		double maxX = this.getMaxX ();
		double deltaX2 = (maxX - minX) / 2;

		double minY = this.getMinY ();
		double maxY = this.getMaxY ();
		double deltaY2 = (maxY - minY) / 2;

		this.setBorders (-deltaX2, deltaX2, -deltaY2, deltaY2);
	}

	public void zoomIn ()
	{
		double shiftX = getXShift ();
		double minX = this.getMinX () + shiftX;
		double maxX = this.getMaxX () - shiftX;

		double shiftY = getYShift ();
		double minY = this.getMinY () + shiftY;
		double maxY = this.getMaxY () - shiftY;

		this.setBorders (minX, maxX, minY, maxY);
	}

	public void zoomOut ()
	{
		double shiftX = getXShift ();
		double minX = this.getMinX () - shiftX;
		double maxX = this.getMaxX () + shiftX;

		double shiftY = getYShift ();
		double minY = this.getMinY () - shiftY;
		double maxY = this.getMaxY () + shiftY;

		this.setBorders (minX, maxX, minY, maxY);
	}

	public void shiftRight ()
	{
		double shiftX = getXShift ();
		double minX = this.getMinX () + shiftX;
		double maxX = this.getMaxX () + shiftX;

		double minY = this.getMinY ();
		double maxY = this.getMaxY ();

		this.setBorders (minX, maxX, minY, maxY);
	}

	public void shiftLeft ()
	{
		double shiftX = getXShift ();
		double minX = this.getMinX () - shiftX;
		double maxX = this.getMaxX () - shiftX;

		double minY = this.getMinY ();
		double maxY = this.getMaxY ();

		this.setBorders (minX, maxX, minY, maxY);
	}

	public void shiftUp ()
	{
		double minX = this.getMinX ();
		double maxX = this.getMaxX ();

		double shiftY = getYShift ();
		double minY = this.getMinY () + shiftY;
		double maxY = this.getMaxY () + shiftY;

		this.setBorders (minX, maxX, minY, maxY);
	}

	public void shiftDown ()
	{
		double minX = this.getMinX ();
		double maxX = this.getMaxX ();

		double shiftY = getYShift ();
		double minY = this.getMinY () - shiftY;
		double maxY = this.getMaxY () - shiftY;

		this.setBorders (minX, maxX, minY, maxY);
	}
}