001package conexp.fx.gui.graph; 002 003/* 004 * #%L 005 * Concept Explorer FX 006 * %% 007 * Copyright (C) 2010 - 2023 Francesco Kriegel 008 * %% 009 * This program is free software: you can redistribute it and/or modify 010 * it under the terms of the GNU General Public License as 011 * published by the Free Software Foundation, either version 3 of the 012 * License, or (at your option) any later version. 013 * 014 * This program is distributed in the hope that it will be useful, 015 * but WITHOUT ANY WARRANTY; without even the implied warranty of 016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 017 * GNU General Public License for more details. 018 * 019 * You should have received a copy of the GNU General Public 020 * License along with this program. If not, see 021 * <http://www.gnu.org/licenses/gpl-3.0.html>. 022 * #L% 023 */ 024 025import java.util.Arrays; 026import java.util.Collection; 027import java.util.HashSet; 028import java.util.LinkedList; 029import java.util.Map; 030import java.util.Queue; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentLinkedQueue; 034import java.util.stream.Stream; 035 036import com.google.common.base.Function; 037import com.google.common.base.Predicate; 038import com.google.common.base.Predicates; 039import com.google.common.collect.Collections2; 040import com.google.common.collect.Maps; 041 042import be.humphreys.simplevoronoi.GraphEdge; 043import conexp.fx.core.collections.Collections3; 044import conexp.fx.core.collections.Pair; 045import conexp.fx.core.layout.LayoutEvolution; 046import conexp.fx.core.math.GuavaIsomorphism; 047import conexp.fx.core.math.VoronoiGenerator; 048import conexp.fx.gui.graph.option.AnimationSpeed; 049import conexp.fx.gui.graph.option.EdgeHighlight; 050import conexp.fx.gui.graph.option.GraphTransformation; 051import conexp.fx.gui.graph.transformation.PolarTransformation; 052import conexp.fx.gui.graph.transformation.RotationXY; 053import conexp.fx.gui.util.LaTeX; 054import conexp.fx.gui.util.Platform2; 055import conexp.fx.gui.util.SynchronizedPane; 056import conexp.fx.gui.util.TransitionTimer; 057import javafx.animation.FadeTransitionBuilder; 058import javafx.animation.KeyFrame; 059import javafx.animation.KeyValue; 060import javafx.animation.Timeline; 061import javafx.animation.Transition; 062import javafx.beans.InvalidationListener; 063import javafx.beans.Observable; 064import javafx.beans.binding.DoubleBinding; 065import javafx.beans.binding.FloatBinding; 066import javafx.beans.binding.IntegerBinding; 067import javafx.beans.binding.ObjectBinding; 068import javafx.beans.property.BooleanProperty; 069import javafx.beans.property.DoubleProperty; 070import javafx.beans.property.IntegerProperty; 071import javafx.beans.property.ObjectProperty; 072import javafx.beans.property.SimpleBooleanProperty; 073import javafx.beans.property.SimpleDoubleProperty; 074import javafx.beans.property.SimpleIntegerProperty; 075import javafx.beans.property.SimpleObjectProperty; 076import javafx.beans.value.ChangeListener; 077import javafx.beans.value.ObservableValue; 078import javafx.collections.FXCollections; 079import javafx.collections.ObservableList; 080import javafx.event.ActionEvent; 081import javafx.event.EventHandler; 082import javafx.geometry.BoundingBox; 083import javafx.geometry.Point2D; 084import javafx.geometry.Point3D; 085import javafx.geometry.Rectangle2D; 086import javafx.scene.Node; 087import javafx.scene.control.LabelBuilder; 088import javafx.scene.image.ImageView; 089import javafx.scene.input.MouseButton; 090import javafx.scene.input.MouseEvent; 091import javafx.scene.input.ScrollEvent; 092import javafx.scene.layout.BorderPane; 093import javafx.scene.layout.StackPane; 094import javafx.scene.layout.StackPaneBuilder; 095import javafx.scene.paint.Color; 096import javafx.scene.shape.Line; 097import javafx.scene.shape.Rectangle; 098import javafx.scene.shape.StrokeType; 099import javafx.scene.text.Text; 100import javafx.util.Duration; 101 102@SuppressWarnings("deprecation") 103public abstract class Graph<T, N extends Node> extends BorderPane { 104 105 public static final Color COLOR_DEFAULT = Color.valueOf("#0576B0"); 106 public static final Color COLOR_LABEL_DEFAULT = Color.valueOf("#EEEEEE"); 107 108 protected abstract class Vertex { 109 110 protected final T element; 111 protected final N node; 112 protected final ObservableValue<Point3D> position; 113 protected final ObservableList<UpperLabel> upperLabels = 114 FXCollections.observableList(new LinkedList<UpperLabel>()); 115 protected final ObservableList<LowerLabel> lowerLabels = 116 FXCollections.observableList(new LinkedList<LowerLabel>()); 117 protected final Set<Pair<Vertex, Edge>> pendingVertices = new HashSet<Pair<Vertex, Edge>>(); 118 119 protected Vertex( 120 final T element, 121 final N node, 122 final ObservableValue<Point3D> position, 123 final Double layoutX, 124 final Double layoutY) { 125 super(); 126 this.element = element; 127 this.node = node; 128 this.position = position; 129 this.node.translateXProperty().set(layoutX == null ? front.getWidth() / 2d : layoutX); 130 this.node.translateYProperty().set(layoutY == null ? front.getHeight() / 2d : layoutY); 131 synchronized (vertices) { 132 vertices.put(element, this); 133 } 134 synchronized (controller.addVertices) { 135 controller.addVertices.offer(this); 136 } 137 } 138 139 protected abstract void init(); 140 141 protected abstract void dispose(); 142 } 143 144 protected abstract class Edge { 145 146 protected Vertex lower; 147 protected Vertex upper; 148 protected final Pair<T, T> elements; 149 protected final Line line = new Line(); 150 protected DoubleBinding opacity; 151 private boolean isInitialized = false; 152 protected boolean isDisposing = false; 153 154 protected Edge(final Pair<T, T> elements) { 155 super(); 156 this.elements = elements; 157 this.line.setStrokeWidth(2); 158 synchronized (edges) { 159 edges.put(elements, this); 160 } 161 synchronized (controller.addEdges) { 162 controller.addEdges.offer(this); 163 } 164 } 165 166 protected void initialize() throws NullPointerException { 167 if (isInitialized) 168 return; 169 synchronized (vertices) { 170 lower = vertices.get(elements.x()); 171 if (lower == null) 172 throw new NullPointerException(); 173 upper = vertices.get(elements.y()); 174 if (upper == null) 175 throw new NullPointerException(); 176 } 177 bindStart(); 178 bindEnd(); 179 opacity = new DoubleBinding() { 180 181 { 182 bind(lower.node.opacityProperty(), upper.node.opacityProperty()); 183 } 184 185 protected final double computeValue() { 186 return (lower.node.opacityProperty().get() + upper.node.opacityProperty().get()) / 2d; 187 } 188 }; 189 isInitialized = true; 190 } 191 192 protected void dispose() { 193 line.strokeWidthProperty().unbind(); 194 line.opacityProperty().unbind(); 195 unbindStart(); 196 unbindEnd(); 197 } 198 199 protected final void bindOpacity() { 200 line.opacityProperty().bind(opacity); 201 } 202 203 protected final void bindStart() { 204 line.startXProperty().bind(lower.node.translateXProperty()); 205 line.startYProperty().bind(lower.node.translateYProperty()); 206 } 207 208 protected final void bindEnd() { 209 line.endXProperty().bind(upper.node.translateXProperty()); 210 line.endYProperty().bind(upper.node.translateYProperty()); 211 } 212 213 protected final void unbindStart() { 214 line.startXProperty().unbind(); 215 line.startYProperty().unbind(); 216 } 217 218 protected final void unbindEnd() { 219 line.endXProperty().unbind(); 220 line.endYProperty().unbind(); 221 } 222 } 223 224 protected abstract class Label { 225 226 protected final ObservableValue<T> element; 227 protected final ObjectBinding<Vertex> vertex; 228 protected final ImageView tex = new ImageView(); 229 protected final Text text = new Text(); 230 protected final boolean showLaTeX; 231 protected final Rectangle back = new Rectangle(); 232 protected final StackPane content = StackPaneBuilder.create().children(back).build(); 233 protected final IntegerProperty index = new SimpleIntegerProperty(0); 234 protected final ObjectProperty<Point2D> shift = new SimpleObjectProperty<Point2D>(new Point2D(0, 0)); 235 protected boolean isInitialized = false; 236 237 protected Label(final ObservableValue<T> element, final ObservableValue<String> string, final boolean showLaTeX) { 238 super(); 239 this.showLaTeX = showLaTeX; 240 if (showLaTeX) { 241 content.getChildren().add(LabelBuilder.create().graphic(tex).build()); 242 } else { 243 content.getChildren().add(text); 244 } 245 this.element = element; 246 this.vertex = new ObjectBinding<Vertex>() { 247 248 { 249 bind(element); 250 } 251 252 protected final Vertex computeValue() { 253 synchronized (vertices) { 254 return vertices.get(element.getValue()); 255 } 256 } 257 }; 258 this.rebind(null, vertex.get()); 259 this.vertex.addListener(new ChangeListener<Vertex>() { 260 261 public final void 262 changed(final ObservableValue<? extends Vertex> observable, final Vertex from, final Vertex to) { 263 rebind(from, to); 264 } 265 }); 266 if (showLaTeX) { 267 this.tex.imageProperty().bind(LaTeX.toFXImageBinding(string, new FloatBinding() { 268 269 { 270 bind(zoom, textSize); 271 } 272 273 @Override 274 protected final float computeValue() { 275 return (float) (zoom.get() * textSize.get()); 276 } 277 })); 278 } else { 279 this.text.textProperty().bind(string); 280 } 281 this.createContent(); 282 synchronized (controller.addLabels) { 283 controller.addLabels.offer(this); 284 } 285 } 286 287 protected abstract void rebind(final Vertex from, final Vertex to); 288 289 private void createContent() { 290 back.setFill(COLOR_LABEL_DEFAULT); 291 back.setStrokeType(StrokeType.OUTSIDE); 292 back.setStroke(Color.BLACK); 293 back.setStrokeWidth(0.25d); 294 back.setOpacity(0.8d); 295 if (showLaTeX) { 296 back.widthProperty().bind(new DoubleBinding() { 297 298 { 299 bind(tex.boundsInLocalProperty()); 300 } 301 302 protected double computeValue() { 303 return tex.boundsInLocalProperty().getValue().getWidth() + 4; 304 } 305 }); 306 back.heightProperty().bind(new DoubleBinding() { 307 308 { 309 bind(tex.boundsInLocalProperty()); 310 } 311 312 protected double computeValue() { 313 return tex.boundsInLocalProperty().getValue().getHeight(); 314 } 315 }); 316 } else { 317 back.widthProperty().bind(new DoubleBinding() { 318 319 { 320 bind(text.boundsInLocalProperty()); 321 } 322 323 protected double computeValue() { 324 return text.boundsInLocalProperty().getValue().getWidth() + 4; 325 } 326 }); 327 back.heightProperty().bind(new DoubleBinding() { 328 329 { 330 bind(text.boundsInLocalProperty()); 331 } 332 333 protected double computeValue() { 334 return text.boundsInLocalProperty().getValue().getHeight(); 335 } 336 }); 337 } 338 } 339 340 protected void dispose() { 341 index.unbind(); 342 synchronized (controller.removeLabels) { 343 controller.removeLabels.add(this); 344 } 345 } 346 } 347 348 protected abstract class LowerLabel extends Label { 349 350 protected LowerLabel(ObservableValue<T> element, ObservableValue<String> string, final boolean showLaTeX) { 351 super(element, string, showLaTeX); 352 } 353 354 protected void rebind(final Vertex from, final Vertex to) { 355 if (from != null) 356 synchronized (from.lowerLabels) { 357 from.lowerLabels.remove(this); 358 } 359 synchronized (to.lowerLabels) { 360 to.lowerLabels.add(this); 361 } 362 index.bind(new IntegerBinding() { 363 364 { 365 bind(to.lowerLabels); 366 } 367 368 protected int computeValue() { 369 synchronized (to.lowerLabels) { 370 return to.lowerLabels.indexOf(LowerLabel.this); 371 } 372 } 373 }); 374 } 375 376 protected void dispose() { 377 super.dispose(); 378 final Vertex v = vertex.get(); 379 synchronized (v.lowerLabels) { 380 v.lowerLabels.remove(this); 381 } 382 } 383 } 384 385 protected abstract class UpperLabel extends Label { 386 387 protected UpperLabel(ObservableValue<T> element, ObservableValue<String> string, final boolean showLaTeX) { 388 super(element, string, showLaTeX); 389 } 390 391 protected void rebind(final Vertex from, final Vertex to) { 392 if (from != null) 393 synchronized (from.upperLabels) { 394 from.upperLabels.remove(this); 395 } 396 synchronized (to.upperLabels) { 397 to.upperLabels.add(this); 398 } 399 index.bind(new IntegerBinding() { 400 401 { 402 bind(to.upperLabels); 403 } 404 405 protected int computeValue() { 406 synchronized (to.upperLabels) { 407 return -to.upperLabels.indexOf(UpperLabel.this) - 1; 408 } 409 } 410 }); 411 } 412 413 protected void dispose() { 414 super.dispose(); 415 final Vertex v = vertex.get(); 416 synchronized (v.upperLabels) { 417 v.upperLabels.remove(this); 418 } 419 } 420 } 421 422 protected class Tile extends Rectangle { 423 424 protected final Rectangle2D rect; 425 426 protected Tile(final LayoutEvolution<?, ?>.Value value, final Double initialX, final Double initialY) { 427 this(value.rectangle); 428 if (value.result > 1d) 429 this.setFill(Color.RED); 430 else 431 this.setFill(Color.rgb((int) (255d * Math.pow(1d - value.result, 0.125d)), 255, 255)); 432 if (initialX != null) 433 this.translateXProperty().set(initialX); 434 if (initialY != null) 435 this.translateYProperty().set(initialY); 436 } 437 438 private Tile(final Rectangle2D rect) { 439 super(); 440 this.rect = rect; 441 synchronized (controller.addTiles) { 442 controller.addTiles.add(this); 443 } 444 } 445 } 446 447 protected class HighlightRequest { 448 449 protected final Set<T> elements; 450 protected final EdgeHighlight edgeHighlightOption; 451 protected final Color vertexColor; 452 protected final Color edgeColor; 453 protected final Color objectLabelBackColor; 454 protected final Color objectLabelTextColor; 455 protected final Color attributeLabelBackColor; 456 protected final Color attributeLabelTextColor; 457 458 protected HighlightRequest( 459 final Set<T> elements, 460 final EdgeHighlight edgeHighlightPolicy, 461 final Color vertexColor, 462 final Color edgeColor, 463 final Color objectLabelBackColor, 464 final Color objectLabelTextColor, 465 final Color attributeLabelBackColor, 466 final Color attributeLabelTextColor) { 467 super(); 468 this.elements = elements; 469 this.edgeHighlightOption = edgeHighlightPolicy; 470 this.vertexColor = vertexColor; 471 this.edgeColor = edgeColor; 472 this.objectLabelBackColor = objectLabelBackColor; 473 this.objectLabelTextColor = objectLabelTextColor; 474 this.attributeLabelBackColor = attributeLabelBackColor; 475 this.attributeLabelTextColor = attributeLabelTextColor; 476 } 477 } 478 479 protected final class Config { 480 481 public final GuavaIsomorphism<Point3D, Point3D> iso; 482 public final BoundingBox box; 483 public final double f, x0, y0, w, h; 484 485 protected Config( 486 final GuavaIsomorphism<Point3D, Point3D> iso, 487 final BoundingBox box, 488 final double f, 489 final double x0, 490 final double y0, 491 final double w, 492 final double h) { 493 super(); 494 this.iso = iso; 495 this.box = box; 496 this.f = f; 497 this.x0 = x0; 498 this.y0 = y0; 499 this.w = w; 500 this.h = h; 501 } 502 503 protected final Point3D toPane(final Point3D p) { 504 final Point3D q = iso.apply(p); 505 final double z = (q.getZ() - box.getMinZ()) / (box.getMaxZ() - box.getMinZ()); 506 return new Point3D(q.getX() * f + x0, q.getY() * f + y0, z); 507 } 508 509 protected final Point3D toContent(final Point3D p) { 510 return iso.invert(new Point3D((p.getX() - x0) / f, (p.getY() - y0) / f, 0)); 511 } 512 513 protected final Point3D toContent(final Point2D p) { 514 return iso.invert(new Point3D((p.getX() - x0) / f, (p.getY() - y0) / f, 0)); 515 } 516 } 517 518// protected final class Cleaner { 519// 520//// private final class CleanTask extends TimerTask { 521//// 522//// public void run() { 523//// if (Platform.isFxApplicationThread()) 524//// clean(); 525//// else 526//// Platform.runLater(new Runnable() { 527//// 528//// public void run() { 529//// clean(); 530//// } 531//// }); 532//// } 533//// } 534//// 535//// private final Timer timer = new Timer(); 536//// private TimerTask task = new TimerTask() { 537//// 538//// public void run() {} 539//// }; 540//// 541//// protected final void schedule() { 542//// task.cancel(); 543//// timer.purge(); 544//// task = new CleanTask(); 545//// timer.schedule(task, (int) (10d * speed.frameSize.toMillis())); 546//// } 547// 548// private Cleaner(final Observable... observable) { 549// Arrays.asList(observable).forEach(o -> o.addListener(__ -> clean())); 550// } 551// } 552 553 public class Controller { 554 555 protected final ObjectBinding<Config> config; 556 private boolean isLocked = false; 557 private boolean isDragging = false; 558 private AnimationSpeed speedBeforeDrag; 559 protected final BooleanProperty showVoronoi = new SimpleBooleanProperty(false); 560// protected final Cleaner cleaner = new Cleaner(); 561 protected final Queue<Vertex> addVertices = new ConcurrentLinkedQueue<Vertex>(); 562 protected final Queue<Edge> addEdges = new ConcurrentLinkedQueue<Edge>(); 563 protected final Queue<Label> addLabels = new ConcurrentLinkedQueue<Label>(); 564 protected final Queue<Tile> addTiles = new ConcurrentLinkedQueue<Tile>(); 565 protected final Queue<Pair<T, T>> removeVertices = new ConcurrentLinkedQueue<Pair<T, T>>(); 566 protected final Queue<Pair<T, T>> removeEdges = new ConcurrentLinkedQueue<Pair<T, T>>(); 567 protected final Queue<Label> removeLabels = new ConcurrentLinkedQueue<Label>(); 568 protected final Set<Vertex> pendingVertices = Collections3.newConcurrentHashSet(); 569 protected final Set<Edge> pendingEdges = Collections3.newConcurrentHashSet(); 570 public Vertex polarBottom = null; 571 572 protected Controller(final Observable... observable) { 573 super(); 574 this.config = new ObjectBinding<Config>() { 575 576 { 577 Arrays.asList(observable).forEach(this::bind); 578 bind(front.widthProperty(), front.heightProperty(), transformation, showVoronoi, zoom, pan); 579 } 580 581 private Config value; 582 583 protected Config computeValue() { 584 if (!isDragging) { 585 final BoundingBox box = getContentBoundingBox(); 586 final double w = front.widthProperty().get(); 587 final double h = front.heightProperty().get(); 588 final GuavaIsomorphism<Point3D, Point3D> iso; 589 final double f, x0, y0; 590 switch (transformation.get()) { 591 case GRAPH_3D: 592 case GRAPH_2D: 593 iso = GuavaIsomorphism.<Point3D> identity(); 594 final double widthRatio = box.getWidth() < 0.001d ? Double.MAX_VALUE : w / box.getWidth(); 595 final double heightRatio = box.getHeight() < 0.001d ? Double.MAX_VALUE : h / box.getHeight(); 596 final double depthRatio = box.getDepth() < 0.001d ? Double.MAX_VALUE : w / box.getDepth(); 597 final double paneToLayoutRatio = Math.min(widthRatio, Math.min(heightRatio, depthRatio)); 598 f = (0.9d * (paneToLayoutRatio == Double.MAX_VALUE ? 1d : paneToLayoutRatio)); 599 x0 = (-box.getMinX() * f + (w - box.getWidth() * f) / 2d); 600 y0 = (-box.getMinY() * f + (h - box.getHeight() * f) / 2d); 601 break; 602 case XY: 603 iso = new RotationXY(new Point3D(0, 0, 0), -Math.PI * 3d / 4d); 604 final Point3D top = iso.apply(new Point3D(box.getMaxX(), box.getMaxY(), 0)); 605 final Point3D left = iso.apply(new Point3D(box.getMaxX(), box.getMinY(), 0)); 606 final Point3D right = iso.apply(new Point3D(box.getMinX(), box.getMaxY(), 0)); 607 final Point3D bot = iso.apply(new Point3D(box.getMinX(), box.getMinY(), 0)); 608 final double width = Math.abs(right.getX() - left.getX()); 609 final double height = Math.abs(top.getY() - bot.getY()); 610 f = 0.9d * Math.min(w / width, h / height); 611 x0 = -left.getX() * f + (w - width * f) / 2d; 612 y0 = -top.getY() * f + (h - height * f) / 2d; 613 break; 614 case POLAR: 615 iso = new PolarTransformation(box.getMinX(), box.getWidth()); 616 f = box.getHeight() < 0.001d ? 1d : 0.9d * (Math.min(w, h) / (2d * box.getHeight())); 617 x0 = w / 2d; 618 y0 = h / 2d; 619 break; 620 default: 621 iso = GuavaIsomorphism.<Point3D> identity(); 622 f = 1d; 623 x0 = 0d; 624 y0 = 0d; 625 break; 626 } 627 value = new Config(iso, box, f * zoom.get(), x0 + pan.get().getX(), y0 + pan.get().getY(), w, h); 628 } else 629 invalidate(); 630 return value; 631 } 632 }; 633 config.addListener(new InvalidationListener() { 634 635 public final void invalidated(final Observable observable) { 636 refresh(); 637 } 638 }); 639 showVoronoi.addListener(new ChangeListener<Boolean>() { 640 641 public final void changed( 642 final ObservableValue<? extends Boolean> observable, 643 final Boolean oldValue, 644 final Boolean newValue) { 645 if (!newValue) 646 Platform2.runOnFXThread(back.getChildren()::clear); 647 } 648 }); 649 refresh(); 650// new Timer().schedule(new TimerTask() { 651// 652// @Override 653// public void run() { 654// refresh(); 655// } 656// }, 0, 2l * (long) speed.frameSize.toMillis()); 657 } 658 659 public boolean graphLock = false; 660 661 public final void drag() { 662 isDragging = true; 663 speedBeforeDrag = speed; 664 speed = AnimationSpeed.FASTESTEST; 665 } 666 667 public final void dragDone() { 668 isDragging = false; 669 speed = speedBeforeDrag; 670// refresh(); 671 } 672 673 public final boolean isDragging() { 674 return isDragging; 675 } 676 677 protected final void disposeVertex(final T element, final T to) { 678// if (element == null || to == null) 679// throw new NullPointerException(); 680 synchronized (removeVertices) { 681 removeVertices.offer(Pair.of(element, to)); 682 } 683 } 684 685 protected final void disposeEdge(final Pair<T, T> elements) { 686// if (elements.first() == null || elements.second() == null) 687// throw new NullPointerException(); 688 synchronized (removeEdges) { 689 removeEdges.offer(elements); 690 } 691 } 692 693 protected final void clearBack() { 694 Platform2.runOnFXThread(back::clear); 695 } 696 697 private final void clean() { 698// final Collection<Node> circles = Collections2.transform(vertices.values(), new Function<Vertex, Node>() { 699// 700// public final Node apply(final Vertex v) { 701// return v.node; 702// } 703// }); 704// final Collection<Node> lines = Collections2.transform(edges.values(), new Function<Edge, Node>() { 705// 706// public final Node apply(final Edge e) { 707// return e.line; 708// } 709// }); 710// final Collection<Node> labels = 711// Collections3.union(Collections2.transform(vertices.values(), new Function<Vertex, Collection<Node>>() { 712// 713// public final Collection<Node> apply(final Vertex v) { 714// return Collections2 715// .transform(Collections3.union(v.lowerLabels, v.upperLabels), new Function<Label, Node>() { 716// 717// public final Node apply(final Label l) { 718// return l.content; 719// } 720// }); 721// } 722// })); 723//// Platform2.runOnFXThread(() -> front.getChildren().retainAll(Collections3.union(circles, lines, labels))); 724// front.getChildren().retainAll(Collections3.union(circles, lines, labels)); 725 final Set<Node> children = Collections3.newConcurrentHashSet(); 726 synchronized (vertices) { 727 vertices.values().parallelStream().map(v -> v.node).forEach(children::add); 728 vertices.values().parallelStream().map(v -> v.lowerLabels.parallelStream()).reduce(Stream::concat).ifPresent( 729 s -> s.map(l -> l.content).forEach(children::add)); 730 vertices.values().parallelStream().map(v -> v.upperLabels.parallelStream()).reduce(Stream::concat).ifPresent( 731 s -> s.map(l -> l.content).forEach(children::add)); 732 } 733 synchronized (edges) { 734 edges.values().parallelStream().map(e -> e.line).forEach(children::add); 735 } 736 front.getChildren().retainAll(children); 737 } 738 739 public synchronized final void refresh() { 740 if (isLocked) 741 return; 742 final Timeline t = new Timeline(); 743// if (!isLocked) { 744 final Config c = config.get(); 745 removeContent(t); 746 addContent(t); 747 refreshBottom(c, t); 748 refreshVertices(c, t); 749 if (showVoronoi.get()) 750 refreshVoronoi(c, t); 751 else 752 refreshTiles(c, t); 753// } else { 754// t.getKeyFrames().add(new KeyFrame(speed.frameSize)); 755// } 756 t.setOnFinished(__ -> { 757 clean(); 758// refresh(); 759 }); 760 Platform2.runOnFXThread(t::play); 761 } 762 763 private final void addContent(final Timeline t) { 764 synchronized (addVertices) { 765 while (!addVertices.isEmpty()) { 766 final Vertex v = addVertices.poll(); 767 final Transition fadeIn = fadeIn(v.node); 768 t.getKeyFrames().add(new KeyFrame(Duration.ONE, __ -> { 769 front.add(v.node); 770 fadeIn.play(); 771 })); 772 } 773 } 774 synchronized (addEdges) { 775 while (!addEdges.isEmpty()) { 776 final Edge e = addEdges.poll(); 777 e.initialize(); 778 t.getKeyFrames().add(new KeyFrame(Duration.ONE, __ -> { 779 e.initialize(); 780 front.add(e.line); 781 e.line.toBack(); 782 fadeIn(e).play(); 783 })); 784 } 785 } 786 synchronized (addLabels) { 787 while (!addLabels.isEmpty()) { 788 final Label l = addLabels.poll(); 789 l.content.opacityProperty().set(0d); 790 final Transition fadeIn = 791 FadeTransitionBuilder.create().node(l.content).duration(speed.frameSize).toValue(0d).build(); 792 final Transition fadeIn2 = fadeIn(l.content); 793 fadeIn.setOnFinished(__ -> { 794 l.content.toFront(); 795 l.content.layoutXProperty().set(l.shift.getValue().getX() - l.content.widthProperty().get() / 2d); 796 l.content.layoutYProperty().set( 797 l.shift.getValue().getY() + l.index.doubleValue() * l.content.heightProperty().get()); 798 l.content.setVisible(true); 799 l.isInitialized = true; 800 fadeIn2.play(); 801 }); 802 t.getKeyFrames().add(new KeyFrame(Duration.ZERO, __ -> { 803 l.content.setVisible(false); 804 front.add(l.content); 805 fadeIn.play(); 806 })); 807 } 808 } 809 } 810 811 private final void removeContent(final Timeline t) { 812 synchronized (removeVertices) { 813 synchronized (removeEdges) { 814 removeEdges.removeAll(removeVertices); 815 for (Pair<T, T> e = removeEdges.poll(); e != null; e = removeEdges.poll()) { 816 final Edge edge; 817 synchronized (edges) { 818 edge = edges.remove(e); 819 } 820 // TODO 821// if (edge != null) { 822 edge.isDisposing = true; 823 edge.line.opacityProperty().unbind(); 824 synchronized (pendingEdges) { 825 pendingEdges.add(edge); 826 } 827// } else { 828// System.err.println(new NullPointerException()); 829// } 830 } 831 } 832 for (Pair<T, T> v = removeVertices.poll(); v != null; v = removeVertices.poll()) { 833 final Vertex vertex; 834 synchronized (vertices) { 835 vertex = vertices.remove(v.first()); 836 } 837 synchronized (vertex.lowerLabels) { 838 for (LowerLabel l : vertex.lowerLabels) 839 l.dispose(); 840 } 841 synchronized (vertex.upperLabels) { 842 for (UpperLabel u : vertex.upperLabels) 843 u.dispose(); 844 } 845 if (v.second() == null) 846 synchronized (pendingVertices) { 847 pendingVertices.add(vertex); 848 } 849 else { 850 final Vertex to; 851 synchronized (vertices) { 852 to = vertices.get(v.second()); 853 } 854 final Edge edge; 855 synchronized (edges) { 856 edge = edges.remove(v); 857 } 858// if (edge != null) { 859 edge.isDisposing = true; 860 edge.line.opacityProperty().unbind(); 861 synchronized (to.pendingVertices) { 862 to.pendingVertices.add(Pair.of(vertex, edge)); 863 } 864// } 865 } 866 } 867 } 868 synchronized (pendingEdges) { 869 for (Edge edge : pendingEdges) 870 t.getKeyFrames().add(new KeyFrame(speed.frameSize, dispose(edge), fadeOut(edge.line))); 871 } 872 synchronized (pendingVertices) { 873 for (Vertex vertex : pendingVertices) 874 t.getKeyFrames().add(new KeyFrame(speed.frameSize, dispose(vertex), fadeOut(vertex.node))); 875 } 876 synchronized (removeLabels) { 877 for (Label label : removeLabels) 878 t.getKeyFrames().add(new KeyFrame(speed.frameSize, dispose(label), fadeOut(label.content))); 879 } 880 } 881 882 private final void refreshBottom(final Config c, final Timeline t) { 883 switch (transformation.get()) { 884 case GRAPH_3D: 885 case GRAPH_2D: 886 case XY: 887 if (polarBottom != null) 888 resetPolarBottom(c, t); 889 break; 890 case POLAR: 891 if (polarBottom == null) 892 initPolarBottom(c, t); 893 if (polarBottom != null) 894 drawPolarBottom(c, t); 895 break; 896 } 897 } 898 899// private final Predicate<Label> isInitialized = new Predicate<Label>() 900// { 901// public final boolean apply(final Label label) 902// { 903// return label.isInitialized; 904// }; 905// }; 906 private final void refreshVertices(final Config c, final Timeline t) { 907 synchronized (vertices) { 908 for (Vertex v : vertices.values()) { 909 if (!v.equals(polarBottom)) { 910 final Point3D p = c.toPane(v.position.getValue()); 911 t.getKeyFrames().add( 912 new KeyFrame(speed.frameSize, translateX(v.node, p), translateY(v.node, p), fadeZ(v.node, p))); 913 synchronized (v.lowerLabels) { 914 for (LowerLabel l : v.lowerLabels) 915 if (l.isInitialized) 916 t.getKeyFrames().add( 917 new KeyFrame( 918 speed.frameSize, 919 translateX(l.content, p), 920 translateY(l.content, p), 921 fadeZ(l.content, p), 922 layoutX(l), 923 layoutY(l))); 924 else 925 t.getKeyFrames().add( 926 new KeyFrame( 927 speed.frameSize, 928 translateX(l.content, p), 929 translateY(l.content, p), 930 fadeZ(l.content, p))); 931 } 932 synchronized (v.upperLabels) { 933 for (UpperLabel u : v.upperLabels) 934 if (u.isInitialized) 935 t.getKeyFrames().add( 936 new KeyFrame( 937 speed.frameSize, 938 translateX(u.content, p), 939 translateY(u.content, p), 940 fadeZ(u.content, p), 941 layoutX(u), 942 layoutY(u))); 943 else 944 t.getKeyFrames().add( 945 new KeyFrame( 946 speed.frameSize, 947 translateX(u.content, p), 948 translateY(u.content, p), 949 fadeZ(u.content, p))); 950 } 951 synchronized (v.pendingVertices) { 952 for (Pair<Vertex, Edge> pv : v.pendingVertices) 953 t.getKeyFrames().add( 954 new KeyFrame( 955 speed.frameSize, 956 dispose(v, pv), 957 translateX(pv.first().node, p), 958 translateY(pv.first().node, p), 959 fadeZ(pv.first().node, p), 960 fadeOut(pv.first().node), 961 fadeOut(pv.second().line))); 962 } 963 } 964 } 965 } 966 } 967 968 private final void refreshTiles(final Config c, final Timeline t) { 969 synchronized (addTiles) { 970 while (!addTiles.isEmpty()) { 971 final Tile tile = addTiles.poll(); 972 back.add(tile); 973 t.getKeyFrames().add( 974 new KeyFrame( 975 speed.frameSize, 976 new KeyValue(tile.translateXProperty(), c.x0 + c.f * tile.rect.getMinX()), 977 new KeyValue(tile.translateYProperty(), c.y0 + c.f * tile.rect.getMinY()), 978 new KeyValue(tile.widthProperty(), c.f * tile.rect.getWidth()), 979 new KeyValue(tile.heightProperty(), c.f * tile.rect.getHeight()))); 980 } 981 } 982 // for (Tile tile : Collections3.getTypedElements(back.getChildren(), Tile.class)) 983 // t.getKeyFrames().add( 984 // new KeyFrame( 985 // speed.frameSize, 986 // new KeyValue(tile.translateXProperty(), c.x0 + c.f * tile.rect.getMinX()), 987 // new KeyValue(tile.translateYProperty(), c.y0 + c.f * tile.rect.getMinY()), 988 // new KeyValue(tile.widthProperty(), c.f * tile.rect.getWidth()), 989 // new KeyValue(tile.heightProperty(), c.f * tile.rect.getHeight()))); 990 } 991 992 private final void refreshVoronoi(final Config c, final Timeline t) { 993 t.getKeyFrames().add(new KeyFrame(Duration.ONE, __ -> new TransitionTimer(speed.frameSize, ___ -> { 994 back.clear(); 995 synchronized (vertices) { 996 for (GraphEdge e : VoronoiGenerator.generate( 997 Collections2.transform( 998 Collections2.filter(vertices.values(), Predicates.not(Predicates.equalTo(polarBottom))), 999 new Function<Vertex, Point3D>() { 1000 1001 public final Point3D apply(final Vertex v) { 1002 return new Point3D(v.node.translateXProperty().get(), v.node.translateYProperty().get(), 0); 1003 } 1004 }), 1005 0, 1006 c.w, 1007 0, 1008 c.h)) { 1009 final Line l = new Line(e.x1, e.y1, e.x2, e.y2); 1010 l.setStroke(Color.RED); 1011 back.add(l); 1012 } 1013 } 1014 }).play())); 1015 } 1016 1017 protected final KeyValue translateX(final Node n, final Point3D p) { 1018 return new KeyValue(n.translateXProperty(), p.getX()); 1019 } 1020 1021 protected final KeyValue translateY(final Node n, final Point3D p) { 1022 return new KeyValue(n.translateYProperty(), p.getY()); 1023 } 1024 1025 protected final KeyValue fadeZ(final Node n, final Point3D p) { 1026 return new KeyValue(n.opacityProperty(), Math.pow(1d - 0.9d * p.getZ(), 2.5d)); 1027 } 1028 1029 private final KeyValue layoutX(final Label label) { 1030 return new KeyValue( 1031 label.content.layoutXProperty(), 1032 label.shift.getValue().getX() - label.content.widthProperty().get() / 2d); 1033 } 1034 1035 private final KeyValue layoutY(final Label label) { 1036 return new KeyValue( 1037 label.content.layoutYProperty(), 1038 label.shift.getValue().getY() + label.index.doubleValue() * label.content.heightProperty().get()); 1039 } 1040 1041 protected final Transition fadeIn(final Node n) { 1042 n.opacityProperty().set(0d); 1043 return FadeTransitionBuilder.create().node(n).duration(speed.frameSize).toValue(1d).build(); 1044 } 1045 1046 protected final Transition fadeIn(final Edge e) { 1047 e.line.opacityProperty().set(0d); 1048 return FadeTransitionBuilder 1049 .create() 1050 .node(e.line) 1051 .duration(speed.frameSize) 1052 .toValue(e.opacity.get()) 1053 .onFinished(__ -> { 1054 if (!e.isDisposing) 1055 e.bindOpacity(); 1056 }) 1057 .build(); 1058 } 1059 1060 protected final KeyValue fadeOut(final Node n) { 1061 return new KeyValue(n.opacityProperty(), 0d); 1062 } 1063 1064 private final EventHandler<ActionEvent> dispose(final Vertex vertex) { 1065 return __ -> { 1066 synchronized (pendingVertices) { 1067 pendingVertices.remove(vertex); 1068 } 1069 vertex.dispose(); 1070 front.remove(vertex.node); 1071 synchronized (vertex.pendingVertices) { 1072 if (!vertex.pendingVertices.isEmpty()) 1073 synchronized (pendingVertices) { 1074 synchronized (pendingEdges) { 1075 for (Pair<Vertex, Edge> p : vertex.pendingVertices) { 1076 pendingVertices.add(p.first()); 1077 pendingEdges.add(p.second()); 1078 } 1079 } 1080 } 1081 } 1082 }; 1083 } 1084 1085 private final EventHandler<ActionEvent> dispose(final Edge edge) { 1086 return __ -> { 1087 synchronized (pendingEdges) { 1088 pendingEdges.remove(edge); 1089 } 1090 edge.dispose(); 1091 front.remove(edge.line); 1092 }; 1093 } 1094 1095 private final EventHandler<ActionEvent> dispose(final Vertex v, final Pair<Vertex, Edge> pv) { 1096 return __ -> { 1097 synchronized (v.pendingVertices) { 1098 v.pendingVertices.remove(pv); 1099 } 1100 pv.second().dispose(); 1101 pv.first().dispose(); 1102 front.remove(pv.second().line); 1103 front.remove(pv.first().node); 1104 synchronized (pv.first().pendingVertices) { 1105 if (!pv.first().pendingVertices.isEmpty()) 1106 synchronized (pendingVertices) { 1107 synchronized (pendingEdges) { 1108 for (Pair<Vertex, Edge> p : pv.first().pendingVertices) { 1109 pendingVertices.add(p.first()); 1110 pendingEdges.add(p.second()); 1111 } 1112 } 1113 } 1114 } 1115 }; 1116 } 1117 1118 private final EventHandler<ActionEvent> dispose(final Label label) { 1119 return __ -> { 1120 synchronized (removeLabels) { 1121 removeLabels.remove(label); 1122 } 1123 front.remove(label.content); 1124 }; 1125 } 1126 } 1127 1128 public final Controller controller; 1129 protected final Map<T, Vertex> vertices = new ConcurrentHashMap<T, Vertex>(); 1130 protected final Map<Pair<T, T>, Edge> edges = new ConcurrentHashMap<Pair<T, T>, Edge>(); 1131 protected final SynchronizedPane front = new SynchronizedPane(); 1132 protected final SynchronizedPane back = new SynchronizedPane(); 1133 protected AnimationSpeed speed = AnimationSpeed.DEFAULT; 1134 protected final ObjectProperty<GraphTransformation> transformation = 1135 new SimpleObjectProperty<GraphTransformation>(GraphTransformation.GRAPH_3D); 1136 protected final DoubleProperty zoom = new SimpleDoubleProperty(1d); 1137 protected final IntegerProperty textSize = new SimpleIntegerProperty(12); 1138 protected final ObjectProperty<Point2D> pan = 1139 new SimpleObjectProperty<Point2D>(new Point2D(0d, 0d)); 1140 1141 protected Graph(final Observable... observable) { 1142 super(); 1143 this.controller = new Controller(observable); 1144// new Cleaner(observable); 1145 this.setCenter(StackPaneBuilder.create().children(back, front).build()); 1146 front.addEventHandler(ScrollEvent.SCROLL, new EventHandler<ScrollEvent>() { 1147 1148 public final void handle(final ScrollEvent event) { 1149 if (event.getDeltaY() > 0) 1150 zoom.set(zoom.get() * 1.1d); 1151 else if (event.getDeltaY() < 0) 1152 zoom.set(zoom.get() / 1.1d); 1153 } 1154 }); 1155 final EventHandler<MouseEvent> panHandler = new EventHandler<MouseEvent>() { 1156 1157 private double startX, startY; 1158 private double panX, panY; 1159 1160 public final void handle(final MouseEvent event) { 1161 if (!event.getButton().equals(MouseButton.SECONDARY)) 1162 return; 1163 if (event.getEventType().equals(MouseEvent.MOUSE_PRESSED)) { 1164 startX = event.getSceneX(); 1165 startY = event.getSceneY(); 1166 panX = pan.get().getX(); 1167 panY = pan.get().getY(); 1168 } else if (event.getEventType().equals(MouseEvent.MOUSE_RELEASED)) { 1169 // do nothing 1170 } else if (event.getEventType().equals(MouseEvent.MOUSE_DRAGGED)) { 1171 double deltaX = event.getSceneX() - startX; 1172 double deltaY = event.getSceneY() - startY; 1173 pan.set(new Point2D(panX + deltaX, panY + deltaY)); 1174 } 1175 } 1176 }; 1177 front.addEventHandler(MouseEvent.MOUSE_PRESSED, panHandler); 1178 front.addEventHandler(MouseEvent.MOUSE_DRAGGED, panHandler); 1179 front.addEventHandler(MouseEvent.MOUSE_RELEASED, panHandler); 1180 } 1181 1182 protected final Collection<Edge> lowerEdges(final T element) { 1183 synchronized (edges) { 1184 return 1185// new HashSet<Edge>( 1186 Maps.filterKeys(edges, new Predicate<Pair<T, T>>() { 1187 1188 public final boolean apply(final Pair<T, T> p) { 1189 return p.second().equals(element); 1190 } 1191 }).values() 1192// ) 1193 ; 1194 } 1195 } 1196 1197 protected final Collection<Edge> upperEdges(final T element) { 1198 synchronized (edges) { 1199 return 1200// new HashSet<Edge>( 1201 Maps.filterKeys(edges, new Predicate<Pair<T, T>>() { 1202 1203 public final boolean apply(final Pair<T, T> p) { 1204 return p.first().equals(element); 1205 } 1206 }).values() 1207// ) 1208 ; 1209 } 1210 } 1211 1212 protected abstract BoundingBox getContentBoundingBox(); 1213 1214 protected abstract void initPolarBottom(final Config c, final Timeline t); 1215 1216 protected abstract void drawPolarBottom(final Config c, final Timeline t); 1217 1218 protected abstract void resetPolarBottom(final Config c, final Timeline t); 1219 1220 public abstract void 1221 highlight(boolean fadeComplement, @SuppressWarnings("unchecked") Iterable<HighlightRequest>... requests); 1222 1223 public void removeContent() { 1224 front.getChildren().clear(); 1225 back.getChildren().clear(); 1226 edges.clear(); 1227 vertices.clear(); 1228// edges.keySet().forEach(controller::disposeEdge); 1229// vertices.keySet().forEach(v -> controller.disposeVertex(v, null)); 1230// edges.clear(); 1231// vertices.clear(); 1232 } 1233 1234}