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 selected=new ArrayList(); 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 mouseLocation = new SimpleObjectProperty(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 = 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 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 = graph.getAlleKnoten(); List 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 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 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 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 w) w = w2; } // Rechteck mit Text ausgeben p.rect(x, y, w+16, h+10); p.fill("303030"); for(int i = 0; i getSelectedKnotenListe() { List l = new ArrayList(); 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 }