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.result;
22 
23 import java.awt.Color;
24 import java.awt.Desktop;
25 import java.io.File;
26 import java.io.FileOutputStream;
27 import java.io.IOException;
28 import java.util.ArrayList;
29 import java.util.Collection;
30 import java.util.HashMap;
31 import java.util.Iterator;
32 import java.util.LinkedList;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipOutputStream;
37 
38 import javax.xml.stream.XMLOutputFactory;
39 import javax.xml.stream.XMLStreamException;
40 import javax.xml.stream.XMLStreamWriter;
41 
42 import de.lmu.ifi.dbs.elki.data.Cluster;
43 import de.lmu.ifi.dbs.elki.data.Clustering;
44 import de.lmu.ifi.dbs.elki.data.NumberVector;
45 import de.lmu.ifi.dbs.elki.data.model.Model;
46 import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
47 import de.lmu.ifi.dbs.elki.data.spatial.PolygonsObject;
48 import de.lmu.ifi.dbs.elki.data.spatial.SpatialUtil;
49 import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
50 import de.lmu.ifi.dbs.elki.database.Database;
51 import de.lmu.ifi.dbs.elki.database.DatabaseUtil;
52 import de.lmu.ifi.dbs.elki.database.ids.ArrayModifiableDBIDs;
53 import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
54 import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
55 import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
56 import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
57 import de.lmu.ifi.dbs.elki.database.relation.DoubleRelation;
58 import de.lmu.ifi.dbs.elki.database.relation.Relation;
59 import de.lmu.ifi.dbs.elki.logging.Logging;
60 import de.lmu.ifi.dbs.elki.math.geometry.FilteredConvexHull2D;
61 import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult;
62 import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
63 import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter;
64 import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.It;
65 import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
66 import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
67 import de.lmu.ifi.dbs.elki.utilities.io.FormatUtil;
68 import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
69 import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
70 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
71 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter;
72 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
73 import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter;
74 import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleObjPair;
75 import de.lmu.ifi.dbs.elki.utilities.scaling.outlier.OutlierLinearScaling;
76 import de.lmu.ifi.dbs.elki.utilities.scaling.outlier.OutlierScaling;
77 import de.lmu.ifi.dbs.elki.workflow.OutputStep;
78 import net.jafama.FastMath;
79 
80 /**
81  * Class to handle KML output.
82  * <p>
83  * Reference:
84  * <p>
85  * Erich Achtert, Ahmed Hettab, Hans-Peter Kriegel, Erich Schubert, Arthur
86  * Zimek<br>
87  * Spatial Outlier Detection: Data, Algorithms, Visualizations<br>
88  * Proc. 12th Int. Symp. Spatial and Temporal Databases (SSTD 2011)
89  * <p>
90  * Note: This class - currently - is an ugly hack. This code needs to be
91  * modularized to make it more reusable, to support multiple results and
92  * different result types.
93  *
94  * @author Erich Schubert
95  * @since 0.4.0
96  */
97 // TODO: make configurable color scheme
98 @Reference(authors = "Erich Achtert, Ahmed Hettab, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek", //
99     title = "Spatial Outlier Detection: Data, Algorithms, Visualizations", //
100     booktitle = "Proc. 12th Int. Symp. Spatial and Temporal Databases (SSTD 2011)", //
101     url = "https://doi.org/10.1007/978-3-642-22922-0_41", //
102     bibkey = "DBLP:conf/ssd/AchtertHKSZ11")
103 public class KMLOutputHandler implements ResultHandler {
104   /**
105    * Logger class to use.
106    */
107   private static final Logging LOG = Logging.getLogger(KMLOutputHandler.class);
108 
109   /**
110    * Number of styles to use (lower reduces rendering complexity a bit)
111    */
112   private static final int NUMSTYLES = 20;
113 
114   /**
115    * Output file name
116    */
117   File filename;
118 
119   /**
120    * Scaling function
121    */
122   OutlierScaling scaling;
123 
124   /**
125    * Compatibility mode.
126    */
127   private boolean compat;
128 
129   /**
130    * Automatically open at the end
131    */
132   private boolean autoopen;
133 
134   /**
135    * Constructor.
136    *
137    * @param filename Output filename
138    * @param scaling Scaling function
139    * @param compat Compatibility mode
140    * @param autoopen Automatically open
141    */
KMLOutputHandler(File filename, OutlierScaling scaling, boolean compat, boolean autoopen)142   public KMLOutputHandler(File filename, OutlierScaling scaling, boolean compat, boolean autoopen) {
143     super();
144     this.filename = filename;
145     this.scaling = scaling;
146     this.compat = compat;
147     this.autoopen = autoopen;
148   }
149 
150   @Override
processNewResult(ResultHierarchy hier, Result newResult)151   public void processNewResult(ResultHierarchy hier, Result newResult) {
152     ArrayList<OutlierResult> ors = ResultUtil.filterResults(hier, newResult, OutlierResult.class);
153     ArrayList<Clustering<?>> crs = ResultUtil.filterResults(hier, newResult, Clustering.class);
154     if(ors.size() + crs.size() > 1) {
155       throw new AbortException("More than one visualizable result found. The KML writer only supports a single result!");
156     }
157     Database database = ResultUtil.findDatabase(hier);
158     for(OutlierResult outlierResult : ors) {
159       try {
160         XMLOutputFactory factory = XMLOutputFactory.newInstance();
161         ZipOutputStream out = new ZipOutputStream(new FileOutputStream(filename));
162         out.putNextEntry(new ZipEntry("doc.kml"));
163         final XMLStreamWriter xmlw = factory.createXMLStreamWriter(out);
164         writeOutlierResult(xmlw, outlierResult, database);
165         xmlw.flush();
166         xmlw.close();
167         out.closeEntry();
168         out.flush();
169         out.close();
170         if(autoopen) {
171           Desktop.getDesktop().open(filename);
172         }
173       }
174       catch(XMLStreamException e) {
175         LOG.exception(e);
176         throw new AbortException("XML error in KML output.", e);
177       }
178       catch(IOException e) {
179         LOG.exception(e);
180         throw new AbortException("IO error in KML output.", e);
181       }
182     }
183     for(Clustering<?> clusteringResult : crs) {
184       try {
185         XMLOutputFactory factory = XMLOutputFactory.newInstance();
186         ZipOutputStream out = new ZipOutputStream(new FileOutputStream(filename));
187         out.putNextEntry(new ZipEntry("doc.kml"));
188         final XMLStreamWriter xmlw = factory.createXMLStreamWriter(out);
189         @SuppressWarnings("unchecked")
190         Clustering<Model> cres = (Clustering<Model>) clusteringResult;
191         writeClusteringResult(xmlw, cres, database);
192         xmlw.flush();
193         xmlw.close();
194         out.closeEntry();
195         out.flush();
196         out.close();
197         if(autoopen) {
198           Desktop.getDesktop().open(filename);
199         }
200       }
201       catch(XMLStreamException e) {
202         LOG.exception(e);
203         throw new AbortException("XML error in KML output.", e);
204       }
205       catch(IOException e) {
206         LOG.exception(e);
207         throw new AbortException("IO error in KML output.", e);
208       }
209     }
210   }
211 
writeOutlierResult(XMLStreamWriter xmlw, OutlierResult outlierResult, Database database)212   private void writeOutlierResult(XMLStreamWriter xmlw, OutlierResult outlierResult, Database database) throws XMLStreamException {
213     Relation<PolygonsObject> polys = database.getRelation(TypeUtil.POLYGON_TYPE);
214     Relation<String> labels = DatabaseUtil.guessObjectLabelRepresentation(database);
215 
216     xmlw.writeStartDocument();
217     xmlw.writeCharacters("\n");
218     xmlw.writeStartElement("kml");
219     xmlw.writeDefaultNamespace("http://earth.google.com/kml/2.2");
220     xmlw.writeStartElement("Document");
221     {
222       // TODO: can we automatically generate more helpful data here?
223       xmlw.writeStartElement("name");
224       xmlw.writeCharacters("ELKI KML output for " + outlierResult.getLongName());
225       xmlw.writeEndElement(); // name
226       writeNewlineOnDebug(xmlw);
227       // TODO: e.g. list the settings in the description?
228       xmlw.writeStartElement("description");
229       xmlw.writeCharacters("ELKI KML output for " + outlierResult.getLongName());
230       xmlw.writeEndElement(); // description
231       writeNewlineOnDebug(xmlw);
232     }
233     {
234       // TODO: generate styles from color scheme
235       for(int i = 0; i < NUMSTYLES; i++) {
236         Color col = getColorForValue(i / (NUMSTYLES - 1.0));
237         xmlw.writeStartElement("Style");
238         xmlw.writeAttribute("id", "s" + i);
239         writeNewlineOnDebug(xmlw);
240         {
241           xmlw.writeStartElement("LineStyle");
242           xmlw.writeStartElement("width");
243           xmlw.writeCharacters("0");
244           xmlw.writeEndElement(); // width
245 
246           xmlw.writeEndElement(); // LineStyle
247         }
248         writeNewlineOnDebug(xmlw);
249         {
250           xmlw.writeStartElement("PolyStyle");
251           xmlw.writeStartElement("color");
252           // KML uses AABBGGRR format!
253           xmlw.writeCharacters(String.format("%02x%02x%02x%02x", col.getAlpha(), col.getBlue(), col.getGreen(), col.getRed()));
254           xmlw.writeEndElement(); // color
255           // out.writeStartElement("fill");
256           // out.writeCharacters("1"); // Default 1
257           // out.writeEndElement(); // fill
258           xmlw.writeStartElement("outline");
259           xmlw.writeCharacters("0");
260           xmlw.writeEndElement(); // outline
261           xmlw.writeEndElement(); // PolyStyle
262         }
263         writeNewlineOnDebug(xmlw);
264         xmlw.writeEndElement(); // Style
265         writeNewlineOnDebug(xmlw);
266       }
267     }
268 
269     DoubleRelation scores = outlierResult.getScores();
270     Collection<Relation<?>> otherrel = new LinkedList<>(database.getRelations());
271     otherrel.remove(scores);
272     otherrel.remove(polys);
273     otherrel.remove(labels);
274     otherrel.remove(database.getRelation(TypeUtil.DBID));
275 
276     ArrayModifiableDBIDs ids = DBIDUtil.newArray(scores.getDBIDs());
277 
278     scaling.prepare(outlierResult);
279 
280     for(DBIDIter iter = outlierResult.getOrdering().order(ids).iter(); iter.valid(); iter.advance()) {
281       double score = scores.doubleValue(iter);
282       PolygonsObject poly = polys.get(iter);
283       String label = labels.get(iter);
284       if(Double.isNaN(score)) {
285         LOG.warning("No score for object " + DBIDUtil.toString(iter));
286       }
287       if(poly == null) {
288         LOG.warning("No polygon for object " + DBIDUtil.toString(iter) + " - skipping.");
289         continue;
290       }
291       xmlw.writeStartElement("Placemark");
292       {
293         xmlw.writeStartElement("name");
294         xmlw.writeCharacters(score + " " + label);
295         xmlw.writeEndElement(); // name
296         StringBuilder buf = makeDescription(otherrel, iter);
297         xmlw.writeStartElement("description");
298         xmlw.writeCData("<div>" + buf.toString() + "</div>");
299         xmlw.writeEndElement(); // description
300         xmlw.writeStartElement("styleUrl");
301         int style = (int) (scaling.getScaled(score) * NUMSTYLES);
302         style = Math.max(0, Math.min(style, NUMSTYLES - 1));
303         xmlw.writeCharacters("#s" + style);
304         xmlw.writeEndElement(); // styleUrl
305       }
306       {
307         xmlw.writeStartElement("Polygon");
308         writeNewlineOnDebug(xmlw);
309         if(compat) {
310           xmlw.writeStartElement("altitudeMode");
311           xmlw.writeCharacters("relativeToGround");
312           xmlw.writeEndElement(); // close altitude mode
313           writeNewlineOnDebug(xmlw);
314         }
315         // First polygon clockwise?
316         boolean first = true;
317         for(Polygon p : poly.getPolygons()) {
318           if(first) {
319             xmlw.writeStartElement("outerBoundaryIs");
320           }
321           else {
322             xmlw.writeStartElement("innerBoundaryIs");
323           }
324           xmlw.writeStartElement("LinearRing");
325           xmlw.writeStartElement("coordinates");
326 
327           // Reverse anti-clockwise polygons.
328           boolean reverse = (p.testClockwise() >= 0);
329           ArrayListIter<double[]> it = p.iter();
330           if(reverse) {
331             it.seek(p.size() - 1);
332           }
333           while(it.valid()) {
334             double[] v = it.get();
335             xmlw.writeCharacters(FormatUtil.format(v, ","));
336             if(compat && (v.length == 2)) {
337               xmlw.writeCharacters(",50");
338             }
339             xmlw.writeCharacters(" ");
340             if(!reverse) {
341               it.advance();
342             }
343             else {
344               it.retract();
345             }
346           }
347           xmlw.writeEndElement(); // close coordinates
348           xmlw.writeEndElement(); // close LinearRing
349           xmlw.writeEndElement(); // close *BoundaryIs
350           first = false;
351         }
352         writeNewlineOnDebug(xmlw);
353         xmlw.writeEndElement(); // Polygon
354       }
355       xmlw.writeEndElement(); // Placemark
356       writeNewlineOnDebug(xmlw);
357     }
358     xmlw.writeEndElement(); // Document
359     xmlw.writeEndElement(); // kml
360     xmlw.writeEndDocument();
361   }
362 
writeClusteringResult(XMLStreamWriter xmlw, Clustering<Model> clustering, Database database)363   private void writeClusteringResult(XMLStreamWriter xmlw, Clustering<Model> clustering, Database database) throws XMLStreamException {
364     xmlw.writeStartDocument();
365     xmlw.writeCharacters("\n");
366     xmlw.writeStartElement("kml");
367     xmlw.writeDefaultNamespace("http://earth.google.com/kml/2.2");
368     xmlw.writeStartElement("Document");
369     {
370       // TODO: can we automatically generate more helpful data here?
371       xmlw.writeStartElement("name");
372       xmlw.writeCharacters("ELKI KML output for " + clustering.getLongName());
373       xmlw.writeEndElement(); // name
374       writeNewlineOnDebug(xmlw);
375       // TODO: e.g. list the settings in the description?
376       xmlw.writeStartElement("description");
377       xmlw.writeCharacters("ELKI KML output for " + clustering.getLongName());
378       xmlw.writeEndElement(); // description
379       writeNewlineOnDebug(xmlw);
380     }
381 
382     List<Cluster<Model>> clusters = clustering.getAllClusters();
383     Relation<NumberVector> coords = database.getRelation(TypeUtil.NUMBER_VECTOR_FIELD_2D);
384     List<Cluster<Model>> topc = clustering.getToplevelClusters();
385     Hierarchy<Cluster<Model>> hier = clustering.getClusterHierarchy();
386     Map<Object, DoubleObjPair<Polygon>> hullmap = new HashMap<>();
387     for(Cluster<Model> clu : topc) {
388       buildHullsRecursively(clu, hier, hullmap, coords);
389     }
390 
391     {
392       final double projarea = 360. * 180. * .01;
393       // TODO: generate styles from color scheme
394       Iterator<Cluster<Model>> it = clusters.iterator();
395       for(int i = 0; it.hasNext(); i++) {
396         Cluster<Model> clus = it.next();
397         // This is a prime based magic number, to produce a colorful output
398         Color col = Color.getHSBColor(i / 4.294967291f, 1.f, .5f);
399         DoubleObjPair<Polygon> pair = hullmap.get(clus);
400         // Approximate area (using bounding box)
401         double hullarea = SpatialUtil.volume(pair.second);
402         final double relativeArea = Math.max(1. - (hullarea / projarea), 0.);
403         // final double relativeSize = pair.first / coords.size();
404         final double opacity = .65 * FastMath.sqrt(relativeArea) + .1;
405         xmlw.writeStartElement("Style");
406         xmlw.writeAttribute("id", "s" + i);
407         writeNewlineOnDebug(xmlw);
408         {
409           xmlw.writeStartElement("LineStyle");
410           xmlw.writeStartElement("width");
411           xmlw.writeCharacters("0");
412           xmlw.writeEndElement(); // width
413 
414           xmlw.writeEndElement(); // LineStyle
415         }
416         writeNewlineOnDebug(xmlw);
417         {
418           xmlw.writeStartElement("PolyStyle");
419           xmlw.writeStartElement("color");
420           // KML uses AABBGGRR format!
421           xmlw.writeCharacters(String.format("%02x%02x%02x%02x", (int) (255 * Math.min(.75, opacity)), col.getBlue(), col.getGreen(), col.getRed()));
422           xmlw.writeEndElement(); // color
423           // out.writeStartElement("fill");
424           // out.writeCharacters("1"); // Default 1
425           // out.writeEndElement(); // fill
426           xmlw.writeStartElement("outline");
427           xmlw.writeCharacters("0");
428           xmlw.writeEndElement(); // outline
429           xmlw.writeEndElement(); // PolyStyle
430         }
431         writeNewlineOnDebug(xmlw);
432         xmlw.writeEndElement(); // Style
433         writeNewlineOnDebug(xmlw);
434       }
435     }
436 
437     Cluster<?> ignore = topc.size() == 1 ? topc.get(0) : null;
438     Iterator<Cluster<Model>> it = clusters.iterator();
439     for(int cnum = 0; it.hasNext(); cnum++) {
440       Cluster<?> c = it.next();
441       // Ignore sole toplevel cluster (usually: noise)
442       if(c == ignore) {
443         continue;
444       }
445       Polygon p = hullmap.get(c).second;
446       xmlw.writeStartElement("Placemark");
447       {
448         xmlw.writeStartElement("name");
449         xmlw.writeCharacters(c.getNameAutomatic());
450         xmlw.writeEndElement(); // name
451         xmlw.writeStartElement("description");
452         xmlw.writeCData(makeDescription(c).toString());
453         xmlw.writeEndElement(); // description
454         xmlw.writeStartElement("styleUrl");
455         xmlw.writeCharacters("#s" + cnum);
456         xmlw.writeEndElement(); // styleUrl
457       }
458       {
459         xmlw.writeStartElement("Polygon");
460         writeNewlineOnDebug(xmlw);
461         if(compat) {
462           xmlw.writeStartElement("altitudeMode");
463           xmlw.writeCharacters("relativeToGround");
464           xmlw.writeEndElement(); // close altitude mode
465           writeNewlineOnDebug(xmlw);
466         }
467         {
468           xmlw.writeStartElement("outerBoundaryIs");
469           xmlw.writeStartElement("LinearRing");
470           xmlw.writeStartElement("coordinates");
471 
472           // Reverse anti-clockwise polygons.
473           boolean reverse = (p.testClockwise() >= 0);
474           ArrayListIter<double[]> itp = p.iter();
475           if(reverse) {
476             itp.seek(p.size() - 1);
477           }
478           while(itp.valid()) {
479             double[] v = itp.get();
480             xmlw.writeCharacters(FormatUtil.format(v, ","));
481             if(compat && (v.length == 2)) {
482               xmlw.writeCharacters(",100");
483             }
484             xmlw.writeCharacters(" ");
485             if(!reverse) {
486               itp.advance();
487             }
488             else {
489               itp.retract();
490             }
491           }
492           xmlw.writeEndElement(); // close coordinates
493           xmlw.writeEndElement(); // close LinearRing
494           xmlw.writeEndElement(); // close *BoundaryIs
495         }
496         writeNewlineOnDebug(xmlw);
497         xmlw.writeEndElement(); // Polygon
498       }
499       xmlw.writeEndElement(); // Placemark
500       writeNewlineOnDebug(xmlw);
501     }
502     xmlw.writeEndElement(); // Document
503     xmlw.writeEndElement(); // kml
504     xmlw.writeEndDocument();
505   }
506 
507   /**
508    * Recursively step through the clusters to build the hulls.
509    *
510    * @param clu Current cluster
511    * @param hier Clustering hierarchy
512    * @param hulls Hull map
513    */
buildHullsRecursively(Cluster<Model> clu, Hierarchy<Cluster<Model>> hier, Map<Object, DoubleObjPair<Polygon>> hulls, Relation<? extends NumberVector> coords)514   private DoubleObjPair<Polygon> buildHullsRecursively(Cluster<Model> clu, Hierarchy<Cluster<Model>> hier, Map<Object, DoubleObjPair<Polygon>> hulls, Relation<? extends NumberVector> coords) {
515     final DBIDs ids = clu.getIDs();
516 
517     FilteredConvexHull2D hull = new FilteredConvexHull2D();
518     for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) {
519       hull.add(coords.get(iter).toArray());
520     }
521     double weight = ids.size();
522     if(hier != null && hulls != null) {
523       final int numc = hier.numChildren(clu);
524       if(numc > 0) {
525         for(It<Cluster<Model>> iter = hier.iterChildren(clu); iter.valid(); iter.advance()) {
526           final Cluster<Model> iclu = iter.get();
527           DoubleObjPair<Polygon> poly = hulls.get(iclu);
528           if(poly == null) {
529             poly = buildHullsRecursively(iclu, hier, hulls, coords);
530           }
531           // Add inner convex hull to outer convex hull.
532           for(ArrayListIter<double[]> vi = poly.second.iter(); vi.valid(); vi.advance()) {
533             hull.add(vi.get());
534           }
535           weight += poly.first / numc;
536         }
537       }
538     }
539     DoubleObjPair<Polygon> pair = new DoubleObjPair<>(weight, hull.getHull());
540     hulls.put(clu, pair);
541     return pair;
542   }
543 
544   /**
545    * Make an HTML description.
546    *
547    * @param relations Relations
548    * @param id Object ID
549    * @return Buffer
550    */
makeDescription(Collection<Relation<?>> relations, DBIDRef id)551   private StringBuilder makeDescription(Collection<Relation<?>> relations, DBIDRef id) {
552     StringBuilder buf = new StringBuilder();
553     for(Relation<?> rel : relations) {
554       Object o = rel.get(id);
555       if(o == null) {
556         continue;
557       }
558       String s = o.toString();
559       // FIXME: strip html characters
560       if(s != null) {
561         if(buf.length() > 0) {
562           buf.append("<br />");
563         }
564         buf.append(s);
565       }
566     }
567     return buf;
568   }
569 
570   /**
571    * Make an HTML description.
572    *
573    * @param c Cluster
574    * @return Buffer
575    */
makeDescription(Cluster<?> c)576   private StringBuilder makeDescription(Cluster<?> c) {
577     return new StringBuilder(200).append("<div>")//
578         .append(c.getNameAutomatic())//
579         .append("<br />")//
580         .append("Size: ").append(c.size()) //
581         .append("</div>");
582   }
583 
584   /**
585    * Print a newline when debugging.
586    *
587    * @param out Output XML stream
588    * @throws XMLStreamException
589    */
writeNewlineOnDebug(XMLStreamWriter out)590   private void writeNewlineOnDebug(XMLStreamWriter out) throws XMLStreamException {
591     if(LOG.isDebugging()) {
592       out.writeCharacters("\n");
593     }
594   }
595 
596   /**
597    * Get color from a simple heatmap.
598    *
599    * @param val Score value
600    * @return Color in heatmap
601    */
getColorForValue(double val)602   public static final Color getColorForValue(double val) {
603     // Color positions
604     double[] pos = new double[] { 0.0, 0.6, 0.8, 1.0 };
605     // Colors at these positions
606     Color[] cols = new Color[] { new Color(0.0f, 0.0f, 0.0f, 0.6f), new Color(0.0f, 0.0f, 1.0f, 0.8f), new Color(1.0f, 0.0f, 0.0f, 0.9f), new Color(1.0f, 1.0f, 0.0f, 1.0f) };
607     assert (pos.length == cols.length);
608     if(val < pos[0]) {
609       val = pos[0];
610     }
611     // Linear interpolation:
612     for(int i = 1; i < pos.length; i++) {
613       if(val <= pos[i]) {
614         Color prev = cols[i - 1];
615         Color next = cols[i];
616         final double mix = (val - pos[i - 1]) / (pos[i] - pos[i - 1]);
617         final int r = (int) ((1 - mix) * prev.getRed() + mix * next.getRed());
618         final int g = (int) ((1 - mix) * prev.getGreen() + mix * next.getGreen());
619         final int b = (int) ((1 - mix) * prev.getBlue() + mix * next.getBlue());
620         final int a = (int) ((1 - mix) * prev.getAlpha() + mix * next.getAlpha());
621         Color col = new Color(r, g, b, a);
622         return col;
623       }
624     }
625     return cols[cols.length - 1];
626   }
627 
628   /**
629    * Parameterization class
630    *
631    * @author Erich Schubert
632    */
633   public static class Parameterizer extends AbstractParameterizer {
634     /**
635      * Parameter for scaling functions
636      */
637     public static final OptionID SCALING_ID = new OptionID("kml.scaling", "Additional scaling function for KML colorization.");
638 
639     /**
640      * Parameter for compatibility mode.
641      */
642     public static final OptionID COMPAT_ID = new OptionID("kml.compat", "Use simpler KML objects, compatibility mode.");
643 
644     /**
645      * Parameter for automatically opening the output file.
646      */
647     public static final OptionID AUTOOPEN_ID = new OptionID("kml.autoopen", "Automatically open the result file.");
648 
649     /**
650      * Output file name
651      */
652     File filename;
653 
654     /**
655      * Scaling function
656      */
657     OutlierScaling scaling;
658 
659     /**
660      * Compatibility mode
661      */
662     boolean compat;
663 
664     /**
665      * Automatically open at the end
666      */
667     boolean autoopen = false;
668 
669     @Override
makeOptions(Parameterization config)670     protected void makeOptions(Parameterization config) {
671       super.makeOptions(config);
672       FileParameter outputP = new FileParameter(OutputStep.Parameterizer.OUTPUT_ID, FileParameter.FileType.OUTPUT_FILE);
673       outputP.setShortDescription("Filename the KMZ file (compressed KML) is written to.");
674       if(config.grab(outputP)) {
675         filename = outputP.getValue();
676       }
677 
678       ObjectParameter<OutlierScaling> scalingP = new ObjectParameter<>(SCALING_ID, OutlierScaling.class, OutlierLinearScaling.class);
679       if(config.grab(scalingP)) {
680         scaling = scalingP.instantiateClass(config);
681       }
682 
683       Flag compatF = new Flag(COMPAT_ID);
684       if(config.grab(compatF)) {
685         compat = compatF.getValue();
686       }
687 
688       Flag autoopenF = new Flag(AUTOOPEN_ID);
689       if(config.grab(autoopenF)) {
690         autoopen = autoopenF.getValue();
691       }
692     }
693 
694     @Override
makeInstance()695     protected KMLOutputHandler makeInstance() {
696       return new KMLOutputHandler(filename, scaling, compat, autoopen);
697     }
698   }
699 }
700