1 /* 2 * This file is part of ELKI: 3 * Environment for Developing KDD-Applications Supported by Index-Structures 4 * 5 * Copyright (C) 2018 6 * ELKI Development Team 7 * 8 * This program is free software: you can redistribute it and/or modify 9 * it under the terms of the GNU Affero General Public License as published by 10 * the Free Software Foundation, either version 3 of the License, or 11 * (at your option) any later version. 12 * 13 * This program is distributed in the hope that it will be useful, 14 * but WITHOUT ANY WARRANTY; without even the implied warranty of 15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 * GNU Affero General Public License for more details. 17 * 18 * You should have received a copy of the GNU Affero General Public License 19 * along with this program. If not, see <http://www.gnu.org/licenses/>. 20 */ 21 package de.lmu.ifi.dbs.elki.visualization.parallel3d; 22 23 import java.awt.Font; 24 import java.awt.event.MouseAdapter; 25 import java.awt.event.MouseEvent; 26 import java.awt.event.WindowAdapter; 27 import java.awt.event.WindowEvent; 28 import java.util.ArrayList; 29 import java.util.List; 30 31 import javax.media.opengl.DebugGL2; 32 import javax.media.opengl.GL; 33 import javax.media.opengl.GL2; 34 import javax.media.opengl.GLAutoDrawable; 35 import javax.media.opengl.GLCapabilities; 36 import javax.media.opengl.GLEventListener; 37 import javax.media.opengl.GLProfile; 38 import javax.media.opengl.awt.GLCanvas; 39 import javax.media.opengl.glu.GLU; 40 import javax.swing.JFrame; 41 import javax.swing.SwingUtilities; 42 43 import com.jogamp.opengl.util.awt.TextRenderer; 44 45 import de.lmu.ifi.dbs.elki.data.Clustering; 46 import de.lmu.ifi.dbs.elki.data.NumberVector; 47 import de.lmu.ifi.dbs.elki.data.model.Model; 48 import de.lmu.ifi.dbs.elki.data.type.TypeUtil; 49 import de.lmu.ifi.dbs.elki.data.type.VectorFieldTypeInformation; 50 import de.lmu.ifi.dbs.elki.database.Database; 51 import de.lmu.ifi.dbs.elki.database.relation.Relation; 52 import de.lmu.ifi.dbs.elki.database.relation.RelationUtil; 53 import de.lmu.ifi.dbs.elki.evaluation.AutomaticEvaluation; 54 import de.lmu.ifi.dbs.elki.logging.Logging; 55 import de.lmu.ifi.dbs.elki.math.statistics.dependence.DependenceMeasure; 56 import de.lmu.ifi.dbs.elki.result.Result; 57 import de.lmu.ifi.dbs.elki.result.ResultHandler; 58 import de.lmu.ifi.dbs.elki.result.ResultHierarchy; 59 import de.lmu.ifi.dbs.elki.result.ResultUtil; 60 import de.lmu.ifi.dbs.elki.result.ScalesResult; 61 import de.lmu.ifi.dbs.elki.utilities.Alias; 62 import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil; 63 import de.lmu.ifi.dbs.elki.utilities.ELKIServiceRegistry; 64 import de.lmu.ifi.dbs.elki.utilities.documentation.Reference; 65 import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException; 66 import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer; 67 import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID; 68 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.EmptyParameterization; 69 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.ListParameterization; 70 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization; 71 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter; 72 import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.AbstractLayout3DPC; 73 import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.Layout; 74 import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.Layouter3DPC; 75 import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.SimilarityBasedLayouter3DPC; 76 import de.lmu.ifi.dbs.elki.visualization.parallel3d.layout.SimpleCircularMSTLayout3DPC; 77 import de.lmu.ifi.dbs.elki.visualization.parallel3d.util.Arcball1DOFAdapter; 78 import de.lmu.ifi.dbs.elki.visualization.parallel3d.util.Simple1DOFCamera; 79 import de.lmu.ifi.dbs.elki.visualization.parallel3d.util.Simple1DOFCamera.CameraListener; 80 import de.lmu.ifi.dbs.elki.visualization.parallel3d.util.SimpleMenuOverlay; 81 import de.lmu.ifi.dbs.elki.visualization.parallel3d.util.SimpleMessageOverlay; 82 import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel; 83 import de.lmu.ifi.dbs.elki.visualization.projections.SimpleParallel; 84 import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy; 85 import de.lmu.ifi.dbs.elki.visualization.style.PropertiesBasedStyleLibrary; 86 import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary; 87 import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy; 88 89 /** 90 * Simple JOGL2 based parallel coordinates visualization. 91 * <p> 92 * Reference: 93 * <p> 94 * Elke Achtert, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek:<br> 95 * Interactive Data Mining with 3D-Parallel-Coordinate-Trees.<br> 96 * Proc. 2013 ACM Int. Conf. on Management of Data (SIGMOD 2013) 97 * <p> 98 * TODO: Improve generics of Layout3DPC.<br> 99 * TODO: Generalize to multiple relations and non-numeric feature vectors.<br> 100 * FIXME: proper depth-sorting of edges. It's not that simple, unfortunately. 101 * 102 * @author Erich Schubert 103 * @since 0.6.0 104 * @param <O> Object type 105 */ 106 @Alias({ "3dpc", "3DPC" }) 107 @Reference(authors = "Elke Achtert, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek", // 108 title = "Interactive Data Mining with 3D-Parallel-Coordinate-Trees", // 109 booktitle = "Proc. 2013 ACM Int. Conf. on Management of Data (SIGMOD 2013)", // 110 url = "https://doi.org/10.1145/2463676.2463696", // 111 bibkey = "DBLP:conf/sigmod/AchtertKSZ13") 112 public class OpenGL3DParallelCoordinates<O extends NumberVector> implements ResultHandler { 113 /** 114 * Logging class. 115 */ 116 private static final Logging LOG = Logging.getLogger(OpenGL3DParallelCoordinates.class); 117 118 /** 119 * Settings 120 */ 121 Settings<O> settings = new Settings<>(); 122 123 /** 124 * Constructor. 125 * 126 * @param layout Layout 127 */ OpenGL3DParallelCoordinates(Layouter3DPC<? super O> layout)128 public OpenGL3DParallelCoordinates(Layouter3DPC<? super O> layout) { 129 settings.layout = layout; 130 } 131 132 @Override processNewResult(ResultHierarchy hier, Result newResult)133 public void processNewResult(ResultHierarchy hier, Result newResult) { 134 List<Relation<?>> rels = ResultUtil.getRelations(newResult); 135 for(Relation<?> rel : rels) { 136 if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) { 137 continue; 138 } 139 @SuppressWarnings("unchecked") 140 Relation<? extends O> vrel = (Relation<? extends O>) rel; 141 ScalesResult scales = ScalesResult.getScalesResult(vrel); 142 ProjectionParallel proj = new SimpleParallel(null, scales.getScales()); 143 PropertiesBasedStyleLibrary stylelib = new PropertiesBasedStyleLibrary(); 144 StylingPolicy stylepol = getStylePolicy(hier, stylelib); 145 new Instance<>(vrel, proj, settings, stylepol, stylelib).run(); 146 } 147 } 148 149 /** 150 * Hack: Get/Create the style result. 151 * 152 * @return Style result 153 */ getStylePolicy(ResultHierarchy hier, StyleLibrary stylelib)154 public StylingPolicy getStylePolicy(ResultHierarchy hier, StyleLibrary stylelib) { 155 Database db = ResultUtil.findDatabase(hier); 156 AutomaticEvaluation.ensureClusteringResult(db, db); 157 List<Clustering<? extends Model>> clusterings = Clustering.getClusteringResults(db); 158 if(clusterings.isEmpty()) { 159 throw new AbortException("No clustering result generated?!?"); 160 } 161 return new ClusterStylingPolicy(clusterings.get(0), stylelib); 162 } 163 164 /** 165 * Class keeping the visualizer settings. 166 * 167 * @author Erich Schubert 168 * 169 * @param <O> Object type 170 */ 171 public static class Settings<O> { 172 /** 173 * Similarity measure in use. 174 */ 175 public DependenceMeasure sim; 176 177 /** 178 * Layouting method. 179 */ 180 public Layouter3DPC<? super O> layout; 181 182 /** 183 * Line width. 184 */ 185 public float linewidth = 2f; 186 187 /** 188 * Texture width. 189 */ 190 public int texwidth = 1 << 8; 191 192 /** 193 * Texture height. 194 */ 195 public int texheight = 1 << 10; 196 197 /** 198 * Number of additional mipmaps to generate. 199 */ 200 public int mipmaps = 1; 201 } 202 203 /** 204 * Visualizer instance. 205 * 206 * @author Erich Schubert 207 * 208 * @param <O> Object type 209 */ 210 public static class Instance<O extends NumberVector> implements GLEventListener { 211 /** 212 * Flag to enable debug rendering. 213 */ 214 static final boolean DEBUG = false; 215 216 /** 217 * Frame 218 */ 219 JFrame frame = null; 220 221 /** 222 * GLU utility class. 223 */ 224 GLU glu; 225 226 /** 227 * 3D parallel coordinates renderer. 228 */ 229 private Parallel3DRenderer<O> prenderer; 230 231 /** 232 * The OpenGL canvas 233 */ 234 GLCanvas canvas; 235 236 /** 237 * Arcball controller. 238 */ 239 Arcball1DOFAdapter arcball; 240 241 /** 242 * Menu overlay. 243 */ 244 SimpleMenuOverlay menuOverlay; 245 246 /** 247 * Message overlay. 248 */ 249 SimpleMessageOverlay messageOverlay; 250 251 /** 252 * Handler to open the menu. 253 */ 254 MouseAdapter menuStarter; 255 256 /** 257 * Current state. 258 */ 259 State state = State.PREPARATION; 260 261 /** 262 * States of the UI. 263 * 264 * @author Erich Schubert 265 */ 266 protected enum State { // 267 PREPARATION, // Preparation phase 268 EXPLORE, // Exploration phase (rotate etc.) 269 MENU, // Menu open 270 } 271 272 /** 273 * Shared data for visualization modules. 274 * 275 * @author Erich Schubert 276 * 277 * @param <O> Relation data type 278 */ 279 protected static class Shared<O> { 280 /** 281 * Dimensionality. 282 */ 283 int dim; 284 285 /** 286 * Relation to visualize 287 */ 288 Relation<? extends O> rel; 289 290 /** 291 * Axis labels 292 */ 293 String[] labels; 294 295 /** 296 * Projection 297 */ 298 ProjectionParallel proj; 299 300 /** 301 * Style result 302 */ 303 StylingPolicy stylepol; 304 305 /** 306 * Style library 307 */ 308 StyleLibrary stylelib; 309 310 /** 311 * Layout 312 */ 313 Layout layout; 314 315 /** 316 * Settings 317 */ 318 Settings<O> settings; 319 320 /** 321 * Camera handling class 322 */ 323 Simple1DOFCamera camera; 324 325 /** 326 * Text renderer 327 */ 328 TextRenderer textrenderer; 329 330 /** 331 * Current similarity matrix. 332 */ 333 double[] mat; 334 }; 335 336 Shared<O> shared = new Shared<>(); 337 338 /** 339 * Constructor. 340 * 341 * @param rel Relation 342 * @param proj Projection 343 * @param settings Settings 344 * @param stylepol Styling policy 345 * @param stylelib Style library 346 */ Instance(Relation<? extends O> rel, ProjectionParallel proj, Settings<O> settings, StylingPolicy stylepol, StyleLibrary stylelib)347 public Instance(Relation<? extends O> rel, ProjectionParallel proj, Settings<O> settings, StylingPolicy stylepol, StyleLibrary stylelib) { 348 super(); 349 350 this.shared.dim = RelationUtil.dimensionality(rel); 351 this.shared.rel = rel; 352 this.shared.proj = proj; 353 this.shared.stylelib = stylelib; 354 this.shared.stylepol = stylepol; 355 this.shared.settings = settings; 356 // Labels: 357 this.shared.labels = new String[this.shared.dim]; 358 { 359 VectorFieldTypeInformation<? extends O> vrel = RelationUtil.assumeVectorField(rel); 360 for(int i = 0; i < this.shared.dim; i++) { 361 this.shared.labels[i] = vrel.getLabel(i); 362 } 363 } 364 365 this.prenderer = new Parallel3DRenderer<>(shared); 366 this.menuOverlay = new SimpleMenuOverlay() { 367 @Override 368 public void menuItemClicked(int item) { 369 if(item < 0) { 370 switchState(State.EXPLORE); 371 return; 372 } 373 final String name = menuOverlay.getOptions().get(item); 374 if(name == null) { 375 switchState(State.EXPLORE); 376 return; 377 } 378 LOG.debug("Relayout chosen: " + name); 379 relayout(name); 380 } 381 }; 382 this.menuStarter = new MouseAdapter() { 383 @Override 384 public void mouseClicked(MouseEvent e) { 385 if(State.EXPLORE.equals(state)) { 386 if(e.getButton() == MouseEvent.BUTTON3) { 387 switchState(State.MENU); 388 // e.consume(); 389 } 390 } 391 } 392 }; 393 this.messageOverlay = new SimpleMessageOverlay(); 394 395 // Init menu for SIGMOD demo. TODO: make more flexible. 396 { 397 ArrayList<String> options = menuOverlay.getOptions(); 398 for(Class<?> clz : ELKIServiceRegistry.findAllImplementations(Layouter3DPC.class)) { 399 options.add(clz.getSimpleName()); 400 } 401 if(options.size() > 0) { 402 options.add(null); // Spacer. 403 } 404 for(Class<?> clz : ELKIServiceRegistry.findAllImplementations(DependenceMeasure.class)) { 405 options.add(clz.getSimpleName()); 406 } 407 } 408 409 GLProfile glp = GLProfile.getDefault(); 410 GLCapabilities caps = new GLCapabilities(glp); 411 caps.setDoubleBuffered(true); 412 canvas = new GLCanvas(caps); 413 canvas.addGLEventListener(this); 414 415 frame = new JFrame("ELKI 3D Parallel Coordinate Visualization"); 416 frame.setSize(600, 600); 417 frame.add(canvas); 418 } 419 initLabels()420 void initLabels() { 421 // Labels: 422 shared.labels = new String[shared.dim]; 423 for(int i = 0; i < shared.dim; i++) { 424 shared.labels[i] = RelationUtil.getColumnLabel(shared.rel, i); 425 } 426 } 427 428 @SuppressWarnings("unchecked") relayout(String parname)429 protected void relayout(String parname) { 430 try { 431 final Class<?> layoutc = ELKIServiceRegistry.findImplementation(Layouter3DPC.class, parname); 432 if(layoutc != null) { 433 ListParameterization params = new ListParameterization(); 434 if(shared.settings.sim != null) { 435 params.addParameter(SimilarityBasedLayouter3DPC.SIM_ID, shared.settings.sim); 436 } 437 shared.settings.layout = ClassGenericsUtil.tryInstantiate(Layouter3DPC.class, layoutc, params); 438 switchState(State.PREPARATION); 439 startLayoutThread(); 440 return; 441 } 442 } 443 catch(Exception e) { 444 LOG.exception(e); 445 // Try with Dimension Similarity instead. 446 } 447 try { 448 final Class<?> simc = ELKIServiceRegistry.findImplementation(DependenceMeasure.class, parname); 449 if(simc != null) { 450 shared.settings.sim = ClassGenericsUtil.tryInstantiate(DependenceMeasure.class, simc, new EmptyParameterization()); 451 if(!(shared.settings.layout instanceof SimilarityBasedLayouter3DPC)) { 452 ListParameterization params = new ListParameterization(); 453 params.addParameter(SimilarityBasedLayouter3DPC.SIM_ID, shared.settings.sim); 454 shared.settings.layout = ClassGenericsUtil.tryInstantiate(Layouter3DPC.class, SimpleCircularMSTLayout3DPC.class, params); 455 } 456 // Clear similarity matrix: 457 shared.mat = null; 458 switchState(State.PREPARATION); 459 startLayoutThread(); 460 return; 461 } 462 } 463 catch(Exception e) { 464 LOG.exception(e); 465 } 466 // TODO: improve menu, to allow pretty names and map name -> class. 467 LOG.warning("Menu parameter did not map to a class name - wrong package?"); 468 } 469 startLayoutThread()470 private void startLayoutThread() { 471 new Thread() { 472 @Override 473 public void run() { 474 messageOverlay.setMessage("Computing axis similarities and layout..."); 475 if(shared.settings.sim != null && shared.settings.layout instanceof SimilarityBasedLayouter3DPC) { 476 final SimilarityBasedLayouter3DPC layouter = (SimilarityBasedLayouter3DPC) shared.settings.layout; 477 if(shared.mat == null) { 478 messageOverlay.setMessage("Recomputing similarity matrix."); 479 shared.mat = AbstractLayout3DPC.computeSimilarityMatrix(shared.settings.sim, shared.rel); 480 } 481 messageOverlay.setMessage("Recomputing layout using similarity matrix."); 482 final Layout newlayout = layouter.layout(shared.dim, shared.mat); 483 setLayout(newlayout); 484 } 485 else { 486 messageOverlay.setMessage("Recomputing layout."); 487 final Layout newlayout = shared.settings.layout.layout(shared.rel); 488 setLayout(newlayout); 489 } 490 } 491 }.start(); 492 } 493 run()494 public void run() { 495 assert (frame != null); 496 frame.setVisible(true); 497 frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 498 frame.addWindowListener(new WindowAdapter() { 499 @Override 500 public void windowClosed(WindowEvent e) { 501 stop(); 502 } 503 }); 504 startLayoutThread(); 505 } 506 stop()507 public void stop() { 508 frame = null; 509 } 510 511 @Override init(GLAutoDrawable drawable)512 public void init(GLAutoDrawable drawable) { 513 GL2 gl = drawable.getGL().getGL2(); 514 if(DEBUG) { 515 gl = new DebugGL2(gl); 516 drawable.setGL(gl); 517 } 518 // As we aren't really rendering models, but just drawing, 519 // We do not need to set up a lot. 520 gl.glClearColor(1f, 1f, 1f, 1f); 521 gl.glDisable(GL.GL_DEPTH_TEST); 522 gl.glDisable(GL.GL_CULL_FACE); 523 524 glu = new GLU(); 525 shared.camera = new Simple1DOFCamera(glu); 526 shared.camera.addCameraListener(new CameraListener() { 527 @Override 528 public void cameraChanged() { 529 canvas.display(); 530 } 531 }); 532 533 // Setup arcball: 534 arcball = new Arcball1DOFAdapter(shared.camera); 535 shared.textrenderer = new TextRenderer(new Font(Font.SANS_SERIF, Font.BOLD, 36)); 536 // Ensure listeners. 537 switchState(state); 538 } 539 540 /** 541 * Switch the current state. 542 * 543 * @param newstate State to switch to. 544 */ switchState(State newstate)545 void switchState(State newstate) { 546 // Reset mouse listeners 547 canvas.removeMouseListener(menuStarter); 548 canvas.removeMouseListener(menuOverlay); 549 canvas.removeMouseListener(arcball); 550 canvas.removeMouseMotionListener(arcball); 551 canvas.removeMouseWheelListener(arcball); 552 switch(newstate){ 553 case EXPLORE: { 554 canvas.addMouseListener(menuStarter); 555 canvas.addMouseListener(arcball); 556 canvas.addMouseMotionListener(arcball); 557 canvas.addMouseWheelListener(arcball); 558 break; 559 } 560 case MENU: { 561 canvas.addMouseListener(menuOverlay); 562 break; 563 } 564 case PREPARATION: { 565 // No listeners. 566 break; 567 } 568 } 569 if(state != newstate) { 570 this.state = newstate; 571 canvas.repaint(); 572 } 573 } 574 575 @Override reshape(GLAutoDrawable drawable, int x, int y, int width, int height)576 public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) { 577 shared.camera.setRatio(width / (double) height); 578 messageOverlay.setSize(width, height); 579 menuOverlay.setSize(width, height); 580 } 581 582 @Override display(GLAutoDrawable drawable)583 public void display(GLAutoDrawable drawable) { 584 GL2 gl = drawable.getGL().getGL2(); 585 gl.glClear(GL.GL_COLOR_BUFFER_BIT /* | GL.GL_DEPTH_BUFFER_BIT */); 586 587 if(shared.layout != null) { 588 int res = prenderer.prepare(gl); 589 if(res == 1) { 590 // Request a repaint, to generate the next texture. 591 canvas.repaint(); 592 } 593 if(res == 2) { 594 messageOverlay.setMessage("Texture rendering completed."); 595 switchState(State.EXPLORE); 596 } 597 } 598 599 shared.camera.apply(gl); 600 if(shared.layout != null) { 601 prenderer.drawParallelPlot(drawable, gl); 602 } 603 604 if(DEBUG) { 605 arcball.debugRender(gl); 606 } 607 608 if(State.MENU.equals(state)) { 609 menuOverlay.render(gl); 610 } 611 if(State.PREPARATION.equals(state)) { 612 messageOverlay.render(gl); 613 } 614 } 615 616 /** 617 * Callback from layouting thread. 618 * 619 * @param newlayout New layout. 620 */ setLayout(final Layout newlayout)621 protected void setLayout(final Layout newlayout) { 622 SwingUtilities.invokeLater(new Runnable() { 623 @Override 624 public void run() { 625 shared.layout = newlayout; 626 prenderer.forgetTextures(null); 627 messageOverlay.setMessage("Rendering Textures."); 628 canvas.repaint(); 629 } 630 }); 631 } 632 633 @Override dispose(GLAutoDrawable drawable)634 public void dispose(GLAutoDrawable drawable) { 635 GL gl = drawable.getGL(); 636 prenderer.forgetTextures(gl); 637 } 638 } 639 640 /** 641 * Parameterization class. 642 * 643 * @author Erich Schubert 644 * 645 * @hidden 646 * 647 * @param <O> Object type 648 */ 649 public static class Parameterizer<O extends NumberVector> extends AbstractParameterizer { 650 /** 651 * Option for layouting method 652 */ 653 public static final OptionID LAYOUT_ID = new OptionID("parallel3d.layout", "Layouting method for 3DPC."); 654 655 /** 656 * Similarity measure 657 */ 658 Layouter3DPC<O> layout; 659 660 @Override makeOptions(Parameterization config)661 protected void makeOptions(Parameterization config) { 662 super.makeOptions(config); 663 ObjectParameter<Layouter3DPC<O>> layoutP = new ObjectParameter<>(LAYOUT_ID, Layouter3DPC.class, SimpleCircularMSTLayout3DPC.class); 664 if(config.grab(layoutP)) { 665 layout = layoutP.instantiateClass(config); 666 } 667 } 668 669 @Override makeInstance()670 protected OpenGL3DParallelCoordinates<O> makeInstance() { 671 return new OpenGL3DParallelCoordinates<>(layout); 672 } 673 } 674 } 675