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