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.visualizers.optics;
22 
23 import org.apache.batik.util.SVG12Constants;
24 import org.apache.batik.util.SVGConstants;
25 import org.w3c.dom.Element;
26 import org.w3c.dom.events.Event;
27 import org.w3c.dom.svg.SVGPoint;
28 
29 import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
30 import de.lmu.ifi.dbs.elki.data.Clustering;
31 import de.lmu.ifi.dbs.elki.data.model.Model;
32 import de.lmu.ifi.dbs.elki.utilities.io.FormatUtil;
33 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
34 import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
35 import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
36 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask.RenderFlag;
37 import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
38 import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
39 import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
40 import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSCut;
41 import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
42 import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
43 import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
44 import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
45 import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
46 import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory;
47 import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
48 
49 /**
50  * Visualizes a cut in an OPTICS Plot to select an Epsilon value and generate a
51  * new clustering result.
52  *
53  * @author Heidi Kolb
54  * @author Erich Schubert
55  * @since 0.4.0
56  *
57  * @stereotype factory
58  * @navassoc - create - Instance
59  */
60 public class OPTICSPlotCutVisualization implements VisFactory {
61   /**
62    * A short name characterizing this Visualizer.
63    */
64   private static final String NAME = "OPTICS Cut";
65 
66   /**
67    * Constructor.
68    */
OPTICSPlotCutVisualization()69   public OPTICSPlotCutVisualization() {
70     super();
71   }
72 
73   @Override
processNewResult(VisualizerContext context, Object result)74   public void processNewResult(VisualizerContext context, Object result) {
75     VisualizationTree.findVis(context, result).filter(OPTICSProjector.class).forEach(p -> {
76       context.addVis(p, new VisualizationTask(this, NAME, p.getResult(), null) //
77           .level(VisualizationTask.LEVEL_INTERACTIVE) //
78           .with(RenderFlag.NO_THUMBNAIL).with(RenderFlag.NO_EXPORT));
79     });
80   }
81 
82   @Override
makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)83   public Visualization makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
84     return new Instance(context, task, plot, width, height, proj);
85   }
86 
87   @Override
allowThumbnails(VisualizationTask task)88   public boolean allowThumbnails(VisualizationTask task) {
89     // Don't use thumbnails
90     return false;
91   }
92 
93   /**
94    * Instance.
95    *
96    * @author Heidi Kolb
97    * @author Erich Schubert
98    */
99   public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener {
100     /**
101      * CSS-Styles
102      */
103     protected static final String CSS_LINE = "opticsPlotLine";
104 
105     /**
106      * CSS-Styles
107      */
108     protected static final String CSS_EPSILON = "opticsPlotEpsilonValue";
109 
110     /**
111      * The current epsilon value.
112      */
113     private double epsilon = 0.0;
114 
115     /**
116      * Sensitive (clickable) area
117      */
118     private DragableArea eventarea = null;
119 
120     /**
121      * The label element
122      */
123     private Element elemText = null;
124 
125     /**
126      * The line element
127      */
128     private Element elementLine = null;
129 
130     /**
131      * The drag handle element
132      */
133     private Element elementPoint = null;
134 
135     /**
136      * Constructor.
137      *
138      * @param context Visualizer context
139      * @param task Task
140      * @param plot Plot to draw to
141      * @param width Embedding width
142      * @param height Embedding height
143      * @param proj Projection
144      */
Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)145     public Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
146       super(context, task, plot, width, height, proj);
147     }
148 
149     @Override
fullRedraw()150     public void fullRedraw() {
151       incrementalRedraw();
152     }
153 
154     @Override
incrementalRedraw()155     public void incrementalRedraw() {
156       if(layer == null) {
157         makeLayerElement();
158         addCSSClasses();
159       }
160 
161       // TODO make the number of digits configurable
162       final String label = (epsilon > 0.0) ? FormatUtil.NF4.format(epsilon) : "";
163       // compute absolute y-value of bar
164       final double yAct = getYFromEpsilon(epsilon);
165 
166       if(elemText == null) {
167         elemText = svgp.svgText(StyleLibrary.SCALE * 1.05, yAct, label);
168         SVGUtil.setAtt(elemText, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_EPSILON);
169         layer.appendChild(elemText);
170       }
171       else {
172         elemText.setTextContent(label);
173         SVGUtil.setAtt(elemText, SVGConstants.SVG_Y_ATTRIBUTE, yAct);
174       }
175 
176       // line and handle
177       if(elementLine == null) {
178         elementLine = svgp.svgLine(0, yAct, StyleLibrary.SCALE * 1.04, yAct);
179         SVGUtil.addCSSClass(elementLine, CSS_LINE);
180         layer.appendChild(elementLine);
181       }
182       else {
183         SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y1_ATTRIBUTE, yAct);
184         SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y2_ATTRIBUTE, yAct);
185       }
186       if(elementPoint == null) {
187         elementPoint = svgp.svgCircle(StyleLibrary.SCALE * 1.04, yAct, StyleLibrary.SCALE * 0.004);
188         SVGUtil.addCSSClass(elementPoint, CSS_LINE);
189         layer.appendChild(elementPoint);
190       }
191       else {
192         SVGUtil.setAtt(elementPoint, SVG12Constants.SVG_CY_ATTRIBUTE, yAct);
193       }
194 
195       if(eventarea == null) {
196         eventarea = new DragableArea(svgp, StyleLibrary.SCALE, -StyleLibrary.SCALE * 0.01, //
197             StyleLibrary.SCALE * 0.1, plotheight + StyleLibrary.SCALE * 0.02, this);
198         layer.appendChild(eventarea.getElement());
199       }
200     }
201 
202     @Override
destroy()203     public void destroy() {
204       super.destroy();
205       eventarea.destroy();
206     }
207 
208     /**
209      * Get epsilon from y-value
210      *
211      * @param y y-Value
212      * @return epsilon
213      */
getEpsilonFromY(double y)214     protected double getEpsilonFromY(double y) {
215       OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
216       y = (y < 0) ? 0 : (y > plotheight) ? 1. : y / plotheight;
217       return optics.getOPTICSPlot(context).scaleFromPixel(y * opticsplot.getHeight());
218     }
219 
220     /**
221      * Get y-value from epsilon
222      *
223      * @param epsilon epsilon
224      * @return y-Value
225      */
getYFromEpsilon(double epsilon)226     protected double getYFromEpsilon(double epsilon) {
227       OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
228       int h = opticsplot.getHeight();
229       double y = opticsplot.getScale().getScaled(epsilon, h - .5, .5) / (double) h * plotheight;
230       return (y < 0.) ? 0. : (y > plotheight) ? plotheight : y;
231     }
232 
233     @Override
startDrag(SVGPoint start, Event evt)234     public boolean startDrag(SVGPoint start, Event evt) {
235       epsilon = getEpsilonFromY(plotheight - start.getY());
236       // opvis.unsetEpsilonExcept(this);
237       svgp.requestRedraw(this.task, this);
238       return true;
239     }
240 
241     @Override
duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside)242     public boolean duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) {
243       if(inside) {
244         epsilon = getEpsilonFromY(plotheight - end.getY());
245       }
246       // opvis.unsetEpsilonExcept(this);
247       svgp.requestRedraw(this.task, this);
248       return true;
249     }
250 
251     @Override
endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside)252     public boolean endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) {
253       if(inside) {
254         epsilon = getEpsilonFromY(plotheight - end.getY());
255         // opvis.unsetEpsilonExcept(this);
256 
257         // FIXME: replace an existing optics cut result!
258         final ClusterOrder order = optics.getResult();
259         Clustering<Model> cl = OPTICSCut.makeOPTICSCut(order, epsilon);
260         order.addChildResult(cl);
261       }
262       svgp.requestRedraw(this.task, this);
263       return true;
264     }
265 
266     /**
267      * Reset the epsilon value.
268      */
unsetEpsilon()269     public void unsetEpsilon() {
270       epsilon = 0.0;
271     }
272 
273     /**
274      * Adds the required CSS-Classes
275      */
addCSSClasses()276     private void addCSSClasses() {
277       // Class for the epsilon-value
278       final StyleLibrary style = context.getStyleLibrary();
279       if(!svgp.getCSSClassManager().contains(CSS_EPSILON)) {
280         final CSSClass label = new CSSClass(svgp, CSS_EPSILON);
281         label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL));
282         label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL));
283         label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL));
284         svgp.addCSSClassOrLogError(label);
285       }
286       // Class for the epsilon cut line
287       if(!svgp.getCSSClassManager().contains(CSS_LINE)) {
288         final CSSClass lcls = new CSSClass(svgp, CSS_LINE);
289         lcls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT));
290         lcls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, 0.5 * style.getLineWidth(StyleLibrary.PLOT));
291         svgp.addCSSClassOrLogError(lcls);
292       }
293     }
294   }
295 }
296