Merge branch 'main' into charts

This commit is contained in:
ngb
2022-07-06 21:29:58 +02:00
143 changed files with 3151 additions and 4584 deletions

View File

@@ -28,7 +28,7 @@ public class Constants {
/**
* Patchversion der Zeichenmaschine.
*/
public static final int APP_VERSION_REV = 12;
public static final int APP_VERSION_REV = 21;
/**
* Version der Zeichenmaschine als Text-String.
@@ -71,6 +71,11 @@ public class Constants {
*/
public static final int STD_FONTSIZE = 14;
/**
* Standardwert für den Abstand von Formen.
*/
public static final int STD_BUFFER = 10;
public static final Options.StrokeType SOLID = Options.StrokeType.SOLID;
public static final Options.StrokeType DASHED = Options.StrokeType.DASHED;
@@ -242,6 +247,11 @@ public class Constants {
* Zeichenmaschine selbst vorgenommen werden.
*/
/**
* Aktuell dargestellte Bilder pro Sekunde.
*/
public static int framesPerSecond = STD_FPS;
/**
* Anzahl der Ticks (Frames), die das Programm bisher läuft.
*/
@@ -279,6 +289,18 @@ public class Constants {
*/
public static double pmouseY = 0.0;
/**
* Die aktuelle (<em>current</em>) {@code x}-Koordinate der Maus
* (wird bei jeder Mausbewegung aktualisiert).
*/
public static double cmouseX = 0.0;
/**
* Die aktuelle (<em>current</em>) {@code y}-Koordinate der Maus
* (wird bei jeder Mausbewegung aktualisiert).
*/
public static double cmouseY = 0.0;
/**
* Gibt an, ob ein Mausknopf derzeit gedrückt ist.
*/
@@ -364,8 +386,8 @@ public class Constants {
// Farben
/**
* Erstellt eine graue Farbe. Der Parameter {@code gray} gibt einen
* Grauwert zwischen <code>0</code> und <code>255</code> an, wobei
* Erstellt eine graue Farbe. Der Parameter {@code gray} gibt einen Grauwert
* zwischen <code>0</code> und <code>255</code> an, wobei
* <code>0</code> schwarz und <code>255</code> weiß ist.
*
* @param gray Grauwert zwischen <code>0</code> und <code>255</code>.
@@ -376,8 +398,8 @@ public class Constants {
}
/**
* Erstellt eine graue Farbe. Der Parameter {@code gray} gibt einen
* Grauwert zwischen <code>0</code> und <code>255</code> an, wobei
* Erstellt eine graue Farbe. Der Parameter {@code gray} gibt einen Grauwert
* zwischen <code>0</code> und <code>255</code> an, wobei
* <code>0</code> schwarz und <code>255</code> weiß ist.
* {@code alpha} gibt den den Transparentwert an (auch zwischen
* <code>0</code> und <code>255</code>), wobei
@@ -395,8 +417,8 @@ public class Constants {
/**
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen <code>0</code> und <code>255</code>.
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>.
*
* @param red Rotwert zwischen <code>0</code> und <code>255</code>.
* @param green Grünwert zwischen <code>0</code> und <code>255</code>.
@@ -409,10 +431,10 @@ public class Constants {
/**
* Erstellt eine Farbe. Die Parameter {@code red}, {@code green} und
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die
* Werte liegen zwischen <code>0</code> und <code>255</code>.
* {@code alpha} gibt den den Transparentwert an (auch zwischen
* code>0</code> und <code>255</code>), wobei
* {@code blue} geben die Rot-, Grün- und Blauanteile der Farbe. Die Werte
* liegen zwischen <code>0</code> und <code>255</code>. {@code alpha} gibt
* den den Transparentwert an (auch zwischen code>0</code> und
* <code>255</code>), wobei
* <code>0</code> komplett durchsichtig ist und <code>255</code> komplett
* deckend.
*
@@ -705,9 +727,9 @@ public class Constants {
}
/**
* Beschränkt die Zahl {@code x} auf das Intervall <code>[min,
* max]</code>. Liegt {@code x} außerhalb des Intervalls, wird eine der
* Grenzen zurückgegeben.
* Beschränkt die Zahl {@code x} auf das Intervall <code>[min, max]</code>.
* Liegt {@code x} außerhalb des Intervalls, wird eine der Grenzen
* zurückgegeben.
*
* @param x Eine Zahl.
* @param max Das Maximum des Intervalls.
@@ -724,9 +746,9 @@ public class Constants {
}
/**
* Beschränkt die Zahl {@code x} auf das Intervall <code>[min,
* max]</code>. Liegt {@code x} außerhalb des Intervalls, wird eine der
* Grenzen zurückgegeben.
* Beschränkt die Zahl {@code x} auf das Intervall <code>[min, max]</code>.
* Liegt {@code x} außerhalb des Intervalls, wird eine der Grenzen
* zurückgegeben.
*
* @param x Eine Zahl.
* @param max Das Maximum des Intervalls.
@@ -743,8 +765,8 @@ public class Constants {
}
/**
* Interpoliert einen Wert zwischen {@code from} und {@code to}
* linear, abhängig von {@code t}. Das Ergebnis entspricht der Formel
* Interpoliert einen Wert zwischen {@code from} und {@code to} linear,
* abhängig von {@code t}. Das Ergebnis entspricht der Formel
*
* <pre>
* from - t * (from + to)
@@ -759,6 +781,10 @@ public class Constants {
return from + t * (to - from);
}
public static final double morph( double from, double to, double t ) {
return interpolate(from, to, limit(t, 0.0, 1.0));
}
public static final double map( double value, double fromMin, double fromMax, double toMin, double toMax ) {
return interpolate(toMin, toMax, (value - fromMin) / (fromMax - fromMin));
}
@@ -1038,4 +1064,320 @@ public class Constants {
return Boolean.parseBoolean(value);
}
// Konstants for Key events (Copied from KeyEvent)
/**
* Constant for the ENTER virtual key.
*/
public static final int KEY_ENTER = KeyEvent.VK_ENTER;
/**
* Constant for the BACK_SPACE virtual key.
*/
public static final int KEY_BACK_SPACE = KeyEvent.VK_BACK_SPACE;
/**
* Constant for the TAB virtual key.
*/
public static final int KEY_TAB = KeyEvent.VK_TAB;
/**
* Constant for the CANCEL virtual key.
*/
public static final int KEY_CANCEL = KeyEvent.VK_CANCEL;
/**
* Constant for the CLEAR virtual key.
*/
public static final int KEY_CLEAR = KeyEvent.VK_CLEAR;
/**
* Constant for the SHIFT virtual key.
*/
public static final int KEY_SHIFT = KeyEvent.VK_SHIFT;
/**
* Constant for the CONTROL virtual key.
*/
public static final int KEY_CONTROL = KeyEvent.VK_CONTROL;
/**
* Constant for the ALT virtual key.
*/
public static final int KEY_ALT = KeyEvent.VK_ALT;
/**
* Constant for the PAUSE virtual key.
*/
public static final int KEY_PAUSE = KeyEvent.VK_PAUSE;
/**
* Constant for the CAPS_LOCK virtual key.
*/
public static final int KEY_CAPS_LOCK = KeyEvent.VK_CAPS_LOCK;
/**
* Constant for the ESCAPE virtual key.
*/
public static final int KEY_ESCAPE = KeyEvent.VK_ESCAPE;
/**
* Constant for the SPACE virtual key.
*/
public static final int KEY_SPACE = KeyEvent.VK_SPACE;
/**
* Constant for the PAGE_UP virtual key.
*/
public static final int KEY_PAGE_UP = KeyEvent.VK_PAGE_UP;
/**
* Constant for the PAGE_DOWN virtual key.
*/
public static final int KEY_PAGE_DOWN = KeyEvent.VK_PAGE_DOWN;
/**
* Constant for the END virtual key.
*/
public static final int KEY_END = KeyEvent.VK_END;
/**
* Constant for the HOME virtual key.
*/
public static final int KEY_HOME = KeyEvent.VK_HOME;
/**
* Constant for the non-numpad <b>left</b> arrow key.
*/
public static final int KEY_LEFT = KeyEvent.VK_LEFT;
/**
* Constant for the non-numpad <b>up</b> arrow key.
*/
public static final int KEY_UP = KeyEvent.VK_UP;
/**
* Constant for the non-numpad <b>right</b> arrow key.
*/
public static final int KEY_RIGHT = KeyEvent.VK_RIGHT;
/**
* Constant for the non-numpad <b>down</b> arrow key.
*/
public static final int KEY_DOWN = KeyEvent.VK_DOWN;
/**
* Constant for the comma key, ","
*/
public static final int KEY_COMMA = KeyEvent.VK_COMMA;
/**
* Constant for the minus key, "-"
*
* @since 1.2
*/
public static final int KEY_MINUS = KeyEvent.VK_MINUS;
/**
* Constant for the period key, "."
*/
public static final int KEY_PERIOD = KeyEvent.VK_PERIOD;
/**
* Constant for the forward slash key, "/"
*/
public static final int KEY_SLASH = KeyEvent.VK_SLASH;
/**
* Constant for the "0" key.
*/
public static final int KEY_0 = KeyEvent.VK_0;
/**
* Constant for the "1" key.
*/
public static final int KEY_1 = KeyEvent.VK_1;
/**
* Constant for the "2" key.
*/
public static final int KEY_2 = KeyEvent.VK_2;
/**
* Constant for the "3" key.
*/
public static final int KEY_3 = KeyEvent.VK_3;
/**
* Constant for the "4" key.
*/
public static final int KEY_4 = KeyEvent.VK_4;
/**
* Constant for the "5" key.
*/
public static final int KEY_5 = KeyEvent.VK_5;
/**
* Constant for the "6" key.
*/
public static final int KEY_6 = KeyEvent.VK_6;
/**
* Constant for the "7" key.
*/
public static final int KEY_7 = KeyEvent.VK_7;
/**
* Constant for the "8" key.
*/
public static final int KEY_8 = KeyEvent.VK_8;
/**
* Constant for the "9" key.
*/
public static final int KEY_9 = KeyEvent.VK_9;
/**
* Constant for the semicolon key, ";"
*/
public static final int KEY_SEMICOLON = KeyEvent.VK_SEMICOLON;
/**
* Constant for the equals key, "="
*/
public static final int KEY_EQUALS = KeyEvent.VK_EQUALS;
/** VK_A thru VK_Z are the same as ASCII 'A' thru 'Z' (0x41 - 0x5A) */
/**
* Constant for the "A" key.
*/
public static final int KEY_A = KeyEvent.VK_A;
/**
* Constant for the "B" key.
*/
public static final int KEY_B = KeyEvent.VK_B;
/**
* Constant for the "C" key.
*/
public static final int KEY_C = KeyEvent.VK_C;
/**
* Constant for the "D" key.
*/
public static final int KEY_D = KeyEvent.VK_D;
/**
* Constant for the "E" key.
*/
public static final int KEY_E = KeyEvent.VK_E;
/**
* Constant for the "F" key.
*/
public static final int KEY_F = KeyEvent.VK_F;
/**
* Constant for the "G" key.
*/
public static final int KEY_G = KeyEvent.VK_G;
/**
* Constant for the "H" key.
*/
public static final int KEY_H = KeyEvent.VK_H;
/**
* Constant for the "I" key.
*/
public static final int KEY_I = KeyEvent.VK_I;
/**
* Constant for the "J" key.
*/
public static final int KEY_J = KeyEvent.VK_J;
/**
* Constant for the "K" key.
*/
public static final int KEY_K = KeyEvent.VK_K;
/**
* Constant for the "L" key.
*/
public static final int KEY_L = KeyEvent.VK_L;
/**
* Constant for the "M" key.
*/
public static final int KEY_M = KeyEvent.VK_M;
/**
* Constant for the "N" key.
*/
public static final int KEY_N = KeyEvent.VK_N;
/**
* Constant for the "O" key.
*/
public static final int KEY_O = KeyEvent.VK_O;
/**
* Constant for the "P" key.
*/
public static final int KEY_P = KeyEvent.VK_P;
/**
* Constant for the "Q" key.
*/
public static final int KEY_Q = KeyEvent.VK_Q;
/**
* Constant for the "R" key.
*/
public static final int KEY_R = KeyEvent.VK_R;
/**
* Constant for the "S" key.
*/
public static final int KEY_S = KeyEvent.VK_S;
/**
* Constant for the "T" key.
*/
public static final int KEY_T = KeyEvent.VK_T;
/**
* Constant for the "U" key.
*/
public static final int KEY_U = KeyEvent.VK_U;
/**
* Constant for the "V" key.
*/
public static final int KEY_V = KeyEvent.VK_V;
/**
* Constant for the "W" key.
*/
public static final int KEY_W = KeyEvent.VK_W;
/**
* Constant for the "X" key.
*/
public static final int KEY_X = KeyEvent.VK_X;
/**
* Constant for the "Y" key.
*/
public static final int KEY_Y = KeyEvent.VK_Y;
/**
* Constant for the "Z" key.
*/
public static final int KEY_Z = KeyEvent.VK_Z;
}

View File

@@ -0,0 +1,11 @@
package schule.ngb.zm;
import java.awt.Graphics2D;
public class GraphicsLayer extends Layer {
public Graphics2D getGraphics() {
return drawing;
}
}

View File

@@ -73,7 +73,7 @@ public abstract class Layer extends Constants implements Drawable, Updatable {
}
/**
* Erstellt einen neuen Puffer für dei Ebene mit der angegebenen Größe
* Erstellt einen neuen Puffer für die Ebene mit der angegebenen Größe
* und kopiert den Inhalt des alten Puffers in den Neuen.
*
* @param width Width des neuen Puffers.

View File

@@ -41,43 +41,58 @@ public final class Options {
}
public enum Direction {
CENTER(0),
NORTH(1 << 0),
EAST(1 << 1),
SOUTH(1 << 2),
WEST(1 << 3),
CENTER(0, 0),
NORTH(0, -1),
EAST(1, 0),
SOUTH(0, 1),
WEST(-1, 0),
NORTHEAST(NORTH.mask | EAST.mask),
SOUTHEAST(SOUTH.mask | EAST.mask),
NORTHWEST(NORTH.mask | WEST.mask),
SOUTHWEST(SOUTH.mask | WEST.mask),
NORTHEAST(1, -1),
SOUTHEAST(1, 1),
NORTHWEST(-1, -1),
SOUTHWEST(-1, 1),
MIDDLE(CENTER.mask),
UP(NORTH.mask),
DOWN(SOUTH.mask),
LEFT(WEST.mask),
RIGHT(EAST.mask);
MIDDLE(CENTER),
UP(NORTH),
DOWN(SOUTH),
LEFT(WEST),
RIGHT(EAST);
public final byte mask;
public final byte x, y;
Direction( int mask ) {
this.mask = (byte) mask;
Direction( int x, int y ) {
this.x = (byte)x;
this.y = (byte)y;
}
public boolean in( int mask ) {
return (mask & this.mask) == this.mask;
Direction( Direction original ) {
this.x = original.x;
this.y = original.y;
}
public boolean in( Direction dir ) {
return in(dir.mask);
}
public boolean contains( int mask ) {
return (mask & this.mask) == mask;
return (this.x == dir.x && this.y == 0) || (this.y == dir.y && this.x == 0) || (this.x == dir.x && this.y == dir.y);
}
public boolean contains( Direction dir ) {
return contains(dir.mask);
return dir.in(this);
}
public Vector asVector() {
return new Vector(x, y);
}
/**
* Gibt die entgegengesetzte Richtung zu dieser zurück.
* @return
*/
public Direction inverse() {
for( Direction dir: Direction.values() ) {
if( dir.x == -this.x && dir.y == -this.y ) {
return dir;
}
}
return CENTER;
}
}

View File

@@ -0,0 +1,104 @@
package schule.ngb.zm;
import java.awt.Graphics;
import java.util.LinkedList;
public class Spielemaschine extends Zeichenmaschine {
private LinkedList<Drawable> drawables;
private LinkedList<Updatable> updatables;
private GraphicsLayer mainLayer;
public Spielemaschine( String title ) {
this(STD_WIDTH, STD_HEIGHT, title);
}
public Spielemaschine( int width, int height, String title ) {
super(width, height, title, false);
drawables = new LinkedList<>();
updatables = new LinkedList<>();
mainLayer = new GraphicsLayer();
canvas.addLayer(mainLayer);
}
public final void add( Object... pObjects ) {
for( Object obj : pObjects ) {
if( obj instanceof Drawable ) {
addDrawable((Drawable) obj);
}
if( obj instanceof Updatable ) {
addUpdatable((Updatable) obj);
}
}
}
public final void addDrawable( Drawable pDrawable ) {
synchronized( drawables ) {
drawables.add(pDrawable);
}
}
public final void addUpdatable( Updatable pUpdatable ) {
synchronized( updatables ) {
updatables.add(pUpdatable);
}
}
public final void remove( Object... pObjects ) {
for( Object obj : pObjects ) {
if( obj instanceof Drawable ) {
removeDrawable((Drawable) obj);
}
if( obj instanceof Updatable ) {
removeUpdatable((Updatable) obj);
}
}
}
public final void removeDrawable( Drawable pDrawable ) {
synchronized( drawables ) {
drawables.remove(pDrawable);
}
}
public final void removeUpdatable( Updatable pUpdatable ) {
synchronized( updatables ) {
updatables.remove(pUpdatable);
}
}
protected void updateGame( double delta ) {
}
@Override
public final void update( double delta ) {
synchronized( updatables ) {
for( Updatable updatable : updatables ) {
if( updatable.isActive() ) {
updatable.update(delta);
}
}
}
updateGame(delta);
}
@Override
public final void draw() {
mainLayer.clear();
synchronized( drawables ) {
for( Drawable drawable : drawables ) {
if( drawable.isVisible() ) {
drawable.draw(mainLayer.getGraphics());
}
}
}
}
}

View File

@@ -361,6 +361,9 @@ public class Vector extends Point2D.Double {
}
public Vector div( double scalar ) {
if( scalar == 0.0 ) {
throw new IllegalArgumentException("Can't divide by zero.");
}
x /= scalar;
y /= scalar;
return this;

View File

@@ -29,6 +29,7 @@ public class Zeichenleinwand extends Canvas {
/**
* Erstellt eine neue Zeichenleinwand mit einer festen Größe.
*
* @param width Breite der Zeichenleinwand.
* @param height Höhe der Zeichenleinwand.
*/
@@ -42,8 +43,6 @@ public class Zeichenleinwand extends Canvas {
layers = new LinkedList<>();
synchronized( layers ) {
layers.add(new ColorLayer(width, height, Constants.STD_BACKGROUND));
layers.add(new DrawingLayer(width, height));
layers.add(new ShapesLayer(width, height));
}
}
@@ -53,7 +52,7 @@ public class Zeichenleinwand extends Canvas {
* Bei einer Größenänderung wird auch die Größe aller bisher hinzugefügter
* {@link Layer Ebenen} angepasst, sodass sie die gesamte Leinwand füllen.
*
* @param width Neue Width der Leinwand in Pixeln.
* @param width Neue Width der Leinwand in Pixeln.
* @param height Neue Höhe der Leinwand in Pixeln.
*/
@Override
@@ -85,16 +84,24 @@ public class Zeichenleinwand extends Canvas {
}
/**
* Fügt der Zeichenleinwand eine Ebene an einer bestimmten Stelle hinzu.
* Fügt der Zeichenleinwand eine Ebene an einem bestimmten Index hinzu. Wenn
* der Index noch nicht existiert (also größer als die
* {@link #getLayerCount() Anzahl der Ebenen} ist, dann wird die neue Ebene
* als letzte eingefügt. Die aufrufende Methode kann also nicht sicher sein,
* dass die neue Ebene am Ende wirklich am Index {@code i} steht.
*
* @param i Index der Ebene, beginnend mit <code>0</code>.
* @param i Index der Ebene, beginnend mit <code>0</code>.
* @param layer Die neue Ebene.
*/
public void addLayer( int i, Layer layer ) {
if( layer != null ) {
synchronized( layers ) {
layer.setSize(getWidth(), getHeight());
layers.add(i, layer);
if( i > layers.size() ) {
layers.add(layer);
} else {
layers.add(i, layer);
}
}
}
}
@@ -168,6 +175,20 @@ public class Zeichenleinwand extends Canvas {
return result;
}
public boolean removeLayer( Layer pLayer ) {
return layers.remove(pLayer);
}
public void removeLayers( Layer... pLayers ) {
for( Layer layer : pLayers ) {
layers.remove(layer);
}
}
public void clearLayers() {
layers.clear();
}
/**
* Erstellt eine passende {@link BufferStrategy} für diese Ebene.
*/
@@ -193,6 +214,7 @@ public class Zeichenleinwand extends Canvas {
/**
* Zeichnet den Inhalt aller {@link Layer Ebenen} in den Grafik-Kontext.
*
* @param graphics
*/
public void draw( Graphics graphics ) {

View File

@@ -1,7 +1,9 @@
package schule.ngb.zm;
import schule.ngb.zm.shapes.ShapesLayer;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.ImageLoader;
import schule.ngb.zm.util.Log;
import javax.imageio.ImageIO;
import javax.swing.*;
@@ -11,6 +13,8 @@ import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.logging.Level;
/**
* Hauptklasse der Zeichenmaschine.
@@ -19,17 +23,45 @@ import java.io.IOException;
* Die Klasse übernimmt die Initialisierung eines Programmfensters und der
* nötigen Komponenten.
*/
public class Zeichenmaschine extends Constants implements MouseInputListener, KeyListener {
public class Zeichenmaschine extends Constants {
/**
* Gibt an, ob die Zeichenmaschine aus BlueJ heraus gestartet wurde.
*/
public static boolean IN_BLUEJ;
public static final boolean IN_BLUEJ;
static {
IN_BLUEJ = System.getProperty("java.class.path").contains("bluej");
}
public static final boolean MACOS;
public static final boolean WINDOWS;
public static final boolean LINUX;
static {
final String name = System.getProperty("os.name");
if( name.contains("Mac") ) {
MACOS = true;
WINDOWS = false;
LINUX = false;
} else if( name.contains("Windows") ) {
MACOS = false;
WINDOWS = true;
LINUX = false;
} else if( name.equals("Linux") ) { // true for the ibm vm
MACOS = false;
WINDOWS = false;
LINUX = true;
} else {
MACOS = false;
WINDOWS = false;
LINUX = false;
}
}
/*
* Objektvariablen, die von Unterklassen benutzt werden können.
*/
@@ -106,11 +138,17 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
private boolean stop_after_update = false, run_once = true;
// Aktuelle Frames pro Sekunde der Zeichenmaschine.
private int framesPerSecond;
private int framesPerSecondInternal;
// Hauptthread der Zeichenmaschine.
private Thread mainThread;
// Queue für geplante Aufgaben
private DelayQueue<DelayedTask> taskQueue = new DelayQueue<>();
// Queue für abgefangene InputEvents
private BlockingQueue<InputEvent> eventQueue = new LinkedBlockingQueue<>();
// Gibt an, ob nach Ende des Hauptthreads das Programm beendet werden soll,
// oder das Zeichenfenster weiter geöffnet bleibt.
private boolean quitAfterTeardown = false;
@@ -125,7 +163,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Erstellt eine neue Zeichenmaschine mit Standardwerten für Titel und
* Größe.
* <p>
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr Details.
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
* Details.
*/
public Zeichenmaschine() {
this(APP_NAME + " " + APP_VERSION);
@@ -135,10 +174,11 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Erstellt eine neue Zeichenmaschine mit Standardwerten für Titel und
* Größe.
* <p>
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr Details.
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
* Details.
*
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}.
* Aufruf von {@code draw()}.
*/
public Zeichenmaschine( boolean run_once ) {
this(APP_NAME + " " + APP_VERSION, run_once);
@@ -148,7 +188,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Erstellt eine neue Zeichenmaschine mit dem angegebene Titel und
* Standardwerten für die Größe.
* <p>
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr Details.
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
* Details.
*
* @param title Der Titel, der oben im Fenster steht.
*/
@@ -160,11 +201,12 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Erstellt eine neue Zeichenmaschine mit dem angegebene Titel und
* Standardwerten für die Größe.
* <p>
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr Details.
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
* Details.
*
* @param title Der Titel, der oben im Fenster steht.
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}.
* Aufruf von {@code draw()}.
*/
public Zeichenmaschine( String title, boolean run_once ) {
this(STD_WIDTH, STD_HEIGHT, title, run_once);
@@ -174,11 +216,12 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Erstellt eine neue zeichenmaschine mit einer Leinwand der angegebenen
* Größe und dem angegebenen Titel.
* <p>
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr Details.
* Siehe {@link #Zeichenmaschine(int, int, String, boolean)} für mehr
* Details.
*
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
* @param height Höhe der {@link Zeichenleinwand Zeichenleinwand}.
* @param title Der Titel, der oben im Fenster steht.
* @param title Der Titel, der oben im Fenster steht.
*/
public Zeichenmaschine( int width, int height, String title ) {
this(width, height, title, true);
@@ -191,28 +234,30 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Die Zeichenmaschine öffnet automatisch ein Fenster mit einer
* {@link Zeichenleinwand}, die {@code width} Pixel breit und {@code height}
* Pixel hoch ist. Die Leinwand hat immer eine Mindestgröße von 100x100
* Pixeln und kann nicht größer als der aktuelle Bildschirm werden.
* Das Fenster bekommt den angegebenen Titel.
* Pixeln und kann nicht größer als der aktuelle Bildschirm werden. Das
* Fenster bekommt den angegebenen Titel.
* <p>
* Falls {@code run_once} gleich {@code false} ist, werden
* {@link #update(double)} und {@link #draw()} entsprechend der
* {@link #framesPerSecond} kontinuierlich aufgerufen.
* Falls das Programm als Unterklasse der Zeichenmaschine verfasst wird,
* dann kann auch, {@code update(double)} überschrieben werden, damit die
* Maschine nicht automatisch beendet.
* {@link #framesPerSecond} kontinuierlich aufgerufen. Falls das Programm
* als Unterklasse der Zeichenmaschine verfasst wird, dann kann auch,
* {@code update(double)} überschrieben werden, damit die Maschine nicht
* automatisch beendet.
*
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
* @param width Breite der {@link Zeichenleinwand Zeichenleinwand}.
* @param height Höhe der {@link Zeichenleinwand Zeichenleinwand}.
* @param title Der Titel, der oben im Fenster steht.
* @param title Der Titel, der oben im Fenster steht.
* @param run_once {@code true} beendet die Zeichenmaschine nach einem
* Aufruf von {@code draw()}.
* Aufruf von {@code draw()}.
*/
public Zeichenmaschine( int width, int height, String title, boolean run_once ) {
LOG.info("Starting " + APP_NAME + " " + APP_VERSION);
// Setzen des "Look&Feel"
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch( Exception e ) {
System.err.println("Error setting the look and feel: " + e.getMessage());
} catch( Exception ex ) {
LOG.log(Level.SEVERE, "Error setting the look and feel: " + ex.getMessage(), ex);
}
// Wir suchen den Bildschirm, der derzeit den Mauszeiger enthält, um
@@ -244,16 +289,22 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
// Das Icon des Fensters ändern
try {
ImageIcon icon = new ImageIcon(ImageIO.read(new File("res/icon_64.png")));
frame.setIconImage(icon.getImage());
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon.getImage());
if( MACOS ) {
// Dock Icon in macOS setzen
Taskbar taskbar = Taskbar.getTaskbar();
taskbar.setIconImage(icon.getImage());
} else {
// Kleines Icon des Frames setzen
frame.setIconImage(icon.getImage());
}
} catch( IOException e ) {
}
// Erstellen der Leinwand
canvas = new Zeichenleinwand(width, height);
frame.add(canvas);
@@ -264,7 +315,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
shapes = getShapesLayer();
// FPS setzen
framesPerSecond = STD_FPS;
framesPerSecondInternal = STD_FPS;
this.run_once = run_once;
// Settings der Unterklasse aufrufen, falls das Fenster vor dem Öffnen
@@ -273,9 +324,13 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
settings();
// Listener hinzufügen, um auf Maus- und Tastatureingaben zu hören.
canvas.addMouseListener(this);
canvas.addMouseMotionListener(this);
canvas.addKeyListener(this);
InputListener inputListener = new InputListener();
canvas.addMouseListener(inputListener);
canvas.addMouseMotionListener(inputListener);
canvas.addMouseWheelListener(inputListener);
canvas.addKeyListener(inputListener);
// Programm beenden, wenn Fenster geschlossen wird
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing( WindowEvent e ) {
@@ -343,9 +398,10 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* wiederhergestellt.
*
* @param pEnable Wenn {@code true}, wird der Vollbildmodus aktiviert,
* ansonsten deaktiviert.
* ansonsten deaktiviert.
*/
public final void setFullscreen( boolean pEnable ) {
// See https://docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
if( displayDevice.isFullScreenSupported() ) {
if( pEnable && !fullscreen ) {
// frame.setUndecorated(true);
@@ -354,24 +410,33 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Update width / height
initialWidth = width;
initialHeight = height;
setSize(screenWidth, screenHeight);
changeSize(screenWidth, screenHeight);
// Register ESC as exit fullscreen
canvas.addKeyListener(fullscreenExitListener);
fullscreen = true;
fullscreenChanged();
} else if( !pEnable && fullscreen ) {
fullscreen = false;
canvas.removeKeyListener(fullscreenExitListener);
displayDevice.setFullScreenWindow(null);
setSize(initialWidth, initialHeight);
changeSize(initialWidth, initialHeight);
frame.pack();
// frame.setUndecorated(false);
fullscreenChanged();
}
}
}
public boolean isFullscreen() {
Window win = displayDevice.getFullScreenWindow();
return fullscreen && win != null;
}
/**
* Gibt den aktuellen {@link Options.AppState Zustand} der Zeichenmaschine zurück.
* Gibt den aktuellen {@link Options.AppState Zustand} der Zeichenmaschine
* zurück.
*
* @return Der Zustand der Zeichenmaschine.
*/
@@ -416,8 +481,9 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Pausiert die Ausführung von {@link #update(double)} und {@link #draw()}
* nach dem nächsten vollständigen Frame.
* <p>
* Die Zeichenmaschine wechselt in den Zustand {@link Options.AppState#PAUSED},
* sobald der aktuelle Frame beendet wurde.
* Die Zeichenmaschine wechselt in den Zustand
* {@link Options.AppState#PAUSED}, sobald der aktuelle Frame beendet
* wurde.
*/
public final void pause() {
pause_pending = true;
@@ -436,6 +502,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
/**
* Prüft, ob die Zeichenmaschine gerade pausiert ist.
*
* @return
*/
public final boolean isPaused() {
@@ -446,15 +513,32 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Stoppt die Zeichenmaschine.
* <p>
* Nachdem der aktuelle Frame gezeichnet wurde wechselt die Zeichenmaschine
* in den Zustand {@link Options.AppState#STOPPED} und ruft {@link #teardown()}
* auf. Nachdem {@code teardown()} ausgeführt wurde wechselt der Zustand zu
* {@link Options.AppState#TERMINATED}. Das Zeichenfenster bleibt weiter
* geöffnet.
* in den Zustand {@link Options.AppState#STOPPED} und ruft
* {@link #teardown()} auf. Nachdem {@code teardown()} ausgeführt wurde
* wechselt der Zustand zu {@link Options.AppState#TERMINATED}. Das
* Zeichenfenster bleibt weiter geöffnet.
*/
public final void stop() {
running = false;
}
/**
* Führt interne Aufräumarbeiten durch.
* <p>
* Wird nach dem {@link #stop() Stopp} der Zeichenmaschine aufgerufen und
* verbleibende Threads, Tasks, etc. zu stoppen und aufzuräumen. Die
* Äquivalente Methode für Unterklassen ist {@link #teardown()}, die direkt
* vor {@code cleanup()} aufgerufen wird.
*/
private void cleanup() {
// Alle noch nicht ausgelösten Events werden entfernt
eventQueue.clear();
// Alle noch nicht ausgeführten Tasks werden entfernt
taskQueue.clear();
// TaskRunner stoppen
TaskRunner.shutdown();
}
/**
* Beendet die Zeichenmaschine vollständig.
* <p>
@@ -480,8 +564,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist,
* wird die komplette VM beendet.
* Beendet das Programm. Falls {@code exit} gleich {@code true} ist, wird
* die komplette VM beendet.
*
* @param exit Ob die VM beendet werden soll.
* @see System#exit(int)
@@ -496,6 +580,28 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
}
/**
* Interne Methode um die Größe der Zeichenfläche zu ändern.
* <p>
* Die Methode berücksichtigt nicht den Zustand des Fensters (z.B.
* Vollbildmodus) und geht davon aus, dass die aufrufende Methode
* sichergestellt hat, dass eine Änderung der Größe der Zeichenfläche
* zulässig und sinnvoll ist.
*
* @param newWidth Neue Breite der Zeichenleinwand.
* @param newHeight Neue Höhe der Zeichenleinwand.
* @see #setSize(int, int)
* @see #setFullscreen(boolean)
*/
private void changeSize( int newWidth, int newHeight ) {
width = Math.min(Math.max(newWidth, 100), screenWidth);
height = Math.min(Math.max(newHeight, 100), screenHeight);
if( canvas != null ) {
canvas.setSize(width, height);
}
}
/**
* Ändert die Größe der {@link Zeichenleinwand}.
*
@@ -509,11 +615,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
initialHeight = Math.min(Math.max(height, 100), screenHeight);
setFullscreen(false);
} else {
if( canvas != null ) {
canvas.setSize(width, height);
}
this.width = Math.min(Math.max(width, 100), screenWidth);
this.height = Math.min(Math.max(height, 100), screenHeight);
changeSize(width, height);
//frame.setSize(width, height);
frame.pack();
@@ -557,8 +659,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Fügt der {@link Zeichenleinwand} eine weitere {@link Layer Ebene}
* hinzu.
* Fügt der {@link Zeichenleinwand} eine weitere {@link Layer Ebene} hinzu.
*
* @param layer Die neue Ebene.
*/
@@ -605,9 +706,9 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Gibt die {@link ColorLayer Ebene} mit der Hintergrundfarbe zurück. Gibt es
* keine solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
* hinzugefügt.
* Gibt die {@link ColorLayer Ebene} mit der Hintergrundfarbe zurück. Gibt
* es keine solche Ebene, so wird eine erstellt und der
* {@link Zeichenleinwand} hinzugefügt.
* <p>
* In der Regel sollte dies dieselbe Ebene sein wie {@link #background}.
*
@@ -623,8 +724,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Gibt die Standard-{@link DrawingLayer Zeichenebene} zurück. Gibt es
* keine solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
* Gibt die Standard-{@link DrawingLayer Zeichenebene} zurück. Gibt es keine
* solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
* hinzugefügt.
* <p>
* In der Regel sollte dies dieselbe Ebene sein wie {@link #drawing}.
@@ -641,8 +742,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Gibt die Standard-{@link ShapesLayer Formenebene} zurück. Gibt es
* keine solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
* Gibt die Standard-{@link ShapesLayer Formenebene} zurück. Gibt es keine
* solche Ebene, so wird eine erstellt und der {@link Zeichenleinwand}
* hinzugefügt.
* <p>
* In der Regel sollte dies dieselbe Ebene sein wie {@link #shapes}.
@@ -653,7 +754,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
ShapesLayer layer = canvas.getLayer(ShapesLayer.class);
if( layer == null ) {
layer = new ShapesLayer(getWidth(), getHeight());
canvas.addLayer(layer);
canvas.addLayer(2, layer);
}
return layer;
}
@@ -664,7 +765,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* @return Angepeilte Frames pro Sekunde
*/
public final int getFramesPerSecond() {
return framesPerSecond;
return framesPerSecondInternal;
}
/**
@@ -673,7 +774,13 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* @param pFramesPerSecond Neue FPS.
*/
public final void setFramesPerSecond( int pFramesPerSecond ) {
framesPerSecond = pFramesPerSecond;
if( pFramesPerSecond > 0 ) {
framesPerSecondInternal = pFramesPerSecond;
} else {
framesPerSecondInternal = 1;
// Logger ...
}
framesPerSecond = framesPerSecondInternal;
}
/**
@@ -718,9 +825,9 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
/**
* Erstellt eine Momentanaufnahme des aktuellen Inhalts der
* {@link Zeichenleinwand} und erstellt daraus eine {@link ImageLayer Bildebene}.
* Die Ebene wird automatisch der {@link Zeichenleinwand} vor dem
* {@link #background} hinzugefügt.
* {@link Zeichenleinwand} und erstellt daraus eine
* {@link ImageLayer Bildebene}. Die Ebene wird automatisch der
* {@link Zeichenleinwand} vor dem {@link #background} hinzugefügt.
*
* @return Die neue Bildebene.
*/
@@ -806,8 +913,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Zeigt den Mauszeiger wieder an, falls er zuvor {@link #hideCursor() versteckt}
* wurde.
* Zeigt den Mauszeiger wieder an, falls er zuvor
* {@link #hideCursor() versteckt} wurde.
* <p>
* Nach dem Aufruf gilt {@code cursorVisible == true}.
* <p>
@@ -846,6 +953,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* <pre>
* setCursor(Cursor.HAND_CURSOR);
* </pre>
*
* @param pPredefinedCursor Eine der Cursor-Konstanten.
* @see Cursor
*/
@@ -854,13 +962,14 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
/**
* Setzt den Mauszeiger auf das übergebenen Cursor-Objekt.
* Wenn {@code pCursor} {@code null} ist, wird der Mauszeiger unsichtbar
* gemacht (dies ist dasselbe wie der Aufruf von {@link #hideCursor()}).
* Setzt den Mauszeiger auf das übergebenen Cursor-Objekt. Wenn
* {@code pCursor} {@code null} ist, wird der Mauszeiger unsichtbar gemacht
* (dies ist dasselbe wie der Aufruf von {@link #hideCursor()}).
*
* @param pCursor Ein Cursor-Objekt oder {@code null}.
* <p>
* Nach Aufruf der Methode kann über {@link #cursorVisible} abgefragt werden,
* ob der Cursor zurzeit sichtbar ist oder nicht.
* <p>
* Nach Aufruf der Methode kann über {@link #cursorVisible} abgefragt
* werden, ob der Cursor zurzeit sichtbar ist oder nicht.
*/
public final void setCursor( Cursor pCursor ) {
if( pCursor == null && cursorVisible ) {
@@ -887,7 +996,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
*/
/**
* Die Settings werden einmal beim Erstellten der Zeichenmaschine aufgerufen.
* Die Settings werden einmal beim Erstellten der Zeichenmaschine
* aufgerufen.
* <p>
* {@code settings()} wird nur selten benötigt, wenn das Zeichenfenster
*/
@@ -912,9 +1022,9 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
* Aktualisierungen abhängig von der echten Verzögerung zwischen zwei Frames
* zu berechnen.
* <p>
* {@code delta} wird in Sekunden angegeben. Um eine Form zum Beispiel
* um {@code 50} Pixel pro Sekunde in {@code x}-Richtung zu bewegen,
* kann so vorgegangen werden:
* {@code delta} wird in Sekunden angegeben. Um eine Form zum Beispiel um
* {@code 50} Pixel pro Sekunde in {@code x}-Richtung zu bewegen, kann so
* vorgegangen werden:
* <pre>
* shape.move(50*delta, 0.0);
* </pre>
@@ -928,23 +1038,24 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
/**
* {@code draw()} wird einmal pro Frame aufgerufen. Bei einer
* {@link #getFramesPerSecond() Framerate} von 60 also in etwa 60-Mal
* pro Sekunde. In der {@code draw}-Methode wird der Inhalt der Ebenen
* {@link #getFramesPerSecond() Framerate} von 60 also in etwa 60-Mal pro
* Sekunde. In der {@code draw}-Methode wird der Inhalt der Ebenen
* manipuliert und deren Inhalte gezeichnet. Am Ende des Frames werden alle
* Ebenen auf die {@link Zeichenleinwand} übertragen.
* <p>
* {@code draw()} stellt die wichtigste Methode für eine Zeichenmaschine dar,
* da hier die Zeichnung des Programms erstellt wird.
* {@code draw()} stellt die wichtigste Methode für eine Zeichenmaschine
* dar, da hier die Zeichnung des Programms erstellt wird.
*/
public void draw() {
running = !stop_after_update;
}
/**
* {@code teardown()} wird aufgerufen, sobald die Schleife des Hauptprogramms
* beendet wurde. Dies passiert entweder nach dem ersten Durchlauf (wenn keine
* eigene {@link #update(double)} erstellt wurde), nach dem Aufruf von
* {@link #stop()} oder nachdem das {@link Zeichenfenster} geschlossen wurde.
* {@code teardown()} wird aufgerufen, sobald die Schleife des
* Hauptprogramms beendet wurde. Dies passiert entweder nach dem ersten
* Durchlauf (wenn keine eigene {@link #update(double)} erstellt wurde),
* nach dem Aufruf von {@link #stop()} oder nachdem das
* {@link Zeichenfenster} geschlossen wurde.
* <p>
* In {@code teardown()} kann zum Beispiel der Abschlussbildschirm eines
* Spiels oder der Abspann einer Animation angezeigt werden, oder mit
@@ -954,12 +1065,121 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
/*
* Task scheduling
*/
public void scheduleTask( Runnable runnable, int delay ) {
taskQueue.add(new DelayedTask(delay, runnable));
}
public void scheduleTask( Runnable runnable, int delay, boolean concurrent ) {
DelayedTask task = new DelayedTask(delay, runnable);
task.concurrent = concurrent;
taskQueue.add(task);
}
private void runTasks() {
synchronized( taskQueue ) {
DelayedTask task = taskQueue.poll();
while( task != null ) {
if( task.concurrent ) {
// SwingUtilities.invokeLater(task.runnable);
TaskRunner.run(task.runnable);
} else {
task.runnable.run();
}
task = taskQueue.poll();
}
}
}
/*
* Mouse handling
*/
@Override
public final void mouseClicked( MouseEvent e ) {
mouseEvent = e;
private void enqueueEvent( InputEvent evt ) {
eventQueue.add(evt);
if( isPaused() ) {
dispatchEvents();
}
}
private void dispatchEvents() {
synchronized( eventQueue ) {
while( !eventQueue.isEmpty() ) {
InputEvent evt = eventQueue.poll();
switch( evt.getID() ) {
case KeyEvent.KEY_TYPED:
case KeyEvent.KEY_PRESSED:
case KeyEvent.KEY_RELEASED:
handleKeyEvent((KeyEvent) evt);
break;
case MouseEvent.MOUSE_CLICKED:
case MouseEvent.MOUSE_PRESSED:
case MouseEvent.MOUSE_RELEASED:
case MouseEvent.MOUSE_MOVED:
case MouseEvent.MOUSE_DRAGGED:
case MouseEvent.MOUSE_WHEEL:
handleMouseEvent((MouseEvent) evt);
break;
}
}
}
}
private void handleKeyEvent( KeyEvent evt ) {
keyEvent = evt;
key = evt.getKeyChar();
keyCode = evt.getKeyCode();
switch( evt.getID() ) {
case KeyEvent.KEY_TYPED:
keyTyped(evt);
break;
case KeyEvent.KEY_PRESSED:
keyPressed = true;
keyPressed(evt);
break;
case KeyEvent.KEY_RELEASED:
keyPressed = false;
keyReleased(evt);
break;
}
}
private void handleMouseEvent( MouseEvent evt ) {
mouseEvent = evt;
switch( evt.getID() ) {
case MouseEvent.MOUSE_CLICKED:
mouseClicked(evt);
break;
case MouseEvent.MOUSE_PRESSED:
mousePressed = true;
mouseButton = evt.getButton();
mousePressed(evt);
break;
case MouseEvent.MOUSE_RELEASED:
mousePressed = false;
mouseButton = NOBUTTON;
mousePressed(evt);
break;
case MouseEvent.MOUSE_DRAGGED:
//saveMousePosition(evt);
mouseDragged(evt);
break;
case MouseEvent.MOUSE_MOVED:
//saveMousePosition(evt);
mouseMoved(evt);
break;
}
}
public void mouseClicked( MouseEvent e ) {
mouseClicked();
}
@@ -967,11 +1187,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void mousePressed( MouseEvent e ) {
mouseEvent = e;
mousePressed = true;
mouseButton = e.getButton();
public void mousePressed( MouseEvent e ) {
mousePressed();
}
@@ -979,11 +1195,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void mouseReleased( MouseEvent e ) {
mouseEvent = e;
mousePressed = false;
mouseButton = NOBUTTON;
public void mouseReleased( MouseEvent e ) {
mouseReleased();
}
@@ -991,19 +1203,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void mouseEntered( MouseEvent e ) {
// Intentionally left blank
}
@Override
public final void mouseExited( MouseEvent e ) {
// Intentionally left blank
}
@Override
public final void mouseDragged( MouseEvent e ) {
mouseEvent = e;
public void mouseDragged( MouseEvent e ) {
mouseDragged();
}
@@ -1011,9 +1211,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void mouseMoved( MouseEvent e ) {
mouseEvent = e;
public void mouseMoved( MouseEvent e ) {
mouseMoved();
}
@@ -1026,8 +1224,8 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
pmouseX = mouseX;
pmouseY = mouseY;
mouseX = event.getX();
mouseY = event.getY();
mouseX = cmouseX;
mouseY = cmouseY;
}
}
@@ -1045,9 +1243,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
/*
* Keyboard handling
*/
@Override
public final void keyTyped( KeyEvent e ) {
saveKeys(e);
public void keyTyped( KeyEvent e ) {
keyTyped();
}
@@ -1055,10 +1251,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void keyPressed( KeyEvent e ) {
saveKeys(e);
keyPressed = true;
public void keyPressed( KeyEvent e ) {
keyPressed();
}
@@ -1066,10 +1259,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
@Override
public final void keyReleased( KeyEvent e ) {
saveKeys(e);
keyPressed = false;
public void keyReleased( KeyEvent e ) {
keyReleased();
}
@@ -1077,12 +1267,15 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// Intentionally left blank
}
private final void saveKeys( KeyEvent event ) {
keyEvent = event;
key = event.getKeyChar();
keyCode = event.getKeyCode();
// Window changes
public void fullscreenChanged() {
// Intentionally left blank
}
////
// Zeichenthread
////
class Zeichenthread extends Thread {
@Override
@@ -1125,12 +1318,16 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
// canvas.invalidate();
// frame.repaint();
}
dispatchEvents();
}
runTasks();
// delta time in ns
long afterTime = System.nanoTime();
long dt = afterTime - beforeTime;
long sleep = ((1000000000L / framesPerSecond) - dt) - overslept;
long sleep = ((1000000000L / framesPerSecondInternal) - dt) - overslept;
if( sleep > 0 ) {
@@ -1143,13 +1340,13 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
overslept = (System.nanoTime() - afterTime) - sleep;
} else {
overslept = 0L;
}
_tick += 1;
_runtime = System.currentTimeMillis() - start;
tick = _tick;
runtime = _runtime;
framesPerSecond = framesPerSecondInternal;
if( pause_pending ) {
state = Options.AppState.PAUSED;
@@ -1159,6 +1356,7 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
state = Options.AppState.STOPPED;
teardown();
cleanup();
state = Options.AppState.TERMINATED;
if( quitAfterTeardown ) {
@@ -1184,4 +1382,94 @@ public class Zeichenmaschine extends Constants implements MouseInputListener, Ke
}
class DelayedTask implements Delayed {
long startTime; // in ms
Runnable runnable;
boolean concurrent = false;
public DelayedTask( int delay, Runnable runnable ) {
this.startTime = System.currentTimeMillis() + delay;
this.runnable = runnable;
}
@Override
public long getDelay( TimeUnit unit ) {
int diff = (int) (startTime - System.currentTimeMillis());
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo( Delayed o ) {
return (int) (startTime - ((DelayedTask) o).startTime);
}
}
class InputListener implements MouseInputListener, MouseMotionListener, MouseWheelListener, KeyListener{
@Override
public void mouseClicked( MouseEvent e ) {
enqueueEvent(e);
}
@Override
public void mousePressed( MouseEvent e ) {
enqueueEvent(e);
}
@Override
public void mouseReleased( MouseEvent e ) {
enqueueEvent(e);
}
@Override
public void mouseEntered( MouseEvent e ) {
// Intentionally left blank
}
@Override
public void mouseExited( MouseEvent e ) {
// Intentionally left blank
}
@Override
public void mouseDragged( MouseEvent e ) {
cmouseX = e.getX();
cmouseY = e.getY();
enqueueEvent(e);
}
@Override
public void mouseMoved( MouseEvent e ) {
cmouseX = e.getX();
cmouseY = e.getY();
enqueueEvent(e);
}
@Override
public void keyTyped( KeyEvent e ) {
enqueueEvent(e);
}
@Override
public void keyPressed( KeyEvent e ) {
enqueueEvent(e);
}
@Override
public void keyReleased( KeyEvent e ) {
enqueueEvent(e);
}
@Override
public void mouseWheelMoved( MouseWheelEvent e ) {
// enqueueEvent(e);
}
}
private static final Log LOG = Log.getLogger(Zeichenmaschine.class);
}

View File

@@ -0,0 +1,191 @@
package schule.ngb.zm.media;
import schule.ngb.zm.tasks.TaskRunner;
import schule.ngb.zm.util.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Music {
// size of the byte buffer used to read/write the audio stream
private static final int BUFFER_SIZE = 4096;
private boolean playing = false;
private boolean looping = false;
private String audioSource;
private AudioInputStream audioStream;
private SourceDataLine audioLine;
private float volume = 1.0f;
public Music( String source ) {
this.audioSource = source;
}
public boolean isPlaying() {
return playing;
}
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioLine != null ) {
applyVolume();
}
}
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioLine.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
public void playOnce() {
if( openLine() ) {
TaskRunner.run(new Runnable() {
@Override
public void run() {
stream();
}
});
}
}
public void playOnceAndWait() {
if( openLine() ) {
stream();
}
}
public void loop() {
looping = true;
playOnce();
}
public void stop() {
playing = false;
looping = false;
dispose();
}
public void dispose() {
if( audioLine != null ) {
if( audioLine.isRunning() ) {
playing = false;
audioLine.stop();
}
if( audioLine.isOpen() ) {
audioLine.drain();
audioLine.close();
}
}
try {
if( audioStream != null ) {
audioStream.close();
}
} catch( IOException ex ) {}
audioLine = null;
audioStream = null;
}
private void stream() {
audioLine.start();
playing = true;
byte[] bytesBuffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
try {
while (playing && (bytesRead = audioStream.read(bytesBuffer)) != -1) {
audioLine.write(bytesBuffer, 0, bytesRead);
}
audioLine.drain();
audioLine.stop();
} catch( IOException ex ) {
LOGGER.warning("Error while playing Music source <" + audioSource + ">");
LOGGER.throwing("Music", "stream", ex);
}
// Wait for the remaining audio to play
/*while( audioLine.isRunning() ) {
try {
Thread.sleep(10);
} catch( InterruptedException ex ) {
// Just keep waiting ...
}
}*/
playing = false;
streamingStopped();
}
private boolean openLine() {
if( audioLine != null ) {
return true;
}
try {
InputStream in = ResourceStreamProvider.getResourceStream(audioSource);
if( in != null ) {
final AudioInputStream inStream = AudioSystem.getAudioInputStream(in);
AudioFormat format = inStream.getFormat();
final int ch = format.getChannels();
final float rate = format.getSampleRate();
AudioFormat outFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, ch, ch*2, rate, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, outFormat);
audioLine = (SourceDataLine) AudioSystem.getLine(info);
audioLine.open(outFormat);
applyVolume();
audioStream = AudioSystem.getAudioInputStream(outFormat, inStream);
return true;
} else {
LOGGER.warning("Sound source <" + audioSource + "> could not be played: No audio source found.");
}
} catch( UnsupportedAudioFileException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: The specified audio file is not supported.", ex);
} catch( LineUnavailableException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: Audio line for playing back is unavailable.", ex);
} catch( IOException ex ) {
LOGGER.log(Level.WARNING, "Sound source <" + audioSource + "> could not be played: Error playing the audio file.", ex);
}
return false;
}
private void streamingStopped() {
playing = false;
dispose();
if( looping ) {
playOnce();
}
}
//private static final Logger LOGGER = Logger.getLogger("schule.ngb.zm.media.Music");
private static final Logger LOGGER = Logger.getLogger(Music.class.getName());
}

View File

@@ -0,0 +1,204 @@
package schule.ngb.zm.media;
import schule.ngb.zm.util.ResourceStreamProvider;
import javax.sound.sampled.*;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Logger;
public class Sound {
private boolean playing = false;
private boolean looping = false;
private String audioSource;
private Clip audioClip;
private boolean disposeAfterPlay = false;
private float volume = 1.0f;
public Sound( String source ) {
this.audioSource = source;
}
public boolean isPlaying() {
// return audioClip != null && audioClip.isRunning();
return playing;
}
public boolean isLooping() {
if( !playing ) {
looping = false;
}
return looping;
}
public void setVolume( double volume ) {
this.volume = (float) volume;
if( audioClip != null ) {
applyVolume();
}
}
private void applyVolume() {
FloatControl gainControl =
(FloatControl) audioClip.getControl(FloatControl.Type.MASTER_GAIN);
float vol = 20f * (float) Math.log10(volume);
// vol = (float) Constants.limit(vol, gainControl.getMinimum(), gainControl.getMaximum());
gainControl.setValue(vol);
}
public void stop() {
looping = false;
if( audioClip.isRunning() ) {
audioClip.stop();
}
playing = false;
}
public void play() {
if( this.openClip() ) {
audioClip.start();
playing = true;
}
}
public void playOnce() {
disposeAfterPlay = true;
play();
}
public void playOnceAndWait() {
disposeAfterPlay = true;
playAndWait();
}
public void playAndWait() {
this.play();
long audioLen = audioClip.getMicrosecondLength();
while( playing ) {
try {
long ms = (audioLen - audioClip.getMicrosecondPosition()) / 1000L;
Thread.sleep(ms);
} catch( InterruptedException ex ) {
// Ignore
}
}
audioClip.close();
}
public void loop() {
loop(Clip.LOOP_CONTINUOUSLY);
}
public void loop( int count ) {
int loopCount = count;
if( loopCount != Clip.LOOP_CONTINUOUSLY ) {
if( loopCount <= 0 ) {
return;
}
// Adjust Number of loops
loopCount -= 1;
}
if( openClip() ) {
looping = true;
audioClip.loop(loopCount);
playing = true;
}
}
public void dispose() {
if( audioClip != null ) {
if( audioClip.isRunning() ) {
audioClip.stop();
}
audioClip.close();
audioClip = null;
}
}
private boolean openClip() {
if( audioClip != null ) {
audioClip.setFramePosition(0);
return true;
}
try {
InputStream in = ResourceStreamProvider.getResourceStream(audioSource);
if( in != null ) {
AudioInputStream audioStream = AudioSystem.getAudioInputStream(in);
AudioFormat format = audioStream.getFormat();
DataLine.Info info = new DataLine.Info(Clip.class, format);
audioClip = (Clip) AudioSystem.getLine(info);
audioClip.addLineListener(new LineListener() {
@Override
public void update( LineEvent event ) {
if( event.getType() == LineEvent.Type.STOP ) {
playbackStopped();
}
}
});
audioClip.open(audioStream);
applyVolume();
return true;
} else {
LOGGER.warning("Sound source " + audioSource + " could not be played: No audio source found.");
}
} catch( UnsupportedAudioFileException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: The specified audio file is not supported.");
LOGGER.throwing("Sound", "openClip", ex);
} catch( LineUnavailableException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: Audio line for playing back is unavailable.");
LOGGER.throwing("Sound", "openClip", ex);
} catch( IOException ex ) {
LOGGER.warning("Sound source " + audioSource + " could not be played: Error playing the audio file.");
LOGGER.throwing("Sound", "openClip", ex);
}
return false;
}
/*@Override
public void update( LineEvent event ) {
LineEvent.Type type = event.getType();
if( type == LineEvent.Type.START ) {
playing = true;
} else if( type == LineEvent.Type.STOP ) {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
}*/
private void playbackStopped() {
playing = false;
if( disposeAfterPlay ) {
this.dispose();
disposeAfterPlay = false;
}
}
/*
public void addLineListener( LineListener listener ) {
if( audioClip == null ) {
openClip();
}
if( audioClip != null ) {
audioClip.addLineListener(listener);
}
}
*/
private static final Logger LOGGER = Logger.getLogger(Sound.class.getName());
}

View File

@@ -15,7 +15,7 @@ public abstract class FilledShape extends StrokedShape {
}
public void setFillColor( Color color, int alpha ) {
fillColor = new Color(color, alpha);
setFillColor(new Color(color, alpha));
}
public void setFillColor( int gray ) {
@@ -23,7 +23,7 @@ public abstract class FilledShape extends StrokedShape {
}
public void noFill() {
fillColor = null;
setFillColor(null);
}
public void setFillColor( int gray, int alpha ) {

View File

@@ -14,9 +14,9 @@ public class Picture extends Shape {
private BufferedImage image;
private double width;
private double imgWidth;
private double height;
private double imgHeight;
public Picture( String source ) {
this(0, 0, source);
@@ -30,8 +30,8 @@ public class Picture extends Shape {
throw new IllegalArgumentException("Could not initialize image from source " + source);
}
width = image.getWidth();
height = image.getHeight();
imgWidth = image.getWidth();
imgHeight = image.getHeight();
this.anchor = Options.Direction.CENTER;
}
@@ -55,25 +55,25 @@ public class Picture extends Shape {
g.drawImage(pic.image, 0, 0, null);
g.dispose();
width = image.getWidth();
height = image.getHeight();
imgWidth = image.getWidth();
imgHeight = image.getHeight();
}
}
public double getWidth() {
return width;
return imgWidth;
}
public void setWidth( double width ) {
scale(width / this.width);
scale(width / this.imgWidth);
}
public double getHeight() {
return height;
return imgHeight;
}
public void setHeight( double height ) {
scale(height / this.height);
scale(height / this.imgHeight);
}
public BufferedImage getImage() {
@@ -83,13 +83,13 @@ public class Picture extends Shape {
@Override
public void scale( double factor ) {
super.scale(factor);
width *= factor;
height *= factor;
imgWidth *= factor;
imgHeight *= factor;
}
@Override
public java.awt.Shape getShape() {
return new Rectangle2D.Double(0, 0, width, height);
return new Rectangle2D.Double(0, 0, imgWidth, imgHeight);
}
/*
@@ -161,8 +161,8 @@ public class Picture extends Shape {
}
AffineTransform current = graphics.getTransform();
graphics.transform(getTransform());
graphics.drawImage(image, 0, 0, (int) width, (int) height, null);
graphics.transform(transform);
graphics.drawImage(image, 0, 0, (int) imgWidth, (int) imgHeight, null);
graphics.setTransform(current);
}

View File

@@ -52,8 +52,8 @@ public abstract class Shape extends FilledShape {
* Die Breite einer Form ist immer die Breite ihrer Begrenzung, <b>bevor</b>
* Drehungen und andere Transformationen auf sei angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit {@link #getBounds()}
* abgerufen werden.
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit
* {@link #getBounds()} abgerufen werden.
*
* @return
*/
@@ -65,8 +65,8 @@ public abstract class Shape extends FilledShape {
* Die Höhe einer Form ist immer die Höhe ihrer Begrenzung, <b>bevor</b>
* Drehungen und andere Transformationen auf sei angewandt wurden.
* <p>
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit {@link #getBounds()}
* abgerufen werden.
* Die Begrenzungen der tatsächlich gezeichneten Form kann mit
* {@link #getBounds()} abgerufen werden.
*
* @return
*/
@@ -104,9 +104,10 @@ public abstract class Shape extends FilledShape {
* Setzt den Ankerpunkt der Form basierend auf der angegebenen
* {@link Options.Direction Richtung}.
* <p>
* Für das Setzen des Ankers muss das {@link #getBounds() begrenzende
* Rechteck} berechnet werden. Unterklassen sollten die Methode
* überschreiben, wenn der Anker auch direkt gesetzt werden kann.
* Für das Setzen des Ankers muss das
* {@link #getBounds() begrenzende Rechteck} berechnet werden. Unterklassen
* sollten die Methode überschreiben, wenn der Anker auch direkt gesetzt
* werden kann.
*
* @param anchor
*/
@@ -116,39 +117,58 @@ public abstract class Shape extends FilledShape {
}
}
protected static Point2D.Double getAnchorPoint( double width, double height, Options.Direction anchor ) {
double wHalf = width * .5, hHalf = height * .5;
// anchor == CENTER
Point2D.Double anchorPoint = new Point2D.Double(
wHalf + wHalf * anchor.x,
hHalf + hHalf * anchor.y
);
return anchorPoint;
}
/**
* Bestimmt den Ankerpunkt der Form relativ zur oberen linken Ecke und
* abhängig vom gesetzten {@link #setAnchor(Options.Direction) Anker}.
* Bestimmt den Ankerpunkt der Form relativ zum gesetzten
* {@link #setAnchor(Options.Direction) Ankerpunkt}.
*
* @param anchor Die Richtung des Ankerpunktes.
* @return Der relative Ankerpunkt.
*/
public Point2D.Double getAnchorPoint( Options.Direction anchor ) {
Point2D.Double anchorpoint = new Point2D.Double(0, 0);
double wHalf = getWidth() * .5, hHalf = getHeight() * .5;
double bHalf = getWidth() * .5, hHalf = getHeight() * .5;
// anchor == CENTER
anchorpoint.x = bHalf;
anchorpoint.y = hHalf;
Point2D.Double anchorPoint = new Point2D.Double(
wHalf * (anchor.x - this.anchor.x),
hHalf * (anchor.y - this.anchor.y)
);
if( NORTH.in(anchor) ) {
anchorpoint.y -= hHalf;
}
if( SOUTH.in(anchor) ) {
anchorpoint.y += hHalf;
}
if( WEST.in(anchor) ) {
anchorpoint.x -= bHalf;
}
if( EAST.in(anchor) ) {
anchorpoint.x += bHalf;
}
return anchorPoint;
}
return anchorpoint;
/**
* Ermittelt die absoluten Koordinaten eines angegebenen
* {@link #setAnchor(Options.Direction) Ankers}.
*
* @param anchor Die Richtung des Ankerpunktes.
* @return Der absolute Ankerpunkt.
*/
public Point2D.Double getAbsAnchorPoint( Options.Direction anchor ) {
// TODO: Die absoluten Anker müssten eigentlich die Rotation berücksichtigen.
Point2D.Double ap = getAnchorPoint(anchor);
ap.x += getX();
ap.y += getY();
return ap;
}
/**
* Kopiert die Eigenschaften der übergebenen Form in diese.
* <p>
* Unterklassen sollten diese Methode überschreiben, um weitere Eigenschaften
* zu kopieren (zum Beispiel den Radius eines Kreises). Mit dem Aufruf
* Unterklassen sollten diese Methode überschreiben, um weitere
* Eigenschaften zu kopieren (zum Beispiel den Radius eines Kreises). Mit
* dem Aufruf
* <code>super.copyFrom(shape)</code> sollten die Basiseigenschaften
* kopiert werden.
*
@@ -186,6 +206,96 @@ public abstract class Shape extends FilledShape {
this.y = y;
}
public void moveTo( Shape shape ) {
moveTo(shape.getX(), shape.getY());
}
public void moveTo( Shape shape, Options.Direction dir ) {
moveTo(shape, dir, 0.0);
}
/**
* Bewegt den Ankerpunkt dieser Form zu einem Ankerpunkt einer anderen Form.
* Mit {@code buff} kann ein zusätzlicher Abstand angegeben werden, um den
* die Form entlang des Ankerpunktes {@code anchor} verschoben werden soll.
* Ist der Anker zum Beispiel {@code NORTH}, dann wird die Form um
* {@code buff} nach oben verschoben.
*
* @param shape
* @param anchor
* @param buff
*/
public void moveTo( Shape shape, Options.Direction dir, double buff ) {
Point2D ap = shape.getAbsAnchorPoint(dir);
this.x = ap.getX() + dir.x * buff;
this.y = ap.getY() + dir.y * buff;
}
/**
* Richtet die Form entlang der angegebenen Richtung am Rand der
* Zeichenfläche aus.
*
* @param dir Die Richtung der Ausrichtung.
*/
public void alignTo( Options.Direction dir ) {
alignTo(dir, 0.0);
}
public void alignTo( Options.Direction dir, double buff ) {
Point2D anchorShape = Shape.getAnchorPoint(width, height, dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
public void alignTo( Shape shape, Options.Direction dir ) {
alignTo(shape, dir, 0.0);
}
/**
* Richtet die Form entlang der angegebenen Richtung an einer anderen Form
* aus. Für {@code DOWN} wird beispielsweise die y-Koordinate der unteren
* Kante dieser Form an der unteren Kante der angegebenen Form {@code shape}
* ausgerichtet. Die x-Koordinate wird nicht verändert. {@code buff} gibt
* einen Abstand ab, um den diese From versetzt ausgerichtet werden soll.
*
* @param shape
* @param dir
* @param buff
*/
public void alignTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir);
this.x += Math.abs(dir.x) * (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += Math.abs(dir.y) * (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
public void nextTo( Shape shape, Options.Direction dir ) {
nextTo(shape, dir, STD_BUFFER);
}
/**
* Bewegt die Form neben eine andere in Richtung des angegebenen
* Ankerpunktes. Im Gegensatz zu
* {@link #moveTo(Shape, Options.Direction, double)} wird die Breite bzw.
* Höhe der Formen berücksichtigt und die Formen so platziert, dass keine
* Überlappungen vorhanden sind.
*
* @param shape
* @param dir
* @param buff
*/
public void nextTo( Shape shape, Options.Direction dir, double buff ) {
Point2D anchorShape = shape.getAbsAnchorPoint(dir);
Point2D anchorThis = this.getAbsAnchorPoint(dir.inverse());
this.x += (anchorShape.getX() - anchorThis.getX()) + dir.x * buff;
this.y += (anchorShape.getY() - anchorThis.getY()) + dir.y * buff;
}
public void scale( double factor ) {
scale = factor;
}
@@ -210,7 +320,7 @@ public abstract class Shape extends FilledShape {
this.rotation += angle % 360;
// Rotate x/y position
double x1 = this.x-x, y1 = this.y-y;
double x1 = this.x - x, y1 = this.y - y;
double rad = Math.toRadians(angle);
double x2 = x1 * Math.cos(rad) - y1 * Math.sin(rad);
@@ -225,13 +335,15 @@ public abstract class Shape extends FilledShape {
}*/
public AffineTransform getTransform() {
Point2D.Double anchorPoint = getAnchorPoint(this.anchor);
// Point2D.Double anchorPoint = getAnchorPoint();
Point2D.Double basePoint = getAnchorPoint(Options.Direction.NORTHWEST);
AffineTransform transform = new AffineTransform();
transform.translate(x, y);
transform.rotate(Math.toRadians(rotation));
//transform.scale(scale, scale);
transform.translate(-anchorPoint.x, -anchorPoint.y);
//transform.translate(-anchorPoint.x, -anchorPoint.y);
transform.translate(basePoint.x, basePoint.y);
return transform;
}
@@ -246,8 +358,9 @@ public abstract class Shape extends FilledShape {
}
/**
* Zeichnet die Form, aber wendet zuvor noch eine zusätzliche Transformations-
* matrix an. Wird u.A. von der {@link ShapeGroup} verwendet.
* Zeichnet die Form, aber wendet zuvor noch eine zusätzliche
* Transformations- matrix an. Wird u.A. von der {@link ShapeGroup}
* verwendet.
*
* @param graphics
* @param transform

View File

@@ -2,20 +2,40 @@ package schule.ngb.zm.shapes;
import schule.ngb.zm.Options;
import java.awt.*;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Path2D;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Eine {@code ShapwGroup} ist eine Sammlung von {@link Shape}s, die gemeinsam
* eine Gruppe bilden. Transformationen der Gruppe werden nach den
* Transformationen der einzelnen Formen auf die gesammte Gruppe angewandt. So
* kann ein Rechteck in der Gruppe zunächst um 45 Grad gedreht werden und dann
* die Gruppe um -45 Grad. Das Rechteck wird dann wieder waagerecht dargestellt,
* während alle anderen Formen der Gruppe nun gedreht erscheinen. Da die Gruppe
* selbst ein eigenes Drehzentrum hat, können so komplexe Strukturen als eine
* eigene, zusammenhängende Form verwendet werden. (Im Gegensatz zu einer
* {@link CustomShape} haben Formgruppen den Vorteil, dass die einzelnen Formen
* individuelle Farben und Konturen bekommen können.)
* <p>
* Da die Größe der Gruppe durch seine zugewiesenen Formen fefstgelegt wird,
* sollten Modifikation wie {@link #setAnchor(Options.Direction)},
* {@link #nextTo(Shape, Options.Direction)} oder
* {@link #alignTo(Shape, Options.Direction, double)}, die die Größe der Gruppe
* benötigen, erst nach Hinzufügen der Gruppenelemente ausgeführt werden.
* Nachdem sich die Zusammensetzung der Gruppe geändert hat, muss die Gruppe
* ggf. neu ausgerichtet werden.
*/
public class ShapeGroup extends Shape {
private List<Shape> shapes;
protected double width = -1.0;
private double groupWidth = -1.0;
protected double height = -1.0;
private double groupHeight = -1.0;
public ShapeGroup() {
super();
@@ -33,7 +53,6 @@ public class ShapeGroup extends Shape {
for( Shape pShape : shapes ) {
this.shapes.add(pShape);
}
this.anchor = Options.Direction.CENTER;
}
public Shape copy() {
@@ -68,9 +87,9 @@ public class ShapeGroup extends Shape {
public <ShapeType extends Shape> List<ShapeType> getShapes( Class<ShapeType> typeClass ) {
LinkedList<ShapeType> list = new LinkedList<>();
for( Shape s: shapes ) {
for( Shape s : shapes ) {
if( typeClass.getClass().isInstance(s) ) {
list.add((ShapeType)s);
list.add((ShapeType) s);
}
}
return list;
@@ -99,23 +118,23 @@ public class ShapeGroup extends Shape {
@Override
public double getWidth() {
if( width < 0 ) {
if( groupWidth < 0 ) {
calculateBounds();
}
return width;
return groupWidth;
}
@Override
public double getHeight() {
if( height < 0 ) {
if( groupHeight < 0 ) {
calculateBounds();
}
return height;
return groupHeight;
}
private void invalidateBounds() {
width = -1.0;
height = -1.0;
groupWidth = -1.0;
groupHeight = -1.0;
}
private void calculateBounds() {
@@ -124,13 +143,13 @@ public class ShapeGroup extends Shape {
for( Shape pShape : shapes ) {
Bounds bounds = pShape.getBounds();
minx = Math.min(minx, bounds.x);
maxx = Math.max(maxx, bounds.x+bounds.width);
maxx = Math.max(maxx, bounds.x + bounds.width);
miny = Math.min(miny, bounds.y);
maxy = Math.max(maxy, bounds.y+bounds.height);
maxy = Math.max(maxy, bounds.y + bounds.height);
}
width = maxx-minx;
height = maxy-miny;
groupWidth = maxx - minx;
groupHeight = maxy - miny;
}
@Override
@@ -156,7 +175,7 @@ public class ShapeGroup extends Shape {
verzerrung.translate(-anchor.x, -anchor.y);
*/
for( Shape f: shapes ) {
for( Shape f : shapes ) {
AffineTransform af = f.getTransform();
af.preConcatenate(transform);
f.draw(graphics, af);

View File

@@ -3,6 +3,7 @@ package schule.ngb.zm.shapes;
import schule.ngb.zm.Layer;
import java.awt.Graphics2D;
import java.util.Collection;
import java.util.LinkedList;
public class ShapesLayer extends Layer {
@@ -48,17 +49,35 @@ public class ShapesLayer extends Layer {
return result;
}
public void add( Shape... shape ) {
public void add( Shape... shapes ) {
synchronized( shapes ) {
for( Shape f : shape ) {
shapes.add(f);
for( Shape s : shapes ) {
this.shapes.add(s);
}
}
}
public void remove( Shape shape ) {
public void add( Collection<Shape> shapes ) {
synchronized( shapes ) {
shapes.remove(shape);
for( Shape s : shapes ) {
this.shapes.add(s);
}
}
}
public void remove( Shape... shapes ) {
synchronized( shapes ) {
for( Shape s: shapes ) {
this.shapes.remove(s);
}
}
}
public void remove( Collection<Shape> shapes ) {
synchronized( shapes ) {
for( Shape s: shapes ) {
this.shapes.remove(s);
}
}
}

View File

@@ -27,7 +27,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
}
public void setStrokeColor( Color color , int alpha ) {
this.strokeColor = new Color(color, alpha);
setStrokeColor(new Color(color, alpha));
}
public void setStrokeColor( int gray ) {
@@ -35,7 +35,7 @@ public abstract class StrokedShape extends Constants implements Drawable {
}
public void noStroke() {
strokeColor = null;
setStrokeColor(null);
}
public void setStrokeColor( int gray, int alpha ) {

View File

@@ -1,8 +1,13 @@
package schule.ngb.zm.shapes;
import schule.ngb.zm.Color;
import schule.ngb.zm.Options;
import schule.ngb.zm.util.FontLoader;
import java.awt.*;
import java.awt.Canvas;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
@@ -12,16 +17,33 @@ public class Text extends Shape {
protected Font font;
protected Color fontColor = BLACK;
protected int width = 0, height = 0, ascent = 0;
public Text( double x, double y, String text ) {
this(x, y, text, new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE));
}
public Text( double x, double y, String text, String fontname ) {
super(x, y);
Font userfont = FontLoader.loadFont(fontname);
if( userfont != null ) {
font = userfont;
} else {
font = new Font(Font.SANS_SERIF, Font.PLAIN, STD_FONTSIZE);
}
setText(text);
fillColor = null;
strokeColor = null;
anchor = Options.Direction.CENTER;
}
public Text( double x, double y, String text, Font font ) {
super(x, y);
this.font = font;
setText(text);
fillColor = null;
strokeColor = null;
anchor = Options.Direction.CENTER;
}
@@ -38,6 +60,18 @@ public class Text extends Shape {
return height;
}
public void setFont( String fontname ) {
Font newFont = FontLoader.loadFont(fontname);
if( newFont != null ) {
setFont(newFont);
}
}
public void setFont( Font newFont ) {
font = newFont.deriveFont(font.getSize2D());
calculateBounds();
}
public Font getFont() {
return font;
}
@@ -47,6 +81,10 @@ public class Text extends Shape {
calculateBounds();
}
public double getFontsize() {
return font.getSize2D();
}
public String getText() {
return text;
}
@@ -56,6 +94,46 @@ public class Text extends Shape {
calculateBounds();
}
public Color getFontColor() {
return fontColor;
}
public void setFontColor( Color color ) {
if( color != null ) {
fontColor = color;
} else {
fontColor = BLACK;
}
}
public void setFontColor( Color color, int alpha ) {
if( color != null ) {
fontColor = new Color(color, alpha);
} else {
fontColor = BLACK;
}
}
public void setFontColor( int gray ) {
setFontColor(gray, gray, gray, 255);
}
public void setFontColor( int gray, int alpha ) {
setFontColor(gray, gray, gray, alpha);
}
public void setFontColor( int red, int green, int blue ) {
setFontColor(red, green, blue, 255);
}
public void setFontColor( int red, int green, int blue, int alpha ) {
setFontColor(new Color(red, green, blue, alpha));
}
public void resetFontColor() {
setFontColor(BLACK);
}
private void calculateBounds() {
//GraphicsDevice gd;
//gd.getDefaultConfiguration().createCompatibleImage(1,1);
@@ -64,6 +142,7 @@ public class Text extends Shape {
width = metrics.stringWidth(text);
//height = metrics.getHeight();
height = metrics.getDescent() + metrics.getAscent();
ascent = metrics.getMaxAscent();
}
public Shape copy() {
@@ -100,18 +179,32 @@ public class Text extends Shape {
// Aktuelle Werte speichern
Font currentFont = graphics.getFont();
Color currentColor = graphics.getColor();
java.awt.Color currentColor = graphics.getColor();
AffineTransform af = graphics.getTransform();
// Neue Werte setzen
graphics.setFont(font);
graphics.setColor(strokeColor.getJavaColor());
graphics.transform(transform);
// Draw text
//FontMetrics fm = graphics.getFontMetrics();
//graphics.drawString(text, (float) (x - fm.stringWidth(text)/2.0), (float) (y + fm.getDescent()));
graphics.drawString(text, 0, 0);
// Hintergrund
if( fillColor != null && fillColor.getAlpha() > 0 ) {
graphics.setColor(fillColor.getJavaColor());
graphics.fillRect(0, 0, width, height);
}
if( strokeColor != null && strokeColor.getAlpha() > 0
&& strokeWeight > 0.0 ) {
graphics.setColor(strokeColor.getJavaColor());
graphics.setStroke(createStroke());
graphics.drawRect(0, 0, width, height);
}
// Neue Werte setzen
if( font != null && fontColor != null && fontColor.getAlpha() > 0 ) {
graphics.setFont(font);
graphics.setColor(fontColor.getJavaColor());
// Draw text
//FontMetrics fm = graphics.getFontMetrics();
//graphics.drawString(text, (float) (x - fm.stringWidth(text)/2.0), (float) (y + fm.getDescent()));
graphics.drawString(text, 0, ascent);
}
// Alte Werte wiederherstellen
graphics.setTransform(af);

View File

@@ -0,0 +1,76 @@
package schule.ngb.zm.tasks;
import javax.swing.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
/**
* Führt Aufgaben (Tasks) parallel zum Hauptprogramm aus.
*/
public class TaskRunner {
private static final int POOL_SIZE = 4;
private static final int SHUTDOWN_TIME = 100;
private static TaskRunner runner;
public static TaskRunner getTaskRunner() {
if( runner == null ) {
runner = new TaskRunner();
}
return runner;
}
public static void run( Runnable task ) {
TaskRunner r = getTaskRunner();
r.pool.execute(task);
}
public static void schedule( Runnable task, int ms ) {
TaskRunner r = getTaskRunner();
r.pool.schedule(task, ms, TimeUnit.MILLISECONDS);
}
public static void invokeLater( Runnable task ) {
SwingUtilities.invokeLater(task);
}
public static void shutdown() {
if( runner != null ) {
runner.pool.shutdown();
try {
runner.pool.awaitTermination(SHUTDOWN_TIME, TimeUnit.MILLISECONDS);
} catch( InterruptedException ex ) {
} finally {
if( !runner.pool.isTerminated() ) {
runner.pool.shutdownNow();
}
}
}
}
ScheduledExecutorService pool;
private TaskRunner() {
//pool = new ScheduledThreadPoolExecutor(4);
pool = Executors.newScheduledThreadPool(POOL_SIZE, new ThreadFactory() {
private final ThreadFactory threadFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread( Runnable r ) {
Thread t = threadFactory.newThread(r);
t.setDaemon(true);
return t;
}
});
}
private static final Logger LOGGER = Logger.getLogger(TaskRunner.class.getName());
}

View File

@@ -0,0 +1,59 @@
package schule.ngb.zm.util;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
public class FontLoader {
private static final Map<String, Font> fontCache = new ConcurrentHashMap<>();
public static Font loadFont( String source ) {
Objects.requireNonNull(source, "Font source may not be null");
if( source.length() == 0 ) {
throw new IllegalArgumentException("Font source may not be empty.");
}
if( fontCache.containsKey(source) ) {
LOG.trace("Retrieved font <%s> from font cache.", source);
return fontCache.get(source);
}
// Look for System fonts
Font font = Font.decode(source);
if( font != null && source.toLowerCase().contains(font.getFamily().toLowerCase()) ) {
fontCache.put(source, font);
LOG.debug("Loaded system font for <%s>.", source);
return font;
} else {
font = null;
}
// Load userfonts
try( InputStream in = ResourceStreamProvider.getResourceStream(source) ) {
font = Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(Font.PLAIN);
if( font != null ) {
fontCache.put(source, font);
//ge.registerFont(font);
}
LOG.debug("Loaded custom font from <%s>.", source);
} catch( IOException ioex ) {
LOG.error(ioex, "Error loading custom font file from source <%s>.", source);
} catch( FontFormatException ffex ) {
LOG.error(ffex, "Error creating custom font from source <%s>.", source);
}
return font;
}
private FontLoader() {
}
private static final Log LOG = Log.getLogger(FontLoader.class);
}

View File

@@ -1,169 +1,354 @@
package schule.ngb.zm.util;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public class ImageLoader {
public static boolean cacheing = true;
public static boolean caching = true;
private static HashMap<String, BufferedImage> imageCache = new HashMap<>();
private static Map<String, SoftReference<BufferedImage>> imageCache = new ConcurrentHashMap<>();
private static SoftReference<BufferedImage> NOCACHE = new SoftReference<>(null);
/**
* Lädt ein Bild von der angegebenen Quelle {@code source}.
* <p>
* Die Bilddatei wird nach den Regeln von
* {@link ResourceStreamProvider#getResourceStream(String)} gesucht und
* geöffnet. Tritt dabei ein Fehler auf oder konnte keine passende Datei
* gefunden werden, wird {@code null} zurückgegeben.
* <p>
* Ist der {@link #activateCache() Cache aktiviert} und ein Bild mit der
* angegebenen Quelle schon vorhanden, wird das gespeicherte Bild
* zurückgegeben.
*
* @param source
* @return
* @see #loadImage(String, boolean)
*/
public static BufferedImage loadImage( String source ) {
return loadImage(source, cacheing);
return loadImage(source, caching);
}
/**
* Läadt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* Lädt ein Bild von der angegebenen Quelle <var>source</var> und gibt das
* Bild zurück oder <code>null</code>, wenn das Bild nicht geladen werden
* konnte. Ist ein Bild mit der angegebenen Quelle im Cache, wird das
* gechachete Bild zurückgegeben. Dies kann mit <code>cacheing = false</code>
* gespeicherte Bild zurückgegeben. Dies kann mit {@code caching = false}
* verhindert werden.
* <p>
* Wurde chacheing global deaktiviert, kann mit <code>cacheing = true</code>
* das Bild trotzdem aus dem Cache geladen werden, wenn es vorhanden ist.
* Wurde das Chaching global deaktiviert, kann mit <code>caching =
* true</code> das Bild trotzdem aus dem Cache geladen werden, wenn es
* vorhanden ist.
*
* @param source
* @param cacheing
* @param source Die Quelle des Bildes.
* @param caching Ob das Bild (falls vorhanden) aus dem Zwischenspeicher
* geladen werden soll.
* @return
*/
public static BufferedImage loadImage( String source, boolean cacheing ) {
if( source == null || source.length() == 0 )
throw new IllegalArgumentException("Image source may not be null or empty.");
public static BufferedImage loadImage( String source, boolean caching ) {
Validator.requireNotNull(source, "Image source may not be null");
Validator.requireNotEmpty(source, "Image source may not be empty.");
if( cacheing && imageCache.containsKey(source) ) {
BufferedImage cachedImage = imageCache.get(source);
if( cachedImage != null ) {
return cachedImage;
}
if( caching && isCached(source) ) {
return getCache(source);
}
try {
BufferedImage img;
BufferedImage img = null;
try( InputStream in = ResourceStreamProvider.getResourceStream(source) ) {
//URL url = ResourceStreamProvider.getResourceURL(source);
//BufferedImage img = ImageIO.read(url);
// Load image from working dir
File file = new File(source);
if( file.isFile() ) {
img = ImageIO.read(file);
} else {
// load ressource relative to .class-file
URL url = ImageLoader.class.getResource(source);
img = ImageIO.read(in);
// relative to ClassLoader
if( url == null ) {
url = ImageLoader.class.getClassLoader().getResource(source);
}
// load form web or jar-file
if( url == null ) {
url = new URL(source);
}
img = ImageIO.read(url);
if( caching && img != null ) {
putCache(source, img);
}
if( cacheing && img != null ) {
imageCache.put(source, img);
}
return img;
} catch( IOException ioe ) {
return null;
} catch( IOException ioex ) {
LOG.error(ioex, "Error loading image file from source <%s>.", source);
}
return img;
}
/**
* Loads an image into the cache with a user specified name that may differ
* from the image source string.
* Lädt ein Bild aus der angegebenen Quelle unter dem angegebenen Namen in
* den Cache.
* <p>
* Der {@code name} kann beliebig gewählt werden. Existiert unter dem Namen
* schon ein Bild im Zwischenspeicher, wird dieses überschrieben.
* <p>
* Wenn der Cache aktiviert ist, werden zukünftige Aufrufe von
* {@link #loadImage(String)} mit {@code name} als Quelle das gespeicherte
* Bild zurückgeben.
*
* @param name
* @param source
* @return
* @param name Name des Bildes im Zwischenspeicher.
* @param source Quelle, aus dem das Bild geladen werden soll.
* @return {@code true}, wenn das Bild erfolgreich geladen wurde,
* {@code false} sonst.
* @see #loadImage(String)
*/
public static boolean preloadImage( String name, String source ) {
BufferedImage img = loadImage(source, true);
if( cacheing && img != null ) {
imageCache.put(name, img);
if( img != null ) {
putCache(name, img);
return true;
}
return false;
}
/**
* Checks if an image with the given name is currently cached.
* Speiechert das angegebene Bild unter dem angegebenen Namen im Cache.
* <p>
* Exisitert zu {@code name} schon ein Bild im Zwischenspeicher
* ({@code ImageLoader.isCached(name) == true}), dann wird dieses
* überschrieben.
* <p>
* Wenn der Cache aktiviert ist, werden zukünftige Aufrufe von
* {@link #loadImage(String)} mit {@code name} als Quelle das gespeicherte
* Bild zurückgeben.
*
* @param name
* @return
* @param name Name des Bildes im Zwischenspeicher.
* @param img ZU speicherndes Bild.
*/
public static boolean isCached( String name ) {
if( imageCache.containsKey(name) ) {
return imageCache.get(name) != null;
}
return false;
public static void preloadImage( String name, BufferedImage img ) {
putCache(name, img);
}
/**
* Remove the specified key from the cache.
* Prüft, ob zum angegebenen Namen ein Bild im Cache gespeichert ist.
*
* @param name
* @param name Name des Bildes im Cache.
* @return {@code true}, wenn es ein Bild zum Namen gibt, sonst
* {@code false}.
*/
public static void invalidateCache( String name ) {
public static boolean isCached( String name ) {
SoftReference<BufferedImage> imgRef = imageCache.get(name);
return imgRef != null && imgRef != NOCACHE && imgRef.get() != null;
}
/**
* Entfernt das Bild zum angegebenen Namen aus dem Cache. Gibt es zum Namen
* kein Bild im Zwischenspeicher, dann passiert nichts.
*
* @param name Name des Bildes im Cache.
*/
public static void invalidateCache( final String name ) {
if( imageCache.containsKey(name) ) {
imageCache.remove(name);
}
}
/**
* Prevents caching for the specified source.
* Speichert ein Bild als {@link SoftReference} im Cache.
*
* @param source
* @param name Name des Bildes im Zwischenspeicher.
* @param img Das zu speichernde Bild.
*/
public static void noCache( String source ) {
imageCache.put(source, null);
private static void putCache( final String name, final BufferedImage img ) {
imageCache.put(name, new SoftReference<>(img));
}
/**
* Holt ein Bild aus dem Cache.
* <p>
* Prüft nicht, ob ein Bild vorhanden ist. Dies sollte vom Aufrufenden
* übernommen werden, da sonst eine {@link NullPointerException} erzeugt
* werden kann.
*
* @param name
* @return
*/
private static BufferedImage getCache( final String name ) {
return imageCache.get(name).get();
}
/**
* Deaktiviert den Cache für die angegebene Quelle.
* <p>
* Selbst wenn der {@link #activateCache() Cache aktiviert} ist, wird das
* Bild zur angegebenen Quelle niemals zwischengespeichert und immer neu
* geladen.
*
* @param name Die Quelle des Bildes.
*/
public static void preventCache( final String name ) {
imageCache.put(name, NOCACHE);
}
/**
* Leer den Cache und löschte alle bisher gespeicherten Bilder.
* <p>
* Auch vorher mit {@link #preventCache(String)} verhinderte Caches werden
* gelöscht und müssen neu gesetzt werden.
*/
public static void clearCache() {
imageCache.clear();
}
public static void enableCaching() {
cacheing = true;
/**
* Aktiviert den Cache.
* <p>
* Der Cache ist ein Zwischenspeicher für geladene Bilder. Wenn er aktiviert
* ist (Standard), dann werden mit dem {@code ImageLoader} geladene Bilder
* im Zwischenspeicher abgelegt. Bei jedem folgenden laden desselben Bildes
* (bzw. eines Bildes mit derselben {@code source}), wird das gespeicherte
* Bild zurückgegeben und nicht komplett neu geladen.
* <p>
* <b>Wichtig:</b> Bildreferenzen, die aus dem Cache geladen werden,
* verweisen alle auf dasselbe Objekt. Änderungen schalgen sich daher in
* allen anderen Versionen des Bildes nieder (inklusive dem Bild im
* Zwischenspeicher). Für Änderungen sollte daher immer
* {@link #copyImage(BufferedImage) eine Kopie} des Bildes erstellt werden:
* <pre>
* BufferedImage originalImage = ImageLoader.loadImage("assets/image.gif");
* BufferedImage copiedImage = ImageLoader.copyImage(originalImage);
* </pre>
* <p>
* Alternativ kann der Cache umgangen werden, indem
* {@link #loadImage(String, boolean)} verwendet wird.
*/
public static void activateCache() {
caching = true;
}
public static void disableCaching() {
cacheing = false;
/**
* Deaktiviert den Cache.
*
* @see #activateCache()
*/
public static void deactivateCache() {
caching = false;
}
/**
* Erstellt eine exakte Kopie des angegebenen Bildes als neues Objekt.
* <p>
* Die Methode ist hilfreich, wenn ein Bild aus dem
* {@link #activateCache() Cache} geladen wurde und dann verändert werden
* soll, ohne den Cache (oder andere Referenzen auf das Bild) zu verändern.
*
* @param image Das Originalbild.
* @return Eine exakte Kopie des Originals.
*/
public static BufferedImage copyImage( BufferedImage image ) {
ColorModel cm = image.getColorModel();
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
WritableRaster raster = image.copyData(null);
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
/**
* Erstellt ein {@code BufferedImage} mit demselben Inhalt wie das
* angegebene {@code Image}.
*
* @param image Das Originalbild.
* @return Eine exakte Kopie des Originals.
*/
public static BufferedImage copyImage( Image image ) {
if( image instanceof BufferedImage ) {
return copyImage((BufferedImage) image);
} else {
//BufferedImage outimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB);
BufferedImage outimage = createImage(image.getWidth(null), image.getHeight(null));
Graphics2D g = outimage.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, outimage.getWidth(), outimage.getHeight());
g.drawImage(image, 0, 0, null);
g.dispose();
return outimage;
}
}
/**
* Speichert das angegebene Bild in der angegebenen Datei auf der
* Festplatte.
*
* @param image Das zu speichernde Bild. Falls die Datei schon existiert,
* wird sie überschrieben.
* @param file Die Zieldatei.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
*/
public static void saveImage( Image image, File file ) throws IOException {
saveImage(image, file, false);
saveImage(copyImage(image), file, false);
}
/**
* Speichert das angegebene Bild in der angegebenen Datei auf der
* Festplatte.
*
* @param image Das zu speichernde Bild. Falls die Datei schon existiert,
* wird sie überschrieben.
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
*/
public static void saveImage( Image image, File file, boolean overwriteIfExists ) throws IOException {
//BufferedImage outimage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB);
BufferedImage outimage = createImage(image.getWidth(null), image.getHeight(null));
Graphics2D g = outimage.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, outimage.getWidth(), outimage.getHeight());
g.drawImage(image, 0, 0, null);
g.dispose();
saveImage(outimage, file, overwriteIfExists);
saveImage(copyImage(image), file, overwriteIfExists);
}
/**
* Speichert das angegebene Bild in der angegebenen Datei auf der
* Festplatte.
*
* @param image Das zu speichernde Bild. Falls die Datei schon existiert,
* wird sie überschrieben.
* @param file Die Zieldatei.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
*/
public static void saveImage( BufferedImage image, File file ) throws IOException {
saveImage(image, file, false);
}
/**
* Speichert das angegebene Bild in der angegebenen Datei auf der
* Festplatte.
*
* @param image Das zu speichernde Bild. Falls die Datei schon existiert,
* wird sie überschrieben.
* @param file Die Zieldatei.
* @param overwriteIfExists Bei {@code true} wird eine vorhandene Datei
* überschrieben, bei {@code false} wird eine {@link IOException} geworfen,
* wenn die Datei schon exisitiert.
* @throws NullPointerException Falls {@code image} oder {@code file}
* {@code null} ist.
* @throws IOException Falls es einen Fehler beim Speichern gab.
*/
public static void saveImage( BufferedImage image, File file, boolean overwriteIfExists ) throws IOException {
if( image == null ) {
throw new NullPointerException("Image may not be <null>.");
}
if( file == null ) {
throw new NullPointerException("File may not be <null>.");
}
if( file.isFile() ) {
// Datei existiert schon
if( !overwriteIfExists ) {
throw new IOException("File already exists. Delete target file before saving image.");
} else if( !file.canWrite() ) {
@@ -171,6 +356,7 @@ public class ImageLoader {
}
}
// Dateiformat anhand der Dateiendung ermitteln oder PNG nutzen
String filename = file.getName();
String formatName = "png";
if( filename.lastIndexOf('.') >= 0 ) {
@@ -179,13 +365,31 @@ public class ImageLoader {
file = new File(file.getAbsolutePath() + ".png");
}
// Datei schreiben
ImageIO.write(image, formatName, file);
}
/**
* Erstellt ein neues, leeres {@code BufferedImage} passend für dieses
* Anzeigegerät.
*
* @param width Breite des leeren Bildes.
* @param height Höhe des leeren Bildes.
* @return Ein neues, leeres Bild.
*/
public static BufferedImage createImage( int width, int height ) {
return createImage(width, height, BufferedImage.TYPE_INT_RGB);
}
/**
* Erstellt ein neues, leeres {@code BufferedImage} passend für dieses
* Anzeigegerät.
*
* @param width Breite des neuen Bildes.
* @param height Höhe des neuen Bildes.
* @param type {@link BufferedImage#getType() Typ} des neuen Bildes.
* @return Ein neues, leeres Bild.
*/
public static BufferedImage createImage( int width, int height, int type ) {
return GraphicsEnvironment
.getLocalGraphicsEnvironment()
@@ -194,4 +398,9 @@ public class ImageLoader {
.createCompatibleImage(width, height, type);
}
private ImageLoader() {
}
private static final Log LOG = Log.getLogger(ImageLoader.class);
}

View File

@@ -0,0 +1,318 @@
package schule.ngb.zm.util;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.function.Supplier;
import java.util.logging.*;
import static java.util.logging.Level.*;
/**
* Einfache Logging-API, die auf {@link java.util.logging} aufsetzt.
* <p>
* Klassen, die Informations- oder Fehlernachrichten loggen wollen, erstellen
* ein internes {@code LOG} Objekt dieser Klasse. Die Zeichenmaschine erstellt
* ihren Logger beispielsweise so:
* <pre>
* private static final Log LOG = new Log(Zeichenmaschine.class);
* </pre>
* <p>
* Jedes {@code Log} nutzt intern einen {@link Logger}, der über
* {@link Logger#getLogger(String)} abgerufen wird. Die {@code Log}-Objekte
* selbst werden nicht weiter zwischengespeichert, aber in der Regel wird pro
* Klasse nur genau ein {@code Log}-Objekt erstellt. Mehrere {@code Log}s nutzen
* dann aber denselben {@code Logger}.
* <p>
* Die API orientiert sich lose an <a href="#">Log4j</a> und vereinfacht die
* Nutzung der Java logging API für die häufigsten Anwendungsfälle.
*/
public final class Log {
private static final String ROOT_LOGGER = "schule.ngb.zm";
private static boolean LOGGING_INIT = false;
/**
* Aktiviert das Logging in der Zeichenmaschine global.
* <p>
* Die Methode sollte einmalig möglichst früh im Programm aufgerufen werden,
* um für alle bisher und danach erstellten {@link Logger} das minimale
* Logging-Level auf {@link Level#FINE} zu setzen. Dies entspricht allen
* Nachrichten die mit den Methoden (außer {@code trace}) eines {@link Log}
* erzeugt werden.
*
* @see #enableGlobalLevel(Level)
*/
public static final void enableGlobalDebugging() {
enableGlobalLevel(ALL);
}
/**
* Setzt das Logging-Level aller bisher und danach erzeugten {@link Logger}
* auf den angegebenen {@code Level}.
* <p>
* Das Level für bestehende {@code Logger} wird nur abgesenkt, so dass
* Nachrrichten bis {@code level} auf jeden Fall ausgegeben werden. Besitzt
* der {@code Logger} schon ein niedrigeres Level, wird dieses nicht
* verändert. Gleiches gilt für alle {@link ConsoleHandler}, die den
* bestehenden {@code Logger}n hinzugefügt wurden.
* <p>
* <strong>Hinweis:</strong> Das Setzen des Logging-Level während der
* Programmausführung gilt als <em>bad practice</em>, also schlechter
* Programmierstil. Im Kontext der Zeichenmaschine macht dies Sinn, um
* Programmieranfängern eine einfache Möglichkeit zu geben, in ihren
* Programmen auf Fehlersuche zu gehen. Für andere Einsatzszenarien sollte
* auf die übliche Konfiguration des {@link java.util.logging} Pakets über
* eine Konfigurationsdatei zurückgegriffen werden.
*
* @param level Das Level, auf das alle {@code Logger} und {@code Handler}
* mindestens herabgesenkt werden sollen.
*/
public static final void xenableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
Logger rootLogger = Logger.getLogger(ROOT_LOGGER);
rootLogger.setLevel(level);
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
}
}
}
}
public static final void enableGlobalLevel( Level level ) {
int lvl = Validator.requireNotNull(level).intValue();
// Decrease level of root level ConsoleHandlers for outpu
Logger rootLogger = Logger.getLogger("");
for( Handler handler : rootLogger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
}
}
}
// Decrease level of all existing ZM Loggers
Iterator<String> loggerNames = LogManager.getLogManager().getLoggerNames().asIterator();
while( loggerNames.hasNext() ) {
String loggerName = loggerNames.next();
if( loggerName.startsWith(ROOT_LOGGER) ) {
Logger logger = Logger.getLogger(loggerName);
logger.setLevel(level);
for( Handler handler : logger.getHandlers() ) {
if( handler instanceof ConsoleHandler ) {
Level handlerLevel = handler.getLevel();
if( handlerLevel == null || handler.getLevel().intValue() > lvl ) {
handler.setLevel(level);
}
}
}
}
}
}
public static final Log getLogger( Class<?> clazz ) {
if( !LOGGING_INIT ) {
Logger.getLogger(ROOT_LOGGER);
LOGGING_INIT = true;
}
return new Log(clazz);
}
private final Logger LOGGER;
private final Class<?> sourceClass;
private Log( final Class<?> clazz ) {
sourceClass = clazz;
if( !clazz.getName().startsWith(ROOT_LOGGER) ) {
LOGGER = Logger.getLogger(ROOT_LOGGER + "." + clazz.getSimpleName());
} else {
LOGGER = Logger.getLogger(clazz.getName());
}
}
/*public Log( final String name ) {
LOGGER = Logger.getLogger(name);
}*/
public void log( Level level, final CharSequence msg ) {
//LOGGER.log(level, msg::toString);
if( LOGGER.isLoggable(level) ) {
//LOGGER.logp(level, sourceClass.getName(), inferCallerName(), msg::toString);
doLog(level, null, msg::toString);
}
}
public void log( Level level, final CharSequence msg, Object... params ) {
if( LOGGER.isLoggable(level) ) {
//LOGGER.logp(level, sourceClass.getName(), inferCallerName(), () -> String.format(msg.toString(), params));
doLog(level, null, () -> String.format(msg.toString(), params));
}
}
public void log( Level level, final Supplier<String> msgSupplier ) {
if( LOGGER.isLoggable(level) ) {
//LOGGER.logp(level, sourceClass.getName(), inferCallerName(), msgSupplier);
doLog(level, null, msgSupplier);
}
}
public void log( Level level, final Throwable throwable, final CharSequence msg, Object... params ) {
if( LOGGER.isLoggable(level) ) {
//LOGGER.logp(level, sourceClass.getName(), inferCallerName(), throwable, () -> String.format(msg.toString(), params));
doLog(level, throwable, () -> String.format(msg.toString(), params));
}
}
public void log( Level level, final Throwable throwable, final Supplier<String> msgSupplier ) {
if( LOGGER.isLoggable(level) ) {
//LOGGER.logp(level, sourceClass.getName(), inferCallerName(), throwable, msgSupplier);
doLog(level, throwable, msgSupplier);
}
}
private String inferCallerName() {
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
/// if( trace[i].getClassName().equals(sourceClass.getName()) ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
return trace[i].getMethodName();
}
}
return "unknown";
}
private void doLog( Level level, final Throwable throwable, final Supplier<String> msgSupplier ) {
String clazz = sourceClass.getName();
String method = "unknown";
StackTraceElement[] trace = new Throwable().getStackTrace();
for( int i = 0; i < trace.length; i++ ) {
if( !trace[i].getClassName().equals(Log.class.getName()) ) {
clazz = trace[i].getClassName();
method = trace[i].getMethodName();
break;
}
}
if( throwable != null ) {
LOGGER.logp(level, clazz, method, throwable, msgSupplier);
} else {
LOGGER.logp(level, clazz, method, msgSupplier);
}
}
public boolean isLoggable( Level level ) {
return LOGGER.isLoggable(level);
}
public void info( final CharSequence msg ) {
this.log(INFO, msg);
}
public void info( final CharSequence msg, Object... params ) {
this.log(INFO, () -> String.format(msg.toString(), params));
}
public void info( final Supplier<String> msgSupplier ) {
this.log(INFO, msgSupplier);
}
public void info( final Throwable ex, final CharSequence msg, Object... params ) {
this.log(INFO, ex, () -> String.format(msg.toString(), params));
}
public void info( final Throwable throwable, final Supplier<String> msgSupplier ) {
this.log(INFO, throwable, msgSupplier);
}
public void warn( final CharSequence msg ) {
this.log(WARNING, msg);
}
public void warn( final CharSequence msg, Object... params ) {
this.log(WARNING, () -> String.format(msg.toString(), params));
}
public void warn( final Supplier<String> msgSupplier ) {
this.log(WARNING, msgSupplier);
}
public void warn( final Throwable ex, final CharSequence msg, Object... params ) {
this.log(WARNING, ex, () -> String.format(msg.toString(), params));
}
public void warn( final Throwable throwable, final Supplier<String> msgSupplier ) {
this.log(WARNING, throwable, msgSupplier);
}
public void error( final CharSequence msg ) {
this.log(SEVERE, msg);
}
public void error( final CharSequence msg, Object... params ) {
this.log(SEVERE, () -> String.format(msg.toString(), params));
}
public void error( final Supplier<String> msgSupplier ) {
this.log(SEVERE, msgSupplier);
}
public void error( final Throwable ex, final CharSequence msg, Object... params ) {
this.log(SEVERE, ex, () -> String.format(msg.toString(), params));
}
public void error( final Throwable throwable, final Supplier<String> msgSupplier ) {
this.log(SEVERE, throwable, msgSupplier);
}
public void debug( final CharSequence msg ) {
this.log(FINE, msg);
}
public void debug( final CharSequence msg, Object... params ) {
this.log(FINE, () -> String.format(msg.toString(), params));
}
public void debug( final Supplier<String> msgSupplier ) {
this.log(FINE, msgSupplier);
}
public void debug( final Throwable ex, final CharSequence msg, Object... params ) {
this.log(FINE, ex, () -> String.format(msg.toString(), params));
}
public void debug( final Throwable throwable, final Supplier<String> msgSupplier ) {
this.log(FINE, throwable, msgSupplier);
}
public void trace( final CharSequence msg ) {
this.log(FINER, msg);
}
public void trace( final CharSequence msg, Object... params ) {
this.log(FINER, () -> String.format(msg.toString(), params));
}
public void trace( final Supplier<String> msgSupplier ) {
this.log(FINER, msgSupplier);
}
public void trace( final Throwable ex, final CharSequence msg, Object... params ) {
this.log(FINER, ex, () -> String.format(msg.toString(), params));
}
public void trace( final Throwable throwable, final Supplier<String> msgSupplier ) {
this.log(FINER, throwable, msgSupplier);
}
}

View File

@@ -0,0 +1,171 @@
package schule.ngb.zm.util;
import schule.ngb.zm.Zeichenmaschine;
import java.io.*;
import java.net.URL;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
/**
* Helferklasse, um {@link InputStream}s für Resourcen zu erhalten.
*/
public class ResourceStreamProvider {
/**
* Sucht eine zur angegebenen Quelle passende Resource und öffnet einen
* passenden {@link InputStream}. Die konkrete Art des Streams hängt davon
* ab, welche Art an Resource gefunden wird:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* {@code new File(source}.isFile() == true}, dann wird ein
* {@link FileInputStream} erstellt.</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt, wird ein passender
* Stream mit {@link Class#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} im Classpath enthalten, wird ein passender Stream
* mit {@link ClassLoader#getResourceAsStream(String)} geöffnet.</li>
* <li>Ist {@code source} eine gültige {@link URL}, dann wird ein
* {@link URL#openStream() Netwerkstream} geöffnet.</li>
* </ol>
* <p>
* Die Möglichen Resourcen werden in der gelisteten Reihenfolge durchprobiert
* und der erste gefundene Stream zurückgegeben. Auftretende Exceptions
* werden als {@link IOException} geworfen. Das bedeutet, falls für {@code source}
* keine gültige Resource gefunden werden kann, wird am Ende die
* von {@link URL#openStream()} erzeugte {@code IOException} geworfen.
* <p>
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt.
* Eine {@link SecurityException} beim Öffnen des {@code FileInputStream}
* (zum Beispiel, weil auf eine existierende Datei keine Leserechte bestehen)
* verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Resource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim öffnen des Streams zu
* einer bestehenden Resource oder falls
* keine passende Resource gefunden wurde.
*/
public static InputStream getResourceStream( String source ) throws NullPointerException, IllegalArgumentException, IOException {
if( source == null ) {
throw new NullPointerException("Resource source may not be null");
}
if( source.length() == 0 ) {
throw new IllegalArgumentException("Resource source may not be empty.");
}
InputStream in;
// See if source is a readable file
File file = new File(source);
try {
if( file.isFile() ) {
in = new FileInputStream(file);
}
} catch( FileNotFoundException fnfex ) {
// Somehow an exception occurred, but we still try other sources
in = null;
}
// File does not exist, try other means
// load ressource relative to .class-file
in = Zeichenmaschine.class.getResourceAsStream(source);
// relative to ClassLoader
if( in == null ) {
in = Zeichenmaschine.class.getClassLoader().getResourceAsStream(source);
}
// load form web or jar-file
if( in == null ) {
in = new URL(source).openStream();
}
// One of the above got a valid Stream,
// otherwise an Exception was thrown
return in;
}
/**
* Ermittelt zur angegebenen Quelle einen passenden {@link URL} (<em>Unified
* Resource Locator</em>). Eine passende Datei-Resource wird wie folgt
* ermittelt:
* <ol>
* <li>Ist {@code source} eine existierende Datei
* ({@code new File(source}.isFile() == true})?</li>
* <li>Ist {@code source} ein relativer Pfad im Projekt ({@code getResource(source) != null})?.</li>
* <li>Ist {@code source} im Classpath enthalten ({@code getClassLoader().getResource(source) != null})?</li>
* <li>Ansonten erstellt ein {@link URL}-Objekt.</li>
* </ol>
* <p>
* Ein {@code URL} für die erste gefundene Resource wird zurückgegeben.
* Auftretende Exceptions
* werden als {@link IOException} geworfen.
* <p>
* Bei einer Exception werden die folgenden Quellen nicht mehr abgefragt.
* Eine {@link java.net.MalformedURLException} beim Konstruieren des {@code URL}
* zu einer Datei verhindert daher, dass noch im Classpath gesucht wird.
*
* @param source Eine Quelle für die Resource (Absoluter Dateipfad,
* Dateipfad im Classpath oder Netzwerkresource)
* @return Ein {@code InputStream} für die Resource
* @throws NullPointerException Falls {@code source} {@code null} ist.
* @throws IllegalArgumentException Falls {@code source} ein leerer String
* ist.
* @throws IOException Geworfen beim erzeugen eines URL zu
* einer bestehenden Resource.
*/
public static URL getResourceURL( String source ) throws NullPointerException, IllegalArgumentException, IOException {
if( source == null ) {
throw new NullPointerException("Resource source may not be null");
}
if( source.length() == 0 ) {
throw new IllegalArgumentException("Resource source may not be empty.");
}
File file = new File(source);
if( file.isFile() ) {
return file.toURI().toURL();
}
URL url;
url = Zeichenmaschine.class.getResource(source);
if( url != null ) {
return url;
}
url = Zeichenmaschine.class.getClassLoader().getResource(source);
if( url != null ) {
return url;
}
return new URL(source);
}
public static InputStream getResourceStream( File file ) throws FileNotFoundException, SecurityException {
if( file == null ) {
throw new NullPointerException("Provided file can't be null.");
}
return new FileInputStream(file);
}
public static InputStream getResourceStream( URL url ) throws IOException {
if( url == null ) {
throw new NullPointerException("Provided URL can't be null.");
}
return url.openStream();
}
private ResourceStreamProvider() {
}
private static final Log LOG = Log.getLogger(ResourceStreamProvider.class);
}

View File

@@ -0,0 +1,152 @@
package schule.ngb.zm.util;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class Validator {
public static final <T> T requireNotNull( T obj ) {
return Objects.requireNonNull(obj);
}
public static final <T> T requireNotNull( T obj, CharSequence msg ) {
return Objects.requireNonNull(obj, msg::toString);
}
public static final <T> T requireNotNull( T obj, Supplier<String> msg ) {
return Objects.requireNonNull(obj, msg);
}
public static final String requireNotEmpty( String str ) {
return requireNotEmpty(str, (Supplier<String>)null);
}
public static final String requireNotEmpty( String str, CharSequence msg ) {
return requireNotEmpty(str, msg::toString);
}
public static final String requireNotEmpty( String str, Supplier<String> msg ) {
if( str.isEmpty() )
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be empty string (<%s> provided)", str) : msg.get());
return str;
}
public static final int requirePositive( int i ) {
return requirePositive(i, (Supplier<String>)null);
}
public static final int requirePositive( int i, CharSequence msg ) {
return requirePositive(i, msg::toString);
}
public static final int requirePositive( int i, Supplier<String> msg ) {
if( i <= 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter must be positive (<%d> provided)", i) : msg.get());
return i;
}
public static final int requireNotNegative( int i ) {
return requireNotNegative(i, (Supplier<String>)null);
}
public static final int requireNotNegative( int i, CharSequence msg ) {
return requireNotNegative(i, msg::toString);
}
public static final int requireNotNegative( int i, Supplier<String> msg ) {
if( i < 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be negative (<%d> provided)", i) : msg.get());
return i;
}
public static final int requireInRange( int i, int min, int max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
}
public static final int requireInRange( int i, int min, int max, CharSequence msg ) {
return requireInRange(i, min, max, msg::toString);
}
public static final int requireInRange( int i, int min, int max, Supplier<String> msg ) {
if( i < min || i > max )
throw new IllegalArgumentException(msg == null ? String.format("Parameter out of range <%d> to <%d> (<%d> provided)", min, max, i) : msg.get());
return i;
}
public static final double requirePositive( double i ) {
return requirePositive(i, (Supplier<String>)null);
}
public static final double requirePositive( double i, CharSequence msg ) {
return requirePositive(i, msg::toString);
}
public static final double requirePositive( double i, Supplier<String> msg ) {
if( i <= 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter must be positive (<%f> provided)", i) : msg.get());
return i;
}
public static final double requireNotNegative( double i ) {
return requireNotNegative(i, (Supplier<String>)null);
}
public static final double requireNotNegative( double i, CharSequence msg ) {
return requireNotNegative(i, msg::toString);
}
public static final double requireNotNegative( double i, Supplier<String> msg ) {
if( i < 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be negative (<%f> provided)", i) : msg.get());
return i;
}
public static final double requireInRange( double i, double min, double max ) {
return requireInRange(i, min, max, (Supplier<String>)null);
}
public static final double requireInRange( double i, double min, double max, CharSequence msg ) {
return requireInRange(i, min, max, msg::toString);
}
public static final double requireInRange( double i, double min, double max, Supplier<String> msg ) {
if( i < min || i > max )
throw new IllegalArgumentException(msg == null ? String.format("Parameter out of range <%f> to <%f> (<%f> provided)", min, max, i) : msg.get());
return i;
}
/*
public static final <N extends Number> N requirePositive( N num, Supplier<String> msg ) {
if( num.doubleValue() <= 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter must be positive (<%s> provided)", num.toString()) : msg.get());
return num;
}
public static final <N extends Number> N requireNotNegative( N num, Supplier<String> msg ) {
if( num.doubleValue() < 0 )
throw new IllegalArgumentException(msg == null ? String.format("Parameter may not be negative (<%s> provided)", num.toString()) : msg.get());
return num;
}
public static final <N extends Number> N requireInRange( N num, N min, N max, Supplier<String> msg ) {
if( num.doubleValue() < min.doubleValue() || num.doubleValue() > max.doubleValue() )
throw new IllegalArgumentException(msg == null ? String.format("Parameter out of range <%s> to <%s> (<%s> provided)", min.toString(), max.toString(), num.toString()) : msg.get());
return num;
}
*/
public static final <T> T[] requireNotEmpty( T[] arr, Supplier<String> msg ) {
return requireSize(arr, 0, ()->"Parameter array may not be empty");
}
public static final <T> T[] requireSize( T[] arr, int size, Supplier<String> msg ) {
if( arr.length != size )
throw new IllegalArgumentException(msg == null ? String.format("Parameter array must have <%d> elements (<%d> provided)", size) : msg.get());
return arr;
}
private Validator() {
}
}