package graph; import imp.*; 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 org.apache.commons.io.FileUtils; /** * Der GraphPlotter ist das Herzstueck der Visualisierung und dient als Schnittstelle zur GUI. * * @author Thomas Schaller * @version 09.12.2020 (v6.7) */ 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 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() { options = new GraphOptions(); graph = new Graph(); this.setStyle("-fx-background:#FFFFE8"); // add(jTAMeldungenScrollPane, BorderLayout.SOUTH); setOnMouseClicked(mouseEvent -> mouseClicked(mouseEvent)); // setOnMouseMoved(mouseEvent -> { mouseX = mouseEvent.getSceneX(); mouseY = mouseEvent.getSceneY(); t.hide();}); this.widthProperty().addListener((value, oldWidth, newWidth) -> updateImage()); this.heightProperty().addListener((value, oldWidth, newWidth) -> updateImage()); // t = new Tooltip(); // Tooltip.install(this, t); // t.setPrefWidth(80); // t.setWrapText(true); // t.setHideOnEscape(true); // t.setStyle("-fx-background: rgba(30,30,30); -fx-text-fill: black; -fx-background-color: rgba(230,230,90,0.8);"+ // "-fx-background-radius: 6px; -fx-background-insets: 0; -fx-padding: 0.667em 0.75em 0.667em 0.75em; "+ // " -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.5) , 10, 0.0 , 0 , 3 ); -fx-font-size: 0.85em;"); // t.setShowDelay(Duration.seconds(1)); // t.setOnShowing(ev -> {// called just prior to being shown // Point2D local = this.getContent().sceneToLocal(mouseX, mouseY); // Knoten k = getKnotenAt((int) local.getX(), (int) local.getY()); // if(k == null) { // t.hide(); // } else { // t.setText("Knoten Nr. "+graph.getNummer(k)+"\nWert: "+k.getDoubleWert()); // } // }); } 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 graph.neuerKnoten(new Knoten((int)local.getX(), (int) local.getY())) ; } 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() { 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()); } Picture p = new Picture(2000,2000,"FFFFE8"); 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 && graph.isGewichtet()) { double my = (startY+startY+endY)/3; double mx = (startX+startX+endX)/3; p.fill(255); p.stroke(0); p.strokeWeight(1); p.rect((int) mx-15, (int) my-7, 30, 16); p.fill(0); p.text(format(k.getGewicht()), (int) mx, (int) my); } } } 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) { p.text(format(k.getDoubleWert()), k.getX(), k.getY()); } } // 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()); } } 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 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 }