/**
 * Zeigt ein Bild in einem Scrollbereich an. 
 * Es ist möglich das Bild zu zoomen und mehrere Versionen des Bildes zu speichern, um eine "Rückgängig" Operation durchzuführen.
 * @author Thomas Schaller
 * @version 1.0 vom 01.02.2019
 */

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.awt.image.*;
import java.util.Vector;

public class PictureViewer extends JScrollPane
{
   
  // das aktuelle Bild
  private Picture picture;
    
  // Bilder für den Züruck-Modus speichern
  private static final int ANZ_BACK = 20;  
  private Vector<BufferedImage> history;
  
  // Zeichenfläche
  private ImageIcon scrollImageIcon;
  private JLabel imageLabel;
  
  // Zoom Faktor
  private double zoomFactor;
  public static final int FIT = -1;
  public static final int NORMAL = 1;
  
  
  
  /**
   * Erzeugt ein ScrollPanel mit integriertem Bild der Größe 1000x1000
   */
  public PictureViewer() {
    this(1000,1000);
  }

  /**
   * Erzeugt ein ScrollPanel mit integriertem Bild der angegebenen Größe 
   * @param width Breite des Bildes
   * @param height Höhe des Bildes
   */
  public PictureViewer(int width, int height) {
    this(width,height, "D0D0D0");
  }
  
  /**
   * Erzeugt ein ScrollPanel mit integriertem Bild der angegebenen Größe 
   * @param width Breite des Bildes
   * @param height Höhe des Bildes
   * @param background Farbe des Hintergrunds als HEX-String (z.B. "FF3A45")
   */
  public PictureViewer(int width, int height, String background) {
    this(new Picture(width,height, background));
  }
 
  /**
   * Erzeugt ein ScrollPanel mit integriertem Bild aus einer Bilddatei
   * @param filename Name des Bildes
   */
  public PictureViewer(String filename) {
    this(new Picture(filename));
  }
  
  /**
   * Erzeugt ein ScrollPanel und zeigt das Bild-Objekt an
   * @param picture anzuzeigendes Bild
   */
  public PictureViewer(Picture picture)
  {
    this.picture=picture;
    
    zoomFactor=1;
    
    scrollImageIcon = new ImageIcon(picture.getImage().getScaledInstance(picture.getImage().getWidth(), picture.getImage().getHeight(), Image.SCALE_FAST));
    imageLabel = new JLabel(scrollImageIcon);
    imageLabel.setVerticalAlignment(JLabel.CENTER);
    imageLabel.setHorizontalAlignment(JLabel.CENTER);
    setViewportView(imageLabel);
    
    this.setBorder(BorderFactory.createLineBorder(Color.black));
    picture.setObserver(this);
    history = new Vector<BufferedImage>();
    
  }
  
  /**
   * Setzt das anzuzeigende Bild neu
   * @param picture anzuzeigendes Bild
   */
  public void setImage(Picture picture) {
    this.history = new Vector<BufferedImage>();
    this.picture = picture;
    setZoom(NORMAL);
  }
  
  /**
   * Speichert das übergebene Bild in der History.
   * @param b zu speicherndes Bild
   */
  public void pushImage() {
      if( this.ANZ_BACK > 0) {
        if(history.size() == this.ANZ_BACK) {
            history.removeElementAt(0);
        }
        
        BufferedImage b = new BufferedImage(picture.getWidth(), picture.getHeight(), picture.getImage().getType());
        Graphics g = b.getGraphics();
        g.drawImage(picture.getImage(), 0, 0, null);
        g.dispose();
        
        history.add(b);
      }   
  }   
  
  /**
   * Ruft das letzte abgespeicherte Bild aus der History wieder auf.
   */
  private void popImage() {
    int anz = history.size();
    if(anz>0) {
      BufferedImage img = history.get(anz-1);
      history.removeElementAt(anz-1);
      picture.setImage(img);
      setZoom(zoomFactor);
    }
  }

  /**
   * Ruft das letzte abgespeicherte Bild aus der History wieder auf.
   */
  public void back() {
    popImage();
  }
  
  /**
   * Setzt das angezeigt Bild neu und beachtet dabei den Zoomfaktor.
   */
  public void repaint() {
    if( picture != null) {
      double factor= zoomFactor;
      if (zoomFactor == FIT) {
        double factorw = ((double) getWidth()-2) / picture.getWidth();
        double factorh = ((double) getHeight()-2) / picture.getHeight();
        factor = Math.min(factorw, factorh);
      }
      int width = (int) (picture.getWidth()*factor);
      int height = (int) (picture.getHeight()*factor);
      
      
      
      scrollImageIcon.setImage(picture.getImage().getScaledInstance(width, height, Image.SCALE_DEFAULT));
      revalidate();
    }
  }

  /**
   * Setzt den Zoom-Faktor für das Bild.
   * Als Zoomfaktor sind auch die Konstanten Bildanzeiger.FIT (auf Bildschirmgröße zoomen) und Bildanzeiger.NORMAL (100%) möglich.
   * @param factor Zoomfaktor (1.0 = 100%). 
   */
  public void setZoom(double factor)
  {
    zoomFactor = factor;
    repaint();
  }
  
  
  // Wrappermethoden
    /**
   * Definiert die Dimension der Breite und Höhe des Anzeigefensters in Pixeleinheiten. 
   * Die eingebauten Variablen Breite und Höhe werden durch die an diese Funktion übergebenen Parameter festgelegt. So weist beispielsweise 
   * der Befehl size(640, 480) der Variablen Breite 640 und der Variablen Höhe 480 zu. 
   * @param width Breite des Bildes
   * @param height Höhe des Bildes
   */
  public void size(int width, int height){
    picture.size(width, height);
  }
  
    /** 
   * Liefert die Breite des Bildes zurück.
   * @return Breite des Bildes
   */
  public int getImageWidth() {
    return picture.getWidth();
  }
  
    /** 
   * Liefert die Höhe des Bildes zurück.
   * @return Höhe des Bildes
   */
  public int getImageHeight() {
    return picture.getHeight();
  }

    /**
   * Die Funktion background() setzt die Farbe, die für den Hintergrund des Bildes verwendet wird. Der Standardhintergrund ist hellgrau. 
   * Es ist nicht möglich, den Alpha-Parameter Transparenz mit Hintergrundfarben auf der Hauptzeichnungsoberfläche zu verwenden. 
   * @param c Farbe für den Hintergrund (0-255: Graustufe zwischen 0 schwarz und 255 weiß, sonst: c wird als 3-Byte RGB-Wert interpretiert)
   */
  public void background(int c) {
    picture.background(c);
  }

 /**
   * Die Funktion background() setzt die Farbe, die für den Hintergrund des Bildes verwendet wird. Der Standardhintergrund ist hellgrau. 
   * Es ist nicht möglich, den Alpha-Parameter Transparenz mit Hintergrundfarben auf der Hauptzeichnungsoberfläche zu verwenden. 
   * @param r Rotanteil (0-255) der Hintergrundfarbe
   * @param g Grünanteil (0-255) der Hintergrundfarbe
   * @param b Blauanteil (0-255) der Hintergrundfarbe
   */
  public void background(int r, int g, int b) {
    picture.background(r,g,b);
  }
     
    /**
   * Zeichnet eine Linie (einen direkten Weg zwischen zwei Punkten) auf den Bildschirm. 
   * Um eine Linie einzufärben, verwenden Sie die {@link #stroke(int, int, int) stroke()} Funktion. Eine Zeile kann nicht gefüllt werden, daher hat die Funktion fill() keinen 
   * Einfluss auf die Farbe einer Zeile. Linien werden standardmäßig mit einer Breite von einem Pixel gezeichnet, dies kann jedoch mit der Funktion 
   * {@link #strokeWeight(double) strokeWeight()} geändert werden.
   * @param x1 x-Koordinate des 1. Punktes
   * @param y1 y-Koordinate des 1. Punktes
   * @param x2 x-Koordinate des 2. Punktes
   * @param y2 y-Koordinate des 2. Punktes
   */
  public void line(int x1, int y1, int x2, int y2) {
    picture.line(x1,y1,x2,y2);
  }

  /**
   * Zeichnet ein Rechteck auf das Bild. 
   * Standardmäßig legen die ersten beiden Parameter die Position der linken oberen Ecke fest, der dritte die Breite und der vierte die Höhe. 
   * Die Art und Weise, wie diese Parameter interpretiert werden, kann jedoch mit der Funktion {@link #rectMode(int) rectMode()} geändert werden.
   * Durch den Befehl {@link #fill(int,int,int) fill()} /{@link #noFill() noFill()}  kann die Füllfarbe des Rechtecks gewählt werden, durch {@link #stroke(int, int, int) stroke()}/{@link #noStroke() noStroke()}  die Rahmenfarbe.
   * @param a meist die x-Koordinate der linken oberen Ecke (kann durch rectMode() geändert werden).
   * @param b meist die y-Koordinate der linken oberen Ecke (kann durch rectMode() geändert werden).
   * @param c meist die Breite des Rechtecks (kann durch rectMode() geändert werden).
   * @param d meist die Höhe des Rechtecks (kann durch rectMode() geändert werden).
   * 
   */
  public void rect(int a, int b, int c, int d) {
    picture.rect(a,b,c,d);
  }
  
    /**
   * Zeichnet eine Ellipse/Kreis auf das Bild. 
   * Standardmäßig legen die ersten beiden Parameter die Position des Mittelpunkts fest, der dritte die Breite und der vierte die Höhe. 
   * Die Art und Weise, wie diese Parameter interpretiert werden, kann jedoch mit der Funktion {@link #ellipseMode(int) ellipseMode()} geändert werden.
   * Durch den Befehl {@link #fill(int,int,int) fill()} /{@link #noFill() noFill()} kann die Füllfarbe des Rechtecks gewählt werden, durch {@link #stroke(int, int, int) stroke()}/{@link #noStroke() noStroke()}  die Rahmenfarbe.
   * @param a meist die x-Koordinate des Mittelpunkts (kann durch ellipseMode() geändert werden).
   * @param b meist die y-Koordinate des Mittelpunkts (kann durch ellipseMode() geändert werden).
   * @param c meist die Breite des Rechtecks (kann durch ellipseMode() geändert werden).
   * @param d meist die Höhe des Rechtecks (kann durch ellipseMode() geändert werden).
   * 
   */
  public void ellipse(int a, int b, int c, int d) {
    picture.ellipse(a,b,c,d);
  }
  
  /**
   * Zeichnet ein Dreieck auf das Bild. 
   * Ein Dreieck ist eine Ebene, die durch die Verbindung von drei Punkten entsteht. Die ersten beiden Argumente spezifizieren den 
   * ersten Punkt, die mittleren beiden Argumente spezifizieren den zweiten Punkt und die letzten beiden Argumente spezifizieren den dritten Punkt. 
   * Durch den Befehl {@link #fill(int,int,int) fill()} /{@link #noFill() noFill()} kann die Füllfarbe des Rechtecks gewählt werden, durch {@link #stroke(int, int, int) stroke()}/{@link #noStroke() noStroke()}  die Rahmenfarbe.
   * @param x1 meist die x-Koordinate des 1. Punkts.
   * @param y1 meist die y-Koordinate des 1. Punkts.
   * @param x2 meist die x-Koordinate des 2. Punkts.
   * @param y2 meist die y-Koordinate des 2. Punkts.
   * @param x3 meist die x-Koordinate des 3. Punkts.
   * @param y3 meist die y-Koordinate des 3. Punkts.
   */
  public void triangle(int x1, int y1, int x2, int y2, int x3, int y3) {
    picture.triangle(x1,y1,x2,y2,x3,y3);
  }

  /**
   * Zeichnet ein Viereck auf das Bild. 
   * Ein Viereck ist ein vierseitiges Polygon. Es ist ähnlich wie ein Rechteck, aber die Winkel zwischen seinen Kanten 
   * sind nicht auf neunzig Grad beschränkt. Das erste Paar von Parametern (x1,y1) setzt den ersten Scheitelpunkt und die nachfolgenden 
   * Paare sollten im Uhrzeigersinn oder gegen den Uhrzeigersinn um die definierte Form herum verlaufen. 
   * Durch den Befehl {@link #fill(int,int,int) fill()} /{@link #noFill() noFill()} kann die Füllfarbe des Rechtecks gewählt werden, durch {@link #stroke(int, int, int) stroke()}/{@link #noStroke() noStroke()}  die Rahmenfarbe.
   * @param x1 meist die x-Koordinate des 1. Punkts.
   * @param y1 meist die y-Koordinate des 1. Punkts.
   * @param x2 meist die x-Koordinate des 2. Punkts.
   * @param y2 meist die y-Koordinate des 2. Punkts.
   * @param x3 meist die x-Koordinate des 3. Punkts.
   * @param y3 meist die y-Koordinate des 3. Punkts.
   * @param x4 meist die x-Koordinate des 3. Punkts.
   * @param y4 meist die y-Koordinate des 3. Punkts.
   */
  public void quad(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4) {
    picture.quad(x1,y1,x2,y2,x3,y3,x4,y4);
  }
  
  /**
   * Zeichnet ein Polygon auf das Bild.
   * Gleich lange Listen von x und y-Koordinaten bestimmen die Eckpunkte des Polygons.
   * Durch den Befehl {@link #fill(int,int,int) fill()} /{@link #noFill() noFill()} kann die Füllfarbe des Rechtecks gewählt werden, durch {@link #stroke(int, int, int) stroke()}/{@link #noStroke() noStroke()} die Rahmenfarbe.
   * @param x Liste der x-Koordinaten der Punkte.
   * @param y Liste der y-Koordinaten der Punkte.
   */
  public void polygon(int[] x, int[] y) {
    picture.polygon(x,y);
  }

  /**
   * Zeichnet einen Punkt, d.h. einen Kreis in der Dimension eines Pixels. 
   * Der erste Parameter ist der x-Wert für den Punkt, der zweite Wert ist der y-Wert für den Punkt. 
   * @param x x-Koordinate des Punktes
   * @param y y-Koordinate des Punktes
   */
  public void point(int x, int y) {
    picture.point(x,y);
    
  }
  
    /**
   * Ändert den Koordinaten-Modus beim Zeichnen von Rechtecken.
   * Ändert die Position, von der aus Rechtecke gezeichnet werden, indem es die Art und Weise ändert, wie Parameter, die an rect() übergeben werden, interpretiert werden.
   * Der Standardmodus ist rectMode(Bild.CORNER), der die ersten beiden Parameter von rect() als die linke obere Ecke der Form interpretiert, 
   * während der dritte und vierte Parameter seine Breite und Höhe sind.
   * rectMode(Bild.CORNERS) interpretiert die ersten beiden Parameter von rect() als die Position einer Ecke 
   * und die dritten und vierten Parameter als die Position der gegenüberliegenden Ecke.
   * rectMode(Bild.CENTER) interpretiert die ersten beiden Parameter von rect() als Mittelpunkt der Form, 
   * während der dritte und vierte Parameter seine Breite und Höhe sind.
   * rectMode(RADIUS) verwendet auch die ersten beiden Parameter von rect() als Mittelpunkt der Form, 
   * verwendet aber den dritten und vierten Parameter, um die Hälfte der Breite und Höhe der Formen festzulegen.
   * @param mode Modus der Koordinateninterpretation (CORNER, CORNERS, CENTER oder RADIUS)
   */
  public void rectMode(int mode) {
    picture.rectMode(mode);
  }
  
  
    /**
   * Ändert den Koordinaten-Modus beim Zeichnen von Kreisen/Ellipsen.
   * Ändert die Position, von der aus Kreise/Ellipsen gezeichnet werden, indem es die Art und Weise ändert, wie Parameter, die an ellipse() übergeben werden, interpretiert werden.
   * Der Standardmodus ist ellipseMode(Bild.CENTER), der die ersten beiden Parameter von ellipse() als Mittelpunkt der Form interpretiert, 
   * während der dritte und vierte Parameter seine Breite und Höhe sind.
   * ellipseMode(Bild.CORNER) interpretiert die ersten beiden Parameter von ellipse() als die Position einer Ecke 
   * und die dritten und vierten Parameter als Breite und Höhe der Form.
   * ellipseMode(Bild.CORNERS) interpretiert die ersten beiden Parameter von ellipse() als die Position einer Ecke 
   * und die dritten und vierten Parameter als die Position der gegenüberliegenden Ecke.
   * ellipseMode(RADIUS) verwendet auch die ersten beiden Parameter von ellipse() als Mittelpunkt der Form, 
   * verwendet aber den dritten und vierten Parameter, um die Hälfte der Breite und Höhe der Formen festzulegen.
   * @param mode Modus der Koordinateninterpretation (CORNER, CORNERS, CENTER oder RADIUS)
   */
  public void ellipseMode(int mode) {
    picture.ellipseMode(mode);
  }

  /**
   * Legt die Farbe fest, mit der Linien und Ränder um Formen gezeichnet werden. 
   * Diese Farbe wird hexadezimal in Form der RGB angegeben: z.B.  "CCFFAA" oder "004E23". Die Syntax verwendet sechs Ziffern - je zwei für die roten, grünen und blauen Komponenten,
   * um eine Farbe anzugeben (genau wie Farben typischerweise in HTML und CSS angegeben werden). 
   * @param pencolor Stiftfarbe in Hexadezimaldarstellung
   */
  public void stroke(String pencolor) {
    picture.stroke(pencolor);
  }

  /**
   * Legt die Farbe fest, mit der Linien und Ränder um Formen gezeichnet werden. 
   * Diese Farbe wird entweder als Graustufe (0-255) oder als 3-Byte RGB-Wert angegeben
   * @param pencolor Stiftfarbe (0-255: Graustufe zwischen 0 schwarz und 255 weiß, sonst: c wird als 3-Byte RGB-Wert interpretiert)
   */
  public void stroke(int pencolor) {
    picture.stroke(pencolor);
  }
  
  /**
   * Legt die Farbe fest, mit der Linien und Ränder um Formen gezeichnet werden. 
   * Diese Farbe wird komponentenweise als RGB-Wert angegeben
   * @param r Rotanteil (0-255) der Stiftfarbe
   * @param g Grünanteil (0-255) der Stiftfarbe
   * @param b Blauanteil (0-255) der Stiftfarbe
   */
  public void stroke(int r, int g, int b) {
    picture.stroke(r,g,b);
  }
  
  /**
   * Legt fest, dass keine Linien oder Ränder um Formen gezeichnet werden soll. 
   */
  public void noStroke() {
    picture.noStroke();
  }

  /**
   * Legt die Breite des Strichs für Linien, Punkte und den Rand um Formen fest. 
   * Alle Breiten werden in Pixeleinheiten angegeben. 
   * @param width Breite in Pixel
   */
  public void strokeWeight(double width) {
    picture.strokeWeight(width);
  }

   /**
   * Legt die Farbe fest, mit der Formen gefüllt werden. 
   * Diese Farbe wird hexadezimal in Form der RGB angegeben: z.B.  "CCFFAA" oder "004E23". Die Syntax verwendet sechs Ziffern - je zwei für die roten, grünen und blauen Komponenten,
   * um eine Farbe anzugeben (genau wie Farben typischerweise in HTML und CSS angegeben werden). 
   * @param fillcolor Füllfarbe in Hexadezimaldarstellung
   */
  public void fill(String fillcolor) {
    picture.fill(fillcolor);
  }

   /**
   * Legt die Farbe fest, mit der Formen gefüllt werden.
   * Diese Farbe wird entweder als Graustufe (0-255) oder als 3-Byte RGB-Wert angegeben.
   * @param fillcolor Füllfarbe (0-255: Graustufe zwischen 0 schwarz und 255 weiß, sonst: c wird als 3-Byte RGB-Wert interpretiert)
   */
  public void fill(int fillcolor) {
    picture.fill(fillcolor);
  }
  
    /**
   * Legt die Farbe fest, mit der Formen gefüllt werden.
   * Diese Farbe wird komponentenweise als RGB-Wert angegeben.
   * @param r Rotanteil (0-255) der Füllfarbe
   * @param g Grünanteil (0-255) der Füllfarbe
   * @param b Blauanteil (0-255) der Füllfarbe
   */
  public void fill(int r, int g, int b) {
    picture.fill(r,g,b);
  }
  
  /** Legt fest, dass die Formen nicht gefüllt werden sollen.
  */
  public void noFill() {
    picture.noFill();
  }

  /** 
   * Löscht den Inhalt des Bildes.
   * Der Hintergrund wird mit der Hintergrundfarbe neu gefüllt.
   */
  public void clear(){
    picture.clear();
  }

  /** 
   * Lädt ein Bild aus dem Dateisystem.
   * Lädt ein Bild von einem Datenträger und setzt Stiftfarbe und Füllfarbe auf Standardwerte zurück.
   * @param filename Dateiname des Bildes
   */
  public void load(String filename) {
    picture.load(filename);
  }
    
  /** 
   * Speichert ein Bild.
   * Speichert ein Bild auf einem Datenträger. Zulässig sind die Dateiformate PNG und GIF. Die Dateiendung legt den Typ fest.
   * Standardmäßig wird die Dateiendung .png ergänzt, wenn keine angegeben ist.
   * @param filename Dateiname des Bildes
   */
  public void save(String filename) {
    picture.save(filename);
  }

  /**
   * Gibt einen Text an den gegebenen Koordinaten aus
   * Zur Ausgabe des Textes wird der ausgewählte Font verwendet. Dieser muss vorher mit {@link #textFont(Font) textFont() } festgelegt.
   * @param t Text, der angezeigt werden soll
   * @param x x-Koordinate des Textanfangs
   * @param y y-Koordinate der Grundlinie des Textes.
   */
  public void text(String t, int x, int y) {
    picture.text(t,x,y);
  }

  /**
   * Legt die Schriftart für Textausgaben fest.
   * Jeder übliche Java-Font kann verwendet werden. Er kann mit z.B. Font f = new Font( "Arial", Font.PLAIN, 14 ); definiert werden. 
   * @param font ein Font-Objekt 
   */    
  public void textFont(Font font) {
    picture.textFont(font);
  }
  
 /**
   * Liefert das Bild als zweidimensionales Pixel-Array.
   * @return zweidimensionales Array von Color-Objekten, die den Pixeln des Bildes entsprechen.
   */  
  public Color[][] getPixelArray() {
    return picture.getPixelArray();
  }

  /**
   * Setzt das Bild neu auf Basis des Pixel-Arrays.
   * Die Größe des Bildes wird nicht automatisch an das Array angepasst.
   * @param pixel zweidimensionales Array von Color-Objekten
   */    
  public void setPixelArray(Color[][] pixel) {
    picture.setPixelArray(pixel);
  }
   
  
  
}
