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.parallel;
22 
23 import org.apache.batik.util.SVGConstants;
24 import org.w3c.dom.Element;
25 
26 import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
27 import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
28 import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
29 import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
30 import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
31 import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
32 import de.lmu.ifi.dbs.elki.database.relation.Relation;
33 import de.lmu.ifi.dbs.elki.math.MathUtil;
34 import de.lmu.ifi.dbs.elki.result.SamplingResult;
35 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
36 import de.lmu.ifi.dbs.elki.visualization.VisualizationTask.UpdateFlag;
37 import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
38 import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
39 import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
40 import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
41 import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
42 import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
43 import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
44 import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
45 import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
46 import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary;
47 import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
48 import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
49 import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory;
50 import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
51 
52 /**
53  * Draw spatial objects (except vectors!)
54  *
55  * @author Erich Schubert
56  * @since 0.5.0
57  *
58  * @stereotype factory
59  * @navassoc - create - Instance
60  */
61 // TODO: draw filled instead?
62 public class BoundingBoxVisualization implements VisFactory {
63   /**
64    * A short name characterizing this Visualizer.
65    */
66   public static final String NAME = "Spatial objects";
67 
68   /**
69    * Constructor.
70    */
BoundingBoxVisualization()71   public BoundingBoxVisualization() {
72     super();
73   }
74 
75   @Override
makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)76   public Visualization makeVisualization(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
77     return new Instance(context, task, plot, width, height, proj);
78   }
79 
80   @Override
processNewResult(VisualizerContext context, Object start)81   public void processNewResult(VisualizerContext context, Object start) {
82     VisualizationTree.findVis(context, start).filter(ParallelPlotProjector.class).forEach(p -> {
83       final Relation<?> rel = p.getRelation();
84       if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
85         return;
86       }
87       if(!TypeUtil.SPATIAL_OBJECT.isAssignableFromType(rel.getDataTypeInformation())) {
88         return;
89       }
90       context.addVis(p, new VisualizationTask(this, NAME, p.getRelation(), p.getRelation()) //
91           .level(VisualizationTask.LEVEL_DATA) //
92           .with(UpdateFlag.ON_DATA).with(UpdateFlag.ON_STYLEPOLICY).with(UpdateFlag.ON_SAMPLE));
93     });
94   }
95 
96   /**
97    * Instance for a particular data set.
98    *
99    * @author Robert Rödler
100    */
101   public class Instance extends AbstractParallelVisualization<SpatialComparable> implements DataStoreListener {
102     /**
103      * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
104      * etc.
105      */
106     public static final String DATALINE = "Databox";
107 
108     /**
109      * Sample we visualize.
110      */
111     private SamplingResult sample;
112 
113     /**
114      * Constructor.
115      *
116      * @param context Visualizer context
117      * @param task VisualizationTask
118      * @param plot Plot to draw to
119      * @param width Embedding width
120      * @param height Embedding height
121      * @param proj Projection
122      */
Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj)123     public Instance(VisualizerContext context, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
124       super(context, task, plot, width, height, proj);
125       this.sample = SamplingResult.getSamplingResult(relation);
126       addListeners();
127     }
128 
129     @Override
fullRedraw()130     public void fullRedraw() {
131       super.fullRedraw();
132       final DBIDs sam = sample.getSample();
133       StylingPolicy sp = context.getStylingPolicy();
134       final StyleLibrary style = context.getStyleLibrary();
135       final LineStyleLibrary lines = style.lines();
136       final double width = .5 * style.getLineWidth(StyleLibrary.PLOT) * MathUtil.min(.5, 2. / MathUtil.log2(sam.size()));
137       if(sp instanceof ClassStylingPolicy) {
138         ClassStylingPolicy csp = (ClassStylingPolicy) sp;
139         final int min = csp.getMinStyle();
140         String[] keys = new String[csp.getMaxStyle() - min];
141         for(int c = min; c < csp.getMaxStyle(); c++) {
142           String key = keys[c - min] = DATALINE + "_" + c;
143           if(!svgp.getCSSClassManager().contains(key)) {
144             CSSClass cls = new CSSClass(this, key);
145             cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
146             cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
147             cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
148             lines.formatCSSClass(cls, c, width);
149             svgp.addCSSClassOrLogError(cls);
150           }
151         }
152         for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
153           final int c = csp.getStyleForDBID(iter) + min;
154           if(c < 0) {
155             continue; // No style. Display differently?
156           }
157           Element line = drawLine(iter);
158           if(line == null) {
159             continue;
160           }
161           SVGUtil.addCSSClass(line, keys[c]);
162           layer.appendChild(line);
163         }
164       }
165       else {
166         // No classes available, but individually colored
167         if(!svgp.getCSSClassManager().contains(DATALINE)) {
168           CSSClass cls = new CSSClass(this, DATALINE);
169           cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
170           cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
171           cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
172           lines.formatCSSClass(cls, -1, width);
173           svgp.addCSSClassOrLogError(cls);
174         }
175         StringBuilder buf = new StringBuilder().append(SVGConstants.CSS_STROKE_PROPERTY).append(':');
176         final int prefix = buf.length();
177         for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
178           Element line = drawLine(iter);
179           if(line == null) {
180             continue;
181           }
182           SVGUtil.addCSSClass(line, DATALINE);
183           // assign color
184           buf.delete(prefix, buf.length());
185           buf.append(SVGUtil.colorToString(sp.getColorForDBID(iter)));
186           line.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, buf.toString());
187           layer.appendChild(line);
188         }
189       }
190       svgp.updateStyleElement();
191     }
192 
193     /**
194      * Draw a single line.
195      *
196      * @param iter Object reference
197      * @return Line element
198      */
drawLine(DBIDRef iter)199     private Element drawLine(DBIDRef iter) {
200       SVGPath path = new SVGPath();
201       final SpatialComparable obj = relation.get(iter);
202       final int dims = proj.getVisibleDimensions();
203       boolean drawn = false;
204       int valid = 0; /* run length of valid values */
205       double prevpos = Double.NaN;
206       for(int i = 0; i < dims; i++) {
207         final int d = proj.getDimForAxis(i);
208         double minPos = proj.fastProjectDataToRenderSpace(obj.getMin(d), i);
209         // NaN handling:
210         if(minPos != minPos) {
211           valid = 0;
212           continue;
213         }
214         ++valid;
215         if(valid > 1) {
216           if(valid == 2) {
217             path.moveTo(getVisibleAxisX(d - 1), prevpos);
218           }
219           path.lineTo(getVisibleAxisX(d), minPos);
220           drawn = true;
221         }
222         prevpos = minPos;
223       }
224       valid = 0;
225       for(int i = dims - 1; i >= 0; i--) {
226         final int d = proj.getDimForAxis(i);
227         double maxPos = proj.fastProjectDataToRenderSpace(obj.getMax(d), i);
228         // NaN handling:
229         if(maxPos != maxPos) {
230           valid = 0;
231           continue;
232         }
233         ++valid;
234         if(valid > 1) {
235           if(valid == 2) {
236             path.moveTo(getVisibleAxisX(d + 1), prevpos);
237           }
238           path.lineTo(getVisibleAxisX(d), maxPos);
239           drawn = true;
240         }
241         prevpos = maxPos;
242       }
243       if(!drawn) {
244         return null; // Not enough data.
245       }
246       return path.makeElement(svgp);
247     }
248   }
249 }
250