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