zpg-graphentester/graph/GraphPlotter.java
2024-03-12 17:34:56 +01:00

678 lines
25 KiB
Java

package graph;
import imp.*;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.awt.Graphics2D;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.MouseButton;
import java.nio.file.*;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javafx.scene.control.Tooltip;
import javafx.util.Duration;
import javafx.animation.AnimationTimer;
import java.awt.image.*;
import org.apache.commons.io.FileUtils;
/**
* Der GraphPlotter ist das Herzstueck der Visualisierung und dient als Schnittstelle zur GUI.
*
* @author Thomas Schaller
* @version 07.02.2023 (v7.0)
* v6.9: Context-Menü schließt, wenn an andere Stelle geklickt wird
* v7.0: MouseOver - Infos für Knoten und Kanten, Infos können ausgewählt werden.
*
*/
public class GraphPlotter extends PictureViewer {
// Anfang Attribute
private Graph graph;
private GraphOptions options;
private boolean multiselect = false;
private boolean editable = false;
private ArrayList<GraphElement> selected=new ArrayList<GraphElement>();
private Knoten dragKnoten = null;
private int dragMode = 0;
// Tooltip
private double mouseX, mouseY;
private Tooltip t;
private GraphElement restrictTo = null;
private Point2D offset = new Point2D(0,0);
private ObjectProperty<Point2D> mouseLocation = new SimpleObjectProperty<Point2D>(new Point2D(0, 0));
private BooleanProperty mouseMoving = new SimpleBooleanProperty();
// private JTextArea jTAMeldungen = new JTextArea("");
// private JScrollPane jTAMeldungenScrollPane = new JScrollPane(jTAMeldungen);
// Ende Attribute
/**
* Der Konstruktor legt sowohl Einstellungen des mxGraphen (Drag&Drop, Editable, ...) als auch des Graphen (gewichtet, gerichtet, ...) fest.
*
* @param boolean isDirected Gibt an, ob der Graph gerichtet oder ungerichtet ist
* @param boolean isWeighted Gibt an, ob der Graph gewichtet oder ungewichtet ist
* @param String hintergrundBild Gibt den Namen eines Hintergrundbildes an
*/
public GraphPlotter() {
graph = new Graph();
options = new GraphOptions(graph);
this.setStyle("-fx-background:#FFFFE8");
// add(jTAMeldungenScrollPane, BorderLayout.SOUTH);
setOnMouseClicked(mouseEvent -> mouseClicked(mouseEvent));
this.widthProperty().addListener((value, oldWidth, newWidth) -> updateImage());
this.heightProperty().addListener((value, oldWidth, newWidth) -> updateImage());
setOnMouseMoved(e -> mouseLocation.set(new Point2D(e.getSceneX(), e.getSceneY())));
mouseMoving.addListener((obs, wasMoving, isNowMoving) -> {
updateImage();
});
AnimationTimer timer = new AnimationTimer() {
private double lastMouseX ;
private double lastMouseY ;
long lastMouseMovement ;
long MIN_STATIONARY_TIME = 2000;
@Override
public void handle(long timestamp) {
double x = mouseLocation.get().getX();
double y = mouseLocation.get().getY();
if (Math.abs(lastMouseX-x) > 5 || Math.abs(lastMouseY-y)>5) {
lastMouseMovement = timestamp ;
lastMouseX = x;
lastMouseY = y;
}
mouseMoving.set(timestamp - lastMouseMovement < MIN_STATIONARY_TIME);
}
};
timer.start();
}
public void setEditable() {
editable = true;
//setOnMousePressed(mouseEvent -> mouseDown(mouseEvent)); // wird durch MousePressed in EditTab realisiert und dann hier aufgerufen
setOnMouseReleased(mouseEvent -> mouseUp(mouseEvent));
setOnDragDetected(mouseEvent -> startFullDrag());
setOnMouseDragOver(mouseEvent-> mouseDragged(mouseEvent));
setOnMouseClicked(null);
}
public void setGraph(Graph graph, GraphOptions options) {
this.graph = graph;
this.options = options;
updateImage();
}
public void setRestrictTo(GraphElement k) {
if(restrictTo != k) {
restrictTo = k;
selected.clear();
if (k!= null) selected.add(k);
updateImage();
}
}
public GraphElement getRestrictTo() {
return restrictTo;
}
public void mouseClicked(MouseEvent mouseEvent) {
Point2D local = this.getContent().sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY());
GraphElement k = getKnotenAt((int) local.getX(), (int) local.getY());
if(k!=null && mouseEvent.isShiftDown() && restrictTo != null && restrictTo instanceof Knoten) { // mit Shift-Knotenklick Kante auswählen, damit unsichtbare Kanten gewählt werden können (für TSP)
k = graph.getKante((Knoten) restrictTo, (Knoten) k);
}
if(k!=null && mouseEvent.isShiftDown() && selected.size() == 1 && selected.get(0) instanceof Knoten) { // mit Shift-Knotenklick Kante auswählen, damit unsichtbare Kanten gewählt werden können (für TSP)
k = graph.getKante((Knoten) (selected.get(0)), (Knoten) k);
}
if(k==null) k = getKanteAt((int) local.getX(), (int) local.getY());
if(!multiselect) {
selected.clear();
}
if(k != null) {
if(selected.contains(k)) selected.remove(k);
else selected.add(k);
}
updateImage();
}
public void mouseDown(MouseEvent mouseEvent) {
if(mouseEvent.isPrimaryButtonDown()) {
Point2D local = this.getContent().sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY());
dragKnoten = getKnotenAt((int) local.getX(), (int) local.getY());
dragMode = 3; // Linksclick aber nicht auf Knoten
if(dragKnoten != null) {
mouseEvent.setDragDetect(true);
double distance = Math.sqrt(Math.pow(local.getX()-dragKnoten.getX(),2)+Math.pow(local.getY()-dragKnoten.getY(),2));
if(distance < options.vertexSize/4)
dragMode = 1; // Knoten verschieben
else
dragMode = 2; // Kante ziehen
}
}
}
public void mouseDragged(MouseEvent mouseEvent) {
if(dragKnoten != null) {
Point2D local = this.getContent().sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY());
if(dragMode == 1) {
dragKnoten.setX((int) (local.getX()));
dragKnoten.setY((int) (local.getY()));
}
updateImage();
if(dragMode == 2) {
Picture p = getImage();
p.stroke(0);
p.strokeWeight(3);
//p.line(dragKnoten.getX(), dragKnoten.getY(), (int) local.getX(), (int) local.getY());
drawArrow(p,dragKnoten.getX(), dragKnoten.getY(), (int) local.getX(), (int) local.getY());
setImage(p,false);
}
}
}
public void mouseUp(MouseEvent mouseEvent) {
Point2D local = this.getContent().sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY());
Knoten k = getKnotenAt((int) local.getX(), (int) local.getY());
if(dragMode == 3 && k==null && getKanteAt((int) local.getX(), (int) local.getY())==null) { // neuer Knoten
if(getContextMenu() == null) {
graph.neuerKnoten(new Knoten((int)local.getX(), (int) local.getY())) ;
} else { setContextMenu(null); }
} else {
if(dragMode == 2 && k != null && k != dragKnoten) {
graph.neueKante(dragKnoten, k, 0.0);
}
}
dragKnoten = null;
dragMode = 0;
mouseEvent.setDragDetect(false);
updateImage();
}
private Knoten getKnotenAt(int x, int y) {
List<Knoten> knoten = graph.getAlleKnoten();
if(restrictTo != null) {
knoten.clear();
if(restrictTo instanceof Knoten) {
knoten = graph.getNachbarknoten((Knoten) restrictTo);
knoten.add((Knoten) restrictTo);
}
if(restrictTo instanceof Kante) {
knoten.add(((Kante) restrictTo).getStart());
knoten.add(((Kante) restrictTo).getZiel());
}
}
for(Knoten k : knoten) {
if(Math.sqrt(Math.pow(k.getX()-x,2)+Math.pow(k.getY()-y,2)) < options.vertexSize/2+1) {
return k;
}
}
return null;
}
private Kante getKanteAt(int x, int y) {
List<Kante> kanten = graph.getAlleKanten();
if(restrictTo != null) {
kanten.clear();
if(restrictTo instanceof Knoten) {
kanten = graph.getAusgehendeKanten((Knoten) restrictTo);
}
if(restrictTo instanceof Kante) {
kanten.add(((Kante) restrictTo));
}
}
for (Kante k: kanten) {
if (x>=Math.min(k.getStart().getX(), k.getZiel().getX())-2 &&
x<=Math.max(k.getStart().getX(), k.getZiel().getX())+2 &&
y>=Math.min(k.getStart().getY(), k.getZiel().getY())-2 &&
y<=Math.max(k.getStart().getY(), k.getZiel().getY())+2) {
double startX = k.getStart().getX();
double startY = k.getStart().getY();
double endX = k.getZiel().getX();
double endY = k.getZiel().getY();
double dy = (endY-startY);
double dx = (endX-startX);
double l = Math.sqrt(dx*dx+dy*dy);
dy = dy /l; dx = dx / l;
startX += dx * (options.vertexSize/2+1);
startY += dy * (options.vertexSize/2+1);
endX -= dx * (options.vertexSize/2+1);
endY -= dy * (options.vertexSize/2+1);
double dx2 = dy*5;
double dy2 = -dx*5;
if (graph.isGerichtet() && graph.getKante(k.getZiel(), k.getStart())!=null){
startX += dx2;
startY += dy2;
endX += dx2;
endY += dy2;
}
double nx = dy;
double ny = -dx;
double abx = x - startX;
double aby = y - startY;
double abs = Math.abs(abx*nx+aby*ny);
if (abs < 3) return k;
}
}
return null;
}
private void drawArrow(Picture p, int startx, int starty, int endx, int endy) {
double deltax = startx - endx;
double result;
if (deltax == 0.0d) {
result = (starty > endy ? Math.PI / 2 : -Math.PI / 2);
}
else {
result = Math.atan((starty - endy) / deltax) + (startx < endx ? Math.PI : 0);
}
double angle = result;
double arrowAngle = Math.PI / 8.0d;
double arrowSize = 10;
double x1 = arrowSize * Math.cos(angle - arrowAngle);
double y1 = arrowSize * Math.sin(angle - arrowAngle);
double x2 = arrowSize * Math.cos(angle + arrowAngle);
double y2 = arrowSize * Math.sin(angle + arrowAngle);
double cx = (arrowSize / 2.0f) * Math.cos(angle);
double cy = (arrowSize / 2.0f) * Math.sin(angle);
int x[] = {(int) endx, (int) (endx+x1), (int) (endx+x2), (int) endx};
int y[] = {(int) endy, (int) (endy+y1), (int) (endy+y2), (int) endy};
p.line(startx, starty, endx, endy);
p.polygon(x,y);
}
private String darker(String color) {
String red = color.substring(0, 2);
String green = color.substring(2,4);
String blue = color.substring(4,6);
long r = Long.parseLong(red,16)/2;
long g = Long.parseLong(green,16) /2;
long b = Long.parseLong(blue,16) / 2;
String sr = "0"+Long.toHexString(r);
String sg = "0"+Long.toHexString(g);
String sb = "0"+Long.toHexString(b);
return sr.substring(sr.length()-2)+sg.substring(sg.length()-2)+sb.substring(sb.length()-2);
}
private String brighter(String color) {
String red = color.substring(0, 2);
String green = color.substring(2,4);
String blue = color.substring(4,6);
long r = (Long.parseLong(red,16)+255)/2;
long g = (Long.parseLong(green,16)) /2;
long b = (Long.parseLong(blue,16)) / 2;
String sr = "0"+Long.toHexString(r);
String sg = "0"+Long.toHexString(g);
String sb = "0"+Long.toHexString(b);
return sr.substring(sr.length()-2)+sg.substring(sg.length()-2)+sb.substring(sb.length()-2);
}
private String format(double d) {
if((int) d == d) {
return ""+(int) d;
} else {
return ""+d;
}
}
public Picture updateImage() {
Picture p = new Picture(2000,2000,"FFFFE8");
Graphics2D g = (Graphics2D) p.getImage().getGraphics();
Knoten restrictToKnoten = null;
Kante restrictToKante = null;
if(restrictTo != null && restrictTo instanceof Knoten) restrictToKnoten = (Knoten) restrictTo;
if(restrictTo != null && restrictTo instanceof Kante) restrictToKante = (Kante) restrictTo;
List<Knoten> knoten = graph.getAlleKnoten();
List<Kante> kanten = graph.getAlleKanten();
int maxx = Integer.MIN_VALUE; int maxy = Integer.MIN_VALUE;
int minx = Integer.MAX_VALUE; int miny = Integer.MAX_VALUE;
for(Knoten k: knoten) {
maxx = Math.max(maxx,k.getX());
minx = Math.min(minx,k.getX());
maxy = Math.max(maxy,k.getY());
miny = Math.min(miny,k.getY());
}
if(restrictToKnoten != null) {
knoten = graph.getNachbarknoten(restrictToKnoten);
kanten = graph.getAusgehendeKanten(restrictToKnoten);
knoten.add(restrictToKnoten);
// this.getContent().setTranslateY(this.getHeight()/2-restrictToKnoten.getY());
// this.getContent().setTranslateX(this.getWidth()/2-restrictToKnoten.getX());
this.getContent().setTranslateY(1000-(restrictToKnoten.getY()));
this.getContent().setTranslateX(1000-(restrictToKnoten.getX()));
} else if (restrictToKante != null ) {
kanten.clear();
kanten.add(restrictToKante);
knoten.clear();
knoten.add(restrictToKante.getStart());
knoten.add(restrictToKante.getZiel());
// this.getContent().setTranslateY(this.getHeight()/2-restrictToKnoten.getY());
// this.getContent().setTranslateX(this.getWidth()/2-restrictToKnoten.getX());
this.getContent().setTranslateX(1000-(restrictToKante.getStart().getX()+restrictToKante.getZiel().getX())/2);
this.getContent().setTranslateY(1000-(restrictToKante.getStart().getY()+restrictToKante.getZiel().getY())/2);
}
else
{
/*this.getContent().setTranslateX(1000-(minx+maxx)/2);
this.getContent().setTranslateY(1000-(miny+maxy)/2);*/
}
p.textMode(Picture.CENTER);
if(options.bildAnzeigen && !options.bildDatei.isEmpty()) {
Picture p2 = new Picture("./images/"+options.bildDatei);
p.getImage().getGraphics().drawImage(p2.getImage(), 0, 0, null);
minx = options.vertexSize;
miny = options.vertexSize;
maxx = p2.getWidth()-options.vertexSize;
maxy = p2.getHeight()-options.vertexSize;
}
// Zone in der Mitte markieren
if(restrictToKnoten!=null) {
p.fill("FFE8E8");
p.stroke("FFE8E8");
p.ellipse(restrictToKnoten.getX(), restrictToKnoten.getY(), options.vertexSize*2, options.vertexSize*2);
}
if(restrictToKante!=null) {
p.fill("FFE8E8");
p.stroke("FFE8E8");
p.strokeWeight(30);
p.line(restrictToKante.getStart().getX(), restrictToKante.getStart().getY(), restrictToKante.getZiel().getX(),restrictToKante.getZiel().getY());
}
for (Kante k : kanten) {
if (!options.farbenKanten[k.getFarbe()].equals("invisible") || selected.contains(k)) {
double startX = k.getStart().getX();
double startY = k.getStart().getY();
double endX = k.getZiel().getX();
double endY = k.getZiel().getY();
double dy = (endY-startY);
double dx = (endX-startX);
double l = Math.sqrt(dx*dx+dy*dy);
dy = dy /l; dx = dx / l;
startX += dx * (options.vertexSize/2+1);
startY += dy * (options.vertexSize/2+1);
endX -= dx * (options.vertexSize/2+1);
endY -= dy * (options.vertexSize/2+1);
double dx2 = dy*5;
double dy2 = -dx*5;
if (graph.isGerichtet() && graph.getKante(k.getZiel(), k.getStart())!=null){
startX += dx2;
startY += dy2;
endX += dx2;
endY += dy2;
}
if(selected.contains(k)) {
p.stroke("FF0000");
p.strokeWeight(4);
p.line((int) startX, (int) startY, (int) endX, (int) endY);
}
if (!options.farbenKanten[k.getFarbe()].equals("invisible")){
p.stroke(options.farbenKanten[k.getFarbe()]);
p.fill(options.farbenKanten[k.getFarbe()]);
}
else
{
p.stroke("505050");
p.fill("505050");
}
p.strokeWeight(2);
if(graph.isGerichtet()) {
drawArrow(p, (int) startX, (int) startY, (int) endX, (int) endY);
} else {
p.line((int) startX, (int) startY, (int) endX, (int) endY);
}
}
}
for (Kante k : kanten) {
if (!options.farbenKanten[k.getFarbe()].equals("invisible") || selected.contains(k)) {
double startX = k.getStart().getX();
double startY = k.getStart().getY();
double endX = k.getZiel().getX();
double endY = k.getZiel().getY();
double dy = (endY-startY);
double dx = (endX-startX);
double l = Math.sqrt(dx*dx+dy*dy);
dy = dy /l; dx = dx / l;
startX += dx * (options.vertexSize/2+1);
startY += dy * (options.vertexSize/2+1);
endX -= dx * (options.vertexSize/2+1);
endY -= dy * (options.vertexSize/2+1);
double dx2 = dy*5;
double dy2 = -dx*5;
if(graph.isGerichtet()) {
if (graph.getKante(k.getZiel(), k.getStart())!=null){
startX += dx2;
startY += dy2;
endX += dx2;
endY += dy2;
}
}
if(options.showEdgeWeights) {
double my = (startY+startY+endY)/3;
double mx = (startX+startX+endX)/3;
p.fill(255);
p.stroke(0);
p.strokeWeight(1);
int lh = g.getFontMetrics().getAscent();
List<String> t = k.getKurztext(options.kanteKurztext);
if(t.size() == 1) {
p.rect((int) mx-15, (int) my-(lh+4)/2, 30, lh+4);
p.fill(0);
p.text(t.get(0), (int) mx, (int) my);
}
if(t.size() > 1) {
p.rect((int) mx-15, (int) my-(lh+2), 30, lh*2+4);
p.fill(0);
p.text(t.get(0), (int) mx, (int) my-lh/2);
p.text(t.get(1), (int) mx, (int) my+lh/2);
}
}
}
}
for (Knoten k : knoten) {
p.fill(options.farbenKnoten[k.getFarbe()]);
p.stroke(darker(options.farbenKnoten[k.getFarbe()]));
p.strokeWeight(3);
p.ellipse(k.getX(), k.getY(), options.vertexSize, options.vertexSize);
if(selected.contains(k)) {
p.noFill();
p.strokeWeight(2);
p.stroke(255,0,0);
p.ellipse(k.getX(), k.getY(), options.vertexSize+2, options.vertexSize+2);
}
p.fill(0);
p.stroke(0);
p.strokeWeight(0);
p.textMode(Picture.CENTER);
if (options.showVertexText) {
p.text(""+graph.getNummer(k), k.getX(), k.getY());
} else {
if (options.showVertexValue) {
List<String> t = k.getKurztext(options.knotenKurztext);
if(t.size() == 1) {
p.text(t.get(0), k.getX(), k.getY());
} else {
int lh = g.getFontMetrics().getAscent();
p.text(t.get(0), k.getX(), k.getY()-lh/2);
p.text(t.get(1), k.getX(), k.getY()+lh/2);
}
}
}
// Knotenbezeichnung
p.textMode(Picture.CORNER);
p.fill("000080");
if(options.showVertexInfo && !k.getInfotext().equals("")) {
p.text(k.getInfotext(), k.getX()+options.vertexSize/2+5,k.getY());
}
}
// Tooltip anzeigen, aber nicht wenn im Editiermodus
if(!mouseMoving.get() && !editable) {
Point2D local = this.getContent().sceneToLocal(mouseLocation.get().getX(), mouseLocation.get().getY());
int x = (int) local.getX();
int y = (int) local.getY();
// sowohl bei Kante wie auch Knoten
GraphElement k = getKnotenAt(x,y);
if(k == null) { k = getKanteAt(x,y);}
if(k != null) {
p.fill(200);
p.stroke(0);
p.strokeWeight(2);
List<String> t;
if(k instanceof Knoten) t = k.getLangtext(options.knotenLangtext);
else t = k.getLangtext(options.kanteLangtext);
// Größe des Kastens berechnen
int w = 0;
int lh = g.getFontMetrics().getAscent();
int h = t.size() * lh;
for(int i = 0; i<t.size(); i++) {
int w2 = g.getFontMetrics().stringWidth(t.get(i));
if(w2 > w) w = w2;
}
// Rechteck mit Text ausgeben
p.rect(x, y, w+16, h+10);
p.fill("303030");
for(int i = 0; i<t.size(); i++) {
p.text(t.get(i), x+8, y+(i+1)*lh+3);
}
}
}
this.setImage(p, false);
Picture zugeschnitten = new Picture(maxx-minx+2*options.vertexSize,maxy-miny+2*options.vertexSize);
zugeschnitten.getImage().getGraphics().drawImage(p.getImage(), 0, 0, maxx-minx+2*options.vertexSize, maxy-miny+2*options.vertexSize, minx-options.vertexSize, miny-options.vertexSize, maxx+options.vertexSize, maxy+options.vertexSize, null);
return zugeschnitten;
}
public GraphOptions getGraphOptions() {
return options;
}
/**
* Gibt den Graphen zurueck.
*
* @return Graph
*/
public Graph getGraph() {
return graph;
}
/**
* Gibt das selektierte Knotenobjekt zurueck.
*
* @return Object
*/
public Knoten getSelectedKnoten() {
if(selected.size()==1 && selected.get(0) instanceof Knoten)
return ((Knoten) (selected.get(0)));
else
return null;
}
/**
* Gibt die selektierte KnotenobjektListe (als Array) zurueck.
*
* @return Object[]
*/
public List<Knoten> getSelectedKnotenListe() {
List<Knoten> l = new ArrayList<Knoten>();
for(GraphElement g : selected) {
if(g instanceof Knoten) l.add((Knoten) g);
}
return l;
}
/**
* Gibt das selektierte Kantenobjekt zurueck.
*
* @return Object
*/
public Kante getSelectedKante() {
if(selected.size()==1 && selected.get(0) instanceof Kante)
return ((Kante) (selected.get(0)));
else
return null;
}
/**
* Ueberschreibt die Methode toString. Eine String-Repraesentation des GraphPlotters wird ausgegeben.
*
* @return String Die String-Repraesentation des GraphPlotters
*/
public String toString() {
String s = "";
if(graph.isGerichtet())
s += "Gerichteter, ";
else
s += "Ungerichteter, ";
if(graph.isGewichtet())
s += "gewichteter Graph mit Hintergrundbild: ";
else
s += "ungewichteter Graph mit Hintergrundbild: ";
if(options.bildDatei.equals(""))
s += " kein Bild!";
else
s += options.bildDatei + "!";
return s;
}
/**
* Gibt die String-Repraesentation des GraphPlotters auf der Konsole aus.
*/
public void ausgabe() {
System.out.println(toString() + "\n"+graph.toString());
}
// Ende Methoden
}