summaryrefslogtreecommitdiff
path: root/addons/batikvis/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'addons/batikvis/src/main')
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java350
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java296
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java38
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java40
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java43
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java50
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java37
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java316
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java395
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java452
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java354
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java62
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java74
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java65
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java110
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java144
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java368
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java171
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java184
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java116
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java89
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java66
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java75
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java62
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java62
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java251
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java72
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java27
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java93
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java73
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java27
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java317
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java216
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java190
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java641
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java376
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java149
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java81
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java422
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java75
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java160
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java661
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java227
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java546
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java28
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java101
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java301
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java29
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java232
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java82
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java110
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java277
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java139
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java174
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java93
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java76
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java84
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java146
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java196
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java114
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java205
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java293
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java116
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java122
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java101
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java54
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java94
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java95
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java45
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java44
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java151
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java180
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java198
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java324
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java74
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java167
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java344
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java34
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java277
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java48
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java159
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java76
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java106
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java90
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java56
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java90
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java235
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java27
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java114
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java165
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java187
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java60
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java147
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java340
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java289
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java842
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java720
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java158
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java263
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java703
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java161
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java46
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java155
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java73
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java214
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java60
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java77
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java61
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java116
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java70
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java437
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java94
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java233
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java298
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java360
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java143
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java211
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java26
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java30
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java746
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java324
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java170
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java300
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java298
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java254
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java226
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java216
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java308
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java193
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java252
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java26
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java180
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java198
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java314
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java337
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java124
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java190
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java167
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java164
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java178
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java157
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java280
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java214
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java403
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java207
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java162
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java182
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java487
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java323
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java254
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java252
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java321
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java333
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java189
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java26
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java33
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java368
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java239
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java181
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java253
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java153
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java298
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java280
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java26
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java191
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java190
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java301
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java162
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java224
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java27
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java216
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java181
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java334
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java122
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java146
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java154
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java176
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java205
-rw-r--r--addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java178
-rwxr-xr-xaddons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java27
-rw-r--r--addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication1
-rw-r--r--addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler2
-rw-r--r--addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor60
-rw-r--r--addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory4
-rw-r--r--addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory56
-rw-r--r--addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry1
-rw-r--r--addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler1
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties79
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties81
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties79
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties83
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties79
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties85
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties85
-rw-r--r--addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties77
215 files changed, 36387 insertions, 0 deletions
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java
new file mode 100644
index 00000000..28435915
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/application/greedyensemble/VisualizePairwiseGainMatrix.java
@@ -0,0 +1,350 @@
+package de.lmu.ifi.dbs.elki.application.greedyensemble;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.BufferedImage;
+
+import org.apache.batik.util.SVGConstants;
+
+import de.lmu.ifi.dbs.elki.application.AbstractApplication;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.ids.ArrayModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.DBID;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.evaluation.scores.ROCEvaluation;
+import de.lmu.ifi.dbs.elki.evaluation.scores.adapter.DecreasingVectorIter;
+import de.lmu.ifi.dbs.elki.evaluation.scores.adapter.VectorNonZero;
+import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage;
+import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage.SimilarityMatrix;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.logging.progress.FiniteProgress;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.utilities.DatabaseUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
+import de.lmu.ifi.dbs.elki.utilities.ensemble.EnsembleVoting;
+import de.lmu.ifi.dbs.elki.utilities.ensemble.EnsembleVotingMean;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter;
+import de.lmu.ifi.dbs.elki.utilities.scaling.LinearScaling;
+import de.lmu.ifi.dbs.elki.utilities.scaling.ScalingFunction;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerParameterizer;
+import de.lmu.ifi.dbs.elki.visualization.gui.SimpleSVGViewer;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer;
+import de.lmu.ifi.dbs.elki.workflow.InputStep;
+
+/**
+ * Class to load an outlier detection summary file, as produced by
+ * {@link ComputeKNNOutlierScores}, and compute a matrix with the pairwise
+ * gains. It will have one column / row obtained for each combination.
+ *
+ * The gain is always computed in relation to the better of the two input
+ * methods. Green colors indicate the result has improved, red indicate it
+ * became worse.
+ *
+ * Reference:
+ * <p>
+ * E. Schubert, R. Wojdanowski, A. Zimek, H.-P. Kriegel<br />
+ * On Evaluation of Outlier Rankings and Outlier Scores<br/>
+ * In Proceedings of the 12th SIAM International Conference on Data Mining
+ * (SDM), Anaheim, CA, 2012.
+ * </p>
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf VisualizerParameterizer
+ * @apiviz.composedOf SimilarityMatrixVisualizer
+ */
+@Reference(authors = "E. Schubert, R. Wojdanowski, A. Zimek, H.-P. Kriegel", //
+title = "On Evaluation of Outlier Rankings and Outlier Scores", //
+booktitle = "Proc. 12th SIAM International Conference on Data Mining (SDM), Anaheim, CA, 2012.", //
+url = "http://dx.doi.org/10.1137/1.9781611972825.90")
+public class VisualizePairwiseGainMatrix extends AbstractApplication {
+ /**
+ * Get static logger.
+ */
+ private static final Logging LOG = Logging.getLogger(VisualizePairwiseGainMatrix.class);
+
+ /**
+ * The data input part.
+ */
+ private InputStep inputstep;
+
+ /**
+ * Parameterizer for visualizers.
+ */
+ private VisualizerParameterizer vispar;
+
+ /**
+ * Outlier scaling to apply during preprocessing.
+ */
+ private ScalingFunction prescaling;
+
+ /**
+ * Ensemble voting function.
+ */
+ private EnsembleVoting voting;
+
+ /**
+ * Constructor.
+ *
+ * @param inputstep Input step
+ * @param prescaling Scaling function for input scores.
+ * @param voting Voting function
+ * @param vispar Visualizer parameterizer
+ */
+ public VisualizePairwiseGainMatrix(InputStep inputstep, ScalingFunction prescaling, EnsembleVoting voting, VisualizerParameterizer vispar) {
+ super();
+ this.inputstep = inputstep;
+ this.prescaling = prescaling;
+ this.voting = voting;
+ this.vispar = vispar;
+ }
+
+ @Override
+ public void run() {
+ final Database database = inputstep.getDatabase();
+ ResultHierarchy hier = database.getHierarchy();
+ Relation<NumberVector> relation = database.getRelation(TypeUtil.NUMBER_VECTOR_FIELD);
+ final Relation<String> labels = DatabaseUtil.guessLabelRepresentation(database);
+ final DBID firstid = DBIDUtil.deref(labels.iterDBIDs());
+ final String firstlabel = labels.get(firstid);
+ if(!firstlabel.matches(".*by.?label.*")) {
+ throw new AbortException("No 'by label' reference outlier found, which is needed for weighting!");
+ }
+ relation = GreedyEnsembleExperiment.applyPrescaling(prescaling, relation, firstid);
+
+ // Dimensionality and reference vector
+ final int dim = RelationUtil.dimensionality(relation);
+ final NumberVector refvec = relation.get(firstid);
+
+ // Build the truth vector
+ VectorNonZero pos = new VectorNonZero(refvec);
+
+ ArrayModifiableDBIDs ids = DBIDUtil.newArray(relation.getDBIDs());
+ ids.remove(firstid);
+ ids.sort();
+ final int size = ids.size();
+
+ double[][] data = new double[size][size];
+ DoubleMinMax minmax = new DoubleMinMax(), commax = new DoubleMinMax();
+
+ {
+ FiniteProgress prog = LOG.isVerbose() ? new FiniteProgress("Computing ensemble gain.", size * (size + 1) >> 1, LOG) : null;
+ double[] buf = new double[2]; // Vote combination buffer.
+ int a = 0;
+ for(DBIDIter id = ids.iter(); id.valid(); id.advance(), a++) {
+ final NumberVector veca = relation.get(id);
+ // Direct AUC score:
+ {
+ double auc = ROCEvaluation.computeROCAUC(pos, new DecreasingVectorIter(veca));
+ data[a][a] = auc;
+ // minmax.put(auc);
+ LOG.incrementProcessed(prog);
+ }
+ // Compare to others, exploiting symmetry
+ DBIDArrayIter id2 = ids.iter();
+ id2.seek(a + 1);
+ for(int b = a + 1; b < size; b++, id2.advance()) {
+ final NumberVector vecb = relation.get(id2);
+ double[] combined = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ buf[0] = veca.doubleValue(d);
+ buf[1] = vecb.doubleValue(d);
+ combined[d] = voting.combine(buf);
+ }
+ double auc = ROCEvaluation.computeROCAUC(pos, new DecreasingVectorIter(new Vector(combined)));
+ // logger.verbose(auc + " " + labels.get(ids.get(a)) + " " +
+ // labels.get(ids.get(b)));
+ data[a][b] = auc;
+ data[b][a] = auc;
+ commax.put(data[a][b]);
+ // minmax.put(auc);
+ LOG.incrementProcessed(prog);
+ }
+ }
+ LOG.ensureCompleted(prog);
+ }
+ for(int a = 0; a < size; a++) {
+ for(int b = a + 1; b < size; b++) {
+ double ref = Math.max(data[a][a], data[b][b]);
+ data[a][b] = (data[a][b] - ref) / (1 - ref);
+ data[b][a] = (data[b][a] - ref) / (1 - ref);
+ // logger.verbose(data[a][b] + " " + labels.get(ids.get(a)) + " " +
+ // labels.get(ids.get(b)));
+ minmax.put(data[a][b]);
+ }
+ }
+ for(int a = 0; a < size; a++) {
+ data[a][a] = 0;
+ }
+
+ LOG.verbose("Gain: " + minmax.toString() + " AUC: " + commax.toString());
+
+ boolean hasneg = (minmax.getMin() < -1E-3);
+ LinearScaling scale;
+ if(!hasneg) {
+ scale = LinearScaling.fromMinMax(0., minmax.getMax());
+ }
+ else {
+ scale = LinearScaling.fromMinMax(0.0, Math.max(minmax.getMax(), -minmax.getMin()));
+ }
+ scale = LinearScaling.fromMinMax(0., .5);
+
+ BufferedImage img = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
+ for(int x = 0; x < size; x++) {
+ for(int y = x; y < size; y++) {
+ double val = data[x][y];
+ val = Math.max(-1, Math.min(1., scale.getScaled(val)));
+ // Compute color:
+ final int col;
+ {
+ if(val >= 0) {
+ int ival = 0xFF & (int) (255 * val);
+ col = 0xff000000 | (ival << 8);
+ }
+ else {
+ int ival = 0xFF & (int) (255 * -val);
+ col = 0xff000000 | (ival << 16);
+ }
+ }
+ img.setRGB(x, y, col);
+ img.setRGB(y, x, col);
+ }
+ }
+ SimilarityMatrix smat = new ComputeSimilarityMatrixImage.SimilarityMatrix(img, relation, ids);
+ hier.add(database, smat);
+
+ VisualizerContext context = vispar.newContext(hier, smat);
+
+ // Attach visualizers to results
+ SimilarityMatrixVisualizer factory = new SimilarityMatrixVisualizer();
+ factory.processNewResult(context, database);
+
+ Hierarchy.Iter<VisualizationTask> it = VisualizationTree.filter(context, VisualizationTask.class);
+ for(; it.valid(); it.advance()) {
+ VisualizationTask task = it.get();
+ if(task.getFactory() == factory) {
+ showVisualization(context, factory, task);
+ }
+ }
+ }
+
+ /**
+ * Show a single visualization.
+ *
+ * @param context Visualization context
+ * @param factory Visualizer factory
+ * @param task Visualization task
+ */
+ private void showVisualization(VisualizerContext context, SimilarityMatrixVisualizer factory, VisualizationTask task) {
+ VisualizationPlot plot = new VisualizationPlot();
+ Visualization vis = factory.makeVisualization(task, plot, 1.0, 1.0, null);
+ plot.getRoot().appendChild(vis.getLayer());
+ plot.getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm");
+ plot.getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "20cm");
+ plot.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 1 1");
+ plot.updateStyleElement();
+
+ (new SimpleSVGViewer()).setPlot(plot);
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractApplication.Parameterizer {
+ /**
+ * Data source.
+ */
+ private InputStep inputstep;
+
+ /**
+ * Parameterizer for visualizers.
+ */
+ private VisualizerParameterizer vispar;
+
+ /**
+ * Outlier scaling to apply during preprocessing.
+ */
+ private ScalingFunction prescaling;
+
+ /**
+ * Voring function.
+ */
+ private EnsembleVoting voting;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ // Data input
+ inputstep = config.tryInstantiate(InputStep.class);
+ // Visualization options
+ vispar = config.tryInstantiate(VisualizerParameterizer.class);
+
+ // Prescaling
+ ObjectParameter<ScalingFunction> prescalingP = new ObjectParameter<>(GreedyEnsembleExperiment.Parameterizer.PRESCALING_ID, ScalingFunction.class);
+ prescalingP.setOptional(true);
+ if(config.grab(prescalingP)) {
+ prescaling = prescalingP.instantiateClass(config);
+ }
+
+ ObjectParameter<EnsembleVoting> votingP = new ObjectParameter<>(GreedyEnsembleExperiment.Parameterizer.VOTING_ID, EnsembleVoting.class, EnsembleVotingMean.class);
+ if(config.grab(votingP)) {
+ voting = votingP.instantiateClass(config);
+ }
+ }
+
+ @Override
+ protected VisualizePairwiseGainMatrix makeInstance() {
+ return new VisualizePairwiseGainMatrix(inputstep, prescaling, voting, vispar);
+ }
+ }
+
+ /**
+ * Main method.
+ *
+ * @param args Command line parameters.
+ */
+ public static void main(String[] args) {
+ runCLIApplication(VisualizePairwiseGainMatrix.class, args);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java
new file mode 100644
index 00000000..bf552880
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/ExportVisualizations.java
@@ -0,0 +1,296 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.batik.util.SVGConstants;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHandler;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.FileParameter.FileType;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Class that automatically generates all visualizations and exports them into
+ * SVG files. To configure the export, you <em>will</em> want to configure the
+ * {@link VisualizerParameterizer}, in particular the pattern for choosing which
+ * visualizers to run.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf VisualizerParameterizer
+ */
+public class ExportVisualizations implements ResultHandler {
+ /**
+ * Get a logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(ExportVisualizations.class);
+
+ /**
+ * Output folder
+ */
+ File output;
+
+ /**
+ * Visualization manager.
+ */
+ VisualizerParameterizer manager;
+
+ /**
+ * Ratio for canvas
+ */
+ double ratio;
+
+ /**
+ * Base result
+ */
+ Result baseResult = null;
+
+ /**
+ * Visualizer context
+ */
+ VisualizerContext context = null;
+
+ /**
+ * Output counter.
+ */
+ Map<String, Integer> counter = new HashMap<>();
+
+ /**
+ * Constructor.
+ *
+ * @param output Output folder
+ * @param manager Parameterizer
+ * @param ratio Canvas ratio
+ */
+ public ExportVisualizations(File output, VisualizerParameterizer manager, double ratio) {
+ super();
+ this.output = output;
+ this.manager = manager;
+ this.ratio = ratio;
+ }
+
+ @Override
+ public void processNewResult(ResultHierarchy hier, Result newResult) {
+ if(output.isFile()) {
+ throw new AbortException("Output folder cannot be an existing file.");
+ }
+ if(!output.exists()) {
+ if(!output.mkdirs()) {
+ throw new AbortException("Could not create output directory.");
+ }
+ }
+ if(this.baseResult == null) {
+ this.baseResult = newResult;
+ context = null;
+ counter = new HashMap<>();
+ LOG.warning("Note: Reusing visualization exporter for more than one result is untested.");
+ }
+ if(context == null) {
+ context = manager.newContext(hier, baseResult);
+ }
+
+ // Projected visualizations
+ Hierarchy<Object> vistree = context.getVisHierarchy();
+ for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) {
+ if(!(iter2.get() instanceof Projector)) {
+ continue;
+ }
+ Projector proj = (Projector) iter2.get();
+ // TODO: allow selecting individual projections only.
+ Collection<PlotItem> items = proj.arrange(context);
+ for(PlotItem item : items) {
+ processItem(item);
+ }
+ }
+ for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) {
+ if(!(iter2.get() instanceof VisualizationTask)) {
+ continue;
+ }
+ VisualizationTask task = (VisualizationTask) iter2.get();
+ boolean isprojected = false;
+ for(Hierarchy.Iter<?> iter = vistree.iterParents(task); iter.valid(); iter.advance()) {
+ if(iter.get() instanceof Projector) {
+ isprojected = true;
+ break;
+ }
+ }
+ if(isprojected) {
+ continue;
+ }
+ PlotItem pi = new PlotItem(ratio, 1.0, null);
+ pi.add(task);
+ processItem(pi);
+ }
+ }
+
+ private void processItem(PlotItem item) {
+ // Descend into subitems
+ for(Iterator<PlotItem> iter = item.subitems.iterator(); iter.hasNext();) {
+ processItem(iter.next());
+ }
+ if(item.taskSize() <= 0) {
+ return;
+ }
+ item.sort();
+ final double width = item.w, height = item.h;
+
+ VisualizationPlot svgp = new VisualizationPlot();
+ svgp.getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm");
+ svgp.getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 * height / width) + "cm");
+ svgp.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + width + " " + height);
+
+ ArrayList<Visualization> layers = new ArrayList<>();
+ for(Iterator<VisualizationTask> iter = item.tasks.iterator(); iter.hasNext();) {
+ VisualizationTask task = iter.next();
+ if(task.hasAnyFlags(VisualizationTask.FLAG_NO_DETAIL | VisualizationTask.FLAG_NO_EXPORT) || !task.visible) {
+ continue;
+ }
+ try {
+ Visualization v = task.getFactory().makeVisualization(task, svgp, width, height, item.proj);
+ layers.add(v);
+ }
+ catch(Exception e) {
+ if(Logging.getLogger(task.getFactory().getClass()).isDebugging()) {
+ LOG.exception("Visualization failed.", e);
+ }
+ else {
+ LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed - enable debugging to see details.");
+ }
+ }
+ }
+ if(layers.size() <= 0) {
+ return;
+ }
+ for(Visualization layer : layers) {
+ if(layer.getLayer() == null) {
+ LOG.warning("NULL layer seen.");
+ continue;
+ }
+ svgp.getRoot().appendChild(layer.getLayer());
+ }
+ svgp.updateStyleElement();
+
+ String prefix = null;
+ prefix = (prefix == null && item.proj != null) ? item.proj.getMenuName() : prefix;
+ prefix = (prefix == null && item.tasks.size() > 0) ? item.tasks.get(0).name : prefix;
+ prefix = (prefix != null ? prefix : "plot");
+ // TODO: generate names...
+ Integer count = counter.get(prefix);
+ counter.put(prefix, count = count == null ? 1 : (count + 1));
+ File outname = new File(output, prefix + "-" + count + ".svg");
+ try {
+ svgp.saveAsSVG(outname);
+ }
+ catch(Exception e) {
+ LOG.warning("Export of visualization failed.", e);
+ }
+ for(Visualization layer : layers) {
+ layer.destroy();
+ }
+ }
+
+ /**
+ * Parameterization class
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Parameter to specify the canvas ratio
+ * <p>
+ * Key: {@code -vis.ratio}
+ * </p>
+ * <p>
+ * Default value: 1.33
+ * </p>
+ */
+ public static final OptionID RATIO_ID = new OptionID("vis.ratio", "The width/heigh ratio of the output.");
+
+ /**
+ * Parameter to specify the output folder
+ * <p>
+ * Key: {@code -vis.output}
+ * </p>
+ */
+ public static final OptionID FOLDER_ID = new OptionID("vis.output", "The output folder.");
+
+ /**
+ * Visualization manager.
+ */
+ VisualizerParameterizer manager;
+
+ /**
+ * Output folder
+ */
+ File output;
+
+ /**
+ * Ratio for canvas
+ */
+ double ratio;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ FileParameter outputP = new FileParameter(FOLDER_ID, FileType.OUTPUT_FILE);
+ if(config.grab(outputP)) {
+ output = outputP.getValue();
+ }
+
+ DoubleParameter ratioP = new DoubleParameter(RATIO_ID, 1.33);
+ ratioP.addConstraint(CommonConstraints.GREATER_THAN_ZERO_DOUBLE);
+ if(config.grab(ratioP)) {
+ ratio = ratioP.doubleValue();
+ }
+
+ manager = config.tryInstantiate(VisualizerParameterizer.class);
+ }
+
+ @Override
+ protected ExportVisualizations makeInstance() {
+ return new ExportVisualizations(output, manager, ratio);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java
new file mode 100644
index 00000000..3f6c6f19
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationItem.java
@@ -0,0 +1,38 @@
+package de.lmu.ifi.dbs.elki.visualization;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Currently an empty interface for visualization items, that serves the purpose
+ * of improving type safety.
+ *
+ * @author Erich Schubert
+ */
+public interface VisualizationItem {
+ /**
+ * Name to display in the menu. May be {@code null} or empty string.
+ *
+ * @return Menu name.
+ */
+ String getMenuName();
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java
new file mode 100644
index 00000000..4a8b3f03
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationListener.java
@@ -0,0 +1,40 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.EventListener;
+
+/**
+ * Listener for visualization events.
+ *
+ * @author Erich Schubert
+ */
+public interface VisualizationListener extends EventListener {
+ /**
+ * Visualization has changed.
+ *
+ * @param item Changed visualization
+ */
+ void visualizationChanged(VisualizationItem item);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java
new file mode 100644
index 00000000..f16a208e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuAction.java
@@ -0,0 +1,43 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Visualizer actions.
+ *
+ * @author Erich Schubert
+ */
+public interface VisualizationMenuAction extends VisualizationItem {
+ /**
+ * Menu item was activated.
+ */
+ void activate();
+
+ /**
+ * Indicate if the menu option is enabled or greyed out.
+ *
+ * @return {@code true} when enabled.
+ */
+ boolean enabled();
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java
new file mode 100644
index 00000000..ad0ec9b3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationMenuToggle.java
@@ -0,0 +1,50 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Toggle action.
+ *
+ * @author Erich Schubert
+ */
+public interface VisualizationMenuToggle extends VisualizationItem {
+ /**
+ * Menu item was activated.
+ */
+ void toggle();
+
+ /**
+ * Return the current state.
+ *
+ * @return {@code true} when selected.
+ */
+ boolean active();
+
+ /**
+ * Indicate if the menu option is enabled or greyed out.
+ *
+ * @return {@code true} when enabled.
+ */
+ boolean enabled();
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java
new file mode 100644
index 00000000..70271c1a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationProcessor.java
@@ -0,0 +1,37 @@
+package de.lmu.ifi.dbs.elki.visualization;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Visualization processor
+ *
+ * @author Erich Schubert
+ */
+public interface VisualizationProcessor {
+ /**
+ * Add visualization items for the given result (tree) to the result tree.
+ * @param context Visualization context
+ * @param start Result to process
+ */
+ public void processNewResult(VisualizerContext context, Object start);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java
new file mode 100644
index 00000000..7be18864
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTask.java
@@ -0,0 +1,316 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory;
+
+/**
+ * Container class, with ugly casts to reduce generics crazyness.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.has VisualizerContext
+ * @apiviz.has VisFactory
+ */
+public class VisualizationTask implements VisualizationItem, Comparable<VisualizationTask> {
+ /**
+ * Meta data key: Level for visualizer ordering
+ *
+ * Returns an integer indicating the "height" of this Visualizer. It is
+ * intended to impose an ordering on the execution of Visualizers as a
+ * Visualizer may depend on another Visualizer running earlier. <br>
+ * Lower numbers should result in a earlier use of this Visualizer, while
+ * higher numbers should result in a later use. If more Visualizers have the
+ * same level, no ordering is guaranteed. <br>
+ * Note that this value is only a recommendation, as it is totally up to the
+ * framework to ignore it.
+ */
+ public int level = 0;
+
+ /**
+ * Flag to control visibility.
+ */
+ public boolean visible = true;
+
+ /**
+ * Capabilities
+ */
+ private int flags;
+
+ /**
+ * Flag to signal there is no thumbnail needed.
+ */
+ public static final int FLAG_NO_THUMBNAIL = 1;
+
+ /**
+ * Mark as not having a (sensible) detail view.
+ */
+ public static final int FLAG_NO_DETAIL = 2;
+
+ /**
+ * Flag to signal the visualizer should not be exported.
+ */
+ public static final int FLAG_NO_EXPORT = 4;
+
+ /**
+ * Flag to signal the visualizer should not be embedded.
+ */
+ public static final int FLAG_NO_EMBED = 8;
+
+ /**
+ * Flag to signal default visibility of a visualizer.
+ */
+ public boolean default_visibility = true;
+
+ /**
+ * Flag to mark the visualizer as tool.
+ */
+ public boolean tool = false;
+
+ /**
+ * Background layer
+ */
+ public static final int LEVEL_BACKGROUND = 0;
+
+ /**
+ * Data layer
+ */
+ public static final int LEVEL_DATA = 100;
+
+ /**
+ * Static plot layer
+ */
+ public static final int LEVEL_STATIC = 200;
+
+ /**
+ * Passive foreground layer
+ */
+ public static final int LEVEL_FOREGROUND = 300;
+
+ /**
+ * Active foreground layer (interactive elements)
+ */
+ public static final int LEVEL_INTERACTIVE = 1000;
+
+ /**
+ * The update event mask. See {@link #ON_DATA}, {@link #ON_SELECTION},
+ * {@link #ON_STYLEPOLICY}, {@link #ON_SAMPLE}.
+ */
+ public int updatemask;
+
+ /**
+ * Constant to listen for data changes
+ */
+ public static final int ON_DATA = 1;
+
+ /**
+ * Constant to listen for selection changes
+ */
+ public static final int ON_SELECTION = 2;
+
+ /**
+ * Constant to listen for style result changes
+ */
+ public static final int ON_STYLEPOLICY = 4;
+
+ /**
+ * Constant to listen for sampling result changes
+ */
+ public static final int ON_SAMPLE = 8;
+
+ /**
+ * Name
+ */
+ String name;
+
+ /**
+ * The active context
+ */
+ VisualizerContext context;
+
+ /**
+ * The factory
+ */
+ VisFactory factory;
+
+ /**
+ * The result we are attached to
+ */
+ Object result;
+
+ /**
+ * The main representation
+ */
+ Relation<?> relation;
+
+ /**
+ * Width request
+ */
+ public double reqwidth;
+
+ /**
+ * Height request
+ */
+ public double reqheight;
+
+ /**
+ * Visualization task.
+ *
+ * @param name Name
+ * @param context Visualization context
+ * @param result Result
+ * @param relation Relation to use
+ * @param factory Factory
+ */
+ public VisualizationTask(String name, VisualizerContext context, Object result, Relation<?> relation, VisFactory factory) {
+ super();
+ this.name = name;
+ this.context = context;
+ this.result = result;
+ this.relation = relation;
+ this.factory = factory;
+ }
+
+ /**
+ * Get the visualizer context.
+ *
+ * @return context
+ */
+ public VisualizerContext getContext() {
+ return context;
+ }
+
+ /**
+ * Get the visualizer factory.
+ *
+ * @return Visualizer factory
+ */
+ public VisFactory getFactory() {
+ return factory;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <R> R getResult() {
+ return (R) result;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <R extends Relation<?>> R getRelation() {
+ return (R) relation;
+ }
+
+ /**
+ * Init the default visibility of a task.
+ *
+ * @param vis Visibility.
+ */
+ public void initDefaultVisibility(boolean vis) {
+ visible = vis;
+ default_visibility = vis;
+ }
+
+ @Override
+ public String getMenuName() {
+ return name;
+ }
+
+ @Override
+ public int compareTo(VisualizationTask other) {
+ // sort by levels first
+ if(this.level != other.level) {
+ return this.level - other.level;
+ }
+ // sort by name otherwise.
+ String name1 = this.getMenuName();
+ String name2 = other.getMenuName();
+ if(name1 != null && name2 != null && name1 != name2) {
+ return name1.compareTo(name2);
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ buf.append("VisTask: ").append(factory.getClass().getName()).append(' ');
+ if(result != null && result instanceof Result) {
+ buf.append("Result: ").append(((Result) result).getLongName()).append(' ');
+ }
+ buf.append(super.toString());
+ return buf.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ // We can't have our hashcode change with the map contents!
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ // Also don't inherit equals based on list contents!
+ return (this == o);
+ }
+
+ /**
+ * Set (OR) the update flags.
+ *
+ * @param bits Bits to set
+ */
+ public void addUpdateFlags(int bits) {
+ updatemask |= bits;
+ }
+
+ /**
+ * Update if any oft these bits is set.
+ *
+ * @param bits Bits to check.
+ * @return {@code true} if any bit is set.
+ */
+ public boolean updateOnAny(int bits) {
+ return (updatemask & bits) != 0;
+ }
+
+ /**
+ * Update if any oft these flags is set.
+ *
+ * @param bits Bits to check.
+ * @return {@code true} if any bit is set.
+ */
+ public boolean hasAnyFlags(int bits) {
+ return (flags & bits) != 0;
+ }
+
+ /**
+ * Set a task flag.
+ *
+ * @param bits Flag to set
+ */
+ public void addFlags(int bits) {
+ flags |= bits;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java
new file mode 100644
index 00000000..8e76716c
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizationTree.java
@@ -0,0 +1,395 @@
+package de.lmu.ifi.dbs.elki.visualization;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.HashMapHierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.StackedIter;
+
+/**
+ * Tree - actually a forest - to manage visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has Handler1
+ * @apiviz.has Handler2
+ * @apiviz.has Handler3
+ */
+public class VisualizationTree extends HashMapHierarchy<Object> {
+ /**
+ * The event listeners for this context.
+ */
+ private ArrayList<VisualizationListener> vlistenerList = new ArrayList<>();
+
+ /**
+ * Constructor.
+ */
+ public VisualizationTree() {
+ super();
+ }
+
+ /**
+ * Add a listener.
+ *
+ * @param listener Listener to add
+ */
+ public void addVisualizationListener(VisualizationListener listener) {
+ for(int i = 0; i < vlistenerList.size(); i++) {
+ if(vlistenerList.get(i) == listener) {
+ return;
+ }
+ }
+ vlistenerList.add(listener);
+ }
+
+ /**
+ * Add a listener.
+ *
+ * @param listener Listener to remove
+ */
+ public void removeVisualizationListener(VisualizationListener listener) {
+ vlistenerList.remove(listener);
+ }
+
+ /**
+ * A visualization item has changed.
+ *
+ * @param item Item that has changed
+ */
+ public void visChanged(VisualizationItem item) {
+ for(int i = vlistenerList.size(); --i >= 0;) {
+ vlistenerList.get(i).visualizationChanged(item);
+ }
+ }
+
+ /**
+ * Handler for a single result.
+ *
+ * @author Erich Schubert
+ *
+ * @param <A> Object type
+ */
+ public static interface Handler1<A> {
+ /**
+ * Process a new result.
+ *
+ * @param context Context
+ * @param result First result
+ */
+ void process(VisualizerContext context, A result);
+ }
+
+ /**
+ * Handler for two result.
+ *
+ * @author Erich Schubert
+ *
+ * @param <A> Object type
+ * @param <B> Object type
+ */
+ public static interface Handler2<A, B> {
+ /**
+ * Process a new result.
+ *
+ * @param context Context
+ * @param result First result
+ * @param result2 Second result
+ */
+ void process(VisualizerContext context, A result, B result2);
+ }
+
+ /**
+ * Handler for three result.
+ *
+ * @author Erich Schubert
+ *
+ * @param <A> Object type
+ * @param <B> Object type
+ * @param <C> Object type
+ */
+ public static interface Handler3<A, B, C> {
+ /**
+ * Process a new result.
+ *
+ * @param context Context
+ * @param result First result
+ * @param result2 Second result
+ * @param result3 Third result
+ */
+ void process(VisualizerContext context, A result, B result2, C result3);
+ }
+
+ /**
+ * Filtered iteration over a stacked hierarchy.
+ *
+ * This is really messy because the visualization hierarchy is typed Object.
+ *
+ * @param context Visualization context
+ * @param clazz Type filter
+ * @param <O> Object type
+ * @return Iterator of results.
+ */
+ @SuppressWarnings("unchecked")
+ public static <O extends VisualizationItem> Hierarchy.Iter<O> filter(VisualizerContext context, Class<? super O> clazz) {
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterAll();
+ StackedIter<Object, Result> it2 = new StackedIter<>(it1, context.getVisHierarchy());
+ if(!it2.valid()) {
+ return HashMapHierarchy.emptyIterator();
+ }
+ return new FilteredIter<O>(it2, (Class<O>) clazz);
+ }
+
+ /**
+ * Filtered iteration over a stacked hierarchy.
+ *
+ * This is really messy because the visualization hierarchy is typed Object.
+ *
+ * @param context Visualization context
+ * @param start Starting object (in primary hierarchy!)
+ * @param clazz Type filter
+ * @param <O> Object type
+ * @return Iterator of results.
+ */
+ @SuppressWarnings("unchecked")
+ public static <O extends VisualizationItem> Hierarchy.Iter<O> filter(VisualizerContext context, Object start, Class<? super O> clazz) {
+ if(start instanceof Result) { // In first hierarchy.
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start);
+ StackedIter<Object, Result> it2 = new StackedIter<>(it1, context.getVisHierarchy());
+ if(!it2.valid()) {
+ return HashMapHierarchy.emptyIterator();
+ }
+ return new FilteredIter<O>(it2, (Class<O>) clazz);
+ }
+ Hierarchy.Iter<Object> it2 = context.getVisHierarchy().iterDescendantsSelf(start);
+ if(!it2.valid()) {
+ return HashMapHierarchy.emptyIterator();
+ }
+ return new FilteredIter<O>(it2, (Class<O>) clazz);
+ }
+
+ /**
+ * Filtered iteration over the primary result tree.
+ *
+ * @param context Visualization context
+ * @param start Starting object (in primary hierarchy!)
+ * @param clazz Type filter
+ * @param <O> Result type type
+ * @return Iterator of results.
+ */
+ @SuppressWarnings("unchecked")
+ public static <O extends Result> Hierarchy.Iter<O> filterResults(VisualizerContext context, Object start, Class<? super O> clazz) {
+ if(start instanceof Result) { // In first hierarchy.
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start);
+ return new FilteredIter<O>(it1, (Class<O>) clazz);
+ }
+ return HashMapHierarchy.emptyIterator();
+ }
+
+ /**
+ * Process new results.
+ *
+ * This is a bit painful, because we have two hierarchies with different
+ * types: results, and visualizations.
+ *
+ * @param context Context
+ * @param start Starting point
+ * @param type1 First type
+ * @param handler Handler
+ */
+ @SuppressWarnings("unchecked")
+ public static <A> void findNew(VisualizerContext context, Object start, Class<? super A> type1, Handler1<A> handler) {
+ final Hierarchy<Object> hier = context.getVisHierarchy();
+ // Children of start in first hierarchy:
+ if(start instanceof Result) {
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start);
+ for(; it1.valid(); it1.advance()) {
+ final Result o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ handler.process(context, (A) o1);
+ }
+ }
+ // Children of start in second hierarchy:
+ if(start instanceof VisualizationItem) {
+ Iter<Object> it1 = hier.iterDescendantsSelf(start);
+ for(; it1.valid(); it1.advance()) {
+ final Object o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ handler.process(context, (A) start);
+ }
+ }
+ }
+
+ /**
+ * Process new result combinations of an object type1 (in first hierarchy) and
+ * any child of type2 (in second hierarchy)
+ *
+ * This is a bit painful, because we have two hierarchies with different
+ * types: results, and visualizations.
+ *
+ * @param context Context
+ * @param start Starting point
+ * @param type1 First type, in first hierarchy
+ * @param type2 Second type, in second hierarchy
+ * @param handler Handler
+ */
+ @SuppressWarnings("unchecked")
+ public static <A extends Result, B extends VisualizationItem> void findNewSiblings(VisualizerContext context, Object start, Class<? super A> type1, Class<? super B> type2, Handler2<A, B> handler) {
+ final Hierarchy<Object> vistree = context.getVisHierarchy();
+ // Search start in first hierarchy:
+ if(start instanceof Result) {
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start);
+ for(; it1.valid(); it1.advance()) {
+ final Result o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ Iter<Object> it2 = vistree.iterDescendantsSelf(context.getBaseResult());
+ for(; it2.valid(); it2.advance()) {
+ final Object o2 = it2.get();
+ if(!(type2.isInstance(o2))) {
+ continue;
+ }
+ handler.process(context, (A) o1, (B) o2);
+ }
+ }
+ }
+ // Search start in second hierarchy:
+ if(start instanceof VisualizationItem) {
+ Iter<Object> it2 = vistree.iterDescendantsSelf(start);
+ for(; it2.valid(); it2.advance()) {
+ final Object o2 = it2.get();
+ if(!(type2.isInstance(o2))) {
+ continue;
+ }
+ Iter<Result> it1 = context.getHierarchy().iterAll();
+ for(; it1.valid(); it1.advance()) {
+ final Result o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ handler.process(context, (A) o1, (B) o2);
+ }
+ }
+ }
+ }
+
+ /**
+ * Process new result combinations of an object type1 (in first hierarchy)
+ * having a child of type2 (in second hierarchy).
+ *
+ * This is a bit painful, because we have two hierarchies with different
+ * types: results, and visualizations.
+ *
+ * @param context Context
+ * @param start Starting point
+ * @param type1 First type, in first hierarchy
+ * @param type2 Second type, in second hierarchy
+ * @param handler Handler
+ */
+ @SuppressWarnings("unchecked")
+ public static <A extends Result, B extends VisualizationItem> void findNewResultVis(VisualizerContext context, Object start, Class<? super A> type1, Class<? super B> type2, Handler2<A, B> handler) {
+ final Hierarchy<Object> hier = context.getVisHierarchy();
+ // Search start in first hierarchy:
+ if(start instanceof Result) {
+ Hierarchy.Iter<Result> it1 = context.getHierarchy().iterDescendantsSelf((Result) start);
+ for(; it1.valid(); it1.advance()) {
+ final Result o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ // Nasty: we now need to search backwards for crossover points:
+ Iter<Result> it3 = context.getHierarchy().iterDescendantsSelf(o1);
+ for(; it3.valid(); it3.advance()) {
+ Iter<Object> it2 = hier.iterDescendantsSelf(it3.get());
+ for(; it2.valid(); it2.advance()) {
+ final Object o2 = it2.get();
+ if(!(type2.isInstance(o2))) {
+ continue;
+ }
+ handler.process(context, (A) o1, (B) o2);
+ }
+ }
+ }
+ }
+ // Search start in second hierarchy:
+ if(start instanceof VisualizationItem) {
+ Iter<Object> it2 = hier.iterDescendantsSelf(start);
+ for(; it2.valid(); it2.advance()) {
+ final Object o2 = it2.get();
+ if(!(type2.isInstance(o2))) {
+ continue;
+ }
+ // Nasty: we now need to search backwards for crossover points:
+ Iter<Object> it3 = hier.iterAncestorsSelf(start);
+ for(; it3.valid(); it3.advance()) {
+ if(!(it3.get() instanceof Result)) {
+ continue;
+ }
+ // Now cross-over into primary hierarchy:
+ Iter<Result> it1 = context.getHierarchy().iterAncestorsSelf((Result) it3.get());
+ for(; it1.valid(); it1.advance()) {
+ final Result o1 = it1.get();
+ if(!(type1.isInstance(o1))) {
+ continue;
+ }
+ handler.process(context, (A) o1, (B) o2);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Utility function to change Visualizer visibility.
+ *
+ * @param context Visualization context
+ * @param task Visualization task
+ * @param visibility Visibility value
+ */
+ public static void setVisible(VisualizerContext context, VisualizationTask task, boolean visibility) {
+ // Hide other tools
+ if(visibility && task.tool) {
+ Hierarchy<Object> vistree = context.getVisHierarchy();
+ for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) {
+ if(!(iter2.get() instanceof VisualizationTask)) {
+ continue;
+ }
+ VisualizationTask other = (VisualizationTask) iter2.get();
+ if(other != task && other.tool && other.visible) {
+ other.visible = false;
+ context.visChanged(other);
+ }
+ }
+ }
+ task.visible = visibility;
+ context.visChanged(task);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java
new file mode 100644
index 00000000..8d288ce6
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerContext.java
@@ -0,0 +1,452 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.trivial.ByLabelHierarchicalClustering;
+import de.lmu.ifi.dbs.elki.algorithm.clustering.trivial.TrivialAllInOne;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.type.NoSupportedDataTypeException;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreEvent;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SelectionResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+
+/**
+ * Map to store context information for the visualizer. This can be any data
+ * that should to be shared among plots, such as line colors, styles etc.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.composedOf StyleLibrary
+ * @apiviz.composedOf StylingPolicy
+ * @apiviz.composedOf SelectionResult
+ * @apiviz.composedOf ResultHierarchy
+ * @apiviz.composedOf VisualizationTree
+ * @apiviz.composedOf DataStoreListener
+ * @apiviz.composedOf VisualizationProcessor
+ */
+public class VisualizerContext implements DataStoreListener, Result {
+ /**
+ * Logger.
+ */
+ private static final Logging LOG = Logging.getLogger(VisualizerContext.class);
+
+ /**
+ * Tree of visualizations.
+ */
+ private VisualizationTree vistree = new VisualizationTree();
+
+ /**
+ * The full result object
+ */
+ private ResultHierarchy hier;
+
+ /**
+ * The event listeners for this context.
+ */
+ private ArrayList<DataStoreListener> listenerList = new ArrayList<>();
+
+ /**
+ * Factories to use
+ */
+ private Collection<VisualizationProcessor> factories;
+
+ /**
+ * Selection result
+ */
+ private SelectionResult selection;
+
+ /**
+ * Styling policy
+ */
+ StylingPolicy stylepolicy;
+
+ /**
+ * Style library
+ */
+ StyleLibrary stylelibrary;
+
+ /**
+ * Starting point of the result tree, may be {@code null}.
+ */
+ private Result baseResult;
+
+ /**
+ * Relation currently visualized.
+ */
+ private Relation<?> relation;
+
+ /**
+ * Constructor. We currently require a Database and a Result.
+ *
+ * @param hier Result hierarchy
+ * @param start Starting result
+ * @param factories Visualizer Factories to use
+ */
+ public VisualizerContext(ResultHierarchy hier, Result start, Relation<?> relation, StyleLibrary stylelib, Collection<VisualizationProcessor> factories) {
+ super();
+ this.hier = hier;
+ this.baseResult = start;
+ this.factories = factories;
+
+ // Ensure that various common results needed by visualizers are
+ // automatically created
+ final Database db = ResultUtil.findDatabase(hier);
+ if(db == null) {
+ LOG.warning("No database reachable from " + hier);
+ return;
+ }
+ ResultUtil.ensureClusteringResult(db, db);
+ this.selection = ResultUtil.ensureSelectionResult(db);
+ for(Relation<?> rel : ResultUtil.getRelations(db)) {
+ ResultUtil.getSamplingResult(rel);
+ // FIXME: this is a really ugly workaround. :-(
+ if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ @SuppressWarnings("unchecked")
+ Relation<? extends NumberVector> vrel = (Relation<? extends NumberVector>) rel;
+ ResultUtil.getScalesResult(vrel);
+ }
+ }
+ makeStyleResult(stylelib);
+
+ // Add visualizers.
+ notifyFactories(db);
+
+ // For proxying events.
+ db.addDataStoreListener(this);
+ // Add a result listener.
+ // Don't expose these methods to avoid inappropriate use.
+ addResultListener(new ResultListener() {
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ notifyFactories(child);
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ // FIXME: need to do anything?
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ // FIXME: implement
+ }
+ });
+ }
+
+ /**
+ * Generate a new style result for the given style library.
+ *
+ * @param stylelib Style library
+ */
+ protected void makeStyleResult(StyleLibrary stylelib) {
+ final Database db = ResultUtil.findDatabase(hier);
+ stylelibrary = stylelib;
+ List<Clustering<? extends Model>> clusterings = ResultUtil.getClusteringResults(db);
+ if(clusterings.size() > 0) {
+ stylepolicy = new ClusterStylingPolicy(clusterings.get(0), stylelib);
+ }
+ else {
+ Clustering<Model> c = generateDefaultClustering();
+ stylepolicy = new ClusterStylingPolicy(c, stylelib);
+ }
+ }
+
+ /**
+ * Get the hierarchy object
+ *
+ * @return hierarchy object
+ */
+ public ResultHierarchy getHierarchy() {
+ return hier;
+ }
+
+ /**
+ * Get the active styling policy
+ *
+ * @return Styling policy
+ */
+ public StylingPolicy getStylingPolicy() {
+ return stylepolicy;
+ }
+
+ /**
+ * Set the active styling policy
+ *
+ * @param policy new Styling policy
+ */
+ public void setStylingPolicy(StylingPolicy policy) {
+ this.stylepolicy = policy;
+ visChanged(policy);
+ }
+
+ /**
+ * Get the style library
+ *
+ * @return Style library
+ */
+ public StyleLibrary getStyleLibrary() {
+ return stylelibrary;
+ }
+
+ /**
+ * Get the style library
+ *
+ * @param library Style library
+ */
+ public void setStyleLibrary(StyleLibrary library) {
+ this.stylelibrary = library;
+ }
+
+ /**
+ * Generate a default (fallback) clustering.
+ *
+ * @return generated clustering
+ */
+ private Clustering<Model> generateDefaultClustering() {
+ final Database db = ResultUtil.findDatabase(hier);
+ Clustering<Model> c = null;
+ try {
+ // Try to cluster by labels
+ ByLabelHierarchicalClustering split = new ByLabelHierarchicalClustering();
+ c = split.run(db);
+ }
+ catch(NoSupportedDataTypeException e) {
+ // Put everything into one
+ c = new TrivialAllInOne().run(db);
+ }
+ return c;
+ }
+
+ // TODO: add ShowVisualizer,HideVisualizer with tool semantics.
+
+ // TODO: add ShowVisualizer,HideVisualizer with tool semantics.
+
+ /**
+ * Get the current selection result.
+ *
+ * @return selection result
+ */
+ public SelectionResult getSelectionResult() {
+ return selection;
+ }
+
+ /**
+ * Get the current selection.
+ *
+ * @return selection
+ */
+ public DBIDSelection getSelection() {
+ return selection.getSelection();
+ }
+
+ /**
+ * Set a new selection.
+ *
+ * @param sel Selection
+ */
+ public void setSelection(DBIDSelection sel) {
+ selection.setSelection(sel);
+ getHierarchy().resultChanged(selection);
+ }
+
+ /**
+ * Current relation.
+ */
+ public Relation<?> getRelation() {
+ return relation;
+ }
+
+ /**
+ * Set the current relation.
+ *
+ * @param rel Relation
+ */
+ public void setRelation(Relation<?> rel) {
+ this.relation = rel;
+ getHierarchy().resultChanged(this);
+ }
+
+ /**
+ * Adds a listener for the <code>DataStoreEvent</code> posted after the
+ * content changes.
+ *
+ * @param l the listener to add
+ * @see #removeDataStoreListener
+ */
+ public void addDataStoreListener(DataStoreListener l) {
+ for(int i = 0; i < listenerList.size(); i++) {
+ if(listenerList.get(i) == l) {
+ return;
+ }
+ }
+ listenerList.add(l);
+ }
+
+ /**
+ * Removes a listener previously added with <code>addDataStoreListener</code>.
+ *
+ * @param l the listener to remove
+ * @see #addDataStoreListener
+ */
+ public void removeDataStoreListener(DataStoreListener l) {
+ listenerList.remove(l);
+ }
+
+ /**
+ * Proxy datastore event to child listeners.
+ */
+ @Override
+ public void contentChanged(DataStoreEvent e) {
+ for(int i = 0; i < listenerList.size(); i++) {
+ listenerList.get(i).contentChanged(e);
+ }
+ }
+
+ /**
+ * Register a result listener.
+ *
+ * @param listener Result listener.
+ */
+ public void addResultListener(ResultListener listener) {
+ getHierarchy().addResultListener(listener);
+ }
+
+ /**
+ * Remove a result listener.
+ *
+ * @param listener Result listener.
+ */
+ public void removeResultListener(ResultListener listener) {
+ getHierarchy().removeResultListener(listener);
+ }
+
+ /**
+ * Add a listener.
+ *
+ * @param listener Listener to add
+ */
+ public void addVisualizationListener(VisualizationListener listener) {
+ vistree.addVisualizationListener(listener);
+ }
+
+ /**
+ * Add a listener.
+ *
+ * @param listener Listener to remove
+ */
+ public void removeVisualizationListener(VisualizationListener listener) {
+ vistree.removeVisualizationListener(listener);
+ }
+
+ @Override
+ public String getLongName() {
+ return "Visualizer context";
+ }
+
+ @Override
+ public String getShortName() {
+ return "vis-context";
+ }
+
+ /**
+ * Starting point for visualization, may be {@code null}.
+ *
+ * @return Starting point in the result tree, may be {@code null}.
+ */
+ public Result getBaseResult() {
+ return baseResult;
+ }
+
+ /**
+ * Add (register) a visualization.
+ *
+ * @param parent Parent object
+ * @param vis Visualization
+ */
+ public void addVis(Object parent, VisualizationItem vis) {
+ vistree.add(parent, vis);
+ notifyFactories(vis);
+ visChanged(vis);
+ }
+
+ /**
+ * A visualization item has changed.
+ *
+ * @param item Item that has changed
+ */
+ public void visChanged(VisualizationItem item) {
+ vistree.visChanged(item);
+ }
+
+ /**
+ * Notify factories of a change.
+ *
+ * @param item Item that has changed.
+ */
+ private void notifyFactories(Object item) {
+ for(VisualizationProcessor f : factories) {
+ try {
+ f.processNewResult(this, item);
+ }
+ catch(Throwable e) {
+ LOG.warning("VisFactory " + f.getClass().getCanonicalName() + " failed:", e);
+ }
+ }
+ }
+
+ public List<VisualizationTask> getVisTasks(VisualizationItem item) {
+ List<VisualizationTask> out = new ArrayList<>();
+ for(Hierarchy.Iter<?> iter = vistree.iterDescendants(item); iter.valid(); iter.advance()) {
+ Object o = iter.get();
+ if(o instanceof VisualizationTask) {
+ out.add((VisualizationTask) o);
+ }
+ }
+ return out;
+ }
+
+ public VisualizationTree getVisHierarchy() {
+ return vistree;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java
new file mode 100644
index 00000000..df3d99de
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/VisualizerParameterizer.java
@@ -0,0 +1,354 @@
+package de.lmu.ifi.dbs.elki.visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import de.lmu.ifi.dbs.elki.algorithm.DistanceBasedAlgorithm;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.datasource.FileBasedDatabaseConnection;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.math.random.RandomFactory;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.result.SettingsResult;
+import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil;
+import de.lmu.ifi.dbs.elki.utilities.ELKIServiceRegistry;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.WrongParameterValueException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.MergedParameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackedParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Parameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.PatternParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.StringParameter;
+import de.lmu.ifi.dbs.elki.visualization.style.PropertiesBasedStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.workflow.AlgorithmStep;
+
+/**
+ * Utility class to determine the visualizers for a result class.
+ *
+ * You <em>really</em> should use the parameterization API to configure this
+ * class. Manually populating the factory collection is cumbersome, and the
+ * parameterization API takes care of this.
+ *
+ * @author Erich Schubert
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.landmark
+ * @apiviz.has VisualizerContext oneway - - «create»
+ * @apiviz.uses VisualizationProcessor oneway - n «configure»
+ */
+public class VisualizerParameterizer {
+ /**
+ * Get a logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(VisualizerParameterizer.class);
+
+ /**
+ * Default sample size to visualize.
+ */
+ public static final int DEFAULT_SAMPLE_SIZE = 10000;
+
+ /**
+ * Style library to use.
+ */
+ private StyleLibrary stylelib;
+
+ /**
+ * Projections and visualization factories.
+ */
+ private Collection<VisualizationProcessor> factories;
+
+ /**
+ * Sample size
+ */
+ private int samplesize = -1;
+
+ /**
+ * Random seed for sampling.
+ *
+ * FIXME: make parameterizable.
+ */
+ private RandomFactory rnd = RandomFactory.DEFAULT;
+
+ /**
+ * Constructor.
+ *
+ * @param samplesize
+ * @param stylelib Style library
+ * @param factories Factories to use
+ */
+ public VisualizerParameterizer(int samplesize, StyleLibrary stylelib, Collection<VisualizationProcessor> factories) {
+ super();
+ this.samplesize = samplesize;
+ this.stylelib = stylelib;
+ this.factories = factories;
+ }
+
+ /**
+ * Make a new visualization context
+ *
+ * @param hier Result hierarchy
+ * @param start Starting result
+ * @return New context
+ */
+ public VisualizerContext newContext(ResultHierarchy hier, Result start) {
+ Relation<?> relation = null;
+ Collection<Relation<?>> rels = ResultUtil.filterResults(hier, Relation.class);
+ for(Relation<?> rel : rels) {
+ if(!TypeUtil.DBID.isAssignableFrom(rel.getDataTypeInformation()) && relation == null) {
+ relation = rel;
+ }
+ if(samplesize == 0) {
+ continue;
+ }
+ if(!ResultUtil.filterResults(hier, rel, SamplingResult.class).isEmpty()) {
+ continue;
+ }
+ if(rel.size() > samplesize) {
+ SamplingResult sample = new SamplingResult(rel);
+ sample.setSample(DBIDUtil.randomSample(sample.getSample(), samplesize, rnd));
+ ResultUtil.addChildResult(rel, sample);
+ }
+ }
+ return new VisualizerContext(hier, start, relation, stylelib, factories);
+ }
+
+ /**
+ * Try to automatically generate a title for this.
+ *
+ * @param db Database
+ * @param result Result object
+ * @return generated title
+ */
+ public static String getTitle(Database db, Result result) {
+ List<TrackedParameter> settings = new ArrayList<>();
+ for(SettingsResult sr : ResultUtil.getSettingsResults(result)) {
+ settings.addAll(sr.getSettings());
+ }
+ String algorithm = null;
+ String distance = null;
+ String dataset = null;
+
+ for(TrackedParameter setting : settings) {
+ Parameter<?> param = setting.getParameter();
+ OptionID option = param.getOptionID();
+ String value = param.isDefined() ? param.getValueAsString() : null;
+ if(option.equals(AlgorithmStep.Parameterizer.ALGORITHM_ID)) {
+ algorithm = value;
+ }
+ if(option.equals(DistanceBasedAlgorithm.DISTANCE_FUNCTION_ID)) {
+ distance = value;
+ }
+ if(option.equals(FileBasedDatabaseConnection.Parameterizer.INPUT_ID)) {
+ dataset = value;
+ }
+ }
+ StringBuilder buf = new StringBuilder();
+ if(algorithm != null) {
+ buf.append(shortenClassname(algorithm.split(",")[0], '.'));
+ }
+ if(distance != null) {
+ if(buf.length() > 0) {
+ buf.append(" using ");
+ }
+ buf.append(shortenClassname(distance, '.'));
+ }
+ if(dataset != null) {
+ if(buf.length() > 0) {
+ buf.append(" on ");
+ }
+ buf.append(shortenClassname(dataset, File.separatorChar));
+ }
+ if(buf.length() > 0) {
+ return buf.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Shorten the class name.
+ *
+ * @param nam Class name
+ * @param c Splitting character
+ * @return Shortened name
+ */
+ protected static String shortenClassname(String nam, char c) {
+ final int lastdot = nam.lastIndexOf(c);
+ if(lastdot >= 0) {
+ nam = nam.substring(lastdot + 1);
+ }
+ return nam;
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Parameter to get the style properties file.
+ *
+ * <p>
+ * Key: -visualizer.stylesheet
+ *
+ * Default: default properties file <br>
+ * included stylesheets:
+ * <ul>
+ * <li>classic</li>
+ * <li>default</li>
+ * <li>greyscale</li>
+ * <li>neon</li>
+ * <li>presentation</li>
+ * <li>print</li>
+ * </ul>
+ * These are {@code *.properties} files in the package
+ * {@link de.lmu.ifi.dbs.elki.visualization.style}.
+ * </p>
+ */
+ public static final OptionID STYLELIB_ID = new OptionID("visualizer.stylesheet", "Style properties file to use, included properties: classic, default, greyscale, neon, presentation, print");
+
+ /**
+ * Parameter to enable visualizers
+ *
+ * <p>
+ * Key: -vis.enable
+ *
+ * Default: ELKI core
+ * </p>
+ */
+ public static final OptionID ENABLEVIS_ID = new OptionID("vis.enable", "Visualizers to enable by default.");
+
+ /**
+ * Parameter to set the sampling level
+ *
+ * <p>
+ * Key: -vis.sampling
+ * </p>
+ */
+ public static final OptionID SAMPLING_ID = new OptionID("vis.sampling", "Maximum number of objects to visualize by default (for performance reasons).");
+
+ /**
+ * Style library
+ */
+ protected StyleLibrary stylelib = null;
+
+ /**
+ * Pattern to enable visualizers
+ */
+ protected Pattern enableVisualizers = null;
+
+ /**
+ * Visualizer factories
+ */
+ protected Collection<VisualizationProcessor> factories = null;
+
+ /**
+ * Sampling size
+ */
+ protected int samplesize = -1;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ IntParameter samplingP = new IntParameter(SAMPLING_ID, DEFAULT_SAMPLE_SIZE) //
+ .addConstraint(CommonConstraints.GREATER_EQUAL_MINUSONE_INT);
+ if(config.grab(samplingP)) {
+ samplesize = samplingP.intValue();
+ }
+ StringParameter stylelibP = new StringParameter(STYLELIB_ID, PropertiesBasedStyleLibrary.DEFAULT_SCHEME_FILENAME);
+ if(config.grab(stylelibP)) {
+ String filename = stylelibP.getValue();
+ try {
+ stylelib = new PropertiesBasedStyleLibrary(filename, filename);
+ }
+ catch(AbortException e) {
+ config.reportError(new WrongParameterValueException(stylelibP, filename, e));
+ }
+ }
+ PatternParameter enablevisP = new PatternParameter(ENABLEVIS_ID) //
+ .setOptional(true);
+ if(config.grab(enablevisP)) {
+ if(!"all".equals(enablevisP.getValueAsString())) {
+ enableVisualizers = enablevisP.getValue();
+ }
+ }
+ MergedParameterization merged = new MergedParameterization(config);
+ factories = collectFactorys(merged, enableVisualizers);
+ }
+
+ /**
+ * Collect and instantiate all visualizer factories.
+ *
+ * @param config Parameterization
+ * @param filter Filter
+ * @return List of all adapters found.
+ */
+ private static <O> Collection<VisualizationProcessor> collectFactorys(MergedParameterization config, Pattern filter) {
+ ArrayList<VisualizationProcessor> factories = new ArrayList<>();
+ for(Class<?> c : ELKIServiceRegistry.findAllImplementations(VisualizationProcessor.class)) {
+ if(filter != null && !filter.matcher(c.getCanonicalName()).find()) {
+ continue;
+ }
+ try {
+ config.rewind();
+ VisualizationProcessor a = ClassGenericsUtil.tryInstantiate(VisualizationProcessor.class, c, config);
+ factories.add(a);
+ }
+ catch(Throwable e) {
+ if(LOG.isDebugging()) {
+ LOG.exception("Error instantiating visualization processor " + c.getName(), e.getCause());
+ }
+ else {
+ LOG.warning("Error instantiating visualization processor " + c.getName() + ": " + e.getMessage());
+ }
+ }
+ }
+ return factories;
+ }
+
+ @Override
+ protected VisualizerParameterizer makeInstance() {
+ return new VisualizerParameterizer(samplesize, stylelib, factories);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java
new file mode 100755
index 00000000..e32d84a3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AddCSSClass.java
@@ -0,0 +1,62 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+
+/**
+ * Add a CSS class to the event target.
+ *
+ * @author Erich Schubert
+ *
+ */
+public class AddCSSClass implements EventListener {
+ /**
+ * Class to set
+ */
+ private String cssclass;
+
+ /**
+ * Constructor
+ * @param cssclass class to set
+ */
+ public AddCSSClass(String cssclass) {
+ super();
+ this.cssclass = cssclass;
+ }
+
+ /**
+ * Event handler
+ */
+ @Override
+ public void handleEvent(Event evt) {
+ Element e = (Element) evt.getTarget();
+ SVGUtil.addCSSClass(e, cssclass);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java
new file mode 100644
index 00000000..21409018
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/AttributeModifier.java
@@ -0,0 +1,74 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+/**
+ * Runnable wrapper for modifying XML-Attributes.
+ *
+ * @author Remigius Wojdanowski
+ *
+ */
+// FIXME: Unused? Remove?
+public class AttributeModifier implements Runnable {
+
+ /**
+ * Provides the attribute to be modified.
+ */
+ private Element e;
+
+ /**
+ * The name of the attribute to be modified.
+ */
+ private String attribute;
+
+ /**
+ * The new value of the attribute.
+ */
+ private String newValue;
+
+ /**
+ * Trivial constructor.
+ *
+ * @param e provides the attribute to be modified.
+ * @param attribute the name of the attribute to be modified.
+ * @param newValue the new value of the attribute.
+ */
+ public AttributeModifier(Element e, String attribute, String newValue) {
+ this.e = e;
+ this.attribute = attribute;
+ this.newValue = newValue;
+ }
+
+ @Override
+ public void run() {
+ if(newValue != null) {
+ e.setAttribute(attribute, newValue);
+ }
+ else {
+ e.removeAttribute(attribute);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java
new file mode 100644
index 00000000..44eb38d6
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/BatikUtil.java
@@ -0,0 +1,65 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.dom.events.DOMMouseEvent;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGElement;
+import org.w3c.dom.svg.SVGLocatable;
+import org.w3c.dom.svg.SVGMatrix;
+import org.w3c.dom.svg.SVGPoint;
+
+/**
+ * Batik helper class with static methods.
+ *
+ * @author Erich Schubert
+ */
+public final class BatikUtil {
+ /**
+ * Get the relative coordinates of a point within the coordinate system of a
+ * particular SVG Element.
+ *
+ * @param evt Event, needs to be a DOMMouseEvent
+ * @param reference SVG Element the coordinate system is used of
+ * @return Array containing the X and Y values
+ */
+ public static double[] getRelativeCoordinates(Event evt, Element reference) {
+ if(evt instanceof DOMMouseEvent && reference instanceof SVGLocatable && reference instanceof SVGElement) {
+ // Get the screen (pixel!) coordinates
+ DOMMouseEvent gnme = (DOMMouseEvent) evt;
+ SVGMatrix mat = ((SVGLocatable) reference).getScreenCTM();
+ SVGMatrix imat = mat.inverse();
+ SVGPoint cPt = ((SVGElement) reference).getOwnerSVGElement().createSVGPoint();
+ cPt.setX(gnme.getClientX());
+ cPt.setY(gnme.getClientY());
+ // Have Batik transform the screen (pixel!) coordinates into SVG element
+ // coordinates
+ cPt = cPt.matrixTransform(imat);
+
+ return new double[] { cPt.getX(), cPt.getY() };
+ }
+ return null;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java
new file mode 100755
index 00000000..7140b73f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CSSHoverClass.java
@@ -0,0 +1,110 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Do a hover effect using a CSS class.
+ *
+ * @author Erich Schubert
+ *
+ */
+public class CSSHoverClass implements EventListener {
+ /**
+ * Class to set when over
+ */
+ private String overclass;
+
+ /**
+ * Class to set when out
+ */
+ private String outclass;
+
+ /**
+ * Consider a click as 'out'?
+ */
+ private boolean clickisout;
+
+ /**
+ * Constructor
+ *
+ * @param overclass class to set when over
+ * @param outclass class to set when out
+ * @param clickisout consider a click to be an 'out' event
+ */
+ public CSSHoverClass(String overclass, String outclass, boolean clickisout) {
+ super();
+ this.overclass = overclass;
+ this.outclass = outclass;
+ this.clickisout = clickisout;
+ }
+
+ /**
+ * Constructor without 'clickisout' option.
+ *
+ * @param overclass class to set when over
+ * @param outclass class to set when out
+ */
+ public CSSHoverClass(String overclass, String outclass) {
+ this(overclass, outclass, false);
+ }
+
+ /**
+ * Event handler
+ */
+ @Override
+ public void handleEvent(Event evt) {
+ Element e = (Element) evt.getTarget();
+ if (SVGConstants.SVG_EVENT_MOUSEOVER.equals(evt.getType())) {
+ if (overclass != null) {
+ SVGUtil.addCSSClass(e, overclass);
+ }
+ if (outclass != null) {
+ SVGUtil.removeCSSClass(e, outclass);
+ }
+ }
+ if (SVGConstants.SVG_EVENT_MOUSEOUT.equals(evt.getType())) {
+ if (overclass != null) {
+ SVGUtil.removeCSSClass(e, overclass);
+ }
+ if (outclass != null) {
+ SVGUtil.addCSSClass(e, outclass);
+ }
+ }
+ if (clickisout && SVGConstants.SVG_EVENT_CLICK.equals(evt.getType())) {
+ if (overclass != null) {
+ SVGUtil.removeCSSClass(e, overclass);
+ }
+ if (outclass != null) {
+ SVGUtil.addCSSClass(e, outclass);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java
new file mode 100644
index 00000000..93033f01
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/CloneInlineImages.java
@@ -0,0 +1,144 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.renderable.RenderableImage;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+
+import javax.imageio.ImageIO;
+
+import org.apache.batik.svggen.SVGSyntax;
+import org.apache.batik.util.Base64EncoderStream;
+import org.apache.batik.util.ParsedURL;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGCloneVisible;
+
+/**
+ * Clone an SVG document, inlining temporary and in-memory linked images.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has ThumbnailRegistryEntry
+ */
+public class CloneInlineImages extends SVGCloneVisible {
+ @Override
+ public Node cloneNode(Document doc, Node eold) {
+ Node enew = null;
+ if(eold instanceof Element) {
+ Element e = (Element) eold;
+ if(e.getTagName().equals(SVGConstants.SVG_IMAGE_TAG)) {
+ String url = e.getAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE);
+ ParsedURL urldata = new ParsedURL(url);
+ if(ThumbnailRegistryEntry.isCompatibleURLStatic(urldata)) {
+ enew = inlineThumbnail(doc, urldata, eold);
+ }
+ else if("file".equals(urldata.getProtocol())) {
+ enew = inlineExternal(doc, urldata, eold);
+ }
+ }
+ }
+ if(enew != null) {
+ return enew;
+ }
+ return super.cloneNode(doc, eold);
+ }
+
+ /**
+ * Inline a referenced thumbnail.
+ *
+ * @param doc Document (element factory)
+ * @param urldata URL
+ * @param eold Existing node
+ * @return Replacement node, or {@code null}
+ */
+ protected Node inlineThumbnail(Document doc, ParsedURL urldata, Node eold) {
+ RenderableImage img = ThumbnailRegistryEntry.handleURL(urldata);
+ if(img == null) {
+ LoggingUtil.warning("Image not found in registry: " + urldata.toString());
+ return null;
+ }
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ os.write(SVGSyntax.DATA_PROTOCOL_PNG_PREFIX.getBytes());
+ Base64EncoderStream encoder = new Base64EncoderStream(os);
+ ImageIO.write(img.createDefaultRendering(), "png", encoder);
+ encoder.close();
+ }
+ catch(IOException e) {
+ LoggingUtil.exception("Exception serializing image to png", e);
+ return null;
+ }
+ Element i = (Element) super.cloneNode(doc, eold);
+ i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, os.toString().replaceAll("\\s*[\\r\\n]+\\s*", ""));
+ return i;
+ }
+
+ /**
+ * Inline an external file (usually from temp).
+ *
+ * @param doc Document (element factory)
+ * @param urldata URL
+ * @param eold Existing node
+ * @return Replacement node, or {@code null}
+ */
+ protected Node inlineExternal(Document doc, ParsedURL urldata, Node eold) {
+ File in = new File(urldata.getPath());
+ if(!in.exists()) {
+ LoggingUtil.warning("Referencing non-existant file: " + urldata.toString());
+ return null;
+ }
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ try {
+ os.write(SVGSyntax.DATA_PROTOCOL_PNG_PREFIX.getBytes());
+ Base64EncoderStream encoder = new Base64EncoderStream(os);
+ FileInputStream instream = new FileInputStream(in);
+ byte[] buf = new byte[4096];
+ while(true) {
+ int read = instream.read(buf, 0, buf.length);
+ if(read <= 0) {
+ break;
+ }
+ encoder.write(buf, 0, read);
+ }
+ instream.close();
+ encoder.close();
+ }
+ catch(IOException e) {
+ LoggingUtil.exception("Exception serializing image to png", e);
+ return null;
+ }
+
+ Element i = (Element) super.cloneNode(doc, eold);
+ i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_ATTRIBUTE, os.toString().replaceAll("\\s*[\\r\\n]+\\s*", ""));
+ return i;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java
new file mode 100644
index 00000000..dc5dd388
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/DragableArea.java
@@ -0,0 +1,368 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * A simple dragable area for Batik.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has DragListener
+ * @apiviz.has Element
+ */
+public class DragableArea implements EventListener {
+ /**
+ * Our element node.
+ */
+ final protected Element element;
+
+ /**
+ * The coordinate system node.
+ */
+ final protected Element coordref;
+
+ /**
+ * The plot we are attached to.
+ */
+ final protected SVGPlot svgp;
+
+ /**
+ * The point where the drag started.
+ */
+ protected SVGPoint startDragPoint = null;
+
+ /**
+ * A listener to notify on drags (when not subclassing).
+ */
+ protected DragListener listener = null;
+
+ /**
+ * Constructor for a dragable area. use getElement() to get the DOM node.
+ *
+ * Note: always remember to call 'destroy()' to remove listeners!
+ *
+ * @param plot Plot we'll be added to
+ * @param x X position
+ * @param y Y position
+ * @param w Width
+ * @param h Height
+ */
+ public DragableArea(SVGPlot plot, double x, double y, double w, double h) {
+ this.svgp = plot;
+ this.element = plot.svgRect(x, y, w, h);
+ makeInvisible();
+ this.coordref = this.element;
+ enableStart();
+ }
+
+ /**
+ * Constructor for a dragable area. use getElement() to get the DOM node.
+ *
+ * Note: always remember to call 'destroy()' to remove listeners!
+ *
+ * @param plot Plot we'll be added to
+ * @param coordref Element defining the coordinate system
+ * @param x X position
+ * @param y Y position
+ * @param w Width
+ * @param h Height
+ */
+ public DragableArea(SVGPlot plot, Element coordref, double x, double y, double w, double h) {
+ this.svgp = plot;
+ this.element = plot.svgRect(x, y, w, h);
+ makeInvisible();
+ this.coordref = coordref;
+ enableStart();
+ }
+
+ /**
+ * Constructor for a dragable area. use getElement() to get the DOM node.
+ *
+ * Note: always remember to call 'destroy()' to remove listeners!
+ *
+ * @param plot Plot we'll be added to
+ * @param x X position
+ * @param y Y position
+ * @param w Width
+ * @param h Height
+ * @param listener Drag listener
+ */
+ public DragableArea(SVGPlot plot, double x, double y, double w, double h, DragListener listener) {
+ this.svgp = plot;
+ this.element = plot.svgRect(x, y, w, h);
+ makeInvisible();
+ this.coordref = this.element;
+ this.listener = listener;
+ enableStart();
+ }
+
+ /**
+ * Constructor for a dragable area. use getElement() to get the DOM node.
+ *
+ * Note: always remember to call 'destroy()' to remove listeners!
+ *
+ * @param plot Plot we'll be added to
+ * @param coordref Element defining the coordinate system
+ * @param x X position
+ * @param y Y position
+ * @param w Width
+ * @param h Height
+ * @param listener Drag listener
+ */
+ public DragableArea(SVGPlot plot, Element coordref, double x, double y, double w, double h, DragListener listener) {
+ this.svgp = plot;
+ this.element = plot.svgRect(x, y, w, h);
+ makeInvisible();
+ this.coordref = coordref;
+ this.listener = listener;
+ enableStart();
+ }
+
+ /**
+ * Remove the listeners
+ */
+ public void destroy() {
+ disableStart();
+ disableStop();
+ }
+
+ /**
+ * The DOM element.
+ *
+ * @return the element
+ */
+ public Element getElement() {
+ return element;
+ }
+
+ /**
+ * Enable capturing of 'mousedown' events.
+ */
+ public void enableStart() {
+ EventTarget targ = (EventTarget) element;
+ targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEDOWN, this, false);
+ }
+
+ /**
+ * Disable capturing of 'mousedown' events.
+ */
+ public void disableStart() {
+ EventTarget targ = (EventTarget) element;
+ targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEDOWN, this, false);
+ }
+
+ /**
+ * Enable capturing of 'mousemove' and 'mouseup' events.
+ */
+ protected void enableStop() {
+ EventTarget targ = svgp.getDocument().getRootElement();
+ targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEMOVE, this, false);
+ targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEUP, this, false);
+ // FIXME: listen on the background object!
+ targ.addEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, this, false);
+ }
+
+ /**
+ * Disable capturing of 'mousemove' and 'mouseup' events.
+ */
+ protected void disableStop() {
+ EventTarget targ = svgp.getDocument().getRootElement();
+ targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEMOVE, this, false);
+ targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEUP, this, false);
+ // FIXME: listen on the background object!
+ targ.removeEventListener(SVGConstants.SVG_EVENT_MOUSEOUT, this, false);
+ }
+
+ @Override
+ public void handleEvent(Event evt) {
+ if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEDOWN)) {
+ SVGPoint dragPoint = getCoordinates(evt);
+ if (startDrag(dragPoint, evt)) {
+ // LoggingUtil.warning("Starting drag: "+dragPoint);
+ startDragPoint = dragPoint;
+ enableStop();
+ }
+ } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEMOVE)) {
+ if (startDragPoint != null) {
+ SVGPoint dragPoint = getCoordinates(evt);
+ if (!duringDrag(startDragPoint, dragPoint, evt, evt.getTarget() == element)) {
+ // cancel the drag operation
+ startDragPoint = null;
+ disableStop();
+ }
+ }
+ } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEUP)) {
+ if (startDragPoint != null) {
+ SVGPoint dragPoint = getCoordinates(evt);
+ if (endDrag(startDragPoint, dragPoint, evt, evt.getTarget() == element)) {
+ // LoggingUtil.warning("Drag completed: "+dragPoint);
+ startDragPoint = null;
+ disableStop();
+ }
+ }
+ } else if (evt.getType().equals(SVGConstants.SVG_EVENT_MOUSEOUT)) {
+ // When leaving the document with the mouse!
+ if (startDragPoint != null && evt.getTarget() == evt.getCurrentTarget()) {
+ // LoggingUtil.warning("Mouseout: "+evt.getTarget().toString());
+ SVGPoint dragPoint = getCoordinates(evt);
+ if (endDrag(startDragPoint, dragPoint, evt, false)) {
+ // LoggingUtil.warning("Drag completed: "+dragPoint);
+ startDragPoint = null;
+ disableStop();
+ }
+ }
+ } else {
+ LoggingUtil.warning("Unrecognized event: " + evt);
+ }
+ }
+
+ /**
+ * Return the event coordinates for this event.
+ *
+ * @param evt Event
+ * @return Coordinates
+ */
+ protected SVGPoint getCoordinates(Event evt) {
+ return SVGUtil.elementCoordinatesFromEvent(this.svgp.getDocument(), this.coordref, evt);
+ }
+
+ /**
+ * Action to do on drag start.
+ *
+ * @param startPoint Point where the drag was started.
+ * @param evt The event object
+ * @return {@code true} to start the drag operation
+ */
+ protected boolean startDrag(SVGPoint startPoint, Event evt) {
+ if (listener != null) {
+ return listener.startDrag(startPoint, evt);
+ }
+ return true;
+ }
+
+ /**
+ * Method called during drags.
+ *
+ * @param startPoint Drag starting point
+ * @param dragPoint Drag end point
+ * @param evt The event object
+ * @param inside Inside the tracked element
+ * @return {@code true} to continue the drag
+ */
+ protected boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ if (listener != null) {
+ return listener.duringDrag(startPoint, dragPoint, evt, inside);
+ }
+ return true;
+ }
+
+ /**
+ * Method called when a drag was ended.
+ *
+ * @param startPoint Drag starting point
+ * @param dragPoint Drag end point
+ * @param evt The event object
+ * @param inside Success flag
+ * @return {@code true} to complete the drag
+ */
+ protected boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ if (listener != null) {
+ return listener.endDrag(startPoint, dragPoint, evt, inside);
+ }
+ return true;
+ }
+
+ /**
+ * Make the rectangle invisible.
+ */
+ public void makeInvisible() {
+ CSSClass cls = new CSSClass(this, "unused");
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0");
+ cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ SVGUtil.setAtt(element, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ }
+
+ /**
+ * Make the rectangle visible, for debug purposes.
+ */
+ public void makeVisible() {
+ CSSClass cls = new CSSClass(this, "unused");
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREEN_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.2");
+ cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ SVGUtil.setAtt(element, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ }
+
+ /**
+ * Listener interface for drag events.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.excludeSubtypes
+ */
+ public interface DragListener {
+ /**
+ * Action to do on drag start.
+ *
+ * @param startPoint Point where the drag was started.
+ * @param evt The event object
+ * @return {@code true} to start the drag operation
+ */
+ boolean startDrag(SVGPoint startPoint, Event evt);
+
+ /**
+ * Method called during drags.
+ *
+ * @param startPoint Drag starting point
+ * @param dragPoint Drag end point
+ * @param evt The event object
+ * @param inside Inside the tracked element
+ * @return {@code true} to continue the drag
+ */
+ boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside);
+
+ /**
+ * Method called when a drag was ended.
+ *
+ * @param startPoint Drag starting point
+ * @param dragPoint Drag end point
+ * @param evt The event object
+ * @param inside Whether the end point was inside the area
+ * @return {@code true} to complete the drag
+ */
+ boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java
new file mode 100644
index 00000000..2f9ac431
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGSynchronizedCanvas.java
@@ -0,0 +1,171 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.batik.bridge.UpdateManager;
+import org.apache.batik.swing.JSVGCanvas;
+import org.w3c.dom.Document;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * An JSVGCanvas that allows easier synchronization of Updates for SVGPlot
+ * objects.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf JSVGUpdateSynchronizer
+ * @apiviz.has SVGPlot oneway - - displays
+ */
+public class JSVGSynchronizedCanvas extends JSVGCanvas {
+ /**
+ * Serial version number.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Synchronizer to use when synchronizing SVG plots
+ */
+ final private JSVGUpdateSynchronizer synchronizer;
+
+ /**
+ * Current SVG plot.
+ */
+ private SVGPlot plot = null;
+
+ /**
+ * The latest attaching operation.
+ */
+ private final AtomicReference<Runnable> latest = new AtomicReference<>();
+
+ /**
+ * Constructor
+ */
+ public JSVGSynchronizedCanvas() {
+ super();
+ this.synchronizer = new JSVGUpdateSynchronizer(this);
+ super.setDocumentState(JSVGCanvas.ALWAYS_DYNAMIC);
+ }
+
+ /**
+ * Get the currently displayed SVG plot.
+ *
+ * @return current SVG plot. May be {@code null}!
+ */
+ public SVGPlot getPlot() {
+ return this.plot;
+ }
+
+ /**
+ * Use {@link #setPlot} instead if you need synchronization!
+ *
+ * @deprecated Document cannot be synchronized - use {@link #setPlot} and a
+ * {@link SVGPlot} object!
+ */
+ @Override
+ @Deprecated
+ public synchronized void setDocument(Document doc) {
+ // Note: this will call this.setSVGDocument!
+ super.setDocument(doc);
+ }
+
+ /**
+ * Choose a new plot to display.
+ *
+ * @param newplot New plot to display. May be {@code null}!
+ */
+ public void setPlot(final SVGPlot newplot) {
+ synchronized(synchronizer) {
+ super.setSVGDocument(null);
+ scheduleSetPlot(this.plot, newplot);
+ }
+ }
+
+ /**
+ * Schedule a detach.
+ *
+ * @param oldplot Plot to detach from.
+ */
+ private void scheduleSetPlot(final SVGPlot oldplot, final SVGPlot newplot) {
+ UpdateManager um = this.getUpdateManager();
+ if(um != null) {
+ synchronized(um) {
+ if(um.isRunning()) {
+ // LoggingUtil.warning("Scheduling detach: " + this + " " + oldplot);
+ final Runnable detach = new Runnable() {
+ @Override
+ public void run() {
+ if(latest.compareAndSet(this, null)) {
+ detachPlot(oldplot);
+ attachPlot(newplot);
+ }
+ }
+ };
+ latest.set(detach);
+ um.getUpdateRunnableQueue().preemptLater(detach);
+ return;
+ }
+ }
+ }
+ else {
+ if(oldplot != null) {
+ LoggingUtil.warning("No update manager, but a previous plot exists. Incorrectly initialized?");
+ }
+ }
+ detachPlot(oldplot);
+ attachPlot(newplot);
+ }
+
+ /**
+ * Attach to a new plot, and display.
+ *
+ * @param newplot Plot to attach to.
+ */
+ private void attachPlot(SVGPlot newplot) {
+ this.plot = newplot;
+ if(newplot == null) {
+ super.setSVGDocument(null);
+ return;
+ }
+ newplot.synchronizeWith(synchronizer);
+ super.setSVGDocument(newplot.getDocument());
+ super.setDisableInteractions(newplot.getDisableInteractions());
+ }
+
+ /**
+ * Execute the detaching event.
+ *
+ * @param oldplot Plot to detach from.
+ */
+ private void detachPlot(SVGPlot oldplot) {
+ if(oldplot == null) {
+ return;
+ }
+ this.plot = null;
+ oldplot.unsynchronizeWith(JSVGSynchronizedCanvas.this.synchronizer);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java
new file mode 100644
index 00000000..6fe43df1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/JSVGUpdateSynchronizer.java
@@ -0,0 +1,184 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.lang.ref.WeakReference;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.batik.bridge.UpdateManager;
+import org.apache.batik.bridge.UpdateManagerAdapter;
+import org.apache.batik.bridge.UpdateManagerEvent;
+import org.apache.batik.swing.svg.JSVGComponent;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.UpdateRunner;
+import de.lmu.ifi.dbs.elki.visualization.svg.UpdateSynchronizer;
+
+/**
+ * This class is used to synchronize SVG updates with an JSVG canvas.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses UpdateRunner
+ */
+class JSVGUpdateSynchronizer implements UpdateSynchronizer {
+ /**
+ * A weak reference to the component the plot is in.
+ */
+ private final WeakReference<JSVGComponent> cref;
+
+ /**
+ * The UpdateRunner we are put into
+ */
+ private Set<WeakReference<UpdateRunner>> updaterunner = new CopyOnWriteArraySet<>();
+
+ /**
+ * Adapter to track component changes
+ */
+ private final UMAdapter umadapter = new UMAdapter();
+
+ /**
+ * The current Runnable scheduled, prevents repeated invocations.
+ */
+ private final AtomicReference<Runnable> pending = new AtomicReference<>();
+
+ /**
+ * Create an updateSynchronizer for the given component.
+ *
+ * @param component Component to manage updates on.
+ */
+ protected JSVGUpdateSynchronizer(JSVGComponent component) {
+ assert(component != null);
+
+ this.cref = new WeakReference<>(component);
+ // Hook into UpdateManager creation.
+ component.addUpdateManagerListener(umadapter);
+ // makeRunnerIfNeeded();
+ }
+
+ @Override
+ public void activate() {
+ makeRunnerIfNeeded();
+ }
+
+ /**
+ * Join the runnable queue of a component.
+ */
+ protected void makeRunnerIfNeeded() {
+ // We don't need to make a SVG runner when there are no pending updates.
+ boolean stop = true;
+ for(WeakReference<UpdateRunner> wur : updaterunner) {
+ UpdateRunner ur = wur.get();
+ if(ur == null) {
+ updaterunner.remove(wur);
+ }
+ else if(!ur.isEmpty()) {
+ stop = false;
+ }
+ }
+ if(stop) {
+ return;
+ }
+ // We only need a new runner when we don't have one in the queue yet!
+ if(pending.get() != null) {
+ return;
+ }
+ // We need a component
+ JSVGComponent component = this.cref.get();
+ if(component == null) {
+ return;
+ }
+ // Synchronize with all layers:
+ synchronized(this) {
+ synchronized(component) {
+ UpdateManager um = component.getUpdateManager();
+ if(um != null) {
+ synchronized(um) {
+ if(um.isRunning()) {
+ // Create and insert a runner.
+ Runnable newrunner = new Runnable() {
+ @Override
+ public void run() {
+ if(pending.compareAndSet(this, null)) {
+ // Wake up all runners
+ for(WeakReference<UpdateRunner> wur : updaterunner) {
+ UpdateRunner ur = wur.get();
+ if(ur == null || ur.isEmpty()) {
+ continue;
+ }
+ ur.runQueue();
+ }
+ }
+ }
+ };
+ pending.set(newrunner);
+ um.getUpdateRunnableQueue().invokeLater(newrunner);
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addUpdateRunner(UpdateRunner updateRunner) {
+ for(WeakReference<UpdateRunner> wur : updaterunner) {
+ if(wur.get() == null) {
+ updaterunner.remove(wur);
+ }
+ }
+ updaterunner.add(new WeakReference<>(updateRunner));
+ }
+
+ /**
+ * Adapter that will track the component for UpdateManager changes.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ private class UMAdapter extends UpdateManagerAdapter {
+ /**
+ * Constructor. Protected to allow construction above.
+ */
+ protected UMAdapter() {
+ // nothing to do.
+ }
+
+ /**
+ * React to an update manager becoming available.
+ */
+ @Override
+ public void managerStarted(UpdateManagerEvent e) {
+ makeRunnerIfNeeded();
+ }
+
+ @Override
+ public void managerStopped(UpdateManagerEvent e) {
+ pending.set(null);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java
new file mode 100644
index 00000000..c180d329
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/LazyCanvasResizer.java
@@ -0,0 +1,116 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.Component;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+
+/**
+ * Class to lazily process canvas resize events by applying a threshold.
+ *
+ * @author Erich Schubert
+ */
+public abstract class LazyCanvasResizer extends ComponentAdapter {
+ /**
+ * Default threshold for resizing.
+ */
+ public static final double DEFAULT_THRESHOLD = 0.05;
+
+ /**
+ * Active threshold
+ */
+ double threshold;
+
+ /**
+ * Last ratio of the Canvas applied
+ */
+ double activeRatio;
+
+ /**
+ * Component the ratio applies to.
+ */
+ Component component;
+
+ /**
+ * Full constructor.
+ *
+ * @param component Component to track
+ * @param threshold Threshold
+ */
+ public LazyCanvasResizer(Component component, double threshold) {
+ super();
+ this.threshold = threshold;
+ this.component = component;
+ this.activeRatio = getCurrentRatio();
+ }
+
+ /**
+ * Simplified constructor using the default threshold {@link #DEFAULT_THRESHOLD}
+ *
+ * @param component Component to track.
+ */
+ public LazyCanvasResizer(Component component) {
+ this(component, DEFAULT_THRESHOLD);
+ }
+
+ /**
+ * React to a component resize event.
+ */
+ @Override
+ public void componentResized(ComponentEvent e) {
+ if (e.getComponent() == component) {
+ double newRatio = getCurrentRatio();
+ if (Math.abs(newRatio - activeRatio) > threshold) {
+ activeRatio = newRatio;
+ executeResize(newRatio);
+ }
+ }
+ }
+
+ /**
+ * Get the components current ratio.
+ *
+ * @return Current ratio.
+ */
+ public final double getCurrentRatio() {
+ return (double) component.getWidth() / (double) component.getHeight();
+ }
+
+ /**
+ * Callback function that needs to be overridden with actual implementations.
+ *
+ * @param newratio New ratio to apply.
+ */
+ public abstract void executeResize(double newratio);
+
+ /**
+ * Get the components last applied ratio.
+ *
+ * @return Last applied ratio.
+ */
+ public double getActiveRatio() {
+ return activeRatio;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java
new file mode 100644
index 00000000..79bdab54
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeAppendChild.java
@@ -0,0 +1,89 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * Runnable wrapper for appending XML-Elements to existing Elements.
+ *
+ * @author Remigius Wojdanowski
+ */
+public class NodeAppendChild implements Runnable {
+ /**
+ * Parent node to append to.
+ */
+ protected Element parent;
+
+ /**
+ * The child to be appended.
+ */
+ protected Element child;
+
+ /**
+ * The plot (for ID updates). May be {@code null}.
+ */
+ protected SVGPlot plot;
+
+ /**
+ * The ID. May be {code null}.
+ */
+ protected String id;
+
+ /**
+ * Trivial constructor.
+ *
+ * @param parent will become the parent of the appended Element.
+ * @param child the Element to be appended.
+ */
+ public NodeAppendChild(Element parent, Element child) {
+ this(parent, child, null, null);
+ }
+
+ /**
+ * Full constructor.
+ *
+ * @param parent Parent node to append the child to
+ * @param child Child element
+ * @param plot Plot to register the ID (may be {@code null})
+ * @param id ID to register (may be {@code null}, requires plot to be given)
+ */
+ public NodeAppendChild(Element parent, Element child, SVGPlot plot, String id) {
+ super();
+ this.parent = parent;
+ this.child = child;
+ this.plot = plot;
+ this.id = id;
+ }
+
+ @Override
+ public void run() {
+ parent.appendChild(child);
+ if(plot != null && id != null) {
+ plot.putIdElement(id, child);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java
new file mode 100644
index 00000000..45f2b25d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceAllChildren.java
@@ -0,0 +1,66 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * Runnable wrapper to replace all children of a given node.
+ *
+ * @author Erich Schubert
+ */
+public class NodeReplaceAllChildren extends NodeAppendChild {
+ /**
+ * Trivial constructor.
+ *
+ * @param parent will become the parent of the appended Element.
+ * @param child the Element to be appended.
+ */
+ public NodeReplaceAllChildren(Element parent, Element child) {
+ super(parent, child, null, null);
+ }
+
+ /**
+ * Full constructor.
+ *
+ * @param parent Parent node to append the child to
+ * @param child Child element
+ * @param plot Plot to register the ID (may be {@code null})
+ * @param id ID to register (may be {@code null}, requires plot to be given)
+ */
+ public NodeReplaceAllChildren(Element parent, Element child, SVGPlot plot, String id) {
+ super(parent, child, plot, id);
+ }
+
+ @Override
+ public void run() {
+ // remove all existing children.
+ while(parent.hasChildNodes()) {
+ parent.removeChild(parent.getLastChild());
+ }
+ super.run();
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java
new file mode 100644
index 00000000..27ac9672
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeReplaceByID.java
@@ -0,0 +1,75 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * This helper class will replace a node in an SVG plot. This is a Runnable to
+ * be put on the update queue.
+ *
+ * @author Erich Schubert
+ */
+public class NodeReplaceByID implements Runnable {
+ /**
+ * Plot to work in.
+ */
+ private SVGPlot plot;
+
+ /**
+ * ID of element to replace.
+ */
+ private String id;
+
+ /**
+ * Replacement element.
+ */
+ private Element newe;
+
+ /**
+ * Setup a SVG node replacement.
+ *
+ * @param newe New element
+ * @param plot SVG plot to process
+ * @param id Node ID to replace
+ */
+ public NodeReplaceByID(Element newe, SVGPlot plot, String id) {
+ super();
+ this.newe = newe;
+ this.plot = plot;
+ this.id = id;
+ }
+
+ @Override
+ public void run() {
+ Element olde = plot.getIdElement(id);
+ if(olde != null) {
+ olde.getParentNode().replaceChild(newe, olde);
+ plot.putIdElement(id, newe);
+ }
+ // Note: no warning if it is not possible!
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java
new file mode 100644
index 00000000..bef276dc
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/NodeSubstitute.java
@@ -0,0 +1,62 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+/**
+ * This helper class will replace a node in an SVG plot. This is a Runnable to
+ * be put on the update queue.
+ *
+ * @author Erich Schubert
+ */
+public class NodeSubstitute implements Runnable {
+ /**
+ * Existing element to replace.
+ */
+ private Element prev;
+
+ /**
+ * Replacement element.
+ */
+ private Element newe;
+
+ /**
+ * Setup a SVG node replacement.
+ *
+ * @param prev Existing element
+ * @param newe New element
+ */
+ public NodeSubstitute(Element prev, Element newe) {
+ super();
+ this.prev = prev;
+ this.newe = newe;
+ }
+
+ @Override
+ public void run() {
+ prev.getParentNode().replaceChild(newe, prev);
+ // Note: no warning if it is not possible!
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java
new file mode 100755
index 00000000..c7d48690
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/RemoveCSSClass.java
@@ -0,0 +1,62 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+
+/**
+ * Remove a CSS class to the event target.
+ *
+ * @author Erich Schubert
+ *
+ */
+public class RemoveCSSClass implements EventListener {
+ /**
+ * Class to set
+ */
+ private String cssclass;
+
+ /**
+ * Constructor
+ * @param cssclass class to set
+ */
+ public RemoveCSSClass(String cssclass) {
+ super();
+ this.cssclass = cssclass;
+ }
+
+ /**
+ * Event handler
+ */
+ @Override
+ public void handleEvent(Event evt) {
+ Element e = (Element) evt.getTarget();
+ SVGUtil.removeCSSClass(e, cssclass);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java
new file mode 100644
index 00000000..3f1c5cda
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailRegistryEntry.java
@@ -0,0 +1,251 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import gnu.trove.iterator.TIntObjectIterator;
+import gnu.trove.map.TIntObjectMap;
+import gnu.trove.map.hash.TIntObjectHashMap;
+
+import java.awt.image.RenderedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.SoftReference;
+import java.util.Iterator;
+
+import org.apache.batik.ext.awt.image.GraphicsUtil;
+import org.apache.batik.ext.awt.image.renderable.Filter;
+import org.apache.batik.ext.awt.image.renderable.RedRable;
+import org.apache.batik.ext.awt.image.spi.AbstractRegistryEntry;
+import org.apache.batik.ext.awt.image.spi.ImageTagRegistry;
+import org.apache.batik.ext.awt.image.spi.MagicNumberRegistryEntry;
+import org.apache.batik.ext.awt.image.spi.URLRegistryEntry;
+import org.apache.batik.svggen.ErrorConstants;
+import org.apache.batik.util.ParsedURL;
+import org.apache.batik.util.ParsedURLData;
+import org.apache.batik.util.ParsedURLProtocolHandler;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+
+/**
+ * Access images via an internal image registry.
+ *
+ * @author Erich Schubert
+ */
+public class ThumbnailRegistryEntry extends AbstractRegistryEntry implements URLRegistryEntry, ParsedURLProtocolHandler {
+ /**
+ * ELKI internal thumbnail protocol id.
+ */
+ public static final String INTERNAL_PROTOCOL = "thumb";
+
+ /**
+ * ELKI internal thumbnail protocol prefix
+ */
+ public static final String INTERNAL_PREFIX = INTERNAL_PROTOCOL + ":";
+
+ /**
+ * Mime type
+ */
+ public static final String INTERNAL_MIME_TYPE = "internal/thumb";
+
+ /**
+ * The priority of this entry.
+ */
+ public static final float PRIORITY = 1 * MagicNumberRegistryEntry.PRIORITY;
+
+ /**
+ * The logger class.
+ */
+ private static final Logging LOG = Logging.getLogger(ThumbnailRegistryEntry.class);
+
+ /**
+ * The image cache.
+ */
+ private static final TIntObjectMap<SoftReference<RenderedImage>> images = new TIntObjectHashMap<>();
+
+ /**
+ * Object counter
+ */
+ private static int counter = 1;
+
+ /**
+ * Constructor.
+ *
+ * Note: there will usually be two instances created. One for handling the
+ * image type, one for the URL handling. This is ok.
+ */
+ public ThumbnailRegistryEntry() {
+ super("Internal", PRIORITY, new String[0], new String[] { INTERNAL_MIME_TYPE });
+ if(LOG.isDebuggingFiner()) {
+ LOG.debugFiner("Registry initialized.");
+ }
+ }
+
+ /**
+ * Put an image into the repository (note: the repository is only keeping a
+ * weak reference!)
+ *
+ * @param img Image to put
+ * @return Key
+ */
+ public static int registerImage(RenderedImage img) {
+ synchronized(images) {
+ int key = counter;
+ counter++;
+ assert (images.get(key) == null);
+ images.put(key, new SoftReference<>(img));
+ // Reorganize map, purge old entries
+ if(counter % 50 == 49) {
+ for(TIntObjectIterator<SoftReference<RenderedImage>> iter = images.iterator(); iter.hasNext();) {
+ iter.advance();
+ if(iter.value() == null || iter.value().get() == null) {
+ iter.remove();
+ }
+ }
+ }
+ if(LOG.isDebuggingFiner()) {
+ LOG.debugFiner("Registered image: " + key);
+ }
+ return key;
+ }
+ }
+
+ @Override
+ public boolean isCompatibleURL(ParsedURL url) {
+ // logger.warning("isCompatible " + url.toString());
+ return isCompatibleURLStatic(url);
+ }
+
+ /**
+ * Test for a compatible URL.
+ *
+ * @param url URL
+ * @return Success code
+ */
+ public static boolean isCompatibleURLStatic(ParsedURL url) {
+ return url.getProtocol().equals(INTERNAL_PROTOCOL);
+ }
+
+ @Override
+ public Filter handleURL(ParsedURL url, boolean needRawData) {
+ Filter ret = handleURL(url);
+ if(ret != null) {
+ return ret;
+ }
+ // Image not found in registry.
+ return ImageTagRegistry.getBrokenLinkImage(ThumbnailRegistryEntry.this, ErrorConstants.ERR_IMAGE_DIR_DOES_NOT_EXIST, new Object[0]);
+ }
+
+ /**
+ * Statically handle the URL access.
+ *
+ * @param url URL to access
+ * @return Image, or null
+ */
+ public static Filter handleURL(ParsedURL url) {
+ if(LOG.isDebuggingFiner()) {
+ LOG.debugFiner("handleURL " + url.toString());
+ }
+ if(!isCompatibleURLStatic(url)) {
+ return null;
+ }
+ int id;
+ try {
+ id = Integer.parseInt(url.getPath());
+ }
+ catch(NumberFormatException e) {
+ return null;
+ }
+ SoftReference<RenderedImage> ref = images.get(id);
+ if(ref != null) {
+ RenderedImage ri = ref.get();
+ if(ri == null) {
+ LOG.warning("Referenced image has expired from the cache!");
+ }
+ else {
+ return new RedRable(GraphicsUtil.wrap(ri));
+ }
+ }
+ // Image not found in registry.
+ return null;
+ }
+
+ /**
+ * URL representation for internal URLs.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ class InternalParsedURLData extends ParsedURLData {
+ /**
+ * Constructor.
+ */
+ public InternalParsedURLData(String id) {
+ super();
+ this.protocol = INTERNAL_PROTOCOL;
+ this.contentType = INTERNAL_MIME_TYPE;
+ this.path = id;
+ }
+
+ @Override
+ public String getContentType(String userAgent) {
+ return INTERNAL_MIME_TYPE;
+ }
+
+ @Override
+ public boolean complete() {
+ return true;
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Override
+ public InputStream openStream(String userAgent, Iterator mimeTypes) throws IOException {
+ // Return null, since we don't want to use streams.
+ return null;
+ }
+ }
+
+ @Override
+ public ParsedURLData parseURL(String urlStr) {
+ if(LOG.isDebuggingFinest()) {
+ LOG.debugFinest("parseURL: " + urlStr);
+ }
+ if(urlStr.startsWith(INTERNAL_PREFIX)) {
+ InternalParsedURLData ret = new InternalParsedURLData(urlStr.substring(INTERNAL_PREFIX.length()));
+ return ret;
+ }
+ return null;
+ }
+
+ @Override
+ public ParsedURLData parseURL(ParsedURL basepurl, String urlStr) {
+ // Won't happen in a relative way anyway, and is not particularly
+ // supported (as the objects might be dropped from the cache)
+ return parseURL(urlStr);
+ }
+
+ @Override
+ public String getProtocolHandled() {
+ return INTERNAL_PROTOCOL;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java
new file mode 100644
index 00000000..b0c16ef1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/ThumbnailTranscoder.java
@@ -0,0 +1,72 @@
+package de.lmu.ifi.dbs.elki.visualization.batikutil;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.BufferedImage;
+
+import org.apache.batik.transcoder.TranscoderException;
+import org.apache.batik.transcoder.TranscoderOutput;
+import org.apache.batik.transcoder.image.ImageTranscoder;
+
+/**
+ * Transcode images to in-memory thumbnails.
+ *
+ * @author Erich Schubert
+ */
+public class ThumbnailTranscoder extends ImageTranscoder {
+ /**
+ * Last image produced.
+ */
+ private BufferedImage lastimg;
+
+ /**
+ * Constructor.
+ */
+ public ThumbnailTranscoder() {
+ super();
+ hints.put(KEY_FORCE_TRANSPARENT_WHITE, Boolean.FALSE);
+ }
+
+ @Override
+ public BufferedImage createImage(int width, int height) {
+ return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ }
+
+ /**
+ * Output will be ignored!
+ */
+ @Override
+ public void writeImage(BufferedImage img, TranscoderOutput output) throws TranscoderException {
+ lastimg = img;
+ }
+
+ /**
+ * Get the latest image produced.
+ *
+ * @return the last image produced
+ */
+ public BufferedImage getLastImage() {
+ return lastimg;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java
new file mode 100755
index 00000000..280c5046
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/batikutil/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Commonly used functionality useful for Apache Batik.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.batikutil; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java
new file mode 100755
index 00000000..dc668924
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ColorLibrary.java
@@ -0,0 +1,93 @@
+package de.lmu.ifi.dbs.elki.visualization.colors;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Color scheme interface
+ *
+ * @author Erich Schubert
+ */
+public interface ColorLibrary {
+ /**
+ * List of line colors
+ */
+ final static String COLOR_LINE_COLORS = "line.colors";
+
+ /**
+ * Named color for the page background
+ */
+ final static String COLOR_PAGE_BACKGROUND = "page.background";
+
+ /**
+ * Named color for a typical axis
+ */
+ final static String COLOR_AXIS_LINE = "axis.line";
+
+ /**
+ * Named color for a typical axis tick mark
+ */
+ final static String COLOR_AXIS_TICK = "axis.tick";
+
+ /**
+ * Named color for a typical axis tick mark
+ */
+ final static String COLOR_AXIS_MINOR_TICK = "axis.tick.minor";
+
+ /**
+ * Named color for a typical axis label
+ */
+ final static String COLOR_AXIS_LABEL = "axis.label";
+
+ /**
+ * Named color for the background of the key box
+ */
+ final static String COLOR_KEY_BACKGROUND = "key.background";
+
+ /**
+ * Named color for a label in the key part
+ */
+ final static String COLOR_KEY_LABEL = "key.label";
+
+ /**
+ * Background color for plot area
+ */
+ final static String COLOR_PLOT_BACKGROUND = "plot.background";
+
+ /**
+ * Return the number of native colors available. These are guaranteed to be
+ * unique.
+ *
+ * @return number of native colors
+ */
+ public int getNumberOfNativeColors();
+
+ /**
+ * Return the i'th color.
+ *
+ * @param index color index
+ * @return color in hexadecimal notation (#aabbcc) or color name ("red") as
+ * valid in CSS and SVG.
+ */
+ public String getColor(int index);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java
new file mode 100644
index 00000000..60ae0a81
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/ListBasedColorLibrary.java
@@ -0,0 +1,73 @@
+package de.lmu.ifi.dbs.elki.visualization.colors;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+
+/**
+ * Color library using the color names from a list.
+ *
+ * @author Erich Schubert
+ */
+public class ListBasedColorLibrary implements ColorLibrary {
+ /**
+ * Array of color names.
+ */
+ private String[] colors;
+
+ /**
+ * Color scheme name
+ */
+ private String name;
+
+ /**
+ * Constructor without a properties file name.
+ *
+ * @param colors Colors
+ * @param name Library name
+ */
+ public ListBasedColorLibrary(String[] colors, String name) {
+ this.colors = colors;
+ this.name = name;
+ }
+
+ @Override
+ public String getColor(int index) {
+ return colors[Math.abs(index) % colors.length];
+ }
+
+ @Override
+ public int getNumberOfNativeColors() {
+ return colors.length;
+ }
+
+ /**
+ * Get the color scheme name.
+ *
+ * @return the name
+ */
+ protected String getName() {
+ return name;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java
new file mode 100755
index 00000000..cf09bd21
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/colors/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Color scheme handling for ELKI.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.colors; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java
new file mode 100755
index 00000000..56d15810
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClass.java
@@ -0,0 +1,317 @@
+package de.lmu.ifi.dbs.elki.visualization.css;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+
+import de.lmu.ifi.dbs.elki.utilities.pairs.Pair;
+
+/**
+ * Class representing a single CSS class.
+ *
+ * @author Erich Schubert
+ */
+public class CSSClass {
+ /**
+ * CSS class name
+ */
+ private String name;
+
+ /**
+ * Actual CSS statements
+ */
+ private Collection<Pair<String, String>> statements;
+
+ /**
+ * Owner.
+ */
+ private WeakReference<Object> owner;
+
+ /**
+ * Full constructor
+ *
+ * @param owner Class owner (to detect conflicts)
+ * @param name Class name
+ * @param statements Collection of CSS statements
+ */
+ public CSSClass(Object owner, String name, Collection<Pair<String, String>> statements) {
+ this.owner = new WeakReference<>(owner);
+ this.name = name;
+ this.statements = statements;
+ if (!checkName(name)) {
+ throw new InvalidCSS("Given name is not a valid CSS class name.");
+ }
+ if (this.statements != null) {
+ if (!checkCSSStatements(this.statements)) {
+ throw new InvalidCSS("Invalid statement in CSS class "+name);
+ }
+ } else {
+ // if needed, use an array list.
+ this.statements = new ArrayList<>();
+ }
+ }
+
+ /**
+ * Simplified constructor, empty statements list.
+ *
+ * @param owner Class owner.
+ * @param name Class name.
+ */
+ public CSSClass(Object owner, String name) {
+ this(owner, name, (Collection<Pair<String,String>>) null);
+ }
+
+ /**
+ * Cloning constructor
+ *
+ * @param owner Class owner.
+ * @param name Class name.
+ * @param other Class to clone
+ */
+ public CSSClass(Object owner, String name, CSSClass other) {
+ this(owner, name, new ArrayList<>(other.statements));
+ }
+
+ /**
+ * Verify that the name is an admissible CSS class name.
+ *
+ * TODO: implement.
+ *
+ * @param name name to use
+ * @return true if valid CSS class name
+ */
+ public static boolean checkName(String name) {
+ // TODO: implement a sanity check - regexp?
+ return (name != null);
+ }
+
+ /**
+ * Return a sanitized version of the given string.
+ *
+ * TODO: implement extensive checks.
+ *
+ * @param name name to sanitize
+ * @return Sanitized version.
+ */
+ public static String sanitizeName(String name) {
+ // TODO: implement a sanitization - regexp?
+ return name;
+ }
+
+ /**
+ * Validate a single CSS statement.
+ *
+ * TODO: implement extensive checks.
+ *
+ * @param key Key
+ * @param value Value
+ * @return true if valid statement.
+ */
+ public static boolean checkCSSStatement(String key, String value) {
+ // TODO: implement more extensive checks!
+ return (key != null) && (value != null);
+ }
+
+ /**
+ * Validate a set of CSS statements.
+ *
+ * TODO: checks are currently not very extensive.
+ *
+ * @param statements Statements to check
+ * @return true if valid
+ */
+ public static boolean checkCSSStatements(Collection<Pair<String,String>> statements) {
+ for (Pair<String, String> pair : statements) {
+ if (!checkCSSStatement(pair.getFirst(), pair.getSecond())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Get the class name.
+ *
+ * @return class name.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Set the class name.
+ *
+ * @param name new class name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Get class owner.
+ *
+ * @return class owner.
+ */
+ public Object getOwner() {
+ return this.owner.get();
+ }
+
+ /**
+ * Get the current value of a particular CSS statement.
+ *
+ * @param key statement key.
+ * @return current value or null.
+ */
+ public String getStatement(String key) {
+ for (Pair<String, String> pair : statements) {
+ if (pair.getFirst().equals(key)) {
+ return pair.getSecond();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get read-only collection access to all statements.
+ *
+ * @return Collection
+ */
+ public Collection<Pair<String, String>> getStatements() {
+ return Collections.unmodifiableCollection(statements);
+ }
+
+ /**
+ * Set a CSS statement.
+ *
+ * @param key Statement key.
+ * @param value Value or null (to unset)
+ */
+ public void setStatement(String key, String value) {
+ if (value != null) {
+ if (!checkCSSStatement(key, value)) {
+ throw new InvalidCSS("Invalid CSS statement.");
+ }
+ }
+ for (Pair<String, String> pair : statements) {
+ if (pair.getFirst().equals(key)) {
+ if (value != null) {
+ pair.setSecond(value);
+ } else {
+ statements.remove(pair);
+ }
+ return;
+ }
+ }
+ if (value != null) {
+ statements.add(new Pair<>(key, value));
+ }
+ }
+
+ /**
+ * Set a CSS statement.
+ *
+ * @param key Statement key.
+ * @param value Value
+ */
+ public void setStatement(String key, int value) {
+ setStatement(key, Integer.toString(value));
+ }
+
+ /**
+ * Set a CSS statement.
+ *
+ * @param key Statement key.
+ * @param value Value
+ */
+ public void setStatement(String key, double value) {
+ setStatement(key, Double.toString(value));
+ }
+
+ /**
+ * Remove a CSS statement.
+ *
+ * @param key Statement key.
+ */
+ public void removeStatement(String key) {
+ setStatement(key, null);
+ }
+
+ /**
+ * Append CSS definition to a stream
+ *
+ * @param buf String buffer to append to.
+ */
+ public void appendCSSDefinition(StringBuilder buf) {
+ buf.append("\n.");
+ buf.append(name);
+ buf.append('{');
+ for (Pair<String, String> pair : statements) {
+ buf.append(pair.getFirst());
+ buf.append(':');
+ buf.append(pair.getSecond());
+ buf.append(";\n");
+ }
+ buf.append("}\n");
+ }
+
+ /**
+ * Exception class thrown when encountering invalid CSS.
+ *
+ * @apiviz.exclude
+ */
+ public class InvalidCSS extends RuntimeException {
+ /**
+ * Constructor. See {@link RuntimeException}.
+ *
+ * @param msg Error message.
+ */
+ public InvalidCSS(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Serial version UID.
+ */
+ private static final long serialVersionUID = 3130536799704124363L;
+ }
+
+ /**
+ * Render CSS class to inline formatting
+ *
+ * @return string rendition of CSS for inline use
+ */
+ public String inlineCSS() {
+ StringBuilder buf = new StringBuilder();
+ for (Pair<String, String> pair : statements) {
+ buf.append(pair.getFirst());
+ buf.append(':');
+ buf.append(pair.getSecond());
+ buf.append(';');
+ }
+ return buf.toString();
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java
new file mode 100755
index 00000000..003f6f74
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/CSSClassManager.java
@@ -0,0 +1,216 @@
+package de.lmu.ifi.dbs.elki.visualization.css;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Collection;
+import java.util.HashMap;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Text;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Manager class to track CSS classes used in a particular SVG document.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has de.lmu.ifi.dbs.elki.visualization.css.CSSClass
+ */
+public class CSSClassManager {
+ /**
+ * Store the contained CSS classes.
+ */
+ private HashMap<String, CSSClass> store = new HashMap<>();
+
+ /**
+ * Add a single class to the map.
+ *
+ * @param clss new CSS class
+ * @return existing (old) class
+ * @throws CSSNamingConflict when a class of the same name but different owner object exists.
+ */
+ public CSSClass addClass(CSSClass clss) throws CSSNamingConflict {
+ CSSClass existing = store.get(clss.getName());
+ if (existing != null && existing.getOwner() != null && existing.getOwner() != clss.getOwner()) {
+ throw new CSSNamingConflict("CSS class naming conflict between "+clss.getOwner().toString()+" and "+existing.getOwner().toString());
+ }
+ return store.put(clss.getName(), clss);
+ }
+
+ /**
+ * Remove a single CSS class from the map.
+ * Note that classes are removed by reference, not by name!
+ *
+ * @param clss Class to remove
+ */
+ public synchronized void removeClass(CSSClass clss) {
+ CSSClass existing = store.get(clss.getName());
+ if (existing == clss) {
+ store.remove(existing.getName());
+ }
+ }
+
+ /**
+ * Retrieve a single class by name and owner
+ *
+ * @param name Class name
+ * @param owner Class owner
+ * @return existing (old) class
+ * @throws CSSNamingConflict if an owner was specified and doesn't match
+ */
+ public CSSClass getClass(String name, Object owner) throws CSSNamingConflict {
+ CSSClass existing = store.get(name);
+ // Not found.
+ if (existing == null) {
+ return null;
+ }
+ // Different owner
+ if (owner != null && existing.getOwner() != owner) {
+ throw new CSSNamingConflict("CSS class naming conflict between "+owner.toString()+" and "+existing.getOwner().toString());
+ }
+ return existing;
+ }
+
+ /**
+ * Retrieve a single class by name only
+ *
+ * @param name CSS class name
+ * @return existing (old) class
+ */
+ public CSSClass getClass(String name) {
+ return store.get(name);
+ }
+
+ /**
+ * Check if a name is already used in the classes.
+ *
+ * @param name CSS class name
+ * @return true if the class name is already used.
+ */
+ public boolean contains(String name) {
+ return store.containsKey(name);
+ }
+
+ /**
+ * Serialize managed CSS classes to rule file.
+ *
+ * @param buf String buffer
+ */
+ public void serialize(StringBuilder buf) {
+ for (CSSClass clss : store.values()) {
+ clss.appendCSSDefinition(buf);
+ }
+ }
+
+ /**
+ * Get all CSS classes in this manager.
+ *
+ * @return CSS classes.
+ */
+ public Collection<CSSClass> getClasses() {
+ return store.values();
+ }
+
+ /**
+ * Check whether or not CSS classes of two plots can be merged
+ *
+ * @param other Other class
+ * @return true if able to merge
+ */
+ public boolean testMergeable(CSSClassManager other) {
+ for (CSSClass clss : other.getClasses()) {
+ CSSClass existing = store.get(clss.getName());
+ // Check for a naming conflict.
+ if (existing != null && existing.getOwner() != null && clss.getOwner() != null && existing.getOwner() != clss.getOwner()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Merge CSS classes, for example to merge two plots.
+ *
+ * @param other Other class to merge with
+ * @return success code
+ * @throws CSSNamingConflict If there is a naming conflict.
+ */
+ public boolean mergeCSSFrom(CSSClassManager other) throws CSSNamingConflict {
+ for (CSSClass clss : other.getClasses()) {
+ this.addClass(clss);
+ }
+ return true;
+ }
+
+ /**
+ * Class to signal a CSS naming conflict.
+ *
+ * @apiviz.exclude
+ */
+ public class CSSNamingConflict extends Exception {
+ /**
+ * Serial version UID
+ */
+ private static final long serialVersionUID = 4163822727195636747L;
+
+ /**
+ * Exception to signal a CSS naming conflict.
+ *
+ * @param msg Exception message
+ */
+ public CSSNamingConflict(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Update the text contents of an existing style element.
+ *
+ * @param document Document element (factory)
+ * @param style Style element
+ */
+ public void updateStyleElement(Document document, Element style) {
+ StringBuilder buf = new StringBuilder();
+ serialize(buf);
+ Text cont = document.createTextNode(buf.toString());
+ while (style.hasChildNodes()) {
+ style.removeChild(style.getFirstChild());
+ }
+ style.appendChild(cont);
+ }
+
+ /**
+ * Make a (filled) CSS style element for the given document.
+ *
+ * @param document Document
+ * @return Style element
+ */
+ public Element makeStyleElement(Document document) {
+ Element style = SVGUtil.makeStyleElement(document);
+ updateStyleElement(document, style);
+ return style;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java
new file mode 100755
index 00000000..1c936cf1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/css/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Managing CSS styles / classes.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.css; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java
new file mode 100644
index 00000000..64ddfaa3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultVisualizer.java
@@ -0,0 +1,190 @@
+package de.lmu.ifi.dbs.elki.visualization.gui;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import javax.swing.JFrame;
+
+import de.lmu.ifi.dbs.elki.gui.GUIUtil;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHandler;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.utilities.Alias;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.StringParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerParameterizer;
+
+/**
+ * Handler to process and visualize a Result.
+ *
+ * @author Erich Schubert
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.composedOf VisualizerParameterizer
+ * @apiviz.uses ResultWindow oneway
+ */
+@Alias({ "visualizer", "vis", "ResultVisualizer" })
+public class ResultVisualizer implements ResultHandler {
+ /**
+ * Get a logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(ResultVisualizer.class);
+
+ /**
+ * Stores the set title.
+ */
+ String title;
+
+ /**
+ * Default title
+ */
+ protected static final String DEFAULT_TITLE = "ELKI Result Visualization";
+
+ /**
+ * Visualization manager.
+ */
+ VisualizerParameterizer manager;
+
+ /**
+ * Single view mode
+ */
+ boolean single;
+
+ /**
+ * Current result window.
+ */
+ ResultWindow window;
+
+ /**
+ * Constructor.
+ *
+ * @param title Window title
+ * @param manager Parameterization manager for visualizers
+ * @param single Flag to indicat single-view mode.
+ */
+ public ResultVisualizer(String title, VisualizerParameterizer manager, boolean single) {
+ super();
+ this.title = title;
+ this.manager = manager;
+ this.single = single;
+ }
+
+ @Override
+ public void processNewResult(final ResultHierarchy hier, final Result result) {
+ if(window == null) {
+ if(title == null) {
+ title = VisualizerParameterizer.getTitle(ResultUtil.findDatabase(hier), result);
+ if(title == null) {
+ title = DEFAULT_TITLE;
+ }
+ }
+
+ GUIUtil.setLookAndFeel();
+ VisualizerContext context = manager.newContext(hier, result);
+ window = new ResultWindow(title, context, single);
+ }
+
+ javax.swing.SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ window.setVisible(true);
+ window.setExtendedState(window.getExtendedState() | JFrame.MAXIMIZED_BOTH);
+ }
+ catch(Throwable e) {
+ LOG.exception("Error in starting visualizer window.", e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Parameter to specify the window title
+ * <p>
+ * Key: {@code -vis.window.title}
+ * </p>
+ * <p>
+ * Default value: "ELKI Result Visualization"
+ * </p>
+ */
+ public static final OptionID WINDOW_TITLE_ID = new OptionID("vis.window.title", "Title to use for visualization window.");
+
+ /**
+ * Flag to set single display
+ *
+ * <p>
+ * Key: -vis.single
+ * </p>
+ */
+ public static final OptionID SINGLE_ID = new OptionID("vis.window.single", "Embed visualizers in a single window, not using thumbnails and detail views.");
+
+ /**
+ * Stores the set title.
+ */
+ String title;
+
+ /**
+ * Visualization manager.
+ */
+ VisualizerParameterizer manager;
+
+ /**
+ * Single view mode.
+ */
+ boolean single = false;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ StringParameter titleP = new StringParameter(WINDOW_TITLE_ID);
+ titleP.setOptional(true);
+ if(config.grab(titleP)) {
+ title = titleP.getValue();
+ }
+ Flag singleF = new Flag(SINGLE_ID);
+ if(config.grab(singleF)) {
+ single = singleF.isTrue();
+ }
+ manager = config.tryInstantiate(VisualizerParameterizer.class);
+ }
+
+ @Override
+ protected ResultVisualizer makeInstance() {
+ return new ResultVisualizer(title, manager, single);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java
new file mode 100644
index 00000000..f82a21a0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/ResultWindow.java
@@ -0,0 +1,641 @@
+package de.lmu.ifi.dbs.elki.visualization.gui;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import javax.swing.ImageIcon;
+import javax.swing.JCheckBoxMenuItem;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.JRadioButtonMenuItem;
+import javax.swing.SwingUtilities;
+
+import org.apache.batik.swing.svg.GVTTreeBuilderAdapter;
+import org.apache.batik.swing.svg.GVTTreeBuilderEvent;
+
+import de.lmu.ifi.dbs.elki.KDDTask;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationListener;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuAction;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuToggle;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.JSVGSynchronizedCanvas;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.LazyCanvasResizer;
+import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.DetailViewSelectedEvent;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.OverviewPlot;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.savedialog.SVGSaveDialog;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * Swing window to manage a particular result visualization.
+ *
+ * Yes, this is very basic and ad-hoc. Feel free to contribute something more
+ * advanced to ELKI!
+ *
+ * @author Erich Schubert
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.composedOf JSVGSynchronizedCanvas
+ * @apiviz.composedOf OverviewPlot
+ * @apiviz.composedOf SelectionTableWindow
+ * @apiviz.composedOf SVGSaveDialog
+ * @apiviz.composedOf LazyCanvasResizer
+ * @apiviz.has VisualizerContext
+ * @apiviz.uses DetailView oneway
+ * @apiviz.uses DetailViewSelectedEvent oneway - - reacts to
+ */
+public class ResultWindow extends JFrame implements ResultListener, VisualizationListener {
+ /**
+ * Serial version
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Get a logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(ResultWindow.class);
+
+ /**
+ * Dynamic menu.
+ *
+ * @apiviz.exclude
+ */
+ public class DynamicMenu {
+ /**
+ * Menubar component
+ */
+ private JMenuBar menubar;
+
+ /**
+ * File menu.
+ */
+ private JMenu filemenu;
+
+ /**
+ * The "Overview" button, which goes to the overview view.
+ */
+ private JMenuItem overviewItem;
+
+ /**
+ * The "Quit" button, to close the application.
+ */
+ private JMenuItem quitItem;
+
+ /**
+ * The "Export" button, to save the image
+ */
+ private JMenuItem exportItem;
+
+ /**
+ * The "tabular edit" item.
+ */
+ private JMenuItem editItem;
+
+ /**
+ * The "Visualizers" button, to enable/disable visualizers
+ */
+ private JMenu visualizersMenu;
+
+ /**
+ * Simplify the menu.
+ */
+ protected boolean simplify = true;
+
+ /**
+ * Constructor.
+ */
+ public DynamicMenu() {
+ menubar = new JMenuBar();
+ filemenu = new JMenu("File");
+ filemenu.setMnemonic(KeyEvent.VK_F);
+
+ // setup buttons
+ if(!single) {
+ overviewItem = new JMenuItem("Open Overview");
+ overviewItem.setMnemonic(KeyEvent.VK_O);
+ overviewItem.setEnabled(false);
+ overviewItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ showOverview();
+ }
+ });
+ filemenu.add(overviewItem);
+ }
+
+ exportItem = new JMenuItem("Export Plot");
+ exportItem.setMnemonic(KeyEvent.VK_E);
+ exportItem.setEnabled(false);
+ exportItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ saveCurrentPlot();
+ }
+ });
+ filemenu.add(exportItem);
+
+ editItem = new JMenuItem("Table View/Edit");
+ editItem.setMnemonic(KeyEvent.VK_T);
+ editItem.addActionListener(new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ showTableView();
+ }
+ });
+ // FIXME: re-add when it is working again.
+ // filemenu.add(editItem);
+
+ quitItem = new JMenuItem("Quit");
+ quitItem.setMnemonic(KeyEvent.VK_Q);
+ quitItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ close();
+ }
+ });
+
+ filemenu.add(quitItem);
+ menubar.add(filemenu);
+
+ visualizersMenu = new JMenu("Visualizers");
+ visualizersMenu.setMnemonic(KeyEvent.VK_V);
+ menubar.add(visualizersMenu);
+ }
+
+ /**
+ * Update the visualizer menus.
+ */
+ protected synchronized void updateVisualizerMenus() {
+ Projection proj = null;
+ if(svgCanvas.getPlot() instanceof DetailView) {
+ PlotItem item = ((DetailView) svgCanvas.getPlot()).getPlotItem();
+ proj = item.proj;
+ }
+ menubar.removeAll();
+ menubar.add(filemenu);
+ ResultHierarchy hier = context.getHierarchy();
+ Hierarchy<Object> vistree = context.getVisHierarchy();
+ Result start = context.getBaseResult();
+ ArrayList<JMenuItem> items = new ArrayList<>();
+ if(start == null) {
+ for(Hierarchy.Iter<Result> iter = hier.iterAll(); iter.valid(); iter.advance()) {
+ if(hier.numParents(iter.get()) == 0) {
+ recursiveBuildMenu(items, iter.get(), hier, vistree, proj);
+ }
+ }
+ }
+ else {
+ for(Hierarchy.Iter<Result> iter = hier.iterChildren(start); iter.valid(); iter.advance()) {
+ recursiveBuildMenu(items, iter.get(), hier, vistree, proj);
+ }
+ }
+ // Add all items.
+ for(JMenuItem item : items) {
+ menubar.add(item);
+ }
+ menubar.revalidate();
+ menubar.repaint();
+ }
+
+ private void recursiveBuildMenu(Collection<JMenuItem> items, Object r, ResultHierarchy hier, Hierarchy<Object> vistree, Projection proj) {
+ // Make a submenu for this element
+ final String nam;
+ if(r instanceof Result) {
+ nam = ((Result) r).getLongName();
+ }
+ else if(r instanceof VisualizationItem) {
+ nam = ((VisualizationItem) r).getMenuName();
+ }
+ else {
+ return;
+ }
+ ArrayList<JMenuItem> subitems = new ArrayList<>();
+ // Add menus for any child results
+ if(r instanceof Result) {
+ for(Hierarchy.Iter<Result> iter = hier.iterChildren((Result) r); iter.valid(); iter.advance()) {
+ recursiveBuildMenu(subitems, iter.get(), hier, vistree, proj);
+ }
+ }
+ // Add visualizers:
+ for(Hierarchy.Iter<Object> iter = vistree.iterChildren(r); iter.valid(); iter.advance()) {
+ recursiveBuildMenu(subitems, iter.get(), hier, vistree, proj);
+ }
+
+ // Item for the visualizer
+ JMenuItem item = null;
+ if(proj == null) {
+ item = makeMenuItemForVisualizer(r);
+ }
+ else {
+ // Only include items that belong to different projections:
+ for(Hierarchy.Iter<Object> iter = vistree.iterAncestorsSelf(r); iter.valid(); iter.advance()) {
+ if(iter.get() == proj.getProjector()) {
+ item = makeMenuItemForVisualizer(r);
+ break;
+ }
+ }
+ }
+ final int numchild = subitems.size();
+ if(numchild == 0) {
+ if(item != null) {
+ items.add(item);
+ }
+ return;
+ }
+ if(simplify && numchild == 1) {
+ JMenuItem a = subitems.get(0);
+ if(a instanceof JMenu) {
+ if(nam != null) {
+ a.setText(nam + " " + a.getText());
+ }
+ items.add(a);
+ return;
+ }
+ }
+ JMenu submenu = new JMenu((nam != null) ? nam : "unnamed");
+ if(item != null) {
+ submenu.add(item);
+ }
+ for(JMenuItem subitem : subitems) {
+ submenu.add(subitem);
+ }
+ items.add(submenu);
+ }
+
+ private JMenuItem makeMenuItemForVisualizer(Object r) {
+ if(r instanceof VisualizationMenuAction) {
+ final VisualizationMenuAction action = (VisualizationMenuAction) r;
+ JMenuItem visItem = new JMenuItem(action.getMenuName());
+ visItem.setEnabled(action.enabled());
+ visItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ action.activate();
+ }
+ });
+ return visItem;
+ }
+ if(r instanceof VisualizationMenuToggle) {
+ final VisualizationMenuToggle toggle = (VisualizationMenuToggle) r;
+ final JCheckBoxMenuItem visItem = new JCheckBoxMenuItem(toggle.getMenuName(), toggle.active());
+ visItem.setEnabled(toggle.enabled());
+ visItem.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ toggle.toggle();
+ }
+ });
+ return visItem;
+ }
+ if(!(r instanceof VisualizationTask)) {
+ return null;
+ }
+ final VisualizationTask v = (VisualizationTask) r;
+ JMenuItem item;
+
+ // Currently enabled?
+ final String name = v.getMenuName();
+ boolean enabled = v.visible;
+ boolean istool = v.tool;
+ if(!istool) {
+ final JCheckBoxMenuItem visItem = new JCheckBoxMenuItem(name, enabled);
+ visItem.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ VisualizationTree.setVisible(context, v, visItem.getState());
+ }
+ });
+ item = visItem;
+ }
+ else {
+ final JRadioButtonMenuItem visItem = new JRadioButtonMenuItem(name, enabled);
+ visItem.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ VisualizationTree.setVisible(context, v, visItem.isSelected());
+ }
+ });
+ item = visItem;
+ }
+ return item;
+ }
+
+ /**
+ * Get the menu bar component.
+ *
+ * @return Menu bar component
+ */
+ public JMenuBar getMenuBar() {
+ return menubar;
+ }
+
+ /**
+ * Enable / disable the overview menu.
+ *
+ * @param b Flag
+ */
+ public void enableOverview(boolean b) {
+ if(overviewItem != null) {
+ overviewItem.setEnabled(b);
+ }
+ }
+
+ /**
+ * Enable / disable the export menu.
+ *
+ * @param b Flag
+ */
+ public void enableExport(boolean b) {
+ exportItem.setEnabled(b);
+ }
+ }
+
+ private DynamicMenu menubar;
+
+ /**
+ * The SVG canvas.
+ */
+ private JSVGSynchronizedCanvas svgCanvas;
+
+ /**
+ * The overview plot.
+ */
+ private OverviewPlot overview;
+
+ /**
+ * Visualizer context
+ */
+ protected VisualizerContext context;
+
+ /**
+ * Currently selected subplot.
+ */
+ private DetailView currentSubplot = null;
+
+ /**
+ * Single view mode. No overview / detail view split
+ */
+ private boolean single = false;
+
+ /**
+ * Constructor.
+ *
+ * @param title Window title
+ * @param context Visualizer context
+ * @param single Single visualization mode
+ */
+ public ResultWindow(String title, VisualizerContext context, boolean single) {
+ super(title);
+ this.context = context;
+ this.single = single;
+
+ // close handler
+ this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ // ELKI icon
+ try {
+ setIconImage(new ImageIcon(KDDTask.class.getResource("elki-icon.png")).getImage());
+ }
+ catch(Exception e) {
+ // Ignore - icon not found is not fatal.
+ }
+
+ // Create a panel and add the button, status label and the SVG canvas.
+ final JPanel panel = new JPanel(new BorderLayout());
+
+ menubar = new DynamicMenu();
+ panel.add("North", menubar.getMenuBar());
+
+ svgCanvas = new JSVGSynchronizedCanvas();
+ panel.add("Center", svgCanvas);
+
+ this.getContentPane().add(panel);
+
+ overview = new OverviewPlot(context, single);
+ // when a subplot is clicked, show the selected subplot.
+ overview.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if(e instanceof DetailViewSelectedEvent) {
+ showSubplot((DetailViewSelectedEvent) e);
+ }
+ if(OverviewPlot.OVERVIEW_REFRESHING == e.getActionCommand()) {
+ if(currentSubplot == null) {
+ showPlot(null);
+ }
+ }
+ if(OverviewPlot.OVERVIEW_REFRESHED == e.getActionCommand()) {
+ if(currentSubplot == null) {
+ showOverview();
+ }
+ }
+ }
+ });
+
+ // handle screen size
+ Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+ overview.screenwidth = dim.width;
+ overview.screenheight = dim.height;
+
+ // Maximize.
+ this.setSize(dim.width - 50, dim.height - 50);
+ this.setExtendedState(JFrame.MAXIMIZED_BOTH);
+
+ // resize listener
+ final LazyCanvasResizer listener = new LazyCanvasResizer(this, 0.1) {
+ @Override
+ public void executeResize(double newratio) {
+ ResultWindow.this.handleResize(newratio);
+ }
+ };
+ this.addComponentListener(listener);
+ svgCanvas.addGVTTreeBuilderListener(new GVTTreeBuilderAdapter() {
+ @Override
+ public void gvtBuildCompleted(GVTTreeBuilderEvent arg0) {
+ // Supposedly in Swing thread.
+ menubar.updateVisualizerMenus();
+ }
+ });
+
+ context.addResultListener(this);
+ context.addVisualizationListener(this);
+ overview.initialize(listener.getCurrentRatio());
+ }
+
+ @Override
+ public void dispose() {
+ context.removeResultListener(this);
+ context.removeVisualizationListener(this);
+ svgCanvas.setPlot(null);
+ overview.destroy();
+ if(currentSubplot != null) {
+ currentSubplot.dispose();
+ currentSubplot = null;
+ }
+ super.dispose();
+ }
+
+ /**
+ * Close the visualizer window.
+ */
+ protected void close() {
+ this.setVisible(false);
+ this.dispose();
+ }
+
+ /**
+ * Navigate to the overview plot.
+ */
+ public void showOverview() {
+ if(currentSubplot != null) {
+ currentSubplot.destroy();
+ }
+ currentSubplot = null;
+ showPlot(overview.getPlot());
+ }
+
+ /**
+ * Navigate to a subplot.
+ *
+ * @param e
+ */
+ protected void showSubplot(DetailViewSelectedEvent e) {
+ if(!single) {
+ currentSubplot = e.makeDetailView();
+ showPlot(currentSubplot);
+ }
+ }
+
+ /**
+ * Navigate to a particular plot.
+ *
+ * @param plot Plot to show.
+ */
+ private void showPlot(final SVGPlot plot) {
+ if(svgCanvas.getPlot() instanceof DetailView) {
+ ((DetailView) svgCanvas.getPlot()).destroy();
+ }
+ svgCanvas.setPlot(plot);
+ menubar.enableOverview(plot != overview.getPlot());
+ menubar.enableExport(plot != null);
+ updateVisualizerMenus();
+ }
+
+ /**
+ * Save/export the current plot.
+ */
+ protected void saveCurrentPlot() {
+ final SVGPlot currentPlot = svgCanvas.getPlot();
+ if(currentPlot == null) {
+ LOG.warning("saveCurrentPlot() called without a visible plot!");
+ return;
+ }
+ SVGSaveDialog.showSaveDialog(currentPlot, 512, 512);
+ }
+
+ /**
+ * Show a tabular view
+ */
+ protected void showTableView() {
+ (new SelectionTableWindow(context)).setVisible(true);
+ }
+
+ /**
+ * Refresh the overview
+ */
+ protected void update() {
+ updateVisualizerMenus();
+ if(currentSubplot != null) {
+ showPlot(currentSubplot);
+ }
+ overview.lazyRefresh();
+ }
+
+ /**
+ * Handle a resize event.
+ *
+ * @param newratio New window size ratio.
+ */
+ protected void handleResize(double newratio) {
+ if(currentSubplot == null) {
+ ResultWindow.this.overview.setRatio(newratio);
+ }
+ }
+
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ updateVisualizerMenus();
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ updateVisualizerMenus();
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ updateVisualizerMenus();
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem item) {
+ updateVisualizerMenus();
+ }
+
+ /**
+ * Update visualizer menus, but only from Swing thread.
+ */
+ private void updateVisualizerMenus() {
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ menubar.updateVisualizerMenus();
+ }
+ });
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java
new file mode 100644
index 00000000..5e9af1ed
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SelectionTableWindow.java
@@ -0,0 +1,376 @@
+package de.lmu.ifi.dbs.elki.visualization.gui;
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.table.AbstractTableModel;
+
+import de.lmu.ifi.dbs.elki.KDDTask;
+import de.lmu.ifi.dbs.elki.data.ClassLabel;
+import de.lmu.ifi.dbs.elki.data.SimpleClassLabel;
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.UpdatableDatabase;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreEvent;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.ArrayModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.ModifiableRelation;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SelectionResult;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+
+/**
+ * Visualizes selected Objects in a JTable, objects can be selected, changed and
+ * deleted
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ */
+// FIXME: INCOMPLETE TRANSITION TO MULTI-REPRESENTED DATA
+public class SelectionTableWindow extends JFrame implements DataStoreListener, ResultListener {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Selected data objects";
+
+ /**
+ * Serial version
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The JTable
+ */
+ private JTable table;
+
+ /**
+ * Button to close the window
+ */
+ private JButton closeButton;
+
+ /**
+ * Button to delete the selected objects
+ */
+ private JButton deleteButton;
+
+ /**
+ * The table model
+ */
+ private DatabaseTableModel dotTableModel = new DatabaseTableModel();
+
+ /**
+ * The logger
+ */
+ private static final Logging LOG = Logging.getLogger(SelectionTableWindow.class);
+
+ /**
+ * The DBIDs to display
+ */
+ ArrayModifiableDBIDs dbids;
+
+ /**
+ * The database we use
+ */
+ UpdatableDatabase database;
+
+ /**
+ * Class label representation
+ */
+ ModifiableRelation<ClassLabel> crep;
+
+ /**
+ * Object label representation
+ */
+ ModifiableRelation<String> orep;
+
+ /**
+ * Our context
+ */
+ final protected VisualizerContext context;
+
+ /**
+ * The actual visualization instance, for a single projection
+ *
+ * @param context The Context
+ */
+ public SelectionTableWindow(VisualizerContext context) {
+ super(NAME);
+ // ELKI icon
+ try {
+ setIconImage(new ImageIcon(KDDTask.class.getResource("elki-icon.png")).getImage());
+ }
+ catch(Exception e) {
+ // Ignore - icon not found is not fatal.
+ }
+
+ this.context = context;
+ this.database = (UpdatableDatabase) ResultUtil.findDatabase(context.getHierarchy());
+ // FIXME: re-add labels
+ this.crep = null; //database.getClassLabelQuery();
+ this.orep = null; //database.getObjectLabelQuery();
+ updateFromSelection();
+
+ JPanel panel = new JPanel(new BorderLayout());
+
+ table = new JTable(dotTableModel);
+
+ // table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+
+ JScrollPane pane = new JScrollPane(table);
+ panel.add(pane, BorderLayout.CENTER);
+
+ JPanel buttons = new JPanel();
+ panel.add(buttons, BorderLayout.SOUTH);
+
+ closeButton = new JButton("close");
+ closeButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ dispose();
+ }
+ });
+ deleteButton = new JButton("delete");
+ deleteButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ handleDelete();
+ }
+ });
+ buttons.add(closeButton);
+ buttons.add(deleteButton);
+
+ setSize(500, 500);
+ add(panel);
+ setVisible(true);
+ setResizable(true);
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+
+ // Listen for Selection and Database changes.
+ context.addResultListener(this);
+ context.addDataStoreListener(this);
+ }
+
+ @Override
+ public void dispose() {
+ context.removeDataStoreListener(this);
+ context.removeResultListener(this);
+ super.dispose();
+ }
+
+ /**
+ * Update our selection
+ */
+ protected void updateFromSelection() {
+ DBIDSelection sel = context.getSelection();
+ if(sel != null) {
+ this.dbids = DBIDUtil.newArray(sel.getSelectedIds());
+ this.dbids.sort();
+ }
+ else {
+ this.dbids = DBIDUtil.newArray();
+ }
+ }
+
+ /**
+ * Handle delete. <br>
+ * Delete the marked objects in the database.
+ */
+ protected void handleDelete() {
+ ModifiableDBIDs todel = DBIDUtil.newHashSet();
+ ModifiableDBIDs remain = DBIDUtil.newHashSet(dbids);
+ DBIDArrayIter it = dbids.iter();
+ for(int row : table.getSelectedRows()) {
+ it.seek(row);
+ todel.add(it);
+ remain.remove(it);
+ }
+ // Unselect first ...
+ context.setSelection(new DBIDSelection(remain));
+ // Now delete them.
+ for(DBIDIter iter = todel.iter(); iter.valid(); iter.advance()) {
+ database.delete(iter);
+ }
+ }
+
+ /**
+ * View onto the database
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ class DatabaseTableModel extends AbstractTableModel {
+ /**
+ * Serial version
+ */
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public int getColumnCount() {
+ return 3; //RelationUtil.dimensionality(database) + 3;
+ }
+
+ @Override
+ public int getRowCount() {
+ return dbids.size();
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ DBIDRef id = dbids.iter().seek(rowIndex);
+ if(columnIndex == 0) {
+ return DBIDUtil.toString(id);
+ }
+ if(columnIndex == 1) {
+ return orep.get(id);
+ }
+ if(columnIndex == 2) {
+ return crep.get(id);
+ }
+ /*NV obj = database.get(id);
+ if(obj == null) {
+ return null;
+ }
+ return obj.getValue(columnIndex - 3 + 1);*/
+ return null;
+ }
+
+ @Override
+ public String getColumnName(int column) {
+ if(column == 0) {
+ return "DBID";
+ }
+ if(column == 1) {
+ return "Object label";
+ }
+ if(column == 2) {
+ return "Class label";
+ }
+ return "Dim " + (column - 3 + 1);
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ if(columnIndex == 0) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
+ if(columnIndex == 0) {
+ LOG.warning("Tried to edit DBID, this is not allowed.");
+ return;
+ }
+ final DBIDRef id = dbids.iter().seek(rowIndex);
+ if(columnIndex == 1 && aValue instanceof String) {
+ orep.insert(id, (String) aValue);
+ }
+ if(columnIndex == 2 && aValue instanceof String) {
+ // FIXME: better class label handling!
+ SimpleClassLabel lbl = new SimpleClassLabel((String) aValue);
+ crep.insert(id, lbl);
+ }
+ if(!(aValue instanceof String)) {
+ LOG.warning("Was expecting a String value from the input element, got: " + aValue.getClass());
+ return;
+ }
+ throw new AbortException("FIXME: INCOMPLETE TRANSITION");
+ /* NV obj = database.get(id);
+ if(obj == null) {
+ logger.warning("Tried to edit removed object?");
+ return;
+ }
+ final int dimensionality = RelationUtil.dimensionality(database);
+ double[] vals = new double[dimensionality];
+ for(int d = 0; d < dimensionality; d++) {
+ if(d == columnIndex - 3) {
+ vals[d] = FormatUtil.parseDouble((String) aValue);
+ }
+ else {
+ vals[d] = obj.doubleValue(d + 1);
+ }
+ }
+ NV newobj = obj.newInstance(vals);
+ newobj.setID(id);
+ final Representation<DatabaseObjectMetadata> mrep = database.getMetadataQuery();
+ DatabaseObjectMetadata meta = mrep.get(id);
+ try {
+ database.delete(id);
+ database.insert(new Pair<NV, DatabaseObjectMetadata>(newobj, meta));
+ }
+ catch(UnableToComplyException e) {
+ de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e);
+ } */
+ // TODO: refresh wrt. range selection!
+ }
+ }
+
+ @Override
+ public void contentChanged(DataStoreEvent e) {
+ if (e.getInserts().isEmpty() && e.getRemovals().isEmpty() && !e.getUpdates().isEmpty()) {
+ // Updates only.
+ dotTableModel.fireTableDataChanged();
+ }
+ else {
+ dotTableModel.fireTableStructureChanged();
+ }
+ }
+
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ if (current instanceof SelectionResult || current instanceof Database) {
+ updateFromSelection();
+ dotTableModel.fireTableStructureChanged();
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java
new file mode 100644
index 00000000..cdd7758c
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/SimpleSVGViewer.java
@@ -0,0 +1,149 @@
+package de.lmu.ifi.dbs.elki.visualization.gui;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.HeadlessException;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+
+import javax.swing.JFrame;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JPanel;
+import javax.swing.UIManager;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.JSVGSynchronizedCanvas;
+import de.lmu.ifi.dbs.elki.visualization.savedialog.SVGSaveDialog;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * A minimalistic SVG viewer with export dialog.
+ *
+ * @author Erich Schubert
+ */
+public class SimpleSVGViewer extends JFrame {
+ /**
+ * Serial version
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The main canvas.
+ */
+ private JSVGSynchronizedCanvas svgCanvas;
+
+ /**
+ * Constructor.
+ *
+ * @throws HeadlessException
+ */
+ public SimpleSVGViewer() throws HeadlessException {
+ super();
+ // Prefer system look&feel
+ try {
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+ }
+ catch(Exception e) {
+ // ignore
+ }
+
+ // close handler
+ this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ // Maximize.
+ Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
+ setSize(dim.width - 50, dim.height - 50);
+
+ // setup buttons
+ JMenuItem exportItem = new JMenuItem("Export");
+ exportItem.setMnemonic(KeyEvent.VK_E);
+ exportItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent ae) {
+ saveCurrentPlot();
+ }
+ });
+
+ JMenuItem quitItem = new JMenuItem("Quit");
+ quitItem.setMnemonic(KeyEvent.VK_Q);
+ quitItem.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ close();
+ }
+ });
+
+ // Create a panel and add the button, status label and the SVG canvas.
+ final JPanel panel = new JPanel(new BorderLayout());
+
+ JMenuBar menubar = new JMenuBar();
+ menubar.add(exportItem);
+ menubar.add(quitItem);
+
+ panel.add("North", menubar);
+
+ svgCanvas = new JSVGSynchronizedCanvas();
+ panel.add("Center", svgCanvas);
+
+ this.getContentPane().add(panel);
+
+ setExtendedState(JFrame.MAXIMIZED_BOTH);
+ this.setVisible(true);
+ }
+
+ /**
+ * Close the visualizer window.
+ */
+ public void close() {
+ this.setVisible(false);
+ this.dispose();
+ }
+
+ /**
+ * Save/export the current plot.
+ */
+ public void saveCurrentPlot() {
+ // TODO: exclude "do not export" layers!
+ final SVGPlot currentPlot = svgCanvas.getPlot();
+ if(currentPlot != null) {
+ SVGSaveDialog.showSaveDialog(currentPlot, 512, 512);
+ }
+ else {
+ LoggingUtil.warning("saveCurrentPlot() called without a visible plot!");
+ }
+ }
+
+ /**
+ * Set the plot to show
+ *
+ * @param plot Plot
+ */
+ public void setPlot(SVGPlot plot) {
+ svgCanvas.setPlot(plot);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java
new file mode 100644
index 00000000..7360a0da
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/VisualizationPlot.java
@@ -0,0 +1,81 @@
+package de.lmu.ifi.dbs.elki.visualization.gui;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.concurrent.ConcurrentLinkedDeque;
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * SVG plot that allows visualization to schedule updates.
+ *
+ * @author Erich Schubert
+ */
+public class VisualizationPlot extends SVGPlot {
+ /**
+ * Pending redraw request in Batik.
+ */
+ protected Runnable pendingRedraw = null;
+
+ /**
+ * Update queue.
+ */
+ protected ConcurrentLinkedDeque<Visualization> updateQueue = new ConcurrentLinkedDeque<>();
+
+ /**
+ * Trigger a redraw, but avoid excessive redraws.
+ */
+ protected final void synchronizedRedraw() {
+ Runnable pr = new Runnable() {
+ @Override
+ public void run() {
+ if(VisualizationPlot.this.pendingRedraw == this) {
+ VisualizationPlot.this.pendingRedraw = null;
+ VisualizationPlot.this.redraw();
+ }
+ }
+ };
+ pendingRedraw = pr;
+ scheduleUpdate(pr);
+ }
+
+ /**
+ * Redraw all pending updates.
+ */
+ protected void redraw() {
+ while(!updateQueue.isEmpty()) {
+ Visualization vis = updateQueue.pop();
+ vis.incrementalRedraw();
+ }
+ }
+
+ /**
+ * Request a redraw of a visualization.
+ */
+ public void requestRedraw(VisualizationTask task, Visualization vis) {
+ updateQueue.add(vis);
+ synchronizedRedraw();
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java
new file mode 100644
index 00000000..64359463
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/DetailView.java
@@ -0,0 +1,422 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.detail;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationListener;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGEffects;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Manages a detail view.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has Visualization
+ * @apiviz.has PlotItem
+ * @apiviz.uses VisualizerContext
+ * @apiviz.uses VisualizationTask
+ */
+public class DetailView extends VisualizationPlot implements ResultListener, VisualizationListener {
+ /**
+ * Class logger
+ */
+ private static final Logging LOG = Logging.getLogger(DetailView.class);
+
+ /**
+ * Meta information on the visualizers contained.
+ */
+ private PlotItem item;
+
+ /**
+ * Ratio of this view.
+ */
+ double ratio = 1.0;
+
+ /**
+ * The visualizer context
+ */
+ VisualizerContext context;
+
+ /**
+ * Map from tasks to visualizations.
+ */
+ Map<VisualizationTask, Visualization> taskmap = new HashMap<>();
+
+ /**
+ * Map from visualizations to SVG layers.
+ */
+ Map<Visualization, Element> layermap = new HashMap<>();
+
+ /**
+ * The created width
+ */
+ private double width;
+
+ /**
+ * The created height
+ */
+ private double height;
+
+ /**
+ * Pending refresh, for lazy refreshing
+ */
+ AtomicReference<Runnable> pendingRefresh = new AtomicReference<>(null);
+
+ /**
+ * Constructor.
+ *
+ * @param vis Visualizations to use
+ * @param ratio Plot ratio
+ */
+ public DetailView(VisualizerContext context, PlotItem vis, double ratio) {
+ super();
+ this.context = context;
+ this.item = new PlotItem(vis); // Clone!
+ this.ratio = ratio;
+
+ this.item.sort();
+
+ // TODO: only do this when there is an interactive visualizer?
+ setDisableInteractions(true);
+ addBackground(context);
+ SVGEffects.addShadowFilter(this);
+ SVGEffects.addLightGradient(this);
+
+ initialize();
+ context.addVisualizationListener(this);
+ context.addResultListener(this);
+ // FIXME: add datastore listener, too?
+ }
+
+ /**
+ * Create a background node. Note: don't call this at arbitrary times - the
+ * background may cover already drawn parts of the image!
+ *
+ * @param context
+ */
+ private void addBackground(VisualizerContext context) {
+ // Make a background
+ CSSClass cls = new CSSClass(this, "background");
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, context.getStyleLibrary().getBackgroundColor(StyleLibrary.PAGE));
+ Element bg = this.svgElement(SVGConstants.SVG_RECT_TAG);
+ SVGUtil.setAtt(bg, SVGConstants.SVG_X_ATTRIBUTE, "0");
+ SVGUtil.setAtt(bg, SVGConstants.SVG_Y_ATTRIBUTE, "0");
+ SVGUtil.setAtt(bg, SVGConstants.SVG_WIDTH_ATTRIBUTE, "100%");
+ SVGUtil.setAtt(bg, SVGConstants.SVG_HEIGHT_ATTRIBUTE, "100%");
+ SVGUtil.setAtt(bg, NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE);
+ addCSSClassOrLogError(cls);
+ SVGUtil.setCSSClass(bg, cls.getName());
+
+ // Note that we rely on this being called before any other drawing routines.
+ getRoot().appendChild(bg);
+ }
+
+ private void initialize() {
+ // Try to keep the area approximately 1.0
+ width = Math.sqrt(getRatio());
+ height = 1.0 / width;
+
+ ArrayList<Visualization> layers = new ArrayList<>();
+ // TODO: center/arrange visualizations?
+ for(Iterator<VisualizationTask> tit = item.tasks.iterator(); tit.hasNext();) {
+ VisualizationTask task = tit.next();
+ if(task.visible) {
+ Visualization v = instantiateVisualization(task);
+ if(v != null) {
+ layers.add(v);
+ taskmap.put(task, v);
+ layermap.put(v, v.getLayer());
+ }
+ }
+ }
+ // Arrange
+ for(Visualization layer : layers) {
+ if(layer.getLayer() != null) {
+ getRoot().appendChild(layer.getLayer());
+ }
+ else {
+ LOG.warning("NULL layer seen.");
+ }
+ }
+
+ double ratio = width / height;
+ getRoot().setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm");
+ getRoot().setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, (20 / ratio) + "cm");
+ getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + width + " " + height);
+
+ updateStyleElement();
+ }
+
+ /**
+ * Do a refresh (when visibilities have changed).
+ */
+ private synchronized void refresh() {
+ pendingRefresh.set(null); // Clear
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("Refresh in thread " + Thread.currentThread().getName());
+ }
+ boolean updateStyle = false;
+ Iterator<Map.Entry<VisualizationTask, Visualization>> it = taskmap.entrySet().iterator();
+ while(it.hasNext()) {
+ Entry<VisualizationTask, Visualization> ent = it.next();
+ VisualizationTask task = ent.getKey();
+ Visualization vis = ent.getValue();
+ if(vis == null) {
+ vis = instantiateVisualization(task);
+ ent.setValue(vis);
+ }
+ Element prevlayer = layermap.get(vis);
+ Element layer = vis.getLayer();
+ if(prevlayer == layer) { // Unchanged:
+ // Current visibility ("not hidden")
+ boolean isVisible = !SVGConstants.CSS_HIDDEN_VALUE.equals(layer.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY));
+ if(task.visible != isVisible) {
+ // scheduleUpdate(new AttributeModifier(
+ layer.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, //
+ task.visible ? SVGConstants.CSS_VISIBLE_VALUE : SVGConstants.CSS_HIDDEN_VALUE);
+ }
+ }
+ else {
+ if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) {
+ layer.setAttribute(NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE);
+ }
+ if(prevlayer == null) {
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("New layer: " + task);
+ }
+ // Insert new!
+ // TODO: insert position!
+ getRoot().appendChild(layer);
+ }
+ else {
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("Updated layer: " + task);
+ }
+ // Replace
+ final Node parent = prevlayer.getParentNode();
+ if(parent != null) {
+ parent.replaceChild(/* new! */layer, /* old */prevlayer);
+ }
+ }
+ layermap.put(vis, layer);
+ updateStyle = true;
+ }
+ }
+ if(updateStyle) {
+ updateStyleElement();
+ }
+ }
+
+ /**
+ * Instantiate a visualization.
+ *
+ * @param task Task to instantiate
+ * @return Visualization
+ */
+ private Visualization instantiateVisualization(VisualizationTask task) {
+ try {
+ Visualization v = task.getFactory().makeVisualization(task, this, width, height, item.proj);
+ if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) {
+ v.getLayer().setAttribute(NO_EXPORT_ATTRIBUTE, NO_EXPORT_ATTRIBUTE);
+ }
+ return v;
+ }
+ catch(Exception e) {
+ if(LOG.isDebugging()) {
+ LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed.", e);
+ }
+ else {
+ LOG.warning("Visualizer " + task.getFactory().getClass().getName() + " failed - enable debugging to see details: " + e.toString());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Cleanup function. To remove listeners.
+ */
+ public void destroy() {
+ context.removeVisualizationListener(this);
+ context.removeResultListener(this);
+ for(Entry<VisualizationTask, Visualization> v : taskmap.entrySet()) {
+ Visualization vis = v.getValue();
+ if(vis != null) {
+ vis.destroy();
+ }
+ }
+ taskmap.clear();
+ }
+
+ @Override
+ public void dispose() {
+ destroy();
+ super.dispose();
+ }
+
+ /**
+ * Get the plot ratio.
+ *
+ * @return the current ratio
+ */
+ public double getRatio() {
+ return ratio;
+ }
+
+ /**
+ * Set the plot ratio
+ *
+ * @param ratio the new ratio to set
+ */
+ public void setRatio(double ratio) {
+ // TODO: trigger refresh?
+ this.ratio = ratio;
+ }
+
+ /**
+ * Trigger a refresh.
+ */
+ private void lazyRefresh() {
+ Runnable pr = new Runnable() {
+ @Override
+ public void run() {
+ if(DetailView.this.pendingRefresh.compareAndSet(this, null)) {
+ DetailView.this.refresh();
+ }
+ }
+ };
+ DetailView.this.pendingRefresh.set(pr);
+ scheduleUpdate(pr);
+ }
+
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem current) {
+ // Make sure we are affected:
+ if(!(current instanceof VisualizationTask)) {
+ return;
+ }
+ final VisualizationTask task = (VisualizationTask) current;
+ // Get the layer
+ Visualization vis = taskmap.get(task);
+ if(vis == null) { // Unknown only.
+ boolean include = false;
+ Hierarchy.Iter<Object> it = context.getVisHierarchy().iterAncestors(current);
+ for(; it.valid(); it.advance()) {
+ if((item.proj != null && item.proj.getProjector() == it.get()) || taskmap.containsKey(it.get())) {
+ include = true;
+ break;
+ }
+ }
+ if(!include) {
+ return; // Attached to different projection.
+ }
+ }
+ if(vis == null) { // New visualization
+ taskmap.put(task, null);
+ lazyRefresh();
+ }
+ else {
+ Element prevlayer = layermap.get(vis);
+ Element layer = vis.getLayer();
+ if(prevlayer != layer) {
+ lazyRefresh();
+ }
+ else {
+ boolean isVisible = !SVGConstants.CSS_HIDDEN_VALUE.equals(layer.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY));
+ if(task.visible != isVisible) {
+ lazyRefresh(); // Visibility has changed.
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void redraw() {
+ boolean active = false;
+ while(!updateQueue.isEmpty()) {
+ Visualization vis = updateQueue.pop();
+ if(!active) {
+ Element prev = layermap.get(vis);
+ vis.incrementalRedraw();
+ final boolean changed = prev != vis.getLayer();
+ if(LOG.isDebuggingFine() && changed) {
+ LOG.debugFine("Visualization " + vis + " changed.");
+ }
+ active |= changed;
+ }
+ else {
+ vis.incrementalRedraw();
+ }
+ }
+ if(active || true) {
+ refresh();
+ }
+ }
+
+ /**
+ * Get the item visualized by this view.
+ *
+ * @return Plot item
+ */
+ public PlotItem getPlotItem() {
+ return item;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java
new file mode 100755
index 00000000..22dfba6f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/detail/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Classes for managing a detail view.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.gui.detail; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java
new file mode 100644
index 00000000..16c59fee
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/DetailViewSelectedEvent.java
@@ -0,0 +1,75 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.event.ActionEvent;
+
+import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView;
+
+/**
+ * Event when a particular subplot was selected. Plots are currently identified
+ * by their coordinates on the screen.
+ *
+ * @author Erich Schubert
+ */
+public class DetailViewSelectedEvent extends ActionEvent {
+ /**
+ * Serial version
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Parent overview plot.
+ */
+ OverviewPlot overview;
+
+ /**
+ * Plot item selected
+ */
+ PlotItem it;
+
+ /**
+ * Constructor. To be called by OverviewPlot only!
+ *
+ * @param source source plot
+ * @param id ID
+ * @param command command that was invoked
+ * @param modifiers modifiers
+ * @param it Plot item selected
+ */
+ public DetailViewSelectedEvent(OverviewPlot source, int id, String command, int modifiers, PlotItem it) {
+ super(source, id, command, modifiers);
+ this.overview = source;
+ this.it = it;
+ }
+
+ /**
+ * Retrieve a materialized detail plot.
+ *
+ * @return materialized detail plot
+ */
+ public DetailView makeDetailView() {
+ return overview.makeDetailView(it);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java
new file mode 100644
index 00000000..0cdbb29e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/LayerMap.java
@@ -0,0 +1,160 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import java.util.*;
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.utilities.pairs.Pair;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Class to help keeping track of the materialized layers of the different visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has PlotItem
+ * @apiviz.has VisualizationTask
+ */
+public class LayerMap {
+ /**
+ * The actual map
+ */
+ private HashMap<Pair<PlotItem, VisualizationTask>, Pair<Element, Visualization>> map = new HashMap<>();
+
+ /**
+ * Helper function for building a key object
+ *
+ * @param item Plot item
+ * @param task Visualization Task
+ * @return Key
+ */
+ private Pair<PlotItem, VisualizationTask> key(PlotItem item, VisualizationTask task) {
+ return new Pair<>(item, task);
+ }
+
+ /**
+ * Helper function to build a value pair
+ *
+ * @param elem Container element
+ * @param vis Visualization
+ * @return Value object
+ */
+ private Pair<Element, Visualization> value(Element elem, Visualization vis) {
+ return new Pair<>(elem, vis);
+ }
+
+ /**
+ * Get the visualization referenced by a item/key combination.
+ *
+ * @param item Plot ttem
+ * @param task Visualization task
+ * @return Visualization
+ */
+ public Visualization getVisualization(PlotItem item, VisualizationTask task) {
+ Pair<Element, Visualization> pair = map.get(key(item, task));
+ if (pair == null) {
+ return null;
+ } else {
+ return pair.second;
+ }
+ }
+
+ /**
+ * Get the container element referenced by a item/key combination.
+ *
+ * @param item Plot item
+ * @param task Visualization task
+ * @return Container element
+ */
+ public Element getContainer(PlotItem item, VisualizationTask task) {
+ Pair<Element, Visualization> pair = map.get(key(item, task));
+ if (pair == null) {
+ return null;
+ } else {
+ return pair.first;
+ }
+ }
+
+ /**
+ * Iterate over values
+ *
+ * @return Value iterable
+ */
+ public Iterable<Pair<Element, Visualization>> values() {
+ return map.values();
+ }
+
+ /**
+ * Clear a map
+ */
+ public void clear() {
+ map.clear();
+ }
+
+ /**
+ * Put a new combination into the map.
+ *
+ * @param it Plot item
+ * @param task Visualization Task
+ * @param elem Container element
+ * @param vis Visualization
+ */
+ public void put(PlotItem it, VisualizationTask task, Element elem, Visualization vis) {
+ map.put(key(it, task), value(elem, vis));
+ }
+
+ /**
+ * Remove a combination.
+ *
+ * @param it Plot item
+ * @param task Visualization task
+ * @return Previous value
+ */
+ public Pair<Element, Visualization> remove(PlotItem it, VisualizationTask task) {
+ return map.remove(key(it, task));
+ }
+
+ /**
+ * Put a new item into the visualizations
+ *
+ * @param it Plot item
+ * @param task Visualization task
+ * @param pair Pair object
+ */
+ public void put(PlotItem it, VisualizationTask task, Pair<Element, Visualization> pair) {
+ map.put(key(it, task), pair);
+ }
+
+ /**
+ * Get a pair from the map
+ *
+ * @param it Plot item
+ * @param task Visualization task
+ * @return Pair object
+ */
+ public Pair<Element, Visualization> get(PlotItem it, VisualizationTask task) {
+ return map.get(key(it, task));
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java
new file mode 100644
index 00000000..23ab2c21
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/OverviewPlot.java
@@ -0,0 +1,661 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.pairs.Pair;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationListener;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.CSSHoverClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.gui.detail.DetailView;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGEffects;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generate an overview plot for a set of visualizations.
+ *
+ * @author Erich Schubert
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.landmark
+ * @apiviz.has VisualizerContext
+ * @apiviz.composedOf RectangleArranger
+ * @apiviz.composedOf LayerMap
+ * @apiviz.has DetailViewSelectedEvent
+ * @apiviz.uses DetailView
+ */
+public class OverviewPlot implements ResultListener, VisualizationListener {
+ /**
+ * Our logging class
+ */
+ private static final Logging LOG = Logging.getLogger(OverviewPlot.class);
+
+ /**
+ * Event when the overview plot started refreshing.
+ */
+ public static final String OVERVIEW_REFRESHING = "Overview refreshing";
+
+ /**
+ * Event when the overview plot was refreshed.
+ */
+ public static final String OVERVIEW_REFRESHED = "Overview refreshed";
+
+ /**
+ * Draw red borders around items.
+ */
+ private static final boolean DEBUG_LAYOUT = false;
+
+ /**
+ * Visualizer context
+ */
+ private VisualizerContext context;
+
+ /**
+ * The SVG plot object.
+ */
+ private VisualizationPlot plot;
+
+ /**
+ * Map of coordinates to plots.
+ */
+ protected RectangleArranger<PlotItem> plotmap;
+
+ /**
+ * Action listeners for this plot.
+ */
+ private ArrayList<ActionListener> actionListeners = new ArrayList<>();
+
+ /**
+ * Single view mode
+ */
+ private boolean single;
+
+ /**
+ * Screen size (used for thumbnail sizing)
+ */
+ public int screenwidth = 2000;
+
+ /**
+ * Screen size (used for thumbnail sizing)
+ */
+ public int screenheight = 2000;
+
+ /**
+ * React to mouse hover events
+ */
+ private EventListener hoverer;
+
+ /**
+ * Lookup
+ */
+ private LayerMap vistoelem = new LayerMap();
+
+ /**
+ * Layer for plot thumbnail
+ */
+ private Element plotlayer;
+
+ /**
+ * Layer for hover elements
+ */
+ private Element hoverlayer;
+
+ /**
+ * The CSS class used on "selectable" rectangles.
+ */
+ private CSSClass selcss;
+
+ /**
+ * Screen ratio
+ */
+ private double ratio = 1.0;
+
+ /**
+ * Pending refresh, for lazy refreshing
+ */
+ AtomicReference<Runnable> pendingRefresh = new AtomicReference<>(null);
+
+ /**
+ * Reinitialize on refresh
+ */
+ private boolean reinitOnRefresh = false;
+
+ /**
+ * Constructor.
+ *
+ * @param context Visualizer context
+ * @param single Single view mode
+ */
+ public OverviewPlot(VisualizerContext context, boolean single) {
+ super();
+ this.context = context;
+ this.single = single;
+
+ // Important:
+ // You still need to call: initialize(ratio);
+ }
+
+ /**
+ * Recompute the layout of visualizations.
+ *
+ * @param width Initial width
+ * @param height Initial height
+ * @return Arrangement
+ */
+ private RectangleArranger<PlotItem> arrangeVisualizations(double width, double height) {
+ if(!(width > 0. && height > 0.)) {
+ LOG.warning("No size information during arrange()", new Throwable());
+ return new RectangleArranger<>(1., 1.);
+ }
+ RectangleArranger<PlotItem> plotmap = new RectangleArranger<>(width, height);
+
+ Hierarchy<Object> vistree = context.getVisHierarchy();
+ for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) {
+ if(!(iter2.get() instanceof Projector)) {
+ continue;
+ }
+ Projector p = (Projector) iter2.get();
+ Collection<PlotItem> projs = p.arrange(context);
+ for(PlotItem it : projs) {
+ if(it.w <= 0.0 || it.h <= 0.0) {
+ LOG.warning("Plot item with improper size information: " + it);
+ continue;
+ }
+ plotmap.put(it.w, it.h, it);
+ }
+ }
+
+ nextTask: for(Hierarchy.Iter<?> iter2 = vistree.iterAll(); iter2.valid(); iter2.advance()) {
+ if(!(iter2.get() instanceof VisualizationTask)) {
+ continue;
+ }
+ VisualizationTask task = (VisualizationTask) iter2.get();
+ if(!task.visible) {
+ continue;
+ }
+ for(Hierarchy.Iter<?> iter = vistree.iterParents(task); iter.valid(); iter.advance()) {
+ if(iter.get() instanceof Projector) {
+ continue nextTask;
+ }
+ }
+ if(task.reqwidth <= 0.0 || task.reqheight <= 0.0) {
+ LOG.warning("Task with improper size information: " + task);
+ continue;
+ }
+ PlotItem it = new PlotItem(task.reqwidth, task.reqheight, null);
+ it.tasks.add(task);
+ plotmap.put(it.w, it.h, it);
+ }
+ return plotmap;
+ }
+
+ /**
+ * Initialize the plot.
+ *
+ * @param ratio Initial ratio
+ */
+ public void initialize(double ratio) {
+ if(!(ratio > 0 && ratio < Double.POSITIVE_INFINITY)) {
+ LOG.warning("Invalid ratio: " + ratio, new Throwable());
+ ratio = 1.4;
+ }
+ this.ratio = ratio;
+ if(plot != null) {
+ LOG.warning("Already initialized.");
+ lazyRefresh();
+ return;
+ }
+ reinitialize();
+ // register context listener
+ context.addResultListener(this);
+ context.addVisualizationListener(this);
+ }
+
+ /**
+ * Refresh the overview plot.
+ */
+ private synchronized void reinitialize() {
+ if(plot == null) {
+ initializePlot();
+ }
+ else {
+ final ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, OVERVIEW_REFRESHING);
+ for(ActionListener actionListener : actionListeners) {
+ actionListener.actionPerformed(ev);
+ }
+ }
+
+ // Detach existing elements:
+ for(Pair<Element, Visualization> pair : vistoelem.values()) {
+ SVGUtil.removeFromParent(pair.first);
+ }
+ plotmap = arrangeVisualizations(ratio, 1.0);
+
+ recalcViewbox();
+ final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight());
+ // TODO: cancel pending thumbnail requests!
+
+ // Replace the layer map
+ LayerMap oldlayers = vistoelem;
+ vistoelem = new LayerMap();
+
+ // Redo main layers
+ SVGUtil.removeFromParent(plotlayer);
+ SVGUtil.removeFromParent(hoverlayer);
+ plotlayer = plot.svgElement(SVGConstants.SVG_G_TAG);
+ hoverlayer = plot.svgElement(SVGConstants.SVG_G_TAG);
+ hoverlayer.setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE);
+
+ // Redo the layout
+ for(Entry<PlotItem, double[]> e : plotmap.entrySet()) {
+ final double basex = e.getValue()[0];
+ final double basey = e.getValue()[1];
+ for(Iterator<PlotItem> iter = e.getKey().itemIterator(); iter.hasNext();) {
+ PlotItem it = iter.next();
+
+ boolean hasDetails = false;
+ // Container element for main plot item
+ Element g = plot.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + (basex + it.x) + " " + (basey + it.y) + ")");
+ plotlayer.appendChild(g);
+ vistoelem.put(it, null, g, null);
+ // Add the actual tasks:
+ for(VisualizationTask task : it.tasks) {
+ if(!visibleInOverview(task)) {
+ continue;
+ }
+ hasDetails |= !task.hasAnyFlags(VisualizationTask.FLAG_NO_DETAIL);
+ Pair<Element, Visualization> pair = oldlayers.remove(it, task);
+ if(pair == null) {
+ pair = new Pair<>(null, null);
+ pair.first = plot.svgElement(SVGConstants.SVG_G_TAG);
+ }
+ if(pair.second == null) {
+ pair.second = embedOrThumbnail(thumbsize, it, task, pair.first);
+ }
+ g.appendChild(pair.first);
+ vistoelem.put(it, task, pair);
+ }
+ // When needed, add a hover effect
+ if(hasDetails && !single) {
+ Element hover = plot.svgRect(basex + it.x, basey + it.y, it.w, it.h);
+ SVGUtil.addCSSClass(hover, selcss.getName());
+ // link hoverer.
+ EventTarget targ = (EventTarget) hover;
+ targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, hoverer, false);
+ targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, hoverer, false);
+ targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, hoverer, false);
+ targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, new SelectPlotEvent(it), false);
+
+ hoverlayer.appendChild(hover);
+ }
+ }
+ }
+ for(Pair<Element, Visualization> pair : oldlayers.values()) {
+ if(pair.second != null) {
+ pair.second.destroy();
+ }
+ }
+ plot.getRoot().appendChild(plotlayer);
+ plot.getRoot().appendChild(hoverlayer);
+ plot.updateStyleElement();
+
+ // Notify listeners.
+ final ActionEvent ev = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, OVERVIEW_REFRESHED);
+ for(ActionListener actionListener : actionListeners) {
+ actionListener.actionPerformed(ev);
+ }
+ }
+
+ /**
+ * Initialize the SVG plot.
+ */
+ private void initializePlot() {
+ plot = new VisualizationPlot();
+ { // Add a background element:
+ CSSClass cls = new CSSClass(this, "background");
+ final String bgcol = context.getStyleLibrary().getBackgroundColor(StyleLibrary.PAGE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, bgcol);
+ plot.addCSSClassOrLogError(cls);
+ Element background = plot.svgElement(SVGConstants.SVG_RECT_TAG);
+ background.setAttribute(SVGConstants.SVG_X_ATTRIBUTE, "0");
+ background.setAttribute(SVGConstants.SVG_Y_ATTRIBUTE, "0");
+ background.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "100%");
+ background.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "100%");
+ SVGUtil.setCSSClass(background, cls.getName());
+ // Don't export a white background:
+ if("white".equals(bgcol)) {
+ background.setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE);
+ }
+ plot.getRoot().appendChild(background);
+ }
+ { // setup the hover CSS classes.
+ selcss = new CSSClass(this, "s");
+ if(DEBUG_LAYOUT) {
+ selcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ selcss.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, .00001 * StyleLibrary.SCALE);
+ selcss.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, "0.5");
+ }
+ selcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ selcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0");
+ selcss.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ plot.addCSSClassOrLogError(selcss);
+ CSSClass hovcss = new CSSClass(this, "h");
+ hovcss.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0.25");
+ plot.addCSSClassOrLogError(hovcss);
+ // Hover listener.
+ hoverer = new CSSHoverClass(hovcss.getName(), null, true);
+ }
+
+ // Disable Batik default interactions (zoom, rotate, etc.)
+ if(single) {
+ plot.setDisableInteractions(true);
+ }
+ SVGEffects.addShadowFilter(plot);
+ SVGEffects.addLightGradient(plot);
+ }
+
+ /**
+ * Produce thumbnail for a visualizer.
+ *
+ * @param thumbsize Thumbnail size
+ * @param it Plot item
+ * @param task Task
+ * @param parent Parent element to draw to
+ */
+ private Visualization embedOrThumbnail(final int thumbsize, PlotItem it, VisualizationTask task, Element parent) {
+ final Visualization vis;
+ if(!single) {
+ vis = task.getFactory().makeVisualizationOrThumbnail(task, plot, it.w, it.h, it.proj, thumbsize);
+ }
+ else {
+ vis = task.getFactory().makeVisualization(task, plot, it.w, it.h, it.proj);
+ }
+ if(vis == null || vis.getLayer() == null) {
+ LoggingUtil.warning("Visualization returned empty layer: " + vis);
+ return vis;
+ }
+ if(task.hasAnyFlags(VisualizationTask.FLAG_NO_EXPORT)) {
+ vis.getLayer().setAttribute(SVGPlot.NO_EXPORT_ATTRIBUTE, SVGPlot.NO_EXPORT_ATTRIBUTE);
+ }
+ parent.appendChild(vis.getLayer());
+ return vis;
+ }
+
+ /**
+ * Do a refresh (when visibilities have changed).
+ */
+ synchronized void refresh() {
+ if(reinitOnRefresh) {
+ LOG.debug("Reinitialize in thread " + Thread.currentThread().getName());
+ reinitialize();
+ reinitOnRefresh = false;
+ return;
+ }
+ synchronized(plot) {
+ boolean refreshcss = false;
+ if(plotmap == null) {
+ LOG.warning("Plotmap is null", new Throwable());
+ }
+ final int thumbsize = (int) Math.max(screenwidth / plotmap.getWidth(), screenheight / plotmap.getHeight());
+ for(PlotItem pi : plotmap.keySet()) {
+ for(Iterator<PlotItem> iter = pi.itemIterator(); iter.hasNext();) {
+ PlotItem it = iter.next();
+
+ for(Iterator<VisualizationTask> tit = it.tasks.iterator(); tit.hasNext();) {
+ VisualizationTask task = tit.next();
+ Pair<Element, Visualization> pair = vistoelem.get(it, task);
+ // New task?
+ if(pair == null) {
+ if(visibleInOverview(task)) {
+ pair = new Pair<>(null, null);
+ pair.first = plot.svgElement(SVGConstants.SVG_G_TAG);
+ pair.second = embedOrThumbnail(thumbsize, it, task, pair.first);
+ vistoelem.get(it, null).first.appendChild(pair.first);
+ vistoelem.put(it, task, pair);
+ refreshcss = true;
+ }
+ }
+ else {
+ if(visibleInOverview(task)) {
+ // unhide if hidden.
+ if(pair.first.hasAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY)) {
+ pair.first.removeAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY);
+ }
+ }
+ else {
+ // hide if there is anything to hide.
+ if(pair.first != null && pair.first.hasChildNodes()) {
+ pair.first.setAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY, SVGConstants.CSS_HIDDEN_VALUE);
+ }
+ }
+ // TODO: unqueue pending thumbnails
+ }
+ }
+ }
+ }
+ if(refreshcss) {
+ plot.updateStyleElement();
+ }
+ }
+ }
+
+ /**
+ * Test whether a task should be displayed in the overview plot.
+ *
+ * @param task Task to display
+ * @return visibility
+ */
+ protected boolean visibleInOverview(VisualizationTask task) {
+ if(single) {
+ return task.visible && !task.hasAnyFlags(VisualizationTask.FLAG_NO_EMBED);
+ }
+ return task.visible && !task.hasAnyFlags(VisualizationTask.FLAG_NO_THUMBNAIL);
+ }
+
+ /**
+ * Recompute the view box of the plot.
+ */
+ private void recalcViewbox() {
+ final Element root = plot.getRoot();
+ // Reset plot attributes
+ SVGUtil.setAtt(root, SVGConstants.SVG_WIDTH_ATTRIBUTE, "20cm");
+ SVGUtil.setAtt(root, SVGConstants.SVG_HEIGHT_ATTRIBUTE, SVGUtil.fmt(20 * plotmap.getHeight() / plotmap.getWidth()) + "cm");
+ String vb = "0 0 " + SVGUtil.fmt(plotmap.getWidth()) + " " + SVGUtil.fmt(plotmap.getHeight());
+ SVGUtil.setAtt(root, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, vb);
+ }
+
+ /**
+ * Event triggered when a plot was selected.
+ *
+ * @param it Plot item selected
+ * @return sub plot
+ */
+ public DetailView makeDetailView(PlotItem it) {
+ return new DetailView(context, it, ratio);
+ }
+
+ /**
+ * Adds an {@link ActionListener} to the plot.
+ *
+ * @param actionListener the {@link ActionListener} to be added
+ */
+ public void addActionListener(ActionListener actionListener) {
+ actionListeners.add(actionListener);
+ }
+
+ /**
+ * When a subplot was selected, forward the event to listeners.
+ *
+ * @param it PlotItem selected
+ */
+ protected void triggerSubplotSelectEvent(PlotItem it) {
+ // forward event to all listeners.
+ for(ActionListener actionListener : actionListeners) {
+ actionListener.actionPerformed(new DetailViewSelectedEvent(this, ActionEvent.ACTION_PERFORMED, null, 0, it));
+ }
+ }
+
+ /**
+ * Event when a plot was selected.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public class SelectPlotEvent implements EventListener {
+ /**
+ * Plot item clicked
+ */
+ PlotItem it;
+
+ /**
+ * Constructor.
+ *
+ * @param it Item that was clicked
+ */
+ public SelectPlotEvent(PlotItem it) {
+ super();
+ this.it = it;
+ }
+
+ @Override
+ public void handleEvent(Event evt) {
+ triggerSubplotSelectEvent(it);
+ }
+ }
+
+ /**
+ * Destroy this overview plot.
+ */
+ public void destroy() {
+ context.removeVisualizationListener(this);
+ context.removeResultListener(this);
+ plot.dispose();
+ }
+
+ /**
+ * Get the SVGPlot object.
+ *
+ * @return SVG plot
+ */
+ public SVGPlot getPlot() {
+ return plot;
+ }
+
+ /**
+ * @return the ratio
+ */
+ public double getRatio() {
+ return ratio;
+ }
+
+ /**
+ * @param ratio the ratio to set
+ */
+ public void setRatio(double ratio) {
+ if(ratio != this.ratio) {
+ this.ratio = ratio;
+ reinitOnRefresh = true;
+ lazyRefresh();
+ }
+ }
+
+ /**
+ * Trigger a redraw, but avoid excessive redraws.
+ */
+ public final void lazyRefresh() {
+ if(plot == null) {
+ LOG.warning("'lazyRefresh' called before initialized!");
+ }
+ LOG.debug("Scheduling refresh.");
+ Runnable pr = new Runnable() {
+ @Override
+ public void run() {
+ if(OverviewPlot.this.pendingRefresh.compareAndSet(this, null)) {
+ OverviewPlot.this.refresh();
+ }
+ }
+ };
+ OverviewPlot.this.pendingRefresh.set(pr);
+ plot.scheduleUpdate(pr);
+ }
+
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ lazyRefresh();
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem child) {
+ boolean isProjected = false;
+ for(Hierarchy.Iter<Object> iter = context.getVisHierarchy().iterParents(child); iter.valid(); iter.advance()) {
+ final Object o = iter.get();
+ if(o instanceof Projector) {
+ isProjected = true;
+ break;
+ }
+ }
+ if(!isProjected) {
+ reinitOnRefresh = true;
+ }
+ lazyRefresh();
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java
new file mode 100644
index 00000000..b95ef7e2
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/PlotItem.java
@@ -0,0 +1,227 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
+
+import java.util.ArrayList;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+
+/**
+ * Item to collect visualization tasks on a specific position on the plot map.
+ *
+ * Note: this is a {@code LinkedList<VisualizationTask>}!
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf Projection
+ * @apiviz.composedOf VisualizationTask
+ * @apiviz.composedOf PlotItem
+ */
+public class PlotItem {
+ /**
+ * Position: x
+ */
+ public final double x;
+
+ /**
+ * Position: y
+ */
+ public final double y;
+
+ /**
+ * Size: width
+ */
+ public final double w;
+
+ /**
+ * Size: height
+ */
+ public final double h;
+
+ /**
+ * Projection (may be {@code null}!)
+ */
+ public final Projection proj;
+
+ /**
+ * The visualization tasks at this location
+ */
+ public List<VisualizationTask> tasks = new LinkedList<>();
+
+ /**
+ * Subitems to plot
+ */
+ public Collection<PlotItem> subitems = new LinkedList<>();
+
+ /**
+ * Constructor.
+ *
+ * @param w Position: w
+ * @param h Position: h
+ * @param proj Projection
+ */
+ public PlotItem(double w, double h, Projection proj) {
+ this(0, 0, w, h, proj);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param x Position: x
+ * @param y Position: y
+ * @param w Position: w
+ * @param h Position: h
+ * @param proj Projection
+ */
+ public PlotItem(double x, double y, double w, double h, Projection proj) {
+ super();
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ this.proj = proj;
+ }
+
+ /**
+ * Clone constructor.
+ *
+ * @param vis Existing plot item.
+ */
+ public PlotItem(PlotItem vis) {
+ super();
+ this.x = vis.x;
+ this.y = vis.y;
+ this.w = vis.w;
+ this.h = vis.h;
+ this.proj = vis.proj;
+ this.tasks = new ArrayList<>(vis.tasks);
+ this.subitems = new ArrayList<>(vis.subitems.size());
+ for(PlotItem s : vis.subitems) {
+ this.subitems.add(new PlotItem(s));
+ }
+ }
+
+ /**
+ * Sort all visualizers for their proper drawing order
+ */
+ public void sort() {
+ Collections.sort(tasks);
+ for(PlotItem subitem : subitems) {
+ subitem.sort();
+ }
+ }
+
+ /**
+ * Add a task to the item.
+ *
+ * @param task Task to add
+ */
+ public void add(VisualizationTask task) {
+ tasks.add(task);
+ }
+
+ /**
+ * Number of tasks in this item.
+ *
+ * @return Number of tasks.
+ */
+ public int taskSize() {
+ return tasks.size();
+ }
+
+ /**
+ * Iterate (recursively) over all plot items, including itself.
+ *
+ * @return Iterator
+ */
+ public Iterator<PlotItem> itemIterator() {
+ return new ItmItr();
+ }
+
+ @Override
+ public String toString() {
+ return "PlotItem [x=" + x + ", y=" + y + ", w=" + w + ", h=" + h + ",proj=" + proj + "]";
+ }
+
+ /**
+ * Recursive iterator
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ private class ItmItr implements Iterator<PlotItem> {
+ PlotItem next;
+
+ Iterator<PlotItem> cur;
+
+ Iterator<PlotItem> sub;
+
+ /**
+ * Constructor.
+ */
+ public ItmItr() {
+ super();
+ this.next = PlotItem.this;
+ this.cur = null;
+ this.sub = subitems.iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if(next != null) {
+ return true;
+ }
+ if(cur != null && cur.hasNext()) {
+ next = cur.next();
+ return true;
+ }
+ if(sub.hasNext()) {
+ cur = sub.next().itemIterator();
+ return hasNext();
+ }
+ return false;
+ }
+
+ @Override
+ public PlotItem next() {
+ hasNext();
+ PlotItem ret = next;
+ next = null;
+ return ret;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java
new file mode 100644
index 00000000..ef05b6c0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/RectangleArranger.java
@@ -0,0 +1,546 @@
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.logging.LoggingConfiguration;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.arraylike.DoubleArray;
+
+/**
+ * This is a rather naive rectangle arrangement class. It will try to place
+ * rectangles on a canvas while maintaining the canvas size ratio as good as
+ * possible. It does not do an exhaustive search for optimizing the layout, but
+ * a greedy placement strategy, extending the canvas as little as possible.
+ *
+ * @author Erich Schubert
+ *
+ * @param <T> Key type
+ */
+public class RectangleArranger<T> {
+ /**
+ * Logging class
+ */
+ private static final Logging LOG = Logging.getLogger(RectangleArranger.class);
+
+ /**
+ * Target height/width ratio
+ */
+ private double ratio = 1.0;
+
+ /**
+ * Width
+ */
+ private double twidth = 1.0;
+
+ /**
+ * Height
+ */
+ private double theight = 1.0;
+
+ /**
+ * Column widths
+ */
+ private DoubleArray widths = new DoubleArray();
+
+ /**
+ * Column heights
+ */
+ private DoubleArray heights = new DoubleArray();
+
+ /**
+ * Map indicating which cells are used.
+ */
+ private ArrayList<ArrayList<Object>> usage = new ArrayList<>();
+
+ /**
+ * Data
+ */
+ private Map<T, double[]> map = new HashMap<>();
+
+ /**
+ * Constructor.
+ *
+ * @param ratio
+ */
+ public RectangleArranger(double ratio) {
+ this(ratio, 1.0);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param width Canvas width
+ * @param height Canvas height
+ */
+ public RectangleArranger(double width, double height) {
+ this.ratio = width / height;
+ this.twidth = width;
+ this.theight = height;
+ this.widths.add(width);
+ this.heights.add(height);
+ // setup usage matrix
+ ArrayList<Object> u = new ArrayList<>();
+ u.add(null);
+ this.usage.add(u);
+ assertConsistent();
+ }
+
+ /**
+ * Add a new recangle.
+ *
+ * @param w Width
+ * @param h Height
+ * @param data Data object to add (key)
+ */
+ public void put(double w, double h, T data) {
+ if(LOG.isDebuggingFinest()) {
+ LOG.finest("Add: " + w + "x" + h);
+ }
+ final int cols = widths.size();
+ final int rows = heights.size();
+
+ int bestsx = -1;
+ int bestsy = -1;
+ int bestex = cols - 1;
+ int bestey = -1;
+ double bestwi;
+ double besthi;
+ double bestinc;
+ // Baseline: grow by adding to the top or to the right.
+ {
+ double i1 = computeIncreaseArea(w, Math.max(0, h - theight));
+ double i2 = computeIncreaseArea(Math.max(0, w - twidth), h);
+ if(i1 < i2) {
+ bestwi = w;
+ besthi = Math.max(0, h - theight);
+ bestinc = i1;
+ }
+ else {
+ bestwi = Math.max(0, w - twidth);
+ besthi = h;
+ bestinc = i2;
+ }
+ }
+ // Find position with minimum increase
+ for(int sy = 0; sy < rows; sy++) {
+ for(int sx = 0; sx < cols; sx++) {
+ if(usage.get(sy).get(sx) != null) {
+ continue;
+ }
+ // Start with single cell
+ double avw = widths.get(sx);
+ double avh = heights.get(sy);
+ int ex = sx;
+ int ey = sy;
+ while(avw < w || avh < h) {
+ // Grow width first
+ if(avw / avh < w / h) {
+ if(avw < w && ex + 1 < cols) {
+ boolean ok = true;
+ // All unused?
+ for(int y = sy; y <= ey; y++) {
+ if(usage.get(y).get(ex + 1) != null) {
+ ok = false;
+ }
+ }
+ if(ok) {
+ ex += 1;
+ avw += widths.get(ex);
+ continue;
+ }
+ }
+ if(avh < h && ey + 1 < rows) {
+ boolean ok = true;
+ // All unused?
+ for(int x = sx; x <= ex; x++) {
+ if(usage.get(ey + 1).get(x) != null) {
+ ok = false;
+ }
+ }
+ if(ok) {
+ ey += 1;
+ avh += heights.get(ey);
+ continue;
+ }
+ }
+ }
+ else { // Grow height first
+ if(avh < h && ey + 1 < rows) {
+ boolean ok = true;
+ // All unused?
+ for(int x = sx; x <= ex; x++) {
+ if(usage.get(ey + 1).get(x) != null) {
+ ok = false;
+ }
+ }
+ if(ok) {
+ ey += 1;
+ avh += heights.get(ey);
+ continue;
+ }
+ }
+ if(avw < w && ex + 1 < cols) {
+ boolean ok = true;
+ // All unused?
+ for(int y = sy; y <= ey; y++) {
+ if(usage.get(y).get(ex + 1) != null) {
+ ok = false;
+ }
+ }
+ if(ok) {
+ ex += 1;
+ avw += widths.get(ex);
+ continue;
+ }
+ }
+ }
+ break;
+ }
+ // Good match, or extension possible?
+ if(avw < w && ex < cols - 1) {
+ continue;
+ }
+ if(avh < h && ey < rows - 1) {
+ continue;
+ }
+ // Compute increase:
+ double winc = Math.max(0.0, w - avw);
+ double hinc = Math.max(0.0, h - avh);
+ double inc = computeIncreaseArea(winc, hinc);
+
+ if(LOG.isDebuggingFinest()) {
+ LOG.debugFinest("Candidate: " + sx + "," + sy + " - " + ex + "," + ey + ": " + avw + "x" + avh + " " + inc);
+ }
+ if(inc < bestinc) {
+ bestinc = inc;
+ bestsx = sx;
+ bestsy = sy;
+ bestex = ex;
+ bestey = ey;
+ bestwi = w - avw;
+ besthi = h - avh;
+ }
+ if(inc == 0) {
+ // Can't find better
+ // TODO: try to do less splitting maybe?
+ break;
+ }
+ }
+ assert assertConsistent();
+ }
+ if(LOG.isDebuggingFinest()) {
+ LOG.debugFinest("Best: " + bestsx + "," + bestsy + " - " + bestex + "," + bestey + " inc: " + bestwi + "x" + besthi + " " + bestinc);
+ }
+ // Need to increase the total area
+ if(bestinc > 0) {
+ assert(bestex == cols - 1 || bestey == rows - 1);
+ double inc = Math.max(bestwi, besthi * ratio);
+ resize(inc);
+
+ // Resubmit
+ put(w, h, data);
+ return;
+ }
+ // Need to split a column.
+ // TODO: find best column to split. Currently: last
+ if(bestwi < 0.0) {
+ splitCol(bestex, -bestwi);
+ bestwi = 0.0;
+ }
+ // Need to split a row.
+ // TODO: find best row to split. Currently: last
+ if(besthi < 0.0) {
+ splitRow(bestey, -besthi);
+ besthi = 0.0;
+ }
+ for(int x = bestsx; x <= bestex; x++) {
+ for(int y = bestsy; y <= bestey; y++) {
+ usage.get(y).set(x, data);
+ }
+ }
+ double xpos = 0.0;
+ double ypos = 0.0;
+ {
+ for(int x = 0; x < bestsx; x++) {
+ xpos += widths.get(x);
+ }
+ for(int y = 0; y < bestsy; y++) {
+ ypos += heights.get(y);
+ }
+ }
+ map.put(data, new double[] { xpos, ypos, w, h });
+ if(LOG.isDebuggingFinest()) {
+ logSizes();
+ }
+ }
+
+ protected double computeIncreaseArea(double winc, double hinc) {
+ double inc = Math.max(winc, hinc * ratio);
+ inc = inc * (hinc + inc / ratio + winc / ratio);
+ return inc;
+ }
+
+ protected void splitRow(int bestey, double besthi) {
+ assert(bestey < heights.size());
+ if(heights.get(bestey) - besthi <= Double.MIN_NORMAL) {
+ return;
+ }
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("Split row " + bestey);
+ }
+ heights.insert(bestey + 1, besthi);
+ heights.set(bestey, heights.get(bestey) - besthi);
+ // Update used map
+ usage.add(bestey + 1, new ArrayList<>(usage.get(bestey)));
+ }
+
+ protected void splitCol(int bestex, double bestwi) {
+ assert(bestex < widths.size());
+ if(widths.get(bestex) - bestwi <= Double.MIN_NORMAL) {
+ return;
+ }
+ final int rows = heights.size();
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("Split column " + bestex);
+ }
+ widths.insert(bestex + 1, bestwi);
+ widths.set(bestex, widths.get(bestex) - bestwi);
+ // Update used map
+ for(int y = 0; y < rows; y++) {
+ usage.get(y).add(bestex + 1, usage.get(y).get(bestex));
+ }
+ assert assertConsistent();
+ }
+
+ private void resize(double inc) {
+ final int cols = widths.size();
+ final int rows = heights.size();
+ if(LOG.isDebuggingFine()) {
+ LOG.debugFine("Resize by " + inc + "x" + (inc / ratio));
+ if(LOG.isDebuggingFinest()) {
+ logSizes();
+ }
+ }
+ // TODO: if the last row or column is empty, we can do this simpler
+ widths.add(inc);
+ twidth += inc;
+ heights.add(inc / ratio);
+ theight += inc / ratio;
+ // Add column:
+ for(int y = 0; y < rows; y++) {
+ usage.get(y).add(null);
+ }
+ // Add row:
+ {
+ ArrayList<Object> row = new ArrayList<>();
+ for(int x = 0; x <= cols; x++) {
+ row.add(null);
+ }
+ usage.add(row);
+ }
+ assert assertConsistent();
+ if(LOG.isDebuggingFinest()) {
+ logSizes();
+ }
+ }
+
+ /**
+ * Get the position data of the object
+ *
+ * @param object Query object
+ * @return Position information: x,y,w,h
+ */
+ public double[] get(T object) {
+ double[] v = map.get(object);
+ if(v == null) {
+ return null;
+ }
+ return v.clone();
+ }
+
+ private boolean assertConsistent() {
+ final int cols = widths.size();
+ final int rows = heights.size();
+ {
+ double wsum = 0.0;
+ for(int x = 0; x < cols; x++) {
+ assert(widths.get(x) > 0) : "Non-positive width: " + widths.get(x) + " at " + x;
+ wsum += widths.get(x);
+ }
+ assert(Math.abs(wsum - twidth) < 1E-10);
+ }
+ {
+ double hsum = 0.0;
+ for(int y = 0; y < rows; y++) {
+ assert(heights.get(y) > 0) : "Non-positive height: " + heights.get(y) + " at " + y;
+ hsum += heights.get(y);
+ }
+ assert(Math.abs(hsum - theight) < 1E-10);
+ }
+ {
+ assert(usage.size() == rows);
+ for(int y = 0; y < rows; y++) {
+ assert(usage.get(y).size() == cols);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Debug logging
+ */
+ protected void logSizes() {
+ StringBuilder buf = new StringBuilder();
+ final int cols = widths.size();
+ final int rows = heights.size();
+ {
+ buf.append("Widths: ");
+ for(int x = 0; x < cols; x++) {
+ if(x > 0) {
+ buf.append(", ");
+ }
+ buf.append(widths.get(x));
+ }
+ buf.append('\n');
+ }
+ {
+ buf.append("Heights: ");
+ for(int y = 0; y < rows; y++) {
+ if(y > 0) {
+ buf.append(", ");
+ }
+ buf.append(heights.get(y));
+ }
+ buf.append('\n');
+ }
+ {
+ for(int y = 0; y < rows; y++) {
+ for(int x = 0; x < cols; x++) {
+ buf.append(usage.get(y).get(x) != null ? "X" : "_");
+ }
+ buf.append("|\n");
+ }
+ for(int x = 0; x < cols; x++) {
+ buf.append('-');
+ }
+ buf.append("+\n");
+ }
+ LOG.debug(buf);
+ }
+
+ /**
+ * Compute the relative fill. Useful for triggering a relayout if the relative
+ * fill is not satisfactory.
+ *
+ * @return relative fill
+ */
+ public double relativeFill() {
+ double acc = 0.0;
+ final int cols = widths.size();
+ final int rows = heights.size();
+ {
+ for(int y = 0; y < rows; y++) {
+ for(int x = 0; x < cols; x++) {
+ if(usage.get(y).get(x) != null) {
+ acc += widths.get(x) * heights.get(y);
+ }
+ }
+ }
+ }
+ return acc / (twidth * theight);
+ }
+
+ /**
+ * Get the total canvas width
+ *
+ * @return Width
+ */
+ public double getWidth() {
+ return twidth;
+ }
+
+ /**
+ * Get the total canvas height
+ *
+ * @return Height
+ */
+ public double getHeight() {
+ return theight;
+ }
+
+ /**
+ * The items contained in the map.
+ *
+ * @return entry set
+ */
+ public Set<Entry<T, double[]>> entrySet() {
+ return Collections.unmodifiableSet(map.entrySet());
+ }
+
+ /**
+ * The item keys contained in the map.
+ *
+ * @return key set
+ */
+ public Set<T> keySet() {
+ return Collections.unmodifiableSet(map.keySet());
+ }
+
+ /**
+ * Test method.
+ *
+ * @param args
+ */
+ public static void main(String[] args) {
+ LoggingConfiguration.setLevelFor(RectangleArranger.class.getName(), Level.FINEST.getName());
+ RectangleArranger<String> r = new RectangleArranger<>(1.3);
+ r.put(4., 1., "Histogram");
+ r.put(4., 4., "3D view");
+ r.put(1., 1., "Meta 1");
+ r.put(1., 1., "Meta 2");
+ r.put(1., 1., "Meta 3");
+ r.put(2., 2., "Meta 4");
+ r.put(2., 2., "Meta 5");
+
+ r = new RectangleArranger<>(3., 3.);
+ r.put(1., 2., "A");
+ r.put(2., 1., "B");
+ r.put(1., 2., "C");
+ r.put(2., 1., "D");
+ r.put(2., 2., "E");
+
+ r = new RectangleArranger<>(4 - 2.6521739130434785);
+ r.put(4., .5, "A");
+ r.put(4., 3., "B");
+ r.put(4., 1., "C");
+ r.put(1., .1, "D");
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java
new file mode 100755
index 00000000..b42202bb
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/overview/package-info.java
@@ -0,0 +1,28 @@
+/**
+ * <p>Classes for managing the overview plot.</p>
+ *
+ * @apiviz.exclude java.awt.event.*
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.gui.overview;
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java
new file mode 100755
index 00000000..5d7437b5
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/gui/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Package to provide a visualization GUI.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.gui; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java
new file mode 100644
index 00000000..d85a9415
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSCut.java
@@ -0,0 +1,101 @@
+package de.lmu.ifi.dbs.elki.visualization.opticsplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.ClusterModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDVar;
+import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs;
+
+/**
+ * Compute a partitioning from an OPTICS plot by doing a horizontal cut.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ *
+ * @apiviz.uses ClusterOrder
+ */
+// TODO: add non-flat clusterings
+public class OPTICSCut {
+ /**
+ * Compute an OPTICS cut clustering
+ *
+ * @param co Cluster order result
+ * @param epsilon Epsilon value for cut
+ * @return New partitioning clustering
+ */
+ public static <E extends ClusterOrder> Clustering<Model> makeOPTICSCut(E co, double epsilon) {
+ // Clustering model we are building
+ Clustering<Model> clustering = new Clustering<>("OPTICS Cut Clustering", "optics-cut");
+ // Collects noise elements
+ ModifiableDBIDs noise = DBIDUtil.newHashSet();
+
+ double lastDist = Double.MAX_VALUE;
+ double actDist = Double.MAX_VALUE;
+
+ // Current working set
+ ModifiableDBIDs current = DBIDUtil.newHashSet();
+
+ // TODO: can we implement this more nicely with a 1-lookahead?
+ DBIDVar prev = DBIDUtil.newVar();
+ for(DBIDIter it = co.iter(); it.valid(); prev.set(it), it.advance()) {
+ lastDist = actDist;
+ actDist = co.getReachability(it);
+
+ if(actDist <= epsilon) {
+ // the last element before the plot drops belongs to the cluster
+ if(lastDist > epsilon && prev.isSet()) {
+ // So un-noise it
+ noise.remove(prev);
+ // Add it to the cluster
+ current.add(prev);
+ }
+ current.add(it);
+ }
+ else {
+ // 'Finish' the previous cluster
+ if(!current.isEmpty()) {
+ // TODO: do we want a minpts restriction?
+ // But we get have only core points guaranteed anyway.
+ clustering.addToplevelCluster(new Cluster<Model>(current, ClusterModel.CLUSTER));
+ current = DBIDUtil.newHashSet();
+ }
+ // Add to noise
+ noise.add(it);
+ }
+ }
+ // Any unfinished cluster will also be added
+ if(!current.isEmpty()) {
+ clustering.addToplevelCluster(new Cluster<Model>(current, ClusterModel.CLUSTER));
+ }
+ // Add noise
+ clustering.addToplevelCluster(new Cluster<Model>(noise, true, ClusterModel.CLUSTER));
+ return clustering;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java
new file mode 100644
index 00000000..c25a7961
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/OPTICSPlot.java
@@ -0,0 +1,301 @@
+package de.lmu.ifi.dbs.elki.visualization.opticsplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.BufferedImage;
+import java.awt.image.RenderedImage;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+
+/**
+ * Class to produce an OPTICS plot image.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf LinearScale
+ * @apiviz.has ClusterOrder oneway - - renders
+ */
+public class OPTICSPlot implements Result {
+ /**
+ * Logger
+ */
+ private static final Logging LOG = Logging.getLogger(OPTICSPlot.class);
+
+ /**
+ * Minimum and maximum vertical resolution.
+ */
+ private static final int MIN_HEIGHT = 25, MAX_HEIGHT = 300;
+
+ /**
+ * Scale to use
+ */
+ LinearScale scale;
+
+ /**
+ * Width of plot
+ */
+ int width;
+
+ /**
+ * Height of plot
+ */
+ int height;
+
+ /**
+ * Ratio of plot
+ */
+ double ratio;
+
+ /**
+ * The result to plot.
+ */
+ final ClusterOrder co;
+
+ /**
+ * Color adapter to use
+ */
+ final StylingPolicy colors;
+
+ /**
+ * The Optics plot.
+ */
+ protected RenderedImage plot;
+
+ /**
+ * The plot number for Batik
+ */
+ protected int plotnum = -1;
+
+ /**
+ * Constructor, with automatic distance adapter detection.
+ *
+ * @param co Cluster order to plot.
+ * @param colors Coloring strategy
+ */
+ public OPTICSPlot(ClusterOrder co, StylingPolicy colors) {
+ super();
+ this.co = co;
+ this.colors = colors;
+ }
+
+ /**
+ * Trigger a redraw of the OPTICS plot
+ */
+ public void replot() {
+ width = co.size();
+ height = (int) Math.ceil(width * .2);
+ ratio = width / (double) height;
+ height = height < MIN_HEIGHT ? MIN_HEIGHT : height > MAX_HEIGHT ? MAX_HEIGHT : height;
+ if(scale == null) {
+ scale = computeScale(co);
+ }
+
+ BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+
+ int x = 0;
+ for(DBIDIter it = co.iter(); it.valid(); it.advance()) {
+ double reach = co.getReachability(it);
+ final int y = scaleToPixel(reach);
+ try {
+ int col = colors.getColorForDBID(it);
+ for(int y2 = height - 1; y2 >= y; y2--) {
+ img.setRGB(x, y2, col);
+ }
+ }
+ catch(ArrayIndexOutOfBoundsException e) {
+ LOG.error("Plotting out of range: " + x + "," + y + " >= " + width + "x" + height);
+ }
+ x++;
+ }
+
+ plot = img;
+ }
+
+ /**
+ * Scale a reachability distance to a pixel value.
+ *
+ * @param reach Reachability
+ * @return Pixel value.
+ */
+ public int scaleToPixel(double reach) {
+ return (Double.isInfinite(reach) || Double.isNaN(reach)) ? 0 : //
+ (int) Math.round(scale.getScaled(reach, height - .5, .5));
+ }
+
+ /**
+ * Scale a pixel value to a reachability
+ *
+ * @param y Pixel value
+ * @return Reachability
+ */
+ public double scaleFromPixel(double y) {
+ return scale.getUnscaled((y - .5) / (height - 1.));
+ }
+
+ /**
+ * Compute the scale (value range)
+ *
+ * @param order Cluster order to process
+ * @return Scale for value range of cluster order
+ */
+ protected LinearScale computeScale(ClusterOrder order) {
+ DoubleMinMax range = new DoubleMinMax();
+ // calculate range
+ for(DBIDIter it = order.iter(); it.valid(); it.advance()) {
+ final double reach = co.getReachability(it);
+ if(reach < Double.POSITIVE_INFINITY) {
+ range.put(reach);
+ }
+ }
+ // Ensure we have a valid range
+ if(!range.isValid()) {
+ range.put(0.0);
+ range.put(1.0);
+ }
+ return new LinearScale(range.getMin(), range.getMax());
+ }
+
+ /**
+ * @return the scale
+ */
+ public LinearScale getScale() {
+ if(plot == null) {
+ replot();
+ }
+ return scale;
+ }
+
+ /**
+ * @return the width
+ */
+ public int getWidth() {
+ if(plot == null) {
+ replot();
+ }
+ return width;
+ }
+
+ /**
+ * @return the height
+ */
+ public int getHeight() {
+ if(plot == null) {
+ replot();
+ }
+ return height;
+ }
+
+ /**
+ * Get width-to-height ratio of image.
+ *
+ * @return {@code width / height}
+ */
+ public double getRatio() {
+ if(plot == null) {
+ replot();
+ }
+ return ratio;
+ }
+
+ /**
+ * Get the OPTICS plot.
+ *
+ * @return plot image
+ */
+ public synchronized RenderedImage getPlot() {
+ if(plot == null) {
+ replot();
+ }
+ return plot;
+ }
+
+ /**
+ * Free memory used by rendered image.
+ */
+ public void forgetRenderedImage() {
+ plotnum = -1;
+ plot = null;
+ }
+
+ /**
+ * Get the SVG registered plot number
+ *
+ * @return Plot URI
+ */
+ public String getSVGPlotURI() {
+ if(plotnum < 0) {
+ plotnum = ThumbnailRegistryEntry.registerImage(plot);
+ }
+ return ThumbnailRegistryEntry.INTERNAL_PREFIX + plotnum;
+ }
+
+ @Override
+ public String getLongName() {
+ return "OPTICS Plot";
+ }
+
+ @Override
+ public String getShortName() {
+ return "optics plot";
+ }
+
+ /**
+ * Static method to find an optics plot for a result, or to create a new one
+ * using the given context.
+ *
+ * @param co Cluster order
+ * @param context Context (for colors and reference clustering)
+ *
+ * @return New or existing optics plot
+ */
+ public static OPTICSPlot plotForClusterOrder(ClusterOrder co, VisualizerContext context) {
+ // Check for an existing plot
+ // ArrayList<OPTICSPlot<D>> plots = ResultUtil.filterResults(co,
+ // OPTICSPlot.class);
+ // if (plots.size() > 0) {
+ // return plots.get(0);
+ // }
+ final StylingPolicy policy = context.getStylingPolicy();
+ OPTICSPlot opticsplot = new OPTICSPlot(co, policy);
+ // co.addChildResult(opticsplot);
+ return opticsplot;
+ }
+
+ /**
+ * Get the cluster order we are attached to.
+ *
+ * @return Cluster order
+ */
+ public ClusterOrder getClusterOrder() {
+ return co;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java
new file mode 100644
index 00000000..c39c344b
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/opticsplot/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Code for drawing OPTICS plots</p>
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.opticsplot; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java
new file mode 100644
index 00000000..98345a5e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/package-info.java
@@ -0,0 +1,29 @@
+/**
+ * <p>Visualization package of ELKI.</p>
+ *
+ * @apiviz.exclude elki.utilities
+ * @apiviz.exclude java.lang.*
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization;
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java
new file mode 100644
index 00000000..4c57a6f0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractFullProjection.java
@@ -0,0 +1,232 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Abstract base class for full projections.
+ *
+ * Note: the full projection API may be removed at some point, unless we find a
+ * clear use case that cannot be done by the low level fast projections.
+ *
+ * @author Erich Schubert
+ */
+public abstract class AbstractFullProjection extends AbstractProjection implements FullProjection {
+ /**
+ * Constructor.
+ *
+ * @param p Projector
+ * @param scales Scales
+ */
+ public AbstractFullProjection(Projector p, LinearScale[] scales) {
+ super(p, scales);
+ }
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ @Override
+ public double[] projectDataToScaledSpace(NumberVector data) {
+ final int dim = data.getDimensionality();
+ double[] vec = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getScaled(data.doubleValue(d));
+ }
+ return vec;
+ }
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ @Override
+ public double[] projectDataToScaledSpace(double[] data) {
+ final int dim = data.length;
+ double[] dst = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ dst[d] = scales[d].getScaled(data[d]);
+ }
+ return dst;
+ }
+
+ /**
+ * Project a relative data vector from data space to scaled space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in scaled space
+ */
+ @Override
+ public double[] projectRelativeDataToScaledSpace(NumberVector data) {
+ final int dim = data.getDimensionality();
+ double[] vec = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getRelativeScaled(data.doubleValue(d));
+ }
+ return vec;
+ }
+
+ /**
+ * Project a relative data vector from data space to scaled space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in scaled space
+ */
+ @Override
+ public double[] projectRelativeDataToScaledSpace(double[] data) {
+ final int dim = data.length;
+ double[] dst = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ dst[d] = scales[d].getRelativeScaled(data[d]);
+ }
+ return dst;
+ }
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ @Override
+ public double[] projectDataToRenderSpace(NumberVector data) {
+ return projectScaledToRender(projectDataToScaledSpace(data));
+ }
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ @Override
+ public double[] projectDataToRenderSpace(double[] data) {
+ return projectScaledToRender(projectDataToScaledSpace(data));
+ }
+
+ /**
+ * Project a relative data vector from data space to rendering space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in rendering space
+ */
+ @Override
+ public double[] projectRelativeDataToRenderSpace(NumberVector data) {
+ return projectRelativeScaledToRender(projectRelativeDataToScaledSpace(data));
+ }
+
+ /**
+ * Project a relative data vector from data space to rendering space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in rendering space
+ */
+ @Override
+ public double[] projectRelativeDataToRenderSpace(double[] data) {
+ return projectRelativeScaledToRender(projectRelativeDataToScaledSpace(data));
+ }
+
+ /**
+ * Project a vector from scaled space to data space.
+ *
+ * @param <NV> Vector type
+ * @param v vector in scaled space
+ * @param factory Object factory
+ * @return vector in data space
+ */
+ @Override
+ public <NV extends NumberVector> NV projectScaledToDataSpace(double[] v, NumberVector.Factory<NV> factory) {
+ final int dim = v.length;
+ double[] vec = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getUnscaled(v[d]);
+ }
+ return factory.newNumberVector(vec);
+ }
+
+ /**
+ * Project a vector from rendering space to data space.
+ *
+ * @param <NV> Vector type
+ * @param v vector in rendering space
+ * @param prototype Object factory
+ * @return vector in data space
+ */
+ @Override
+ public <NV extends NumberVector> NV projectRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype) {
+ final int dim = v.length;
+ double[] vec = projectRenderToScaled(v);
+ // Not calling {@link #projectScaledToDataSpace} to avoid extra copy of
+ // vector.
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getUnscaled(vec[d]);
+ }
+ return prototype.newNumberVector(vec);
+ }
+
+ /**
+ * Project a relative vector from scaled space to data space.
+ *
+ * @param <NV> Vector type
+ * @param v relative vector in scaled space
+ * @param prototype Object factory
+ * @return relative vector in data space
+ */
+ @Override
+ public <NV extends NumberVector> NV projectRelativeScaledToDataSpace(double[] v, NumberVector.Factory<NV> prototype) {
+ final int dim = v.length;
+ double[] vec = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getRelativeUnscaled(v[d]);
+ }
+ return prototype.newNumberVector(vec);
+ }
+
+ /**
+ * Project a relative vector from rendering space to data space.
+ *
+ * @param <NV> Vector type
+ * @param v relative vector in rendering space
+ * @param prototype Object factory
+ * @return relative vector in data space
+ */
+ @Override
+ public <NV extends NumberVector> NV projectRelativeRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype) {
+ final int dim = v.length;
+ double[] vec = projectRelativeRenderToScaled(v);
+ // Not calling {@link #projectScaledToDataSpace} to avoid extra copy of
+ // vector.
+ for(int d = 0; d < dim; d++) {
+ vec[d] = scales[d].getRelativeUnscaled(vec[d]);
+ }
+ return prototype.newNumberVector(vec);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java
new file mode 100644
index 00000000..bbcd1bcd
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractProjection.java
@@ -0,0 +1,82 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Abstract base projection class.
+ *
+ * @author Erich Schubert
+ */
+public abstract class AbstractProjection implements Projection {
+ /**
+ * Scales in data set
+ */
+ final protected LinearScale[] scales;
+
+ /**
+ * Projector used
+ */
+ final private Projector p;
+
+ /**
+ * Constructor.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ */
+ public AbstractProjection(Projector p, LinearScale[] scales) {
+ super();
+ this.p = p;
+ this.scales = scales;
+ }
+
+ @Override
+ public int getInputDimensionality() {
+ return scales.length;
+ }
+
+ /**
+ * Get the scales used, for rendering scales mostly.
+ *
+ * @param d Dimension
+ * @return Scale used
+ */
+ @Override
+ public LinearScale getScale(int d) {
+ return scales[d];
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Projection";
+ }
+
+ @Override
+ public Projector getProjector() {
+ return p;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java
new file mode 100644
index 00000000..4b0715a9
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AbstractSimpleProjection.java
@@ -0,0 +1,110 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Abstract base class for "simple" projections.
+ *
+ * Simple projections use the given scaling and dimension selection only.
+ *
+ * @author Erich Schubert
+ */
+public abstract class AbstractSimpleProjection extends AbstractFullProjection {
+ /**
+ * Constructor.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ */
+ public AbstractSimpleProjection(Projector p, LinearScale[] scales) {
+ super(p, scales);
+ }
+
+ @Override
+ public double[] projectScaledToRender(double[] v) {
+ v = rearrange(v);
+ VMath.minusEquals(v, .5);
+ v = flipSecondEquals(v);
+ VMath.timesEquals(v, SCALE);
+ return v;
+ }
+
+ @Override
+ public double[] projectRenderToScaled(double[] v) {
+ v = VMath.times(v, INVSCALE);
+ v = flipSecondEquals(v);
+ VMath.plusEquals(v, .5);
+ v = dearrange(v);
+ return v;
+ }
+
+ @Override
+ public double[] projectRelativeScaledToRender(double[] v) {
+ v = rearrange(v);
+ v = flipSecondEquals(v);
+ VMath.timesEquals(v, SCALE);
+ return v;
+ }
+
+ @Override
+ public double[] projectRelativeRenderToScaled(double[] v) {
+ v = VMath.times(v, INVSCALE);
+ v = flipSecondEquals(v);
+ v = dearrange(v);
+ return v;
+ }
+
+ /**
+ * Flip the y axis.
+ *
+ * @param v double[]
+ * @return modified v
+ */
+ protected double[] flipSecondEquals(double[] v) {
+ if(v.length > 1) {
+ v[1] *= -1;
+ }
+ return v;
+ }
+
+ /**
+ * Method to rearrange components.
+ *
+ * @param v double[] to rearrange
+ * @return rearranged copy
+ */
+ protected abstract double[] rearrange(double[] v);
+
+ /**
+ * Undo the rearrangement of components.
+ *
+ * @param v double[] to undo the rearrangement
+ * @return rearranged-undone copy
+ */
+ protected abstract double[] dearrange(double[] v);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java
new file mode 100644
index 00000000..a04c57fc
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/AffineProjection.java
@@ -0,0 +1,277 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Arrays;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.AffineTransformation;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Affine projections are the most general class. They are initialized by an
+ * arbitrary affine transformation matrix, and can thus represent any rotation
+ * and scaling, even simple perspective projections.
+ *
+ * However, this comes at the cost of a matrix multiplication.
+ *
+ * @author Erich Schubert
+ */
+public class AffineProjection extends AbstractFullProjection implements Projection2D {
+ /**
+ * Affine transformation used in projection
+ */
+ private AffineTransformation proj;
+
+ /**
+ * Viewport (cache)
+ */
+ private CanvasSize viewport = null;
+
+ /**
+ * Constructor with a given database and axes.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ * @param proj Projection to use
+ */
+ public AffineProjection(Projector p, LinearScale[] scales, AffineTransformation proj) {
+ super(p, scales);
+ this.proj = proj;
+ }
+
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ @Override
+ public double[] projectScaledToRender(double[] v) {
+ return proj.apply(v);
+ }
+
+ /**
+ * Project a vector from rendering space to scaled space.
+ *
+ * @param v vector in rendering space
+ * @return vector in scaled space
+ */
+ @Override
+ public double[] projectRenderToScaled(double[] v) {
+ return proj.applyInverse(v);
+ }
+
+ /**
+ * Project a relative vector from scaled space to rendering space.
+ *
+ * @param v relative vector in scaled space
+ * @return relative vector in rendering space
+ */
+ @Override
+ public double[] projectRelativeScaledToRender(double[] v) {
+ return proj.applyRelative(v);
+ }
+
+ /**
+ * Project a relative vector from rendering space to scaled space.
+ *
+ * @param v relative vector in rendering space
+ * @return relative vector in scaled space
+ */
+ @Override
+ public double[] projectRelativeRenderToScaled(double[] v) {
+ return proj.applyRelativeInverse(v);
+ }
+
+ @Override
+ public CanvasSize estimateViewport() {
+ if(viewport == null) {
+ final int dim = proj.getDimensionality();
+ DoubleMinMax minmaxx = new DoubleMinMax();
+ DoubleMinMax minmaxy = new DoubleMinMax();
+
+ // Origin
+ final double[] vec = new double[dim];
+ double[] orig = projectScaledToRender(vec);
+ minmaxx.put(orig[0]);
+ minmaxy.put(orig[1]);
+ // Diagonal point
+ Arrays.fill(vec, 1.);
+ double[] diag = projectScaledToRender(vec);
+ minmaxx.put(diag[0]);
+ minmaxy.put(diag[1]);
+ // Axis end points
+ for(int d = 0; d < dim; d++) {
+ Arrays.fill(vec, 0.);
+ vec[d] = 1.;
+ double[] ax = projectScaledToRender(vec);
+ minmaxx.put(ax[0]);
+ minmaxy.put(ax[1]);
+ }
+ viewport = new CanvasSize(minmaxx.getMin(), minmaxx.getMax(), minmaxy.getMin(), minmaxy.getMax());
+ }
+ return viewport;
+ }
+
+ /**
+ * Compute an transformation matrix to show only axis ax1 and ax2.
+ *
+ * @param dim Dimensionality
+ * @param ax1 First axis
+ * @param ax2 Second axis
+ * @return transformation matrix
+ */
+ public static AffineTransformation axisProjection(int dim, int ax1, int ax2) {
+ // setup a projection to get the data into the interval -1:+1 in each
+ // dimension with the intended-to-see dimensions first.
+ AffineTransformation proj = AffineTransformation.reorderAxesTransformation(dim, new int[] { ax1, ax2 });
+ // Assuming that the data was normalized on [0:1], center it:
+ double[] trans = new double[dim];
+ for(int i = 0; i < dim; i++) {
+ trans[i] = -.5;
+ }
+ proj.addTranslation(trans);
+ // mirror on the y axis, since the SVG coordinate system is screen
+ // coordinates (y = down) and not mathematical coordinates (y = up)
+ proj.addAxisReflection(2);
+ // scale it up
+ proj.addScaling(SCALE);
+
+ return proj;
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(double[] data) {
+ return fastProjectScaledToRenderSpace(fastProjectDataToScaledSpace(data));
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(NumberVector data) {
+ return fastProjectScaledToRenderSpace(fastProjectDataToScaledSpace(data));
+ }
+
+ @Override
+ public double[] fastProjectDataToScaledSpace(double[] data) {
+ return projectDataToScaledSpace(data);
+ }
+
+ @Override
+ public double[] fastProjectDataToScaledSpace(NumberVector data) {
+ return projectDataToScaledSpace(data);
+ }
+
+ @Override
+ public double[] fastProjectScaledToRenderSpace(double[] vr) {
+ double x = 0.0;
+ double y = 0.0;
+ double s = 0.0;
+
+ final double[][] matrix = proj.getTransformation().getArrayRef();
+ final double[] colx = matrix[0];
+ final double[] coly = matrix[1];
+ final double[] cols = matrix[vr.length];
+ assert (colx.length == coly.length && colx.length == cols.length && cols.length == vr.length + 1);
+
+ for(int k = 0; k < vr.length; k++) {
+ x += colx[k] * vr[k];
+ y += coly[k] * vr[k];
+ s += cols[k] * vr[k];
+ }
+ // add homogene component:
+ x += colx[vr.length];
+ y += coly[vr.length];
+ s += cols[vr.length];
+ // Note: we may have NaN values here.
+ // assert (s > 0.0 || s < 0.0);
+ return new double[] { x / s, y / s };
+ }
+
+ @Override
+ public double[] fastProjectRelativeDataToRenderSpace(double[] data) {
+ return fastProjectRelativeScaledToRenderSpace(projectRelativeDataToScaledSpace(data));
+ }
+
+ @Override
+ public double[] fastProjectRelativeDataToRenderSpace(NumberVector data) {
+ // FIXME: implement with less objects?
+ return fastProjectRelativeScaledToRenderSpace(projectRelativeDataToScaledSpace(data));
+ }
+
+ @Override
+ public double[] fastProjectRelativeScaledToRenderSpace(double[] vr) {
+ double x = 0.0;
+ double y = 0.0;
+
+ final double[][] matrix = proj.getTransformation().getArrayRef();
+ final double[] colx = matrix[0];
+ final double[] coly = matrix[1];
+ assert (colx.length == coly.length);
+
+ for(int k = 0; k < vr.length; k++) {
+ x += colx[k] * vr[k];
+ y += coly[k] * vr[k];
+ }
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectRenderToDataSpace(double x, double y) {
+ double[] ret = fastProjectRenderToScaledSpace(x, y);
+ for(int d = 0; d < scales.length; d++) {
+ ret[d] = scales[d].getUnscaled(ret[d]);
+ }
+ return ret;
+ }
+
+ @Override
+ public double[] fastProjectRenderToScaledSpace(double x, double y) {
+ double[] c = new double[scales.length];
+ c[0] = x;
+ c[1] = y;
+ Arrays.fill(c, 2, scales.length, 0.5);
+ return projectRenderToScaled(c);
+ }
+
+ @Override
+ public long[] getVisibleDimensions2D() {
+ final int dim = proj.getDimensionality();
+ long[] actDim = BitsUtil.zero(dim);
+ double[] vScale = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ Arrays.fill(vScale, 0);
+ vScale[d] = 1;
+ double[] vRender = fastProjectScaledToRenderSpace(vScale);
+
+ // TODO: Can't we do this by inspecting the projection matrix directly?
+ if(vRender[0] > 0.0 || vRender[0] < 0.0 || vRender[1] != 0) {
+ BitsUtil.setI(actDim, d);
+ }
+ }
+ return actDim;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java
new file mode 100644
index 00000000..b915ed07
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/CanvasSize.java
@@ -0,0 +1,139 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Size of a canvas. A 2D bounding rectangle.
+ *
+ * @author Erich Schubert
+ */
+public class CanvasSize {
+ /**
+ * Minimum X
+ */
+ public final double minx;
+
+ /**
+ * Maximum X
+ */
+ public final double maxx;
+
+ /**
+ * Minimum Y
+ */
+ public final double miny;
+
+ /**
+ * Maximum Y
+ */
+ public final double maxy;
+
+ /**
+ * Constructor.
+ *
+ * @param minx Minimum X
+ * @param maxx Maximum X
+ * @param miny Minimum Y
+ * @param maxy Maximum Y
+ */
+ public CanvasSize(double minx, double maxx, double miny, double maxy) {
+ super();
+ this.minx = minx;
+ this.maxx = maxx;
+ this.miny = miny;
+ this.maxy = maxy;
+ }
+
+ /**
+ * @return the mininum X
+ */
+ public double getMinX() {
+ return minx;
+ }
+
+ /**
+ * @return the maximum X
+ */
+ public double getMaxX() {
+ return maxx;
+ }
+
+ /**
+ * @return the minimum Y
+ */
+ public double getMinY() {
+ return miny;
+ }
+
+ /**
+ * @return the maximum Y
+ */
+ public double getMaxY() {
+ return maxy;
+ }
+
+ /**
+ * @return the length on X
+ */
+ public double getDiffX() {
+ return maxx - minx;
+ }
+
+ /**
+ * @return the length on Y
+ */
+ public double getDiffY() {
+ return maxy - miny;
+ }
+
+ /**
+ * Continue a line along a given direction to the margin.
+ *
+ * @param origin Origin point
+ * @param delta Direction vector
+ * @return scaling factor for delta vector
+ */
+ public double continueToMargin(double[] origin, double[] delta) {
+ assert (delta.length == 2 && origin.length == 2);
+ double factor = Double.POSITIVE_INFINITY;
+ if(delta[0] > 0) {
+ factor = Math.min(factor, (maxx - origin[0]) / delta[0]);
+ }
+ else if(delta[0] < 0) {
+ factor = Math.min(factor, (origin[0] - minx) / -delta[0]);
+ }
+ if(delta[1] > 0) {
+ factor = Math.min(factor, (maxy - origin[1]) / delta[1]);
+ }
+ else if(delta[1] < 0) {
+ factor = Math.min(factor, (origin[1] - miny) / -delta[1]);
+ }
+ return factor;
+ }
+
+ @Override
+ public String toString() {
+ return "CanvasSize[x=" + minx + ":" + maxx + ", y=" + miny + ":" + maxy + "]";
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java
new file mode 100644
index 00000000..553d8faa
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/FullProjection.java
@@ -0,0 +1,174 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+
+/**
+ * Full vector space projections.
+ *
+ * These rather portable projections offer a large choice of functions, at the
+ * cost of often being a bit slower than the low level functions.
+ *
+ * Note: this interface and methods may be removed, unless there is a clear use
+ * case for them as opposed to always using the low-level fast projections.
+ *
+ * @author Erich Schubert
+ */
+public interface FullProjection extends Projection {
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ double[] projectScaledToRender(double[] v);
+
+ /**
+ * Project a vector from rendering space to scaled space.
+ *
+ * @param v vector in rendering space
+ * @return vector in scaled space
+ */
+ double[] projectRenderToScaled(double[] v);
+
+ /**
+ * Project a relative vector from scaled space to rendering space.
+ *
+ * @param v relative vector in scaled space
+ * @return relative vector in rendering space
+ */
+ double[] projectRelativeScaledToRender(double[] v);
+
+ /**
+ * Project a relative vector from rendering space to scaled space.
+ *
+ * @param v relative vector in rendering space
+ * @return relative vector in scaled space
+ */
+ double[] projectRelativeRenderToScaled(double[] v);
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ double[] projectDataToScaledSpace(NumberVector data);
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ double[] projectDataToScaledSpace(double[] data);
+
+ /**
+ * Project a relative data vector from data space to scaled space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in scaled space
+ */
+ double[] projectRelativeDataToScaledSpace(NumberVector data);
+
+ /**
+ * Project a relative data vector from data space to scaled space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in scaled space
+ */
+ double[] projectRelativeDataToScaledSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ double[] projectDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ double[] projectDataToRenderSpace(double[] data);
+
+ /**
+ * Project a vector from scaled space to data space.
+ *
+ * @param <NV> double[] type
+ * @param v vector in scaled space
+ * @param factory Object factory
+ * @return vector in data space
+ */
+ <NV extends NumberVector> NV projectScaledToDataSpace(double[] v, NumberVector.Factory<NV> factory);
+
+ /**
+ * Project a vector from rendering space to data space.
+ *
+ * @param <NV> double[] type
+ * @param v vector in rendering space
+ * @param prototype Object factory
+ * @return vector in data space
+ */
+ <NV extends NumberVector> NV projectRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype);
+
+ /**
+ * Project a relative data vector from data space to rendering space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in rendering space
+ */
+ double[] projectRelativeDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a relative data vector from data space to rendering space.
+ *
+ * @param data relative vector in data space
+ * @return relative vector in rendering space
+ */
+ double[] projectRelativeDataToRenderSpace(double[] data);
+
+ /**
+ * Project a relative vector from scaled space to data space.
+ *
+ * @param <NV> double[] type
+ * @param v relative vector in scaled space
+ * @param prototype Object factory
+ * @return relative vector in data space
+ */
+ <NV extends NumberVector> NV projectRelativeScaledToDataSpace(double[] v, NumberVector.Factory<NV> prototype);
+
+ /**
+ * Project a relative vector from rendering space to data space.
+ *
+ * @param <NV> double[] type
+ * @param v relative vector in rendering space
+ * @param prototype Object factory
+ * @return relative vector in data space
+ */
+ <NV extends NumberVector> NV projectRelativeRenderToDataSpace(double[] v, NumberVector.Factory<NV> prototype);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java
new file mode 100644
index 00000000..b06d0a6d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/OPTICSProjection.java
@@ -0,0 +1,93 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * OPTICS projection. This is not really needed, but a quick hack to have more
+ * consistency in the visualizer API.
+ *
+ * @author Erich Schubert
+ */
+public class OPTICSProjection implements Projection {
+ /**
+ * The projector we were generated from.
+ */
+ OPTICSProjector projector;
+
+ /**
+ * Constructor.
+ *
+ * @param opticsProjector OPTICS projector
+ */
+ public OPTICSProjection(OPTICSProjector opticsProjector) {
+ super();
+ this.projector = opticsProjector;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "OPTICS Plot Projection";
+ }
+
+ @Override
+ public int getInputDimensionality() {
+ return -1;
+ }
+
+ @Override
+ public LinearScale getScale(int d) {
+ return null;
+ }
+
+ /**
+ * Get or produce the actual OPTICS plot.
+ *
+ * @param context Context to use
+ * @return Plot
+ */
+ public OPTICSPlot getOPTICSPlot(VisualizerContext context) {
+ return projector.getOPTICSPlot(context);
+ }
+
+ /**
+ * Get the OPTICS cluster order.
+ *
+ * @return Cluster oder result.
+ */
+ public ClusterOrder getResult() {
+ return projector.getResult();
+ }
+
+ @Override
+ public Projector getProjector() {
+ return projector;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java
new file mode 100644
index 00000000..1a501608
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection.java
@@ -0,0 +1,76 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+
+/**
+ * Base interface used for projections in the ELKI visualizers.
+ *
+ * There are specialized interfaces for 1D and 2D that only compute the
+ * projections in the required dimensions!
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ *
+ * @apiviz.composedOf LinearScale
+ */
+public interface Projection extends VisualizationItem {
+ /**
+ * Scaling constant. Keep in sync with
+ * {@link de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary#SCALE}.
+ */
+ public static final double SCALE = StyleLibrary.SCALE;
+
+ /**
+ * Inverse scaling constant.
+ */
+ public static final double INVSCALE = 1. / SCALE;
+
+ /**
+ * Get the input dimensionality of the projection.
+ *
+ * @return Input dimensionality
+ */
+ public int getInputDimensionality();
+
+ /**
+ * Get the scale class for a particular dimension.
+ *
+ * @param d Dimension
+ * @return Scale class
+ */
+ public LinearScale getScale(int d);
+
+ /**
+ * Projector used for generating this projection.
+ *
+ * @return Projector
+ */
+ public Projector getProjector();
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java
new file mode 100644
index 00000000..a5daf0d3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection1D.java
@@ -0,0 +1,84 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+
+/**
+ * Interface for projections that have a specialization to only compute the
+ * first component.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ */
+public interface Projection1D extends Projection {
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double fastProjectDataToRenderSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double fastProjectDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ public double fastProjectScaledToRender(double[] v);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double fastProjectRelativeDataToRenderSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double fastProjectRelativeDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ public double fastProjectRelativeScaledToRender(double[] v);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java
new file mode 100644
index 00000000..6f9f17b0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Projection2D.java
@@ -0,0 +1,146 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+
+/**
+ * Projections that have specialized methods to only compute the first two
+ * dimensions of the projection.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ *
+ * @apiviz.has CanvasSize
+ */
+public interface Projection2D extends Projection {
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectDataToRenderSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ public double[] fastProjectDataToScaledSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to scaled space.
+ *
+ * @param data vector in data space
+ * @return vector in scaled space
+ */
+ public double[] fastProjectDataToScaledSpace(NumberVector data);
+
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectScaledToRenderSpace(double[] v);
+
+ /**
+ * Project a data vector from rendering space to data space.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @return vector in data space
+ */
+ public double[] fastProjectRenderToDataSpace(double x, double y);
+
+ /**
+ * Project a data vector from rendering space to data space.
+ *
+ * @param data vector in rendering space
+ * @param prototype Prototype to create vector from
+ * @return vector in data space
+ */
+ // public <V extends NumberVector> V fastProjectRenderToDataSpace(double[]
+ // data, V prototype);
+
+ /**
+ * Project a vector from rendering space to scaled space.
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @return vector in scaled space
+ */
+ public double[] fastProjectRenderToScaledSpace(double x, double y);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectRelativeDataToRenderSpace(double[] data);
+
+ /**
+ * Project a data vector from data space to rendering space.
+ *
+ * @param data vector in data space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectRelativeDataToRenderSpace(NumberVector data);
+
+ /**
+ * Project a vector from scaled space to rendering space.
+ *
+ * @param v vector in scaled space
+ * @return vector in rendering space
+ */
+ public double[] fastProjectRelativeScaledToRenderSpace(double[] v);
+
+ // FIXME: add missing relative projection functions
+
+ /**
+ * Estimate the viewport requirements
+ *
+ * @return Canvas size obtained from projecting scale endpoints
+ */
+ public CanvasSize estimateViewport();
+
+ /**
+ * Get a bit set of dimensions that are visible.
+ *
+ * @return Bit set, first dimension is bit 0.
+ */
+ public long[] getVisibleDimensions2D();
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java
new file mode 100644
index 00000000..435a2146
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/ProjectionParallel.java
@@ -0,0 +1,196 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+
+/**
+ * Projection to parallel coordinates that allows reordering and inversion of
+ * axes.
+ *
+ * Note: when using this projection, pay attention to the two different schemes
+ * of dimension numbering: by input dimension and by axis position.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+public interface ProjectionParallel extends Projection {
+ /**
+ * Get inversion flag of axis.
+ *
+ * @param axis Axis (reordered) position
+ * @return Inversion flag
+ */
+ public boolean isAxisInverted(int axis);
+
+ /**
+ * Set inversion flag of axis.
+ *
+ * @param axis Axis (reordered) position
+ * @param bool Value of inversion flag
+ */
+ public void setAxisInverted(int axis, boolean bool);
+
+ /**
+ * Toggle inverted flag of axis.
+ *
+ * @param axis Axis (reordered) position
+ */
+ public void toggleAxisInverted(int axis);
+
+ /**
+ * Get inversion flag of dimension.
+ *
+ * @param truedim Dimension in original numbering
+ * @return Inversion flag
+ */
+ public boolean isDimInverted(int truedim);
+
+ /**
+ * Set inversion flag of a dimension.
+ *
+ * @param truedim Dimension in original numbering
+ * @param bool Value of inversion flag
+ */
+ public void setDimInverted(int truedim, boolean bool);
+
+ /**
+ * Toggle inverted flag of dimension.
+ *
+ * @param truedim Dimension in original numbering
+ */
+ public void toggleDimInverted(int truedim);
+
+ /**
+ * Get scale for the given axis
+ *
+ * @param axis Axis (reordered) position
+ * @return Axis scale
+ */
+ public LinearScale getAxisScale(int axis);
+
+ /**
+ * Test whether the current axis is visible
+ *
+ * @param axis Axis (reordered) position
+ * @return Visibility of axis
+ */
+ public boolean isAxisVisible(int axis);
+
+ /**
+ * Set the visibility of the axis.
+ *
+ * @param axis Axis number
+ * @param vis Visibility status
+ */
+ public void setAxisVisible(int axis, boolean vis);
+
+ /**
+ * Toggle visibility of the axis.
+ *
+ * @param axis Axis number
+ */
+ public void toggleAxisVisible(int axis);
+
+ /**
+ * Get the number of visible dimension.
+ *
+ * @return Number of visible dimensions
+ */
+ public int getVisibleDimensions();
+
+ /**
+ * Exchange axes A and B
+ * @param a First axis
+ * @param b Second axis
+ */
+ public void swapAxes(int a, int b);
+
+ /**
+ * shift a dimension to another position
+ *
+ * @param axis axis to shift
+ * @param rn new position
+ */
+ public void moveAxis(int axis, int rn);
+
+ /**
+ * Get the dimension for the given axis number
+ *
+ * @param axis Axis number
+ * @return Dimension
+ */
+ public int getDimForAxis(int axis);
+
+ /**
+ * Get the dimension for the given visible axis
+ *
+ * @param axis Axis number (visible axes only)
+ * @return Dimension
+ */
+ public int getDimForVisibleAxis(int axis);
+
+ /**
+ * Fast project a vector from data to render space
+ *
+ * @param v Input vector
+ * @return Vector with reordering, inversions and scales applied.
+ */
+ public double[] fastProjectDataToRenderSpace(double[] v);
+
+ /**
+ * Fast project a vector from data to render space
+ *
+ * @param v Input vector
+ * @return Vector with reordering, inversions and scales applied.
+ */
+ public double[] fastProjectDataToRenderSpace(NumberVector v);
+
+ /**
+ * Project the value of a single axis to its display value
+ *
+ * @param value Input value
+ * @param axis Axis to use for scaling and inversion
+ * @return Transformed value
+ */
+ public double fastProjectDataToRenderSpace(double value, int axis);
+
+ /**
+ * Project a display value back to the original data space
+ *
+ * @param value transformed value
+ * @param axis Axis to use for scaling and inversion
+ * @return Original value
+ */
+ public double fastProjectRenderToDataSpace(double value, int axis);
+
+ /**
+ * Find the axis assigned to the given dimension.
+ *
+ * @param truedim Dimension
+ * @return Axis number
+ */
+ public int getAxisForDim(int truedim);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java
new file mode 100644
index 00000000..b4560152
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple1D.java
@@ -0,0 +1,114 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Dimension-selecting 1D projection.
+ *
+ * @author Erich Schubert
+ */
+public class Simple1D extends AbstractSimpleProjection implements Projection1D {
+ /**
+ * Our dimension, starting with 0
+ */
+ final int dnum;
+
+ /**
+ * Simple 1D projection using scaling only.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ * @param dnum Dimension (starting at 0)
+ */
+ public Simple1D(Projector p, LinearScale[] scales, int dnum) {
+ super(p, scales);
+ this.dnum = dnum;
+ }
+
+ @Override
+ public double fastProjectDataToRenderSpace(double[] data) {
+ return (scales[dnum].getScaled(data[dnum]) - 0.5) * SCALE;
+ }
+
+ @Override
+ public double fastProjectDataToRenderSpace(NumberVector data) {
+ return (scales[dnum].getScaled(data.doubleValue(dnum)) - 0.5) * SCALE;
+ }
+
+ @Override
+ public double fastProjectScaledToRender(double[] v) {
+ return (v[dnum] - 0.5) * SCALE;
+ }
+
+ @Override
+ public double fastProjectRelativeDataToRenderSpace(double[] data) {
+ return (scales[dnum].getScaled(data[dnum]) - 0.5) * SCALE;
+ }
+
+ @Override
+ public double fastProjectRelativeDataToRenderSpace(NumberVector data) {
+ return (data.doubleValue(dnum) - 0.5) * SCALE;
+ }
+
+ @Override
+ public double fastProjectRelativeScaledToRender(double[] v) {
+ return v[dnum] * SCALE;
+ }
+
+ @Override
+ protected double[] rearrange(double[] v) {
+ final double[] r = new double[v.length];
+ r[0] = v[dnum];
+ if(dnum > 0) {
+ System.arraycopy(v, 0, r, 1, dnum);
+ }
+ if(dnum + 1 < v.length) {
+ System.arraycopy(v, dnum + 1, r, dnum + 1, v.length - (dnum + 1));
+ }
+ return r;
+ }
+
+ @Override
+ protected double[] dearrange(double[] v) {
+ final double[] r = new double[v.length];
+ if(dnum > 0) {
+ System.arraycopy(v, 1, r, 0, dnum);
+ }
+ r[dnum] = v[0];
+ if(dnum + 1 < v.length) {
+ System.arraycopy(v, dnum + 1, r, dnum + 1, v.length - (dnum + 1));
+ }
+ return r;
+ }
+
+
+ @Override
+ public String getMenuName() {
+ return "Axis";
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java
new file mode 100644
index 00000000..3dff7c3b
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/Simple2D.java
@@ -0,0 +1,205 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+
+/**
+ * Dimension-selecting 2D projection.
+ *
+ * @author Erich Schubert
+ */
+public class Simple2D extends AbstractSimpleProjection implements Projection2D {
+ /**
+ * Dimensions for fast projection mode.
+ */
+ private int dim1;
+
+ /**
+ * Dimensions for fast projection mode.
+ */
+ private int dim2;
+
+ /**
+ * Constructor with a given database and axes.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ * @param ax1 First axis
+ * @param ax2 Second axis
+ */
+ public Simple2D(Projector p, LinearScale[] scales, int ax1, int ax2) {
+ super(p, scales);
+ this.dim1 = ax1;
+ this.dim2 = ax2;
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(double[] data) {
+ double x = (scales[dim1].getScaled(data[dim1]) - 0.5) * SCALE;
+ double y = (scales[dim2].getScaled(data[dim2]) - 0.5) * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(NumberVector data) {
+ double x = (scales[dim1].getScaled(data.doubleValue(dim1)) - 0.5) * SCALE;
+ double y = (scales[dim2].getScaled(data.doubleValue(dim2)) - 0.5) * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectDataToScaledSpace(double[] data) {
+ final int dim = data.length;
+ double[] ds = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ ds[d] = scales[d].getScaled(data[d]);
+ }
+ return ds;
+ }
+
+ @Override
+ public double[] fastProjectDataToScaledSpace(NumberVector data) {
+ final int dim = data.getDimensionality();
+ double[] ds = new double[dim];
+ for(int d = 0; d < dim; d++) {
+ ds[d] = scales[d].getScaled(data.doubleValue(d));
+ }
+ return ds;
+ }
+
+ @Override
+ public double[] fastProjectScaledToRenderSpace(double[] v) {
+ double x = (v[dim1] - 0.5) * SCALE;
+ double y = (v[dim2] - 0.5) * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectRenderToDataSpace(double x, double y) {
+ double[] ret = new double[scales.length];
+ for(int d = 0; d < scales.length; d++) {
+ ret[d] = //
+ (d == dim1) ? scales[d].getUnscaled((x * INVSCALE) + 0.5) : //
+ (d == dim2) ? scales[d].getUnscaled((y * -INVSCALE) + 0.5) : //
+ scales[d].getUnscaled(0.5);
+ }
+ return ret;
+ }
+
+ @Override
+ public double[] fastProjectRenderToScaledSpace(double x, double y) {
+ double[] ret = new double[scales.length];
+ for(int d = 0; d < scales.length; d++) {
+ ret[d] = //
+ (d == dim1) ? (x * INVSCALE) + 0.5 : //
+ (d == dim2) ? (y * -INVSCALE) + 0.5 : //
+ 0.5;
+ }
+ return ret;
+ }
+
+ @Override
+ public double[] fastProjectRelativeDataToRenderSpace(double[] data) {
+ double x = scales[dim1].getRelativeScaled(data[dim1]) * SCALE;
+ double y = scales[dim2].getRelativeScaled(data[dim2]) * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectRelativeDataToRenderSpace(NumberVector data) {
+ double x = scales[dim1].getRelativeScaled(data.doubleValue(dim1)) * SCALE;
+ double y = scales[dim2].getRelativeScaled(data.doubleValue(dim2)) * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public double[] fastProjectRelativeScaledToRenderSpace(double[] vr) {
+ double x = vr[dim1] * SCALE;
+ double y = vr[dim2] * -SCALE;
+ return new double[] { x, y };
+ }
+
+ @Override
+ public long[] getVisibleDimensions2D() {
+ long[] actDim = new long[super.scales.length];
+ BitsUtil.setI(actDim, dim1);
+ BitsUtil.setI(actDim, dim2);
+ return actDim;
+ }
+
+ @Override
+ public CanvasSize estimateViewport() {
+ return new CanvasSize(-SCALE * .5, SCALE * .5, -SCALE * .5, SCALE * .5);
+ }
+
+ @Override
+ protected double[] rearrange(double[] v) {
+ final double[] r = new double[v.length];
+ r[0] = v[dim1];
+ r[1] = v[dim2];
+ final int ldim = Math.min(dim1, dim2);
+ final int hdim = Math.max(dim1, dim2);
+ if(ldim > 0) {
+ System.arraycopy(v, 0, r, 2, ldim);
+ }
+ if(hdim - ldim > 1) {
+ System.arraycopy(v, ldim + 1, r, ldim + 2, hdim - (ldim + 1));
+ }
+ if(hdim + 1 < v.length) {
+ System.arraycopy(v, hdim + 1, r, hdim + 1, v.length - (hdim + 1));
+ }
+ return r;
+ }
+
+ @Override
+ protected double[] dearrange(double[] v) {
+ final double[] r = new double[v.length];
+ r[dim1] = v[0];
+ r[dim2] = v[1];
+ // copy remainder
+ final int ldim = Math.min(dim1, dim2);
+ final int hdim = Math.max(dim1, dim2);
+ if(ldim > 0) {
+ System.arraycopy(v, 2, r, 0, ldim);
+ }
+ // ldim = s[0 or 1]
+ if(hdim - ldim > 1) {
+ System.arraycopy(v, ldim + 2, r, ldim + 1, hdim - (ldim + 1));
+ }
+ // hdim = s[0 or 1]
+ if(hdim + 1 < v.length) {
+ System.arraycopy(v, hdim + 1, r, hdim + 1, v.length - (hdim + 1));
+ }
+ return r;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Scatterplot";
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java
new file mode 100644
index 00000000..e25d4088
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/SimpleParallel.java
@@ -0,0 +1,293 @@
+package de.lmu.ifi.dbs.elki.visualization.projections;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.projector.Projector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+
+/**
+ * Simple parallel projection
+ *
+ * Scaled space: reordered, scaled and inverted. Lower dimensionality! [0:1]
+ * Render space: not used here; no recentering needed.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+public class SimpleParallel implements ProjectionParallel {
+ /**
+ * Number of visible dimensions
+ */
+ int visDims;
+
+ /**
+ * Flags for the dimensions
+ */
+ byte[] flags;
+
+ /**
+ * Ordering of dimensions
+ */
+ int[] dimOrder;
+
+ /**
+ * Scales
+ */
+ private LinearScale[] scales;
+
+ /**
+ * Projector
+ */
+ private Projector p;
+
+ /**
+ * Flag for visibility
+ */
+ final static byte FLAG_HIDDEN = 1;
+
+ /**
+ * Flag for inverted dimensions
+ *
+ * TODO: handle inversions via scales?
+ */
+ final static byte FLAG_INVERTED = 2;
+
+ /**
+ * Constructor.
+ *
+ * @param p Projector
+ * @param scales Scales to use
+ */
+ public SimpleParallel(Projector p, LinearScale[] scales) {
+ super();
+ this.p = p;
+ this.scales = scales;
+ visDims = scales.length;
+ flags = new byte[scales.length];
+ dimOrder = new int[scales.length];
+ for(int i = 0; i < dimOrder.length; i++) {
+ dimOrder[i] = i;
+ }
+ }
+
+ @Override
+ public LinearScale getScale(int dim) {
+ return scales[dim];
+ }
+
+ @Override
+ public boolean isAxisInverted(int axis) {
+ return isDimInverted(dimOrder[axis]);
+ }
+
+ @Override
+ public void setAxisInverted(int axis, boolean bool) {
+ setDimInverted(dimOrder[axis], bool);
+ }
+
+ @Override
+ public void toggleAxisInverted(int axis) {
+ toggleDimInverted(dimOrder[axis]);
+ }
+
+ @Override
+ public boolean isDimInverted(int truedim) {
+ return (flags[truedim] & FLAG_INVERTED) == FLAG_INVERTED;
+ }
+
+ @Override
+ public void setDimInverted(int truedim, boolean bool) {
+ if(bool) {
+ flags[truedim] |= FLAG_INVERTED;
+ }
+ else {
+ flags[truedim] &= ~FLAG_INVERTED;
+ }
+ }
+
+ @Override
+ public void toggleDimInverted(int truedim) {
+ flags[truedim] ^= FLAG_INVERTED;
+ }
+
+ @Override
+ public LinearScale getAxisScale(int axis) {
+ return scales[dimOrder[axis]];
+ }
+
+ protected boolean isDimHidden(int truedim) {
+ return (flags[truedim] & FLAG_HIDDEN) == FLAG_HIDDEN;
+ }
+
+ @Override
+ public boolean isAxisVisible(int dim) {
+ return !isDimHidden(dimOrder[dim]);
+ }
+
+ @Override
+ public void setAxisVisible(int dim, boolean vis) {
+ boolean prev = isAxisVisible(dim);
+ if(prev == vis) {
+ return;
+ }
+ if(vis) {
+ flags[dimOrder[dim]] &= ~FLAG_HIDDEN;
+ visDims++;
+ }
+ else {
+ flags[dimOrder[dim]] |= FLAG_HIDDEN;
+ visDims--;
+ }
+ }
+
+ @Override
+ public void toggleAxisVisible(int dim) {
+ boolean prev = isAxisVisible(dim);
+ if(!prev) {
+ flags[dimOrder[dim]] &= ~FLAG_HIDDEN;
+ visDims++;
+ }
+ else {
+ flags[dimOrder[dim]] |= FLAG_HIDDEN;
+ visDims--;
+ }
+ }
+
+ @Override
+ public int getVisibleDimensions() {
+ return visDims;
+ }
+
+ @Override
+ public int getDimForAxis(int pos) {
+ return dimOrder[pos];
+ }
+
+ @Override
+ public int getDimForVisibleAxis(int pos) {
+ for(int i = 0; i < scales.length; i++) {
+ if(isDimHidden(dimOrder[i])) {
+ continue;
+ }
+ if(pos == 0) {
+ return dimOrder[i];
+ }
+ pos--;
+ }
+ return -1;
+ }
+
+ @Override
+ public void swapAxes(int a, int b) {
+ int temp = dimOrder[a];
+ dimOrder[a] = dimOrder[b];
+ dimOrder[b] = temp;
+ }
+
+ @Override
+ public void moveAxis(int src, int dest) {
+ if(src > dest) {
+ int temp = dimOrder[src];
+ System.arraycopy(dimOrder, dest, dimOrder, dest + 1, src - dest);
+ dimOrder[dest] = temp;
+ }
+ else if(src < dest) {
+ int temp = dimOrder[src];
+ System.arraycopy(dimOrder, src + 1, dimOrder, src, dest - src);
+ dimOrder[dest - 1] = temp;
+ }
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(NumberVector data) {
+ double[] v = new double[visDims];
+ for(int j = 0, o = 0; j < scales.length; j++) {
+ if(isDimHidden(j)) {
+ continue;
+ }
+ int i = dimOrder[j];
+ double w = scales[i].getScaled(data.doubleValue(i));
+ w = isDimInverted(i) ? w : 1 - w;
+ v[o++] = w * StyleLibrary.SCALE;
+ }
+ return v;
+ }
+
+ @Override
+ public double[] fastProjectDataToRenderSpace(double[] data) {
+ double[] v = new double[visDims];
+ for(int j = 0, o = 0; j < scales.length; j++) {
+ if(isDimHidden(j)) {
+ continue;
+ }
+ int i = dimOrder[j];
+ double w = scales[i].getScaled(data[i]);
+ w = isDimInverted(i) ? w : 1 - w;
+ v[o++] = w * StyleLibrary.SCALE;
+ }
+ return v;
+ }
+
+ @Override
+ public double fastProjectRenderToDataSpace(double v, int projdim) {
+ int truedim = dimOrder[projdim];
+ v /= StyleLibrary.SCALE;
+ v = isDimInverted(truedim) ? v : 1 - v;
+ return scales[truedim].getUnscaled(v);
+ }
+
+ @Override
+ public double fastProjectDataToRenderSpace(double value, int dim) {
+ double temp = scales[dimOrder[dim]].getScaled(value);
+ temp *= StyleLibrary.SCALE;
+ return isAxisInverted(dimOrder[dim]) ? 1 - temp : temp;
+ }
+
+ @Override
+ public int getAxisForDim(int truedim) {
+ for(int i = 0; i < dimOrder.length; i++) {
+ if(dimOrder[i] == truedim) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getInputDimensionality() {
+ return scales.length;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Parallel Coordinates";
+ }
+
+ @Override
+ public Projector getProjector() {
+ return p;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java
new file mode 100755
index 00000000..b903fa37
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projections/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualization projections</p>
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.projections; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java
new file mode 100644
index 00000000..c7843576
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramFactory.java
@@ -0,0 +1,116 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+
+/**
+ * Produce one-dimensional projections.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has HistogramProjector
+ */
+public class HistogramFactory implements ProjectorFactory {
+ /**
+ * Maximum dimensionality.
+ */
+ private int maxdim = ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT;
+
+ /**
+ * Constructor.
+ *
+ * @param maxdim Maximum dimensionality
+ */
+ public HistogramFactory(int maxdim) {
+ super();
+ this.maxdim = maxdim;
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<Relation<?>> it1 = VisualizationTree.filterResults(context, start, Relation.class);
+ candidate: for(; it1.valid(); it1.advance()) {
+ Relation<?> rel = it1.get();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ // Do not enable nested relations by default:
+ Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class);
+ for(; it2.valid(); it2.advance()) {
+ // Parent relation
+ final Relation<?> rel2 = (Relation<?>) it2.get();
+ if(TypeUtil.SPATIAL_OBJECT.isAssignableFromType(rel2.getDataTypeInformation())) {
+ continue candidate;
+ }
+ // TODO: add Actions instead.
+ }
+ @SuppressWarnings("unchecked")
+ Relation<NumberVector> vrel = (Relation<NumberVector>) rel;
+ final int dim = RelationUtil.dimensionality(vrel);
+ HistogramProjector<NumberVector> proj = new HistogramProjector<>(vrel, Math.min(dim, maxdim));
+ context.addVis(vrel, proj);
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Stores the maximum number of dimensions to show.
+ */
+ private int maxdim = ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ IntParameter maxdimP = new IntParameter(ScatterPlotFactory.Parameterizer.MAXDIM_ID, ScatterPlotFactory.MAX_DIMENSIONS_DEFAULT);
+ maxdimP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT);
+ if(config.grab(maxdimP)) {
+ maxdim = maxdimP.intValue();
+ }
+ }
+
+ @Override
+ protected HistogramFactory makeInstance() {
+ return new HistogramFactory(maxdim);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java
new file mode 100644
index 00000000..d46265e7
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/HistogramProjector.java
@@ -0,0 +1,122 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.ScalesResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection1D;
+import de.lmu.ifi.dbs.elki.visualization.projections.Simple1D;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization;
+
+/**
+ * ScatterPlotProjector is responsible for producing a set of scatterplot
+ * visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses ScalesResult
+ * @apiviz.uses Projection1D
+ *
+ * @param <V> Vector type
+ */
+public class HistogramProjector<V extends NumberVector> implements Projector {
+ /**
+ * Relation we project.
+ */
+ Relation<V> rel;
+
+ /**
+ * Database dimensionality.
+ */
+ int dmax;
+
+ /**
+ * Constructor.
+ *
+ * @param rel Relation
+ * @param maxdim Maximum dimension to use
+ */
+ public HistogramProjector(Relation<V> rel, int maxdim) {
+ super();
+ this.rel = rel;
+ this.dmax = maxdim;
+ assert(maxdim <= RelationUtil.dimensionality(rel)) : "Requested dimensionality larger than data dimensionality?!?";
+ }
+
+ @Override
+ public Collection<PlotItem> arrange(VisualizerContext context) {
+ List<PlotItem> layout = new ArrayList<>(1 + dmax);
+ List<VisualizationTask> tasks = context.getVisTasks(this);
+ if(tasks.size() > 0) {
+ final double xoff = (dmax > 1) ? .1 : 0.;
+ final double hheight = .5;
+ final double lheight = .1;
+ PlotItem master = new PlotItem(dmax + xoff, hheight + lheight, null);
+ ScalesResult scales = ResultUtil.getScalesResult(rel);
+ for(int d1 = 0; d1 < dmax; d1++) {
+ Projection1D proj = new Simple1D(this, scales.getScales(), d1);
+ final PlotItem it = new PlotItem(d1 + xoff, lheight, 1., hheight, proj);
+ it.tasks = tasks;
+ master.subitems.add(it);
+ }
+ layout.add(master);
+ // Add labels
+ for(int d1 = 0; d1 < dmax; d1++) {
+ PlotItem it = new PlotItem(d1 + xoff, 0, 1., lheight, null);
+ LabelVisualization lbl = new LabelVisualization(RelationUtil.getColumnLabel(rel, d1));
+ final VisualizationTask task = new VisualizationTask("", context, null, null, lbl);
+ task.reqheight = lheight;
+ task.reqwidth = 1;
+ task.addFlags(VisualizationTask.FLAG_NO_DETAIL);
+ it.tasks.add(task);
+ master.subitems.add(it);
+ }
+ }
+ return layout;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Axis plot";
+ }
+
+ /**
+ * Get the relation we project.
+ *
+ * @return Relation
+ */
+ public Relation<V> getRelation() {
+ return rel;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java
new file mode 100644
index 00000000..2e615c89
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjector.java
@@ -0,0 +1,101 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.OPTICSProjection;
+
+/**
+ * Projection for OPTICS plots.
+ *
+ * @author Erich Schubert
+ */
+public class OPTICSProjector implements Projector {
+ /**
+ * Cluster order result
+ */
+ private ClusterOrder clusterOrder;
+
+ /**
+ * OPTICS plot image
+ */
+ private OPTICSPlot plot = null;
+
+ /**
+ * Constructor.
+ *
+ * @param co Cluster order
+ */
+ public OPTICSProjector(ClusterOrder co) {
+ super();
+ this.clusterOrder = co;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "OPTICS Plot Projection";
+ }
+
+ @Override
+ public Collection<PlotItem> arrange(VisualizerContext context) {
+ List<PlotItem> col = new ArrayList<>(1);
+ List<VisualizationTask> tasks = context.getVisTasks(this);
+ if(tasks.size() > 0) {
+ final PlotItem it = new PlotItem(4., 1., new OPTICSProjection(this));
+ it.tasks = tasks;
+ col.add(it);
+ }
+ return col;
+ }
+
+ /**
+ * Get the cluster order
+ *
+ * @return the cluster order
+ */
+ public ClusterOrder getResult() {
+ return clusterOrder;
+ }
+
+ /**
+ * Get or produce the actual OPTICS plot.
+ *
+ * @param context Context to use
+ * @return Plot
+ */
+ public OPTICSPlot getOPTICSPlot(VisualizerContext context) {
+ if(plot == null) {
+ plot = OPTICSPlot.plotForClusterOrder(clusterOrder, context);
+ }
+ return plot;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java
new file mode 100644
index 00000000..32a0e540
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/OPTICSProjectorFactory.java
@@ -0,0 +1,54 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+
+/**
+ * Produce OPTICS plot projections
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has OPTICSProjector
+ */
+public class OPTICSProjectorFactory implements ProjectorFactory {
+ /**
+ * Constructor.
+ */
+ public OPTICSProjectorFactory() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ClusterOrder> it1 = VisualizationTree.filterResults(context, start, ClusterOrder.class);
+ for(; it1.valid(); it1.advance()) {
+ final ClusterOrder or = it1.get();
+ context.addVis(or, new OPTICSProjector(or));
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java
new file mode 100644
index 00000000..1bf12d79
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotFactory.java
@@ -0,0 +1,94 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+
+/**
+ * Produce parallel axes projections.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has ParallelPlotProjector
+ */
+public class ParallelPlotFactory implements ProjectorFactory {
+ /**
+ * Constructor.
+ */
+ public ParallelPlotFactory() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<Relation<?>> it = VisualizationTree.filterResults(context, start, Relation.class);
+ candidate: for(; it.valid(); it.advance()) {
+ Relation<?> rel = it.get();
+ // TODO: multi-relational parallel plots?
+ final int dim = dimensionality(rel);
+ if(dim <= 1) {
+ continue;
+ }
+ // Do not enable nested relations by default:
+ Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class);
+ for(; it2.valid(); it2.advance()) {
+ // Parent relation
+ final Relation<?> rel2 = (Relation<?>) it2.get();
+ final int odim = dimensionality(rel2);
+ if(odim == dim) {
+ // TODO: add Actions instead?
+ continue candidate;
+ }
+ }
+ @SuppressWarnings("unchecked")
+ Relation<SpatialComparable> vrel = (Relation<SpatialComparable>) rel;
+ ParallelPlotProjector<SpatialComparable> proj = new ParallelPlotProjector<>(vrel);
+ context.addVis(vrel, proj);
+ }
+ }
+
+ private int dimensionality(Relation<?> rel) {
+ if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ @SuppressWarnings("unchecked")
+ Relation<NumberVector> vrel = (Relation<NumberVector>) rel;
+ return RelationUtil.dimensionality(vrel);
+ }
+ if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ @SuppressWarnings("unchecked")
+ Relation<UncertainObject> vrel = (Relation<UncertainObject>) rel;
+ return RelationUtil.dimensionality(vrel);
+ }
+ // TODO: allow other spatial objects of fixed dimensionality!
+ return 0;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java
new file mode 100644
index 00000000..28069cc4
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ParallelPlotProjector.java
@@ -0,0 +1,95 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.ScalesResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel;
+import de.lmu.ifi.dbs.elki.visualization.projections.SimpleParallel;
+
+/**
+ * ParallelPlotProjector is responsible for producing a parallel axes
+ * visualization.
+ *
+ * @author Robert Rödler
+ *
+ * @param <V> Vector type
+ */
+// TODO: support categorical features, and multiple relations too
+public class ParallelPlotProjector<V extends SpatialComparable> implements Projector {
+ /**
+ * Relation we project.
+ */
+ Relation<V> rel;
+
+ /**
+ * Constructor.
+ *
+ * @param rel Relation
+ */
+ public ParallelPlotProjector(Relation<V> rel) {
+ super();
+ this.rel = rel;
+ }
+
+ @Override
+ public Collection<PlotItem> arrange(VisualizerContext context) {
+ List<PlotItem> col = new ArrayList<>(1);
+ List<VisualizationTask> tasks = context.getVisTasks(this);
+ if(tasks.size() > 0) {
+ ScalesResult scales = ResultUtil.getScalesResult(rel);
+ ProjectionParallel proj = new SimpleParallel(this, scales.getScales());
+
+ final double width = Math.max(.5, Math.ceil(MathUtil.log2(scales.getScales().length - 1)));
+ final PlotItem it = new PlotItem(width, 1., proj);
+ it.tasks = tasks;
+ col.add(it);
+ }
+ return col;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Parallelplot";
+ }
+
+ /**
+ * The relation we project.
+ *
+ * @return Relation
+ */
+ public Relation<V> getRelation() {
+ return rel;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java
new file mode 100644
index 00000000..b726dfef
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/Projector.java
@@ -0,0 +1,45 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Collection;
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+
+/**
+ * A projector is responsible for adding projections to the visualization.
+ *
+ * @author Erich Schubert
+ */
+public interface Projector extends VisualizationItem {
+ /**
+ * Produce an arrangement of projections.
+ *
+ * @param context Visualization context
+ * @return Arrangement.
+ */
+ public Collection<PlotItem> arrange(VisualizerContext context);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java
new file mode 100644
index 00000000..10ad8bd9
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ProjectorFactory.java
@@ -0,0 +1,44 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+
+/**
+ * A projector is responsible for adding projections to the visualization by
+ * detecting appropriate relations in the database.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has Projector
+ */
+public interface ProjectorFactory extends VisualizationProcessor {
+ /**
+ * Add projections for the given result (tree) to the result tree.
+ * @param context Visualization context
+ * @param start Result to process
+ */
+ @Override
+ public void processNewResult(VisualizerContext context, Object start);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java
new file mode 100644
index 00000000..785ea672
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotFactory.java
@@ -0,0 +1,151 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+
+/**
+ * Produce scatterplot projections.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has ScatterPlotProjector
+ */
+public class ScatterPlotFactory implements ProjectorFactory {
+ /**
+ * Maximum number of dimensions to visualize.
+ *
+ * FIXME: add scrolling function for higher dimensionality!
+ */
+ public static final int MAX_DIMENSIONS_DEFAULT = 10;
+
+ /**
+ * Stores the maximum number of dimensions to show.
+ */
+ private int maxdim = MAX_DIMENSIONS_DEFAULT;
+
+ /**
+ * Constructor.
+ *
+ * @param maxdim Maximum number of dimensions to show.
+ */
+ public ScatterPlotFactory(int maxdim) {
+ super();
+ this.maxdim = maxdim;
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<Relation<?>> it = VisualizationTree.filterResults(context, start, Relation.class);
+ candidate: for(; it.valid(); it.advance()) {
+ Relation<?> rel = it.get();
+ final int dim = dimensionality(rel);
+ if(dim < 1) {
+ continue;
+ }
+ // Do not enable nested relations by default:
+ Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<>(context.getHierarchy().iterAncestors(rel), Relation.class);
+ for(; it2.valid(); it2.advance()) {
+ // Parent relation
+ final Relation<?> rel2 = (Relation<?>) it2.get();
+ final int odim = dimensionality(rel2);
+ if(odim == dim) {
+ // TODO: add Actions instead?
+ continue candidate;
+ }
+ }
+ @SuppressWarnings("unchecked")
+ Relation<SpatialComparable> vrel = (Relation<SpatialComparable>) rel;
+ ScatterPlotProjector<SpatialComparable> proj = new ScatterPlotProjector<>(vrel, Math.min(maxdim, dim));
+ context.addVis(vrel, proj);
+ }
+ }
+
+ private int dimensionality(Relation<?> rel) {
+ if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ @SuppressWarnings("unchecked")
+ Relation<NumberVector> vrel = (Relation<NumberVector>) rel;
+ return RelationUtil.dimensionality(vrel);
+ }
+ if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ @SuppressWarnings("unchecked")
+ Relation<UncertainObject> vrel = (Relation<UncertainObject>) rel;
+ return RelationUtil.dimensionality(vrel);
+ }
+ // TODO: allow other spatial objects of fixed dimensionality!
+ return 0;
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Parameter for the maximum number of dimensions.
+ *
+ * <p>
+ * Code: -vis.maxdim
+ * </p>
+ */
+ public static final OptionID MAXDIM_ID = new OptionID("vis.maxdim", "Maximum number of dimensions to display.");
+
+ /**
+ * Stores the maximum number of dimensions to show.
+ */
+ private int maxdim = MAX_DIMENSIONS_DEFAULT;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ IntParameter maxdimP = new IntParameter(MAXDIM_ID, MAX_DIMENSIONS_DEFAULT);
+ maxdimP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT);
+ if(config.grab(maxdimP)) {
+ maxdim = maxdimP.intValue();
+ }
+ }
+
+ @Override
+ protected ScatterPlotFactory makeInstance() {
+ return new ScatterPlotFactory(maxdim);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java
new file mode 100644
index 00000000..b8705a39
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/ScatterPlotProjector.java
@@ -0,0 +1,180 @@
+package de.lmu.ifi.dbs.elki.visualization.projector;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.AffineTransformation;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.ScalesResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.overview.PlotItem;
+import de.lmu.ifi.dbs.elki.visualization.projections.AffineProjection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projections.Simple2D;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization;
+
+/**
+ * ScatterPlotProjector is responsible for producing a set of scatterplot
+ * visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses ScalesResult
+ * @apiviz.uses Projection2D
+ *
+ * @param <V> Vector type
+ */
+public class ScatterPlotProjector<V extends SpatialComparable> implements Projector {
+ /**
+ * Relation we project.
+ */
+ Relation<V> rel;
+
+ /**
+ * Database dimensionality.
+ */
+ int dmax;
+
+ /**
+ * Constructor.
+ *
+ * @param rel Relation
+ * @param maxdim Maximum dimension to use
+ */
+ public ScatterPlotProjector(Relation<V> rel, int maxdim) {
+ super();
+ this.rel = rel;
+ this.dmax = maxdim;
+ assert(maxdim <= RelationUtil.dimensionality(rel)) : "Requested dimensionality larger than data dimensionality?!?";
+ }
+
+ @Override
+ public Collection<PlotItem> arrange(VisualizerContext context) {
+ List<PlotItem> layout = new ArrayList<>(1);
+ List<VisualizationTask> tasks = context.getVisTasks(this);
+ if(tasks.size() > 0) {
+ ScalesResult scales = ResultUtil.getScalesResult(rel);
+ final PlotItem master;
+ if(dmax == 2) {
+ // In 2d, make the plot twice as big.
+ master = new PlotItem(2 + .1, 2 + .1, null);
+ {
+ Projection2D proj = new Simple2D(this, scales.getScales(), 0, 1);
+ PlotItem it = new PlotItem(.1, 0, 2., 2., proj);
+ it.tasks = tasks;
+ master.subitems.add(it);
+ }
+ // Label at bottom
+ {
+ PlotItem it = new PlotItem(.1, 2., 2., .1, null);
+ final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, 0)));
+ task.reqheight = .1;
+ task.reqwidth = 2.;
+ task.addFlags(VisualizationTask.FLAG_NO_DETAIL);
+ it.tasks.add(task);
+ master.subitems.add(it);
+ }
+ // Label on left
+ {
+ PlotItem it = new PlotItem(0, 0, .1, 2, null);
+ final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, 1), true));
+ task.reqheight = 2.;
+ task.reqwidth = .1;
+ task.addFlags(VisualizationTask.FLAG_NO_DETAIL);
+ it.tasks.add(task);
+ master.subitems.add(it);
+ }
+ }
+ else {
+ final double sizeh = Math.ceil((dmax - 1) / 2.0);
+ master = new PlotItem(sizeh * 2. + .1, dmax - 1 + .1, null);
+
+ for(int d1 = 0; d1 < dmax - 1; d1++) {
+ for(int d2 = d1 + 1; d2 < dmax; d2++) {
+ Projection2D proj = new Simple2D(this, scales.getScales(), d1, d2);
+ PlotItem it = new PlotItem(d1 + .1, d2 - 1, 1., 1., proj);
+ it.tasks = tasks;
+ master.subitems.add(it);
+ }
+ }
+ if(dmax >= 3) {
+ AffineTransformation p = AffineProjection.axisProjection(RelationUtil.dimensionality(rel), 1, 2);
+ p.addRotation(0, 2, Math.PI / 180 * -10.);
+ p.addRotation(1, 2, Math.PI / 180 * 15.);
+ // Wanna try 4d? go ahead:
+ // p.addRotation(0, 3, Math.PI / 180 * -20.);
+ // p.addRotation(1, 3, Math.PI / 180 * 30.);
+ Projection2D proj = new AffineProjection(this, scales.getScales(), p);
+ PlotItem it = new PlotItem(sizeh + .1, 0, sizeh, sizeh, proj);
+ it.tasks = tasks;
+ master.subitems.add(it);
+ }
+ // Labels at bottom
+ for(int d1 = 0; d1 < dmax - 1; d1++) {
+ PlotItem it = new PlotItem(d1 + .1, dmax - 1, 1., .1, null);
+ final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, d1)));
+ task.reqheight = .1;
+ task.reqwidth = 1;
+ task.addFlags(VisualizationTask.FLAG_NO_DETAIL);
+ it.tasks.add(task);
+ master.subitems.add(it);
+ }
+ // Labels on left
+ for(int d2 = 1; d2 < dmax; d2++) {
+ PlotItem it = new PlotItem(0, d2 - 1, .1, 1, null);
+ final VisualizationTask task = new VisualizationTask("", context, null, null, new LabelVisualization(RelationUtil.getColumnLabel(rel, d2), true));
+ task.reqheight = 1;
+ task.reqwidth = .1;
+ task.addFlags(VisualizationTask.FLAG_NO_DETAIL);
+ it.tasks.add(task);
+ master.subitems.add(it);
+ }
+ }
+
+ layout.add(master);
+ }
+ return layout;
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Scatterplot";
+ }
+
+ /**
+ * The relation we project.
+ *
+ * @return Relation
+ */
+ public Relation<V> getRelation() {
+ return rel;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java
new file mode 100755
index 00000000..35a0e9a2
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/projector/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Projectors are responsible for finding appropriate projections for data relations.</p>
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.projector; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java
new file mode 100644
index 00000000..345e1b68
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SVGSaveDialog.java
@@ -0,0 +1,198 @@
+package de.lmu.ifi.dbs.elki.visualization.savedialog;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.Component;
+import java.io.File;
+import java.io.IOException;
+
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+
+import org.apache.batik.transcoder.TranscoderException;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.utilities.FileUtil;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * A save dialog to save/export a SVG image to a file.
+ *
+ * Supported formats:
+ * <ul>
+ * <li>JPEG (broken) (with width and height options)</li>
+ * <li>PNG (with width and height options)</li>
+ * <li>PDF</li>
+ * <li>PS</li>
+ * <li>EPS</li>
+ * <li>SVG</li>
+ * </ul>
+ *
+ * @author Simon Mittermüller
+ *
+ * @apiviz.composedOf SaveOptionsPanel
+ */
+public class SVGSaveDialog {
+ /** The default title. "Save as ...". */
+ public static final String DEFAULT_TITLE = "Save as ...";
+
+ /** Static logger reference */
+ private static final Logging LOG = Logging.getLogger(SVGSaveDialog.class);
+
+ /** Automagic file format */
+ final static String automagic_format = "automatic";
+
+ /** Supported file format (extensions) */
+ final static String[] formats;
+
+ /** Visible file formats */
+ final static String[] visibleformats;
+
+ static {
+ // FOP installed?
+ if (SVGPlot.hasFOPInstalled()) {
+ formats = new String[] { "svg", "png", "jpeg", "jpg", "pdf", "ps", "eps" };
+ visibleformats = new String[] { automagic_format, "svg", "png", "jpeg", "pdf", "ps", "eps" };
+ } else {
+ formats = new String[] { "svg", "png", "jpeg", "jpg" };
+ visibleformats = new String[] { automagic_format, "svg", "png", "jpeg" };
+ }
+ }
+
+ /**
+ * Show a "Save as" dialog.
+ *
+ * @param plot The plot to be exported.
+ * @param width The width of the exported image (when export to JPEG/PNG).
+ * @param height The height of the exported image (when export to JPEG/PNG).
+ * @return Result from {@link JFileChooser#showSaveDialog}
+ */
+ public static int showSaveDialog(SVGPlot plot, int width, int height) {
+ double quality = 1.0;
+ int ret = -1;
+
+ JFileChooser fc = new JFileChooser(new File("."));
+ fc.setDialogTitle(DEFAULT_TITLE);
+ // fc.setFileFilter(new ImageFilter());
+ SaveOptionsPanel optionsPanel = new SaveOptionsPanel(fc, width, height);
+ fc.setAccessory(optionsPanel);
+
+ ret = fc.showSaveDialog(null);
+ fc.setDialogTitle("Saving... Please wait.");
+ if (ret == JFileChooser.APPROVE_OPTION) {
+ File file = fc.getSelectedFile();
+ String format = optionsPanel.getSelectedFormat();
+ if (format == null || automagic_format.equals(format)) {
+ format = guessFormat(file.getName());
+ }
+ try {
+ if (format == null) {
+ showError(fc, "Error saving image.", "File format not recognized.");
+ } else if ("jpeg".equals(format) || "jpg".equals(format)) {
+ quality = optionsPanel.getJPEGQuality();
+ plot.saveAsJPEG(file, width, height, quality);
+ } else if ("png".equals(format)) {
+ plot.saveAsPNG(file, width, height);
+ } else if ("ps".equals(format)) {
+ plot.saveAsPS(file);
+ } else if ("eps".equals(format)) {
+ plot.saveAsEPS(file);
+ } else if ("pdf".equals(format)) {
+ plot.saveAsPDF(file);
+ } else if ("svg".equals(format)) {
+ plot.saveAsSVG(file);
+ } else {
+ showError(fc, "Error saving image.", "Unsupported format: " + format);
+ }
+ } catch (java.lang.IncompatibleClassChangeError e) {
+ showError(fc, "Error saving image.", "It seems that your Java version is incompatible with this version of Batik and Jpeg writing. Sorry.");
+ } catch (ClassNotFoundException e) {
+ showError(fc, "Error saving image.", "A class was not found when saving this image. Maybe installing Apache FOP will help (for PDF, PS and EPS output).\n" + e.toString());
+ } catch (IOException e) {
+ LOG.exception(e);
+ showError(fc, "Error saving image.", e.toString());
+ } catch (TranscoderException e) {
+ LOG.exception(e);
+ showError(fc, "Error saving image.", e.toString());
+ } catch (TransformerFactoryConfigurationError e) {
+ LOG.exception(e);
+ showError(fc, "Error saving image.", e.toString());
+ } catch (TransformerException e) {
+ LOG.exception(e);
+ showError(fc, "Error saving image.", e.toString());
+ } catch (Exception e) {
+ LOG.exception(e);
+ showError(fc, "Error saving image.", e.toString());
+ }
+ } else if (ret == JFileChooser.ERROR_OPTION) {
+ showError(fc, "Error in file dialog.", "Unknown Error.");
+ } else if (ret == JFileChooser.CANCEL_OPTION) {
+ // do nothing - except return result
+ }
+ return ret;
+ }
+
+ /**
+ * Guess a supported format from the file name. For "auto" format handling.
+ *
+ * @param name File name
+ * @return format or "null"
+ */
+ public static String guessFormat(String name) {
+ String ext = FileUtil.getFilenameExtension(name);
+ for (String format : formats) {
+ if (format.equals(ext)) {
+ return ext;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the formats
+ */
+ public static String[] getFormats() {
+ return formats;
+ }
+
+ /**
+ * @return the visibleformats
+ */
+ public static String[] getVisibleFormats() {
+ return visibleformats;
+ }
+
+ /**
+ * Helper method to show a error message as "popup". Calls
+ * {@link JOptionPane#showMessageDialog(java.awt.Component, Object)}.
+ *
+ * @param parent The parent component for the popup.
+ * @param msg The message to be displayed.
+ * */
+ private static void showError(Component parent, String title, String msg) {
+ JOptionPane.showMessageDialog(parent, msg, title, JOptionPane.ERROR_MESSAGE);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java
new file mode 100644
index 00000000..6e45ac99
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/SaveOptionsPanel.java
@@ -0,0 +1,324 @@
+package de.lmu.ifi.dbs.elki.visualization.savedialog;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+
+import javax.swing.BorderFactory;
+import javax.swing.BoxLayout;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFileChooser;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * A component (JPanel) which can be displayed in the save dialog to show
+ * additional options when saving as JPEG or PNG.
+ *
+ * @author Simon Mittermüller
+ */
+public class SaveOptionsPanel extends JPanel {
+ // TODO: externalize strings
+ private static final String STR_IMAGE_SIZE = "Size options:";
+
+ private static final String STR_JPEG_QUALITY = "Quality:";
+
+ private static final String STR_IMAGE_HEIGHT = "Height:";
+
+ private static final String STR_IMAGE_WIDTH = "Width:";
+
+ private static final String STR_CHOOSE_FORMAT = "Choose format:";
+
+ private static final String STR_RESET_IMAGE_SIZE = "Reset image size";
+
+ private static final String STR_LOCK_ASPECT_RATIO = "Lock aspect ratio";
+
+ /**
+ * Serial version.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /** The fileChooser on which this panel is installed. */
+ private JFileChooser fc;
+
+ /**
+ * The width of the exported image (if exported to JPEG or PNG). Default is
+ * 1024.
+ */
+ protected int width = 1024;
+
+ /**
+ * The height of the exported image (if exported to JPEG or PNG). Default is
+ * 768.
+ */
+ protected int height = 768;
+
+ /** Ratio for easier size adjustment */
+ double ratio = 16.0 / 10.0;
+
+ /** Main panel */
+ private JPanel mainPanel;
+
+ /** Shows quality info when saving as JPEG. */
+ private JPanel qualPanel;
+
+ /** If saving as JPEG/PNG show width/height infos here. */
+ private JPanel sizePanel;
+
+ protected JSpinner spinnerWidth;
+
+ protected JSpinner spinnerHeight;
+
+ protected JSpinner spinnerQual;
+
+ protected SpinnerNumberModel modelWidth;
+
+ protected SpinnerNumberModel modelHeight;
+
+ protected SpinnerNumberModel modelQuality;
+
+ protected JCheckBox aspectRatioLock;
+
+ protected JButton resetSizeButton;
+
+ protected JComboBox<String> formatSelector;
+
+ // Not particularly useful for most - hide it for now.
+ private final boolean hasResetButton = false;
+
+ /**
+ * Construct a new Save Options Panel.
+ *
+ * @param fc File chooser to display in
+ * @param w Default image width
+ * @param h Default image height
+ */
+ public SaveOptionsPanel(JFileChooser fc, int w, int h) {
+ this.width = w;
+ this.height = h;
+ this.ratio = (double) w / (double) h;
+ this.fc = fc;
+
+ mainPanel = new JPanel();
+ mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
+
+ sizePanel = new JPanel();
+ sizePanel.setLayout(new BoxLayout(sizePanel, BoxLayout.Y_AXIS));
+ sizePanel.setBorder(BorderFactory.createTitledBorder(STR_IMAGE_SIZE));
+
+ // *** Format panel
+ mainPanel.add(new JLabel(STR_CHOOSE_FORMAT));
+
+ formatSelector = new JComboBox<>(SVGSaveDialog.getVisibleFormats());
+ formatSelector.setSelectedIndex(0);
+ formatSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if(e.getItem() != null) {
+ String format = (String) formatSelector.getSelectedItem();
+ setFormat(format);
+ }
+ }
+ });
+ mainPanel.add(formatSelector);
+
+ // *** Size panel
+ JPanel widthPanel = new JPanel();
+ JPanel heightPanel = new JPanel();
+ widthPanel.add(new JLabel(STR_IMAGE_WIDTH));
+ heightPanel.add(new JLabel(STR_IMAGE_HEIGHT));
+
+ // size models
+ modelWidth = new SpinnerNumberModel(width, 0, 100000, 1);
+ modelHeight = new SpinnerNumberModel(height, 0, 100000, 1);
+
+ // size spinners
+ spinnerWidth = new JSpinner(modelWidth);
+ spinnerWidth.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if(aspectRatioLock.isSelected()) {
+ int val = modelWidth.getNumber().intValue();
+ spinnerHeight.setValue(new Integer((int) Math.round(val / ratio)));
+ }
+ }
+ });
+ widthPanel.add(spinnerWidth);
+
+ spinnerHeight = new JSpinner(modelHeight);
+ spinnerHeight.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if(aspectRatioLock.isSelected()) {
+ int val = modelHeight.getNumber().intValue();
+ spinnerWidth.setValue(new Integer((int) Math.round(val * ratio)));
+ }
+ }
+ });
+ heightPanel.add(spinnerHeight);
+
+ // add subpanels
+ sizePanel.add(widthPanel);
+ sizePanel.add(heightPanel);
+
+ // aspect lock
+ aspectRatioLock = new JCheckBox(STR_LOCK_ASPECT_RATIO);
+ aspectRatioLock.setSelected(true);
+ // aspectRatioLock.addActionListener(x);
+ sizePanel.add(aspectRatioLock);
+
+ // reset size button
+ if(hasResetButton) {
+ resetSizeButton = new JButton(STR_RESET_IMAGE_SIZE);
+ resetSizeButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ modelWidth.setValue(Integer.valueOf(width));
+ modelHeight.setValue(Integer.valueOf(height));
+ aspectRatioLock.setSelected(true);
+ }
+ });
+ sizePanel.add(resetSizeButton);
+ }
+
+ mainPanel.add(sizePanel);
+
+ // Quality settings panel
+ qualPanel = new JPanel();
+ // quality settings will not be visible by default (JPEG only)
+ qualPanel.setVisible(false);
+ qualPanel.setLayout(new BoxLayout(qualPanel, BoxLayout.Y_AXIS));
+ qualPanel.setBorder(BorderFactory.createTitledBorder(STR_JPEG_QUALITY));
+ modelQuality = new SpinnerNumberModel(0.7, 0.1, 1.0, 0.1);
+ spinnerQual = new JSpinner(modelQuality);
+ // spinnerQual.addChangeListener(x);
+ qualPanel.add(spinnerQual);
+
+ mainPanel.add(qualPanel);
+
+ add(mainPanel);
+
+ // setup a listener to react to file name changes
+ this.fc.addPropertyChangeListener(new PropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent e) {
+ if(e.getPropertyName().equals(JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)) {
+ File file = (File) e.getNewValue();
+ if(file != null && file.getName() != null) {
+ String format = SVGSaveDialog.guessFormat(file.getName());
+ if(format != null) {
+ setFormat(format);
+ }
+ }
+ }
+ }
+ });
+ }
+
+ protected void setFormat(String format) {
+ String[] formats = SVGSaveDialog.getVisibleFormats();
+ int index = -1;
+ for(int i = 0; i < formats.length; i++) {
+ if(formats[i].equals(format)) {
+ index = i;
+ break;
+ }
+ }
+ if(index != formatSelector.getSelectedIndex() && index >= 0) {
+ formatSelector.setSelectedIndex(index);
+ }
+ if(format.equals("jpeg") || format.equals("jpg")) {
+ sizePanel.setVisible(true);
+ qualPanel.setVisible(true);
+ }
+ else if(format.equals("png")) {
+ sizePanel.setVisible(true);
+ qualPanel.setVisible(false);
+ }
+ else if(format.equals("pdf")) {
+ sizePanel.setVisible(false);
+ qualPanel.setVisible(false);
+ mainPanel.validate();
+ }
+ else if(format.equals("ps")) {
+ sizePanel.setVisible(false);
+ qualPanel.setVisible(false);
+ mainPanel.validate();
+ }
+ else if(format.equals("eps")) {
+ sizePanel.setVisible(false);
+ qualPanel.setVisible(false);
+ mainPanel.validate();
+ }
+ else if(format.equals("svg")) {
+ sizePanel.setVisible(false);
+ qualPanel.setVisible(false);
+ mainPanel.validate();
+ }
+ else {
+ // TODO: what to do on unknown formats?
+ // LoggingUtil.warning("Unrecognized file extension seen: " + format);
+ }
+ }
+
+ /**
+ * Return the selected file format.
+ *
+ * @return file format identification
+ */
+ public String getSelectedFormat() {
+ String format = (String) formatSelector.getSelectedItem();
+ return format;
+ }
+
+ /**
+ * Returns the quality value in the quality field.
+ *
+ * It is ensured that return value is in the range of [0:1]
+ *
+ * @return Quality value for JPEG.
+ */
+ public double getJPEGQuality() {
+ double qual =modelQuality.getNumber().doubleValue();
+ if(qual > 1.0) {
+ qual = 1.0;
+ }
+ if(qual < 0) {
+ qual = 0.0;
+ }
+ return qual;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java
new file mode 100644
index 00000000..5b56c4ff
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/savedialog/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Save dialog for SVG plots.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.savedialog; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java
new file mode 100644
index 00000000..7bc50182
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClassStylingPolicy.java
@@ -0,0 +1,74 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+
+/**
+ * Styling policy that is based on <em>classes</em>, for example clusters or
+ * labels. This allows for certain optimizations such as marker reuse, and thus
+ * is preferred when possible.
+ *
+ * @author Erich Schubert
+ */
+public interface ClassStylingPolicy extends StylingPolicy {
+ /**
+ * Get the style number for a particular object
+ *
+ * @param id Object ID
+ * @return Style number
+ */
+ int getStyleForDBID(DBIDRef id);
+
+ /**
+ * Get the minimum style in use.
+ *
+ * @return Style number
+ */
+ int getMinStyle();
+
+ /**
+ * Get the maximum style in use.
+ *
+ * @return Style number
+ */
+ int getMaxStyle();
+
+ /**
+ * Iterate over all objects from a given class.
+ *
+ * @param cnum Class number
+ * @return Iterator over object IDs
+ */
+ DBIDIter iterateClass(int cnum);
+
+ /**
+ * Get the number of elements in the styling class.
+ *
+ * @param cnum Class number
+ * @return Size of class.
+ */
+ int classSize(int cnum);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java
new file mode 100644
index 00000000..643f4328
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/ClusterStylingPolicy.java
@@ -0,0 +1,167 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import gnu.trove.list.array.TIntArrayList;
+import gnu.trove.map.TObjectIntMap;
+import gnu.trove.map.hash.TObjectIntHashMap;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Styling policy based on cluster membership.
+ *
+ * @author Erich Schubert
+ *
+ */
+// TODO: fast enough? Some other kind of mapping we can use?
+public class ClusterStylingPolicy implements ClassStylingPolicy {
+ /**
+ * Object IDs
+ */
+ ArrayList<DBIDs> ids;
+
+ /**
+ * Map from cluster objects to color offsets.
+ */
+ TObjectIntMap<Cluster<?>> cmap;
+
+ /**
+ * Colors
+ */
+ TIntArrayList colors;
+
+ /**
+ * Clustering in use.
+ */
+ Clustering<?> clustering;
+
+ /**
+ * Constructor.
+ *
+ * @param clustering Clustering to use.
+ */
+ public ClusterStylingPolicy(Clustering<?> clustering, StyleLibrary style) {
+ super();
+ this.clustering = clustering;
+ ColorLibrary colorset = style.getColorSet(StyleLibrary.PLOT);
+ List<? extends Cluster<?>> clusters = clustering.getAllClusters();
+ ids = new ArrayList<>(clusters.size());
+ colors = new TIntArrayList(clusters.size());
+ cmap = new TObjectIntHashMap<>(clusters.size(), .5f, -1);
+
+ Iterator<? extends Cluster<?>> ci = clusters.iterator();
+ for (int i = 0; ci.hasNext(); i++) {
+ Cluster<?> c = ci.next();
+ ids.add(DBIDUtil.ensureSet(c.getIDs()));
+ cmap.put(c, i);
+ Color col = SVGUtil.stringToColor(colorset.getColor(i));
+ if (col != null) {
+ colors.add(col.getRGB());
+ } else {
+ LoggingUtil.warning("Unrecognized color name: " + colorset.getColor(i));
+ }
+ if (!ci.hasNext()) {
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int getStyleForDBID(DBIDRef id) {
+ for (int i = 0; i < ids.size(); i++) {
+ if (ids.get(i).contains(id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int getColorForDBID(DBIDRef id) {
+ for (int i = 0; i < ids.size(); i++) {
+ if (ids.get(i).contains(id)) {
+ return colors.get(i);
+ }
+ }
+ return 0;
+ }
+
+ @Override
+ public int getMinStyle() {
+ return 0;
+ }
+
+ @Override
+ public int getMaxStyle() {
+ return ids.size();
+ }
+
+ @Override
+ public DBIDIter iterateClass(int cnum) {
+ return ids.get(cnum).iter();
+ }
+
+ @Override
+ public int classSize(int cnum) {
+ return ids.get(cnum).size();
+ }
+
+ /**
+ * Get the clustering used by this styling policy
+ *
+ * @return Clustering in use
+ */
+ public Clustering<?> getClustering() {
+ return clustering;
+ }
+
+ /**
+ * Get the style number for a cluster.
+ *
+ * @param c Cluster
+ * @return Style number
+ */
+ public int getStyleForCluster(Cluster<?> c) {
+ return cmap.get(c);
+ }
+
+ @Override
+ public String getMenuName() {
+ return clustering.getLongName();
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java
new file mode 100644
index 00000000..c8440067
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/PropertiesBasedStyleLibrary.java
@@ -0,0 +1,344 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.utilities.FileUtil;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.colors.ListBasedColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.lines.SolidLineStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.PrettyMarkers;
+
+/**
+ * Style library loading the parameters from a properties file.
+ *
+ * @author Erich Schubert
+ */
+// TODO: also use Caching for String values?
+public class PropertiesBasedStyleLibrary implements StyleLibrary {
+ /**
+ * Logger
+ */
+ private static final Logging LOG = Logging.getLogger(PropertiesBasedStyleLibrary.class);
+
+ /**
+ * Name of the default color scheme.
+ */
+ public static final String DEFAULT_SCHEME_NAME = "Default";
+
+ /**
+ * File name of the default color scheme.
+ */
+ public static final String DEFAULT_SCHEME_FILENAME = "default";
+
+ /**
+ * File extension
+ */
+ public static final String DEFAULT_PROPERTIES_EXTENSION = ".properties";
+
+ /**
+ * Default properties path
+ */
+ private static final String DEFAULT_PROPERTIES_PATH = PropertiesBasedStyleLibrary.class.getPackage().getName().replace('.', File.separatorChar) + File.separatorChar;
+
+ /**
+ * Separator for lists.
+ */
+ public static final String LIST_SEPARATOR = ",";
+
+ /**
+ * Property string for the line style library
+ */
+ public static final String PROP_LINES_LIBRARY = "lines-library";
+
+ /**
+ * Property string for the marker style library
+ */
+ public static final String PROP_MARKER_LIBRARY = "marker-library";
+
+ /**
+ * Properties file to use.
+ */
+ private Properties properties;
+
+ /**
+ * Style scheme name
+ */
+ private String name;
+
+ /**
+ * Cache
+ */
+ private Map<String, Object> cache = new HashMap<>();
+
+ /**
+ * Line style library to use
+ */
+ private LineStyleLibrary linelib = null;
+
+ /**
+ * Marker library to use
+ */
+ private MarkerLibrary markerlib = null;
+
+ /**
+ * Constructor without a properties file name.
+ */
+ public PropertiesBasedStyleLibrary() {
+ this(DEFAULT_SCHEME_FILENAME, DEFAULT_SCHEME_NAME);
+ }
+
+ /**
+ * Constructor with a given file name.
+ *
+ * @param filename Name of properties file.
+ * @param name NAme for this style
+ */
+ public PropertiesBasedStyleLibrary(String filename, String name) {
+ this.properties = new Properties();
+ this.name = name;
+ InputStream stream = null;
+ try {
+ stream = FileUtil.openSystemFile(filename);
+ } catch (FileNotFoundException e) {
+ try {
+ stream = FileUtil.openSystemFile(filename + DEFAULT_PROPERTIES_EXTENSION);
+ } catch (FileNotFoundException e2) {
+ try {
+ stream = FileUtil.openSystemFile(DEFAULT_PROPERTIES_PATH + filename + DEFAULT_PROPERTIES_EXTENSION);
+ } catch (FileNotFoundException e3) {
+ throw new AbortException("Could not find style scheme file '" + filename + "' for scheme '" + name + "'!");
+ }
+ }
+ }
+ try {
+ properties.load(stream);
+ } catch (Exception e) {
+ throw new AbortException("Error loading properties file " + filename + ".\n", e);
+ }
+ }
+
+ /**
+ * Get the style scheme name.
+ *
+ * @return the name
+ */
+ protected String getName() {
+ return name;
+ }
+
+ /**
+ * Get a value from the cache (to avoid repeated parsing)
+ *
+ * @param <T> Type
+ * @param prefix Tree name
+ * @param postfix Property name
+ * @param cls Class restriction
+ * @return Resulting value
+ */
+ private <T> T getCached(String prefix, String postfix, Class<T> cls) {
+ return cls.cast(cache.get(prefix + '.' + postfix));
+ }
+
+ /**
+ * Set a cache value
+ *
+ * @param <T> Type
+ * @param prefix Tree name
+ * @param postfix Property name
+ * @param data Data
+ */
+ private <T> void setCached(String prefix, String postfix, T data) {
+ cache.put(prefix + '.' + postfix, data);
+ }
+
+ /**
+ * Retrieve the property value for a particular path + type pair.
+ *
+ * @param prefix Path
+ * @param postfix Type
+ * @return Value
+ */
+ protected String getPropertyValue(String prefix, String postfix) {
+ String ret = properties.getProperty(prefix + "." + postfix);
+ if (ret != null) {
+ // logger.debugFine("Found property: "+prefix + "." +
+ // postfix+" for "+prefix);
+ return ret;
+ }
+ int pos = prefix.length();
+ while (pos > 0) {
+ pos = prefix.lastIndexOf('.', pos - 1);
+ if (pos <= 0) {
+ break;
+ }
+ ret = properties.getProperty(prefix.substring(0, pos) + '.' + postfix);
+ if (ret != null) {
+ // logger.debugFine("Found property: "+prefix.substring(0, pos) + "." +
+ // postfix+" for "+prefix);
+ return ret;
+ }
+ }
+ ret = properties.getProperty(postfix);
+ if (ret != null) {
+ // logger.debugFine("Found property: "+postfix+" for "+prefix);
+ return ret;
+ }
+ return null;
+ }
+
+ @Override
+ public String getColor(String key) {
+ return getPropertyValue(key, COLOR);
+ }
+
+ @Override
+ public String getBackgroundColor(String key) {
+ return getPropertyValue(key, BACKGROUND_COLOR);
+ }
+
+ @Override
+ public String getTextColor(String key) {
+ return getPropertyValue(key, TEXT_COLOR);
+ }
+
+ @Override
+ public ColorLibrary getColorSet(String key) {
+ ColorLibrary cl = getCached(key, COLORSET, ColorLibrary.class);
+ if (cl == null) {
+ String[] colors = getPropertyValue(key, COLORSET).split(LIST_SEPARATOR);
+ cl = new ListBasedColorLibrary(colors, "Default");
+ setCached(key, COLORSET, cl);
+ }
+ return cl;
+ }
+
+ @Override
+ public double getLineWidth(String key) {
+ Double lw = getCached(key, LINE_WIDTH, Double.class);
+ if (lw == null) {
+ try {
+ lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, LINE_WIDTH)) * SCALE);
+ } catch (NullPointerException e) {
+ throw new AbortException("Missing/invalid value in style library: " + key + '.' + LINE_WIDTH);
+ }
+ }
+ return lw.doubleValue();
+ }
+
+ @Override
+ public double getTextSize(String key) {
+ Double lw = getCached(key, TEXT_SIZE, Double.class);
+ if (lw == null) {
+ try {
+ lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, TEXT_SIZE)) * SCALE);
+ } catch (NullPointerException e) {
+ throw new AbortException("Missing/invalid value in style library: " + key + '.' + TEXT_SIZE);
+ }
+ }
+ return lw.doubleValue();
+ }
+
+ @Override
+ public String getFontFamily(String key) {
+ return getPropertyValue(key, FONT_FAMILY);
+ }
+
+ @Override
+ public double getSize(String key) {
+ Double lw = getCached(key, GENERIC_SIZE, Double.class);
+ if (lw == null) {
+ try {
+ lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, GENERIC_SIZE)) * SCALE);
+ } catch (NullPointerException e) {
+ throw new AbortException("Missing/invalid value in style library: " + key + '.' + GENERIC_SIZE);
+ }
+ }
+ return lw.doubleValue();
+ }
+
+ @Override
+ public double getOpacity(String key) {
+ Double lw = getCached(key, OPACITY, Double.class);
+ if (lw == null) {
+ try {
+ lw = Double.valueOf(FormatUtil.parseDouble(getPropertyValue(key, OPACITY)));
+ } catch (NullPointerException e) {
+ throw new AbortException("Missing/invalid value in style library: " + key + '.' + OPACITY);
+ }
+ }
+ return lw.doubleValue();
+ }
+
+ @Override
+ public LineStyleLibrary lines() {
+ if (linelib == null) {
+ String libname = properties.getProperty(PROP_LINES_LIBRARY, SolidLineStyleLibrary.class.getName());
+ try {
+ Class<?> cls;
+ try {
+ cls = Class.forName(libname);
+ } catch (ClassNotFoundException e) {
+ cls = Class.forName(LineStyleLibrary.class.getPackage().getName() + '.' + libname);
+ }
+ linelib = (LineStyleLibrary) cls.getConstructor(StyleLibrary.class).newInstance(this);
+ } catch (Exception e) {
+ LOG.exception(e);
+ linelib = new SolidLineStyleLibrary(this);
+ }
+ }
+ return linelib;
+ }
+
+ @Override
+ public MarkerLibrary markers() {
+ if (markerlib == null) {
+ String libname = properties.getProperty(PROP_MARKER_LIBRARY, PrettyMarkers.class.getName());
+ try {
+ Class<?> cls;
+ try {
+ cls = Class.forName(libname);
+ } catch (ClassNotFoundException e) {
+ cls = Class.forName(MarkerLibrary.class.getPackage().getName() + '.' + libname);
+ }
+ markerlib = (MarkerLibrary) cls.getConstructor(StyleLibrary.class).newInstance(this);
+ } catch (Exception e) {
+ LOG.exception(e);
+ markerlib = new PrettyMarkers(this);
+ }
+ }
+ return markerlib;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java
new file mode 100644
index 00000000..0e26bf80
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/SingleObjectsStylingPolicy.java
@@ -0,0 +1,34 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Styling policy based on assigning objects individual colors.
+ *
+ * @author Erich Schubert
+ */
+public interface SingleObjectsStylingPolicy extends StylingPolicy {
+ // TODO: finish and use, e.g. for outliers and density visualization
+ // TODO: do we need anything here beyond "not a class styling policy"?
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java
new file mode 100755
index 00000000..5ca93be6
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StyleLibrary.java
@@ -0,0 +1,277 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+
+/**
+ * Style library interface. A style library allows the user to customize the
+ * visual rendering, for example for print media or screen presentation without
+ * having to change program code.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ * @apiviz.composedOf LineStyleLibrary
+ * @apiviz.composedOf MarkerLibrary
+ */
+public interface StyleLibrary {
+ /**
+ * Default
+ */
+ final static String DEFAULT = "";
+
+ /**
+ * Page
+ */
+ final static String PAGE = "page";
+
+ /**
+ * Plot
+ */
+ final static String PLOT = "plot";
+
+ /**
+ * Axis
+ */
+ final static String AXIS = "axis";
+
+ /**
+ * Axis tick
+ */
+ final static String AXIS_TICK = "axis.tick";
+
+ /**
+ * Axis minor tick
+ */
+ final static String AXIS_TICK_MINOR = "axis.tick.minor";
+
+ /**
+ * Axis label
+ */
+ final static String AXIS_LABEL = "axis.label";
+
+ /**
+ * Key
+ */
+ final static String KEY = "key";
+
+ /**
+ * Clusterorder
+ */
+ final static String CLUSTERORDER = "plot.clusterorder";
+
+ /**
+ * Margin
+ */
+ final static String MARGIN = "margin";
+
+ /**
+ * Bubble size
+ */
+ final static String BUBBLEPLOT = "plot.bubble";
+
+ /**
+ * Marker size
+ */
+ final static String MARKERPLOT = "plot.marker";
+
+ /**
+ * Dot size
+ */
+ final static String DOTPLOT = "plot.dot";
+
+ /**
+ * Grayed out objects
+ */
+ final static String PLOTGREY = "plot.grey";
+
+ /**
+ * Reference points color and size
+ */
+ final static String REFERENCE_POINTS = "plot.referencepoints";
+
+ /**
+ * Polygons style
+ */
+ final static String POLYGONS = "plot.polygons";
+
+ /**
+ * Selection color and opacity
+ */
+ final static String SELECTION = "plot.selection";
+
+ /**
+ * Selection color and opacity during selecting process
+ */
+ final static String SELECTION_ACTIVE = "plot.selection.active";
+
+ /**
+ * Scaling constant. Keep in sync with
+ * {@link de.lmu.ifi.dbs.elki.visualization.projections.Projection#SCALE}
+ */
+ public static final double SCALE = 100.0;
+
+ /* ** Property types ** */
+ /**
+ * Color
+ */
+ final static String COLOR = "color";
+
+ /**
+ * Background color
+ */
+ final static String BACKGROUND_COLOR = "background-color";
+
+ /**
+ * Text color
+ */
+ final static String TEXT_COLOR = "text-color";
+
+ /**
+ * Color set
+ */
+ final static String COLORSET = "colorset";
+
+ /**
+ * Line width
+ */
+ final static String LINE_WIDTH = "line-width";
+
+ /**
+ * Text size
+ */
+ final static String TEXT_SIZE = "text-size";
+
+ /**
+ * Font family
+ */
+ final static String FONT_FAMILY = "font-family";
+
+ /**
+ * Generic size
+ */
+ final static String GENERIC_SIZE = "size";
+
+ /**
+ * Opacity (transparency)
+ */
+ final static String OPACITY = "opacity";
+
+ /**
+ * XY curve styling.
+ */
+ static final String XYCURVE = "xycurve";
+
+ /**
+ * Retrieve a color for an item
+ *
+ * @param name Reference name
+ * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such
+ * as "red"
+ */
+ public String getColor(String name);
+
+ /**
+ * Retrieve background color for an item
+ *
+ * @param name Reference name
+ * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such
+ * as "red"
+ */
+ public String getBackgroundColor(String name);
+
+ /**
+ * Retrieve text color for an item
+ *
+ * @param name Reference name
+ * @return color in CSS/SVG valid format: hexadecimal (#aabbcc) or names such
+ * as "red"
+ */
+ public String getTextColor(String name);
+
+ /**
+ * Retrieve colorset for an item
+ *
+ * @param name Reference name
+ * @return color library
+ */
+ public ColorLibrary getColorSet(String name);
+
+ /**
+ * Get line width
+ *
+ * @param key Key
+ * @return line width as double
+ */
+ public double getLineWidth(String key);
+
+ /**
+ * Get generic size
+ *
+ * @param key Key
+ * @return size as double
+ */
+ public double getSize(String key);
+
+ /**
+ * Get text size
+ *
+ * @param key Key
+ * @return line width as double
+ */
+ public double getTextSize(String key);
+
+ /**
+ * Get font family
+ *
+ * @param key Key
+ * @return font family CSS string
+ */
+ public String getFontFamily(String key);
+
+ /**
+ * Get opacity
+ *
+ * @param key Key
+ * @return size as double
+ */
+ public double getOpacity(String key);
+
+ /**
+ * Get the line style library to use.
+ *
+ * @return line style library
+ */
+ public LineStyleLibrary lines();
+
+ /**
+ * Get the marker library to use.
+ *
+ * @return marker library
+ */
+ public MarkerLibrary markers();
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java
new file mode 100644
index 00000000..406b6aff
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/StylingPolicy.java
@@ -0,0 +1,48 @@
+package de.lmu.ifi.dbs.elki.visualization.style;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+
+/**
+ * Styling policy.
+ *
+ * Implementations <em>must</em> implement either {@link ClassStylingPolicy} or
+ * {@link SingleObjectsStylingPolicy} interfaces, as most visualizers will only
+ * support these known interfaces.
+ *
+ * @author Erich Schubert
+ */
+public interface StylingPolicy extends VisualizationItem {
+ /**
+ * Get the color for an individual object.
+ *
+ * Note: if possible, use a class styling policy which can optimize better.
+ *
+ * @param id Object ID
+ * @return Color value
+ */
+ public int getColorForDBID(DBIDRef id);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java
new file mode 100644
index 00000000..49b8cc91
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/DashedLineStyleLibrary.java
@@ -0,0 +1,159 @@
+package de.lmu.ifi.dbs.elki.visualization.style.lines;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.CSSConstants;
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Line library using various dashed and dotted line styles.
+ *
+ * This library is particularly useful for black and white output.
+ *
+ * {@link LineStyleLibrary#FLAG_STRONG} will result in thicker lines.
+ *
+ * {@link LineStyleLibrary#FLAG_WEAK} will result in thinner and
+ * semi-transparent lines.
+ *
+ * {@link LineStyleLibrary#FLAG_INTERPOLATED} will result in shorter dashing
+ * patterns.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ */
+public class DashedLineStyleLibrary implements LineStyleLibrary {
+ /**
+ * The style library we use for colors
+ */
+ private ColorLibrary colors;
+
+ /** Dash patterns to regularly use */
+ private double[][] dashpatterns = {
+ // solid, no dashing
+ {},
+ // half-half
+ { .5, .5 },
+ // quarters
+ { .25, .25, .25, .25 },
+ // alternating long-quart
+ { .75, .25 },
+ // dash-dot
+ { .7, .1, .1, .1 }, };
+
+ /** Replacement for the solid pattern in 'interpolated' mode */
+ private double[] solidreplacement = { .1, .1 };
+
+ private int dashnum = dashpatterns.length;
+
+ /**
+ * Color of "uncolored" dots
+ */
+ private String dotcolor;
+
+ /**
+ * Color of "greyed out" dots
+ */
+ private String greycolor;
+
+ /**
+ * Constructor
+ *
+ * @param style Style library
+ */
+ public DashedLineStyleLibrary(StyleLibrary style) {
+ super();
+ this.colors = style.getColorSet(StyleLibrary.PLOT);
+ this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT);
+ this.greycolor = style.getColor(StyleLibrary.PLOTGREY);
+ }
+
+ @Override
+ public void formatCSSClass(CSSClass cls, int style, double width, Object... flags) {
+ if(style == -2) {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, greycolor);
+ }
+ else if(style == -1) {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, dotcolor);
+ }
+ else {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, colors.getColor(style));
+ }
+ boolean interpolated = false;
+ // process flavoring flags
+ for(Object flag : flags) {
+ if(LineStyleLibrary.FLAG_STRONG.equals(flag)) {
+ width = width * 1.5;
+ }
+ else if(LineStyleLibrary.FLAG_WEAK.equals(flag)) {
+ cls.setStatement(CSSConstants.CSS_STROKE_OPACITY_PROPERTY, ".50");
+ width = width * 0.75;
+ }
+ else if(LineStyleLibrary.FLAG_INTERPOLATED.equals(flag)) {
+ interpolated = true;
+ }
+ }
+ cls.setStatement(CSSConstants.CSS_STROKE_WIDTH_PROPERTY, SVGUtil.fmt(width));
+ // handle dashing
+ int styleflav = (style > 0) ? (style % dashnum) : (-style % dashnum);
+ if(!interpolated) {
+ double[] pat = dashpatterns[styleflav];
+ assert (pat.length % 2 == 0);
+ if(pat.length > 0) {
+ StringBuilder pattern = new StringBuilder();
+ for(int i = 0; i < pat.length; i++) {
+ if(i > 0) {
+ pattern.append(',');
+ }
+ pattern.append(SVGUtil.fmt(pat[i] * width * 30));
+ // pattern.append("%");
+ }
+ cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, pattern.toString());
+ }
+ }
+ else {
+ double[] pat = dashpatterns[styleflav];
+ if(styleflav == 0) {
+ pat = solidreplacement;
+ }
+ assert (pat.length % 2 == 0);
+ // TODO: add dotting.
+ if(pat.length > 0) {
+ StringBuilder pattern = new StringBuilder();
+ for(int i = 0; i < pat.length; i++) {
+ if(i > 0) {
+ pattern.append(',');
+ }
+ pattern.append(SVGUtil.fmt(pat[i] * width));
+ // pattern.append("%");
+ }
+ cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, pattern.toString());
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java
new file mode 100644
index 00000000..48212f16
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/LineStyleLibrary.java
@@ -0,0 +1,76 @@
+package de.lmu.ifi.dbs.elki.visualization.style.lines;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+
+/**
+ * Interface to obtain CSS classes for plot lines.
+ *
+ * {@code meta} is a set of Objects, usually constants that may or may not be
+ * used by the {@link LineStyleLibrary} to generate variants of the style.
+ *
+ * Predefined meta flags that are usually supported are:
+ * <dl>
+ * <dt>{@link #FLAG_STRONG}</dt>
+ * <dd>Request a "stronger" version of the same style</dd>
+ * <dt>{@link #FLAG_WEAK}</dt>
+ * <dd>Request a "weaker" version of the same style</dd>
+ * <dt>{@link #FLAG_INTERPOLATED}</dt>
+ * <dd>Request an "interpolated" version of the same style (e.g. lighter or
+ * dashed)</dd>
+ * </dl>
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses CSSClass oneway
+ */
+public interface LineStyleLibrary {
+ /**
+ * Meta flag to request a 'stronger' version of the style
+ */
+ public static final String FLAG_STRONG = "strong";
+
+ /**
+ * Meta flag to request a 'weaker' version of the style
+ */
+ public static final String FLAG_WEAK = "weak";
+
+ /**
+ * Meta flag to request an 'interpolated' version of the style
+ */
+ public static final String FLAG_INTERPOLATED = "interpolated";
+
+ /**
+ * Add the formatting statements to the given CSS class.
+ *
+ * Note: this can overwrite some existing properties of the CSS class.
+ *
+ * @param cls CSS class to modify
+ * @param style style number
+ * @param width line width
+ * @param meta meta objects to request line variants
+ */
+ public void formatCSSClass(CSSClass cls, int style, double width, Object... meta);
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java
new file mode 100644
index 00000000..61765f1a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/SolidLineStyleLibrary.java
@@ -0,0 +1,106 @@
+package de.lmu.ifi.dbs.elki.visualization.style.lines;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.CSSConstants;
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Line style library featuring solid lines for default styles only (combine
+ * with a color library to obtain enough classes!)
+ *
+ * {@link LineStyleLibrary#FLAG_STRONG} will result in thicker lines.
+ *
+ * {@link LineStyleLibrary#FLAG_WEAK} will result in thinner and
+ * semi-transparent lines.
+ *
+ * {@link LineStyleLibrary#FLAG_INTERPOLATED} will result in dashed lines.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ */
+public class SolidLineStyleLibrary implements LineStyleLibrary {
+ /**
+ * Reference to the color library.
+ */
+ private ColorLibrary colors;
+
+ /**
+ * Color of "uncolored" dots
+ */
+ private String dotcolor;
+
+ /**
+ * Color of "greyed out" dots
+ */
+ private String greycolor;
+
+ /**
+ * Constructor.
+ *
+ * @param style Style library to use.
+ */
+ public SolidLineStyleLibrary(StyleLibrary style) {
+ super();
+ this.colors = style.getColorSet(StyleLibrary.PLOT);
+ this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT);
+ this.greycolor = style.getColor(StyleLibrary.PLOTGREY);
+ }
+
+ @Override
+ public void formatCSSClass(CSSClass cls, int style, double width, Object... flags) {
+ if(style == -2) {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, greycolor);
+ }
+ else if(style == -1) {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, dotcolor);
+ }
+ else {
+ cls.setStatement(CSSConstants.CSS_STROKE_PROPERTY, colors.getColor(style));
+ }
+ boolean interpolated = false;
+ // process flavoring flags
+ for(Object flag : flags) {
+ if(LineStyleLibrary.FLAG_STRONG.equals(flag)) {
+ width = width * 1.5;
+ }
+ else if(LineStyleLibrary.FLAG_WEAK.equals(flag)) {
+ cls.setStatement(CSSConstants.CSS_STROKE_OPACITY_PROPERTY, ".50");
+ width = width * 0.75;
+ }
+ else if(LineStyleLibrary.FLAG_INTERPOLATED.equals(flag)) {
+ interpolated = true;
+ }
+ }
+ cls.setStatement(CSSConstants.CSS_STROKE_WIDTH_PROPERTY, SVGUtil.fmt(width));
+ if(interpolated) {
+ cls.setStatement(CSSConstants.CSS_STROKE_DASHARRAY_PROPERTY, "" + SVGUtil.fmt(width / StyleLibrary.SCALE * 2.) + "," + SVGUtil.fmt(width / StyleLibrary.SCALE * 2.));
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java
new file mode 100755
index 00000000..69790fdf
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/lines/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Generate line styles for plotting in CSS</p>
+ *
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.style.lines; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java
new file mode 100644
index 00000000..c4078091
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/CircleMarkers.java
@@ -0,0 +1,90 @@
+package de.lmu.ifi.dbs.elki.visualization.style.marker;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Simple marker library that just draws colored circles at the given
+ * coordinates.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ */
+public class CircleMarkers implements MarkerLibrary {
+ /**
+ * Color library
+ */
+ private ColorLibrary colors;
+
+ /**
+ * Color of "uncolored" dots
+ */
+ private String dotcolor = "black";
+
+ /**
+ * Color of "greyed out" dots
+ */
+ private String greycolor = "gray";
+
+ /**
+ * Constructor
+ *
+ * @param style Style library to use
+ */
+ public CircleMarkers(StyleLibrary style) {
+ super();
+ this.colors = style.getColorSet(StyleLibrary.MARKERPLOT);
+ this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT);
+ this.greycolor = style.getColor(StyleLibrary.PLOTGREY);
+ }
+
+ /**
+ * Use a given marker on the document.
+ */
+ @Override
+ public Element useMarker(SVGPlot plot, Element parent, double x, double y, int stylenr, double size) {
+ Element marker = plot.svgCircle(x, y, size * .5);
+ final String col;
+ if(stylenr == -1) {
+ col = dotcolor;
+ }
+ else if(stylenr == -2) {
+ col = greycolor;
+ }
+ else {
+ col = colors.getColor(stylenr);
+ }
+ SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + col);
+ parent.appendChild(marker);
+ return marker;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java
new file mode 100755
index 00000000..522097aa
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MarkerLibrary.java
@@ -0,0 +1,56 @@
+package de.lmu.ifi.dbs.elki.visualization.style.marker;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+
+/**
+ * A marker library is a class that can generate and draw various styles of
+ * markers. Different uses might require different marker libraries (e.g. full
+ * screen, thumbnail, print)
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Element oneway - - «create»
+ */
+public interface MarkerLibrary {
+ /**
+ * Insert a marker at the given coordinates. Markers will be defined in the
+ * defs part of the document, and then SVG-"use"d at the given coordinates.
+ * This supposedly is more efficient and significantly reduces file size.
+ * Symbols will be named "s0", "s1" etc.; these names must not be used by
+ * other elements in the SVG document!
+ *
+ * @param plot Plot to draw on
+ * @param parent parent node
+ * @param x coordinate
+ * @param y coordinate
+ * @param style style (enumerated)
+ * @param size size
+ * @return Element node generated.
+ */
+ public Element useMarker(SVGPlot plot, Element parent, double x, double y, int style, double size);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java
new file mode 100755
index 00000000..a3390a6d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/MinimalMarkers.java
@@ -0,0 +1,90 @@
+package de.lmu.ifi.dbs.elki.visualization.style.marker;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Simple marker library that just draws colored rectangles at the given
+ * coordinates.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ */
+public class MinimalMarkers implements MarkerLibrary {
+ /**
+ * Color library
+ */
+ private ColorLibrary colors;
+
+ /**
+ * Color of "uncolored" dots
+ */
+ private String dotcolor = "black";
+
+ /**
+ * Color of "greyed out" dots
+ */
+ private String greycolor = "gray";
+
+ /**
+ * Constructor
+ *
+ * @param style Style library to use
+ */
+ public MinimalMarkers(StyleLibrary style) {
+ super();
+ this.colors = style.getColorSet(StyleLibrary.MARKERPLOT);
+ this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT);
+ this.greycolor = style.getColor(StyleLibrary.PLOTGREY);
+ }
+
+ /**
+ * Use a given marker on the document.
+ */
+ @Override
+ public Element useMarker(SVGPlot plot, Element parent, double x, double y, int stylenr, double size) {
+ Element marker = plot.svgRect(x - size * .5, y - size * .5, size, size);
+ final String col;
+ if(stylenr == -1) {
+ col = dotcolor;
+ }
+ else if(stylenr == -2) {
+ col = greycolor;
+ }
+ else {
+ col = colors.getColor(stylenr);
+ }
+ SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + col);
+ parent.appendChild(marker);
+ return marker;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java
new file mode 100755
index 00000000..be99c6b1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/PrettyMarkers.java
@@ -0,0 +1,235 @@
+package de.lmu.ifi.dbs.elki.visualization.style.marker;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Marker library achieving a larger number of styles by combining different
+ * shapes with different colors. Uses object ID management by SVGPlot.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.composedOf ColorLibrary
+ */
+public class PrettyMarkers implements MarkerLibrary {
+ /**
+ * Color library
+ */
+ private ColorLibrary colors;
+
+ /**
+ * Default prefix to use.
+ */
+ private static final String DEFAULT_PREFIX = "s";
+
+ /**
+ * Prefix for the IDs generated.
+ */
+ private String prefix;
+
+ /**
+ * Color of "uncolored" dots
+ */
+ private String dotcolor;
+
+ /**
+ * Color of "greyed out" dots
+ */
+ private String greycolor;
+
+ /**
+ * Constructor
+ *
+ * @param prefix prefix to use.
+ * @param style style library to use
+ */
+ public PrettyMarkers(String prefix, StyleLibrary style) {
+ this.prefix = prefix;
+ this.colors = style.getColorSet(StyleLibrary.MARKERPLOT);
+ this.dotcolor = style.getColor(StyleLibrary.MARKERPLOT);
+ this.greycolor = style.getColor(StyleLibrary.PLOTGREY);
+ }
+
+ /**
+ * Constructor without prefix argument, will use {@link #DEFAULT_PREFIX} as
+ * prefix.
+ *
+ * @param style Style library to use
+ */
+ public PrettyMarkers(StyleLibrary style) {
+ this(DEFAULT_PREFIX, style);
+ }
+
+ /**
+ * Draw an marker used in scatter plots. If you intend to use the markers
+ * multiple times, you should consider using the {@link #useMarker} method
+ * instead, which exploits the SVG features of symbol definition and use
+ *
+ * @param plot containing plot
+ * @param parent parent node
+ * @param x position
+ * @param y position
+ * @param style marker style (enumerated)
+ * @param size size
+ */
+ public void plotMarker(SVGPlot plot, Element parent, double x, double y, int style, double size) {
+ assert (parent != null);
+ if (style == -1) {
+ plotUncolored(plot, parent, x, y, size);
+ return;
+ }
+ if (style == -2) {
+ plotGray(plot, parent, x, y, size);
+ return;
+ }
+ // TODO: add more styles.
+ String colorstr = colors.getColor(style);
+ String strokestyle = SVGConstants.CSS_STROKE_PROPERTY + ":" + colorstr + ";" + SVGConstants.CSS_STROKE_WIDTH_PROPERTY + ":" + SVGUtil.fmt(size / 6);
+
+ switch(style % 8){
+ case 0: {
+ // + cross
+ Element line1 = plot.svgLine(x, y - size / 2.2, x, y + size / 2.2);
+ SVGUtil.setStyle(line1, strokestyle);
+ parent.appendChild(line1);
+ Element line2 = plot.svgLine(x - size / 2.2, y, x + size / 2.2, y);
+ SVGUtil.setStyle(line2, strokestyle);
+ parent.appendChild(line2);
+ break;
+ }
+ case 1: {
+ // X cross
+ Element line1 = plot.svgLine(x - size / 2.828427, y - size / 2.828427, x + size / 2.828427, y + size / 2.828427);
+ SVGUtil.setStyle(line1, strokestyle);
+ parent.appendChild(line1);
+ Element line2 = plot.svgLine(x - size / 2.828427, y + size / 2.828427, x + size / 2.828427, y - size / 2.828427);
+ SVGUtil.setStyle(line2, strokestyle);
+ parent.appendChild(line2);
+ break;
+ }
+ case 2: {
+ // O hollow circle
+ Element circ = plot.svgCircle(x, y, size / 2.2);
+ SVGUtil.setStyle(circ, "fill: none;" + strokestyle);
+ parent.appendChild(circ);
+ break;
+ }
+ case 3: {
+ // [] hollow rectangle
+ Element rect = plot.svgRect(x - size / 2.4, y - size / 2.4, size / 1.2, size / 1.2);
+ SVGUtil.setStyle(rect, "fill: none;" + strokestyle);
+ parent.appendChild(rect);
+ break;
+ }
+ case 4: {
+ // <> hollow diamond
+ Element rect = plot.svgRect(x - size / 2.7, y - size / 2.7, size / 1.35, size / 1.35);
+ SVGUtil.setStyle(rect, "fill: none;" + strokestyle);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(45," + SVGUtil.fmt(x) + "," + SVGUtil.fmt(y) + ")");
+ parent.appendChild(rect);
+ break;
+ }
+ case 5: {
+ // O filled circle
+ Element circ = plot.svgCircle(x, y, size * .5);
+ SVGUtil.setStyle(circ, SVGConstants.CSS_FILL_PROPERTY + ":" + colorstr);
+ parent.appendChild(circ);
+ break;
+ }
+ case 6: {
+ // [] filled rectangle
+ Element rect = plot.svgRect(x - size / 2.2, y - size / 2.2, size / 1.1, size / 1.1);
+ SVGUtil.setStyle(rect, "fill:" + colorstr);
+ parent.appendChild(rect);
+ break;
+ }
+ case 7: {
+ // <> filled diamond
+ Element rect = plot.svgRect(x - size / 2.5, y - size / 2.5, size / 1.25, size / 1.25);
+ SVGUtil.setStyle(rect, "fill:" + colorstr);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(45," + SVGUtil.fmt(x) + "," + SVGUtil.fmt(y) + ")");
+ parent.appendChild(rect);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Plot a replacement marker when an object is to be plotted as "disabled", usually gray.
+ *
+ * @param plot Plot to draw to
+ * @param parent Parent element
+ * @param x X position
+ * @param y Y position
+ * @param size Size
+ */
+ protected void plotGray(SVGPlot plot, Element parent, double x, double y, double size) {
+ Element marker = plot.svgCircle(x, y, size * .5);
+ SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + greycolor);
+ parent.appendChild(marker);
+ }
+
+ /**
+ * Plot a replacement marker when no color is set; usually black
+ *
+ * @param plot Plot to draw to
+ * @param parent Parent element
+ * @param x X position
+ * @param y Y position
+ * @param size Size
+ */
+ protected void plotUncolored(SVGPlot plot, Element parent, double x, double y, double size) {
+ Element marker = plot.svgCircle(x, y, size * .5);
+ SVGUtil.setStyle(marker, SVGConstants.CSS_FILL_PROPERTY + ":" + dotcolor);
+ parent.appendChild(marker);
+ }
+
+ @Override
+ public Element useMarker(SVGPlot plot, Element parent, double x, double y, int style, double size) {
+ String id = prefix + style + "_" + size;
+ Element existing = plot.getIdElement(id);
+ if(existing == null) {
+ Element symbol = plot.svgElement(SVGConstants.SVG_SYMBOL_TAG);
+ SVGUtil.setAtt(symbol, SVGConstants.SVG_ID_ATTRIBUTE, id);
+ plotMarker(plot, symbol, 2 * size, 2 * size, style, 2 * size);
+ plot.getDefs().appendChild(symbol);
+ plot.putIdElement(id, symbol);
+ }
+ Element use = plot.svgElement(SVGConstants.SVG_USE_TAG);
+ use.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, "#" + id);
+ SVGUtil.setAtt(use, SVGConstants.SVG_X_ATTRIBUTE, x - 2 * size);
+ SVGUtil.setAtt(use, SVGConstants.SVG_Y_ATTRIBUTE, y - 2 * size);
+ if(parent != null) {
+ parent.appendChild(use);
+ }
+ return use;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java
new file mode 100755
index 00000000..293d5a67
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/marker/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Draw plot markers</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.style.marker; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java
new file mode 100755
index 00000000..90c548cd
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/style/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Style management for ELKI visualizations.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.style; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java
new file mode 100644
index 00000000..afa42afd
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGArrow.java
@@ -0,0 +1,114 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+/**
+ * Static class for drawing simple arrows
+ *
+ * @author Erich Schubert
+ * @author Robert Rödler
+ *
+ * @apiviz.uses SVGPath
+ */
+public final class SVGArrow {
+ /**
+ * Direction constants
+ *
+ * @author Erich Schubert
+ * @author Robert Rödler
+ *
+ * @apiviz.exclude
+ */
+ public static enum Direction {
+ LEFT, DOWN, RIGHT, UP, // SWAPWITH, INSERT
+ }
+
+ /**
+ * Constant for "up"
+ */
+ public static final Direction UP = Direction.UP;
+
+ /**
+ * Constant for "down"
+ */
+ public static final Direction DOWN = Direction.DOWN;
+
+ /**
+ * Constant for "right"
+ */
+ public static final Direction RIGHT = Direction.RIGHT;
+
+ /**
+ * Constant for "left"
+ */
+ public static final Direction LEFT = Direction.LEFT;
+
+ /**
+ * Draw an arrow at the given position.
+ *
+ * Note: the arrow is an unstyled svg path. You need to apply style afterwards.
+ *
+ * @param svgp Plot to draw to
+ * @param dir Direction to draw
+ * @param x Center x coordinate
+ * @param y Center y coordinate
+ * @param size Arrow size
+ * @return SVG Element
+ */
+ public static Element makeArrow(SVGPlot svgp, Direction dir, double x, double y, double size) {
+ final SVGPath path = new SVGPath();
+ final double hs = size / 2.;
+
+ switch(dir){
+ case LEFT:
+ path.drawTo(x + hs, y + hs);
+ path.drawTo(x - hs, y);
+ path.drawTo(x + hs, y - hs);
+ path.drawTo(x + hs, y + hs);
+ break;
+ case DOWN:
+ path.drawTo(x - hs, y - hs);
+ path.drawTo(x + hs, y - hs);
+ path.drawTo(x, y + hs);
+ path.drawTo(x - hs, y - hs);
+ break;
+ case RIGHT:
+ path.drawTo(x - hs, y - hs);
+ path.drawTo(x + hs, y);
+ path.drawTo(x - hs, y + hs);
+ path.drawTo(x - hs, y - hs);
+ break;
+ case UP:
+ path.drawTo(x - hs, y + hs);
+ path.drawTo(x, y - hs);
+ path.drawTo(x + hs, y + hs);
+ path.drawTo(x - hs, y + hs);
+ break;
+ }
+ path.close();
+ return path.makeElement(svgp);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java
new file mode 100644
index 00000000..6a542a7e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGButton.java
@@ -0,0 +1,165 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+
+/**
+ * Class to draw a button as SVG.
+ *
+ * @author Erich Schubert
+ */
+public class SVGButton {
+ /**
+ * Default button color
+ */
+ public static final String DEFAULT_BUTTON_COLOR = SVGConstants.CSS_LIGHTGRAY_VALUE;
+
+ /**
+ * Default text color
+ */
+ public static final String DEFAULT_TEXT_COLOR = SVGConstants.CSS_BLACK_VALUE;
+
+ /**
+ * X position
+ */
+ private double x;
+
+ /**
+ * Y position
+ */
+ private double y;
+
+ /**
+ * Width
+ */
+ private double w;
+
+ /**
+ * Height
+ */
+ private double h;
+
+ /**
+ * Corner rounding factor. NaN = no rounding
+ */
+ private double r = Double.NaN;
+
+ /**
+ * Class for the buttons main CSS
+ */
+ private CSSClass butcss;
+
+ /**
+ * Button title, optional
+ */
+ private String title = null;
+
+ /**
+ * Title styling
+ */
+ private CSSClass titlecss = null;
+
+ /**
+ * Constructor.
+ *
+ * @param x Position X
+ * @param y Position Y
+ * @param w Width
+ * @param h Height
+ * @param r Rounded radius
+ */
+ public SVGButton(double x, double y, double w, double h, double r) {
+ super();
+ this.x = x;
+ this.y = y;
+ this.w = w;
+ this.h = h;
+ this.r = r;
+ this.butcss = new CSSClass(this, "button");
+ butcss.setStatement(SVGConstants.CSS_FILL_PROPERTY, DEFAULT_BUTTON_COLOR);
+ butcss.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ butcss.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, ".01");
+ }
+
+ /**
+ * Produce the actual SVG elements for the button.
+ *
+ * @param svgp Plot to draw to
+ * @return Button wrapper element
+ */
+ public Element render(SVGPlot svgp) {
+ Element tag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ Element button = svgp.svgRect(x, y, w, h);
+ if(!Double.isNaN(r)) {
+ SVGUtil.setAtt(button, SVGConstants.SVG_RX_ATTRIBUTE, r);
+ SVGUtil.setAtt(button, SVGConstants.SVG_RY_ATTRIBUTE, r);
+ }
+ SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, butcss.inlineCSS());
+ tag.appendChild(button);
+ // Add light effect:
+ if (svgp.getIdElement(SVGEffects.LIGHT_GRADIENT_ID) != null) {
+ Element light = svgp.svgRect(x, y, w, h);
+ if(!Double.isNaN(r)) {
+ SVGUtil.setAtt(light, SVGConstants.SVG_RX_ATTRIBUTE, r);
+ SVGUtil.setAtt(light, SVGConstants.SVG_RY_ATTRIBUTE, r);
+ }
+ SVGUtil.setAtt(light, SVGConstants.SVG_STYLE_ATTRIBUTE, "fill:url(#"+SVGEffects.LIGHT_GRADIENT_ID+");fill-opacity:.5");
+ tag.appendChild(light);
+ }
+
+ // Add shadow effect:
+ if(svgp.getIdElement(SVGEffects.SHADOW_ID) != null) {
+ //Element shadow = svgp.svgRect(x + (w * .05), y + (h * .05), w, h);
+ //SVGUtil.setAtt(button, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + SVGConstants.CSS_BLACK_VALUE);
+ button.setAttribute(SVGConstants.SVG_FILTER_ATTRIBUTE, "url(#" + SVGEffects.SHADOW_ID + ")");
+ //tag.appendChild(shadow);
+ }
+
+ if(title != null) {
+ Element label = svgp.svgText(x + w * .5, y + h * .7, title);
+ label.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, titlecss.inlineCSS());
+ tag.appendChild(label);
+ }
+ return tag;
+ }
+
+ /**
+ * Set the button title
+ *
+ * @param title Button title
+ * @param textcolor Color
+ */
+ public void setTitle(String title, String textcolor) {
+ this.title = title;
+ if(titlecss == null) {
+ titlecss = new CSSClass(this, "text");
+ titlecss.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE);
+ titlecss.setStatement(SVGConstants.CSS_FILL_PROPERTY, textcolor);
+ titlecss.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, .6 * h);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java
new file mode 100644
index 00000000..1cab95b9
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCheckbox.java
@@ -0,0 +1,187 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+/**
+ * SVG checkbox.
+ *
+ * @author Sascha Goldhofer
+ */
+public class SVGCheckbox {
+ /**
+ * Status flag
+ */
+ protected boolean checked;
+
+ /**
+ * Event listeners
+ */
+ protected EventListenerList listenerList = new EventListenerList();
+
+ /**
+ * Checkbox label
+ */
+ protected String label = null;
+
+ /**
+ * Constructor, without label
+ *
+ * @param checked Checked status
+ */
+ public SVGCheckbox(boolean checked) {
+ this(checked, null);
+ }
+
+ /**
+ * Constructor, with label
+ *
+ * @param checked Checked status
+ * @param label Label
+ */
+ public SVGCheckbox(boolean checked, String label) {
+ this.checked = checked;
+ this.label = label;
+ }
+
+ /**
+ * Render the SVG checkbox to a plot
+ *
+ * @param svgp Plot to draw to
+ * @param x X offset
+ * @param y Y offset
+ * @param size Size factor
+ * @return Container element
+ */
+ public Element renderCheckBox(SVGPlot svgp, double x, double y, double size) {
+ // create check
+ final Element checkmark = SVGEffects.makeCheckmark(svgp);
+ checkmark.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "scale(" + (size / 11) + ") translate(" + x + " " + y + ")");
+ if(!checked) {
+ checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE);
+ }
+
+ // create box
+ Element checkbox_box = SVGUtil.svgRect(svgp.getDocument(), x, y, size, size);
+ checkbox_box.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1");
+ checkbox_box.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0");
+ checkbox_box.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.5");
+
+ // create checkbox
+ final Element checkbox = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ checkbox.appendChild(checkbox_box);
+ checkbox.appendChild(checkmark);
+
+ // create Label
+ if(label != null) {
+ Element labele = svgp.svgText(x + 2 * size, y + size, label);
+ // TODO: font size!
+ checkbox.appendChild(labele);
+ }
+
+ // add click event listener
+ EventTarget targ = (EventTarget) checkbox;
+ targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, new EventListener() {
+ /**
+ * Set the checkmark, and fire the event.
+ */
+ protected void check() {
+ checkmark.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE);
+ checked = true;
+ fireSwitchEvent(new ChangeEvent(SVGCheckbox.this));
+ }
+
+ /**
+ * Remove the checkmark and fire the event.
+ */
+ protected void uncheck() {
+ checkmark.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_DISPLAY_PROPERTY + ":" + SVGConstants.CSS_NONE_VALUE);
+ checked = false;
+ fireSwitchEvent(new ChangeEvent(SVGCheckbox.this));
+ }
+
+ @Override
+ public void handleEvent(Event evt) {
+ if(checked) {
+ uncheck();
+ }
+ else {
+ check();
+ }
+ }
+ }, false);
+
+ return checkbox;
+ }
+
+ /**
+ * Register a listener for this checkbox.
+ *
+ * @param listener Listener to add
+ */
+ public void addCheckBoxListener(ChangeListener listener) {
+ listenerList.add(ChangeListener.class, listener);
+ }
+
+ /**
+ * Remove a listener for this checkbox.
+ *
+ * @param listener Listener to remove
+ */
+ public void removeCheckBoxListener(ChangeListener listener) {
+ listenerList.remove(ChangeListener.class, listener);
+ }
+
+ /**
+ * Return the checkmark status.
+ *
+ * @return true, when checked
+ */
+ public boolean isChecked() {
+ return checked;
+ }
+
+ /**
+ * Fire the event to listeners
+ *
+ * @param evt Event to fire
+ */
+ protected void fireSwitchEvent(ChangeEvent evt) {
+ Object[] listeners = listenerList.getListenerList();
+ for(int i = 0; i < listeners.length; i += 2) {
+ if(listeners[i] == ChangeListener.class) {
+ ((ChangeListener) listeners[i + 1]).stateChanged(evt);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java
new file mode 100644
index 00000000..c09717f7
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGCloneVisible.java
@@ -0,0 +1,60 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import de.lmu.ifi.dbs.elki.utilities.xml.DOMCloner;
+
+/**
+ * Clone visible parts of an SVG document.
+ *
+ * @author Erich Schubert
+ */
+public class SVGCloneVisible extends DOMCloner {
+ @Override
+ public Node cloneNode(Document doc, Node eold) {
+ // Skip elements with visibility=hidden
+ if(eold instanceof Element) {
+ Element eeold = (Element) eold;
+ String vis = eeold.getAttribute(SVGConstants.CSS_VISIBILITY_PROPERTY);
+ if(SVGConstants.CSS_HIDDEN_VALUE.equals(vis)) {
+ return null;
+ }
+ }
+ // Perform clone flat
+ Node enew = doc.importNode(eold, false);
+ // Recurse:
+ for(Node n = eold.getFirstChild(); n != null; n = n.getNextSibling()) {
+ final Node clone = cloneNode(doc, n);
+ if (clone != null) {
+ enew.appendChild(clone);
+ }
+ }
+ return enew;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java
new file mode 100644
index 00000000..4171ce51
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGEffects.java
@@ -0,0 +1,147 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+/**
+ * Class containing some popular SVG effects.
+ *
+ * @author Erich Schubert
+ */
+public class SVGEffects {
+ /**
+ * ID for the drop shadow effect
+ */
+ public static final String SHADOW_ID = "shadow-effect";
+
+ /**
+ * ID for the light gradient fill
+ */
+ public static final String LIGHT_GRADIENT_ID = "light-gradient";
+
+ /**
+ * Static method to prepare a SVG document for drop shadow effects.
+ *
+ * Invoke this from an appropriate update thread!
+ *
+ * @param svgp Plot to prepare
+ */
+ public static void addShadowFilter(SVGPlot svgp) {
+ Element shadow = svgp.getIdElement(SHADOW_ID);
+ if(shadow == null) {
+ shadow = svgp.svgElement(SVGConstants.SVG_FILTER_TAG);
+ shadow.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, SHADOW_ID);
+ shadow.setAttribute(SVGConstants.SVG_WIDTH_ATTRIBUTE, "140%");
+ shadow.setAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE, "140%");
+
+ Element offset = svgp.svgElement(SVGConstants.SVG_FE_OFFSET_TAG);
+ offset.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_ALPHA_VALUE);
+ offset.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "off");
+ offset.setAttribute(SVGConstants.SVG_DX_ATTRIBUTE, "0.1");
+ offset.setAttribute(SVGConstants.SVG_DY_ATTRIBUTE, "0.1");
+ shadow.appendChild(offset);
+
+ Element gauss = svgp.svgElement(SVGConstants.SVG_FE_GAUSSIAN_BLUR_TAG);
+ gauss.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, "off");
+ gauss.setAttribute(SVGConstants.SVG_RESULT_ATTRIBUTE, "blur");
+ gauss.setAttribute(SVGConstants.SVG_STD_DEVIATION_ATTRIBUTE, "0.1");
+ shadow.appendChild(gauss);
+
+ Element blend = svgp.svgElement(SVGConstants.SVG_FE_BLEND_TAG);
+ blend.setAttribute(SVGConstants.SVG_IN_ATTRIBUTE, SVGConstants.SVG_SOURCE_GRAPHIC_VALUE);
+ blend.setAttribute(SVGConstants.SVG_IN2_ATTRIBUTE, "blur");
+ blend.setAttribute(SVGConstants.SVG_MODE_ATTRIBUTE, SVGConstants.SVG_NORMAL_VALUE);
+ shadow.appendChild(blend);
+
+ svgp.getDefs().appendChild(shadow);
+ svgp.putIdElement(SHADOW_ID, shadow);
+ }
+ }
+
+ /**
+ * Static method to prepare a SVG document for light gradient effects.
+ *
+ * Invoke this from an appropriate update thread!
+ *
+ * @param svgp Plot to prepare
+ */
+ public static void addLightGradient(SVGPlot svgp) {
+ Element gradient = svgp.getIdElement(LIGHT_GRADIENT_ID);
+ if(gradient == null) {
+ gradient = svgp.svgElement(SVGConstants.SVG_LINEAR_GRADIENT_TAG);
+ gradient.setAttribute(SVGConstants.SVG_ID_ATTRIBUTE, LIGHT_GRADIENT_ID);
+ gradient.setAttribute(SVGConstants.SVG_X1_ATTRIBUTE, "0");
+ gradient.setAttribute(SVGConstants.SVG_Y1_ATTRIBUTE, "0");
+ gradient.setAttribute(SVGConstants.SVG_X2_ATTRIBUTE, "0");
+ gradient.setAttribute(SVGConstants.SVG_Y2_ATTRIBUTE, "1");
+
+ Element stop0 = svgp.svgElement(SVGConstants.SVG_STOP_TAG);
+ stop0.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white");
+ stop0.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "1");
+ stop0.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "0");
+ gradient.appendChild(stop0);
+
+ Element stop04 = svgp.svgElement(SVGConstants.SVG_STOP_TAG);
+ stop04.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "white");
+ stop04.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0");
+ stop04.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".4");
+ gradient.appendChild(stop04);
+
+ Element stop06 = svgp.svgElement(SVGConstants.SVG_STOP_TAG);
+ stop06.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black");
+ stop06.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, "0");
+ stop06.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, ".6");
+ gradient.appendChild(stop06);
+
+ Element stop1 = svgp.svgElement(SVGConstants.SVG_STOP_TAG);
+ stop1.setAttribute(SVGConstants.SVG_STOP_COLOR_ATTRIBUTE, "black");
+ stop1.setAttribute(SVGConstants.SVG_STOP_OPACITY_ATTRIBUTE, ".5");
+ stop1.setAttribute(SVGConstants.SVG_OFFSET_ATTRIBUTE, "1");
+ gradient.appendChild(stop1);
+
+ svgp.getDefs().appendChild(gradient);
+ svgp.putIdElement(LIGHT_GRADIENT_ID, gradient);
+ }
+ }
+
+ /**
+ * Checkmark path, sized approx. 15x15
+ */
+ public static final String SVG_CHECKMARK_PATH = "M0 6.458 L2.047 4.426 5.442 7.721 12.795 0 15 2.117 5.66 11.922 Z";
+
+ /**
+ * Creates a 15x15 big checkmark
+ *
+ * @param svgp Plot to create the element for
+ * @return Element
+ */
+ public static Element makeCheckmark(SVGPlot svgp) {
+ Element checkmark = svgp.svgElement(SVGConstants.SVG_PATH_TAG);
+ checkmark.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, SVG_CHECKMARK_PATH);
+ checkmark.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.CSS_BLACK_VALUE);
+ checkmark.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.CSS_NONE_VALUE);
+ return checkmark;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java
new file mode 100644
index 00000000..9faad348
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperCube.java
@@ -0,0 +1,340 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+
+/**
+ * Utility class to draw hypercubes, wireframe and filled.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses SVGPath
+ * @apiviz.uses Projection2D
+ */
+public class SVGHyperCube {
+ /**
+ * Wireframe hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param min First corner
+ * @param max Opposite corner
+ * @return path element
+ */
+ public static Element drawFrame(SVGPlot svgp, Projection2D proj, double[] min, double[] max) {
+ SVGPath path = new SVGPath();
+ ArrayList<double[]> edges = getVisibleEdges(proj, min, max);
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Wireframe hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param min First corner
+ * @param max Opposite corner
+ * @return path element
+ */
+ public static Element drawFrame(SVGPlot svgp, Projection2D proj, NumberVector min, NumberVector max) {
+ SVGPath path = new SVGPath();
+ ArrayList<double[]> edges = getVisibleEdges(proj, min, max);
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Wireframe hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param box Bounding box
+ * @return path element
+ */
+ public static Element drawFrame(SVGPlot svgp, Projection2D proj, SpatialComparable box) {
+ SVGPath path = new SVGPath();
+ ArrayList<double[]> edges = getVisibleEdges(proj, box);
+ final int dim = box.getDimensionality();
+ double[] min = new double[dim];
+ for(int i = 0; i < dim; i++) {
+ min[i] = box.getMin(i);
+ }
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawEdges(path, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Filled hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param cls CSS class to use.
+ * @param proj Visualization projection
+ * @param min First corner
+ * @param max Opposite corner
+ * @return group element
+ */
+ public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, double[] min, double[] max) {
+ Element group = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ ArrayList<double[]> edges = getVisibleEdges(proj, min, max);
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return group;
+ }
+
+ /**
+ * Filled hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param cls CSS class to use.
+ * @param proj Visualization projection
+ * @param min First corner
+ * @param max Opposite corner
+ * @return group element
+ */
+ public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, NumberVector min, NumberVector max) {
+ Element group = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ ArrayList<double[]> edges = getVisibleEdges(proj, min, max);
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return group;
+ }
+
+ /**
+ * Filled hypercube.
+ *
+ * @param svgp SVG Plot
+ * @param cls CSS class to use.
+ * @param proj Visualization projection
+ * @param box Bounding box
+ * @return group element
+ */
+ public static Element drawFilled(SVGPlot svgp, String cls, Projection2D proj, SpatialComparable box) {
+ Element group = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ ArrayList<double[]> edges = getVisibleEdges(proj, box);
+ final int dim = box.getDimensionality();
+ double[] min = new double[dim];
+ for(int i = 0; i < dim; i++) {
+ min[i] = box.getMin(i);
+ }
+ double[] rv_min = proj.fastProjectDataToRenderSpace(min);
+ recDrawSides(svgp, group, cls, rv_min[0], rv_min[1], edges, 0, BitsUtil.zero(edges.size()));
+ return group;
+ }
+
+ /**
+ * Get the visible (non-0) edges of a hypercube
+ *
+ * @param proj Projection
+ * @param s_min Minimum value (in data space)
+ * @param s_max Maximum value (in data space)
+ * @return Edge list
+ */
+ private static ArrayList<double[]> getVisibleEdges(Projection2D proj, double[] s_min, double[] s_max) {
+ final int dim = s_min.length;
+ double[] s_deltas = VMath.minus(s_max, s_min);
+ ArrayList<double[]> r_edges = new ArrayList<>(dim);
+ for(int i = 0; i < dim; i++) {
+ double[] delta = new double[dim];
+ delta[i] = s_deltas[i];
+ double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta);
+ if(deltas[0] != 0 || deltas[1] != 0) {
+ r_edges.add(deltas);
+ }
+ }
+ return r_edges;
+ }
+
+ /**
+ * Get the visible (non-0) edges of a hypercube
+ *
+ * @param proj Projection
+ * @param s_min Minimum value (in data space)
+ * @param s_max Maximum value (in data space)
+ * @return Edge list
+ */
+ private static ArrayList<double[]> getVisibleEdges(Projection2D proj, NumberVector s_min, NumberVector s_max) {
+ final int dim = s_min.getDimensionality();
+ double[] s_deltas = new double[dim];
+ for(int i = 0; i < dim; i++) {
+ s_deltas[i] = s_max.doubleValue(i) - s_min.doubleValue(i);
+ }
+ ArrayList<double[]> r_edges = new ArrayList<>(dim);
+ for(int i = 0; i < dim; i++) {
+ double[] delta = new double[dim];
+ delta[i] = s_deltas[i];
+ double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta);
+ if(deltas[0] != 0 || deltas[1] != 0) {
+ r_edges.add(deltas);
+ }
+ }
+ return r_edges;
+ }
+
+ /**
+ * Get the visible (non-0) edges of a hypercube
+ *
+ * @param proj Projection
+ * @param box Box object
+ * @return Edge list
+ */
+ private static ArrayList<double[]> getVisibleEdges(Projection2D proj, SpatialComparable box) {
+ final int dim = box.getDimensionality();
+ double[] s_deltas = new double[dim];
+ for(int i = 0; i < dim; i++) {
+ s_deltas[i] = box.getMax(i) - box.getMin(i);
+ }
+ ArrayList<double[]> r_edges = new ArrayList<>(dim);
+ for(int i = 0; i < dim; i++) {
+ double[] delta = new double[dim];
+ delta[i] = s_deltas[i];
+ double[] deltas = proj.fastProjectRelativeDataToRenderSpace(delta);
+ if(deltas[0] != 0 || deltas[1] != 0) {
+ r_edges.add(deltas);
+ }
+ }
+ return r_edges;
+ }
+
+ /**
+ * Recursive helper for hypercube drawing.
+ *
+ * @param path path
+ * @param minx starting corner
+ * @param miny starting corner
+ * @param r_edges edge vectors
+ * @param off recursion offset (to avoid multi-recursion)
+ * @param b bit set of drawn edges
+ */
+ private static void recDrawEdges(SVGPath path, double minx, double miny, List<double[]> r_edges, int off, long[] b) {
+ // Draw all "missing" edges
+ for(int i = 0; i < r_edges.size(); i++) {
+ if(BitsUtil.get(b, i)) {
+ continue;
+ }
+ final double[] edge = r_edges.get(i);
+ final double x_i = minx + edge[0];
+ if(!isFinite(x_i)) {
+ continue;
+ }
+ final double y_i = miny + edge[1];
+ if(!isFinite(y_i)) {
+ continue;
+ }
+ path.moveTo(minx, miny);
+ path.drawTo(x_i, y_i);
+ // Recursion
+ BitsUtil.setI(b, i);
+ recDrawEdges(path, x_i, y_i , r_edges, i + 1, b);
+ BitsUtil.clearI(b, i);
+ }
+ }
+
+ /**
+ * Recursive helper for hypercube drawing.
+ *
+ * @param plot Plot
+ * @param group Group element
+ * @param cls CSS class
+ * @param minx starting corner
+ * @param miny starting corner
+ * @param r_edges edge vectors
+ * @param off recursion offset (to avoid multi-recursion)
+ * @param b bit set of drawn edges
+ */
+ private static void recDrawSides(SVGPlot plot, Element group, String cls, double minx, double miny, List<double[]> r_edges, int off, long[] b) {
+ StringBuilder pbuf = new StringBuilder();
+ // Draw all "missing" sides
+ for(int i = 0; i < r_edges.size() - 1; i++) {
+ if(BitsUtil.get(b, i)) {
+ continue;
+ }
+ double[] deltai = r_edges.get(i);
+ final double xi = minx + deltai[0];
+ if(!isFinite(xi)) {
+ continue;
+ }
+ final double yi = miny + deltai[1];
+ if(!isFinite(yi)) {
+ continue;
+ }
+ for(int j = i + 1; j < r_edges.size(); j++) {
+ if(BitsUtil.get(b, j)) {
+ continue;
+ }
+ double[] deltaj = r_edges.get(j);
+ final double dxj = deltaj[0];
+ if(!isFinite(xi)) {
+ continue;
+ }
+ final double dyj = deltaj[1];
+ if(!isFinite(dxj)) {
+ continue;
+ }
+ pbuf.delete(0, pbuf.length()); // Clear
+ pbuf.append(SVGUtil.fmt(minx)).append(',');
+ pbuf.append(SVGUtil.fmt(miny)).append(' ');
+ pbuf.append(SVGUtil.fmt(xi)).append(',');
+ pbuf.append(SVGUtil.fmt(yi)).append(' ');
+ pbuf.append(SVGUtil.fmt(xi + dxj)).append(',');
+ pbuf.append(SVGUtil.fmt(yi + dyj)).append(' ');
+ pbuf.append(SVGUtil.fmt(minx + dxj)).append(',');
+ pbuf.append(SVGUtil.fmt(miny + dyj));
+
+ Element poly = plot.svgElement(SVGConstants.SVG_POLYGON_TAG);
+ SVGUtil.setAtt(poly, SVGConstants.SVG_POINTS_ATTRIBUTE, pbuf.toString());
+ SVGUtil.setCSSClass(poly, cls);
+ group.appendChild(poly);
+ }
+ // Recursion
+ BitsUtil.setI(b, i);
+ recDrawSides(plot, group, cls, xi, yi, r_edges, i + 1, b);
+ BitsUtil.clearI(b, i);
+ }
+ }
+
+ /**
+ * Finite (and not NaN) double values.
+ *
+ * @param v Value
+ * @return true, when finite.
+ */
+ private static boolean isFinite(double v) {
+ return v < Double.POSITIVE_INFINITY && v > Double.NEGATIVE_INFINITY;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java
new file mode 100644
index 00000000..e3fc9386
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGHyperSphere.java
@@ -0,0 +1,289 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+
+/**
+ * Utility class to draw hypercubes, wireframe and filled.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses SVGPath
+ * @apiviz.uses Projection2D
+ */
+public class SVGHyperSphere {
+ /**
+ * Factor used for approximating circles with cubic beziers.
+ *
+ * kappa = 4 * (Math.sqrt(2)-1)/3
+ */
+ public static final double EUCLIDEAN_KAPPA = 0.5522847498;
+
+ /**
+ * Wireframe "manhattan" hypersphere
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param mid mean vector
+ * @param radius radius
+ * @return path element
+ */
+ public static Element drawManhattan(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) {
+ final double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy
+ final long[] dims = proj.getVisibleDimensions2D();
+
+ SVGPath path = new SVGPath();
+ for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) {
+ v_mid[dim] += radius;
+ double[] p1 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] -= radius;
+ v_mid[dim] -= radius;
+ double[] p2 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] += radius;
+ for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) {
+ if(dim < dim2) {
+ v_mid[dim2] += radius;
+ double[] p3 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] -= radius;
+ v_mid[dim2] -= radius;
+ double[] p4 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] += radius;
+
+ path.moveTo(p1[0], p1[1]);
+ path.drawTo(p3[0], p3[1]);
+ path.moveTo(p1[0], p1[1]);
+ path.drawTo(p4[0], p4[1]);
+ path.moveTo(p2[0], p2[1]);
+ path.drawTo(p3[0], p3[1]);
+ path.moveTo(p2[0], p2[1]);
+ path.drawTo(p4[0], p4[1]);
+ path.close();
+ }
+ }
+ }
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Wireframe "euclidean" hypersphere
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param mid mean vector
+ * @param radius radius
+ * @return path element
+ */
+ public static Element drawEuclidean(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) {
+ double[] v_mid = mid.getColumnVector().getArrayRef(); // a copy
+ long[] dims = proj.getVisibleDimensions2D();
+
+ SVGPath path = new SVGPath();
+ for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) {
+ v_mid[dim] += radius;
+ double[] p1 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] -= radius;
+ v_mid[dim] -= radius;
+ double[] p2 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] += radius;
+ // delta vector
+ double[] dt1 = new double[v_mid.length];
+ dt1[dim] = radius;
+ double[] d1 = proj.fastProjectRelativeDataToRenderSpace(dt1);
+ for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) {
+ if(dim < dim2) {
+ v_mid[dim2] += radius;
+ double[] p3 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] -= radius;
+ v_mid[dim2] -= radius;
+ double[] p4 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] += radius;
+ // delta vector
+ double[] dt2 = new double[v_mid.length];
+ dt2[dim2] = radius;
+ double[] d2 = proj.fastProjectRelativeDataToRenderSpace(dt2);
+
+ path.moveTo(p1[0], p1[1]);
+ path.cubicTo(p1[0] + d2[0] * EUCLIDEAN_KAPPA, p1[1] + d2[1] * EUCLIDEAN_KAPPA, p3[0] + d1[0] * EUCLIDEAN_KAPPA, p3[1] + d1[1] * EUCLIDEAN_KAPPA, p3[0], p3[1]);
+ path.cubicTo(p3[0] - d1[0] * EUCLIDEAN_KAPPA, p3[1] - d1[1] * EUCLIDEAN_KAPPA, p2[0] + d2[0] * EUCLIDEAN_KAPPA, p2[1] + d2[1] * EUCLIDEAN_KAPPA, p2[0], p2[1]);
+ path.cubicTo(p2[0] - d2[0] * EUCLIDEAN_KAPPA, p2[1] - d2[1] * EUCLIDEAN_KAPPA, p4[0] - d1[0] * EUCLIDEAN_KAPPA, p4[1] - d1[1] * EUCLIDEAN_KAPPA, p4[0], p4[1]);
+ path.cubicTo(p4[0] + d1[0] * EUCLIDEAN_KAPPA, p4[1] + d1[1] * EUCLIDEAN_KAPPA, p1[0] - d2[0] * EUCLIDEAN_KAPPA, p1[1] - d2[1] * EUCLIDEAN_KAPPA, p1[0], p1[1]);
+ path.close();
+ }
+ }
+ }
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Wireframe "Lp" hypersphere
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param mid mean vector
+ * @param radius radius
+ * @param p L_p value
+ * @return path element
+ */
+ public static Element drawLp(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius, double p) {
+ final double[] v_mid = mid.getColumnVector().getArrayRef();
+ final long[] dims = proj.getVisibleDimensions2D();
+
+ final double kappax, kappay;
+ if(p > 1.) {
+ final double kappal = Math.pow(0.5, 1. / p);
+ kappax = Math.min(1.3, 4. * (2 * kappal - 1) / 3.);
+ kappay = 0;
+ }
+ else if(p < 1.) {
+ final double kappal = 1 - Math.pow(0.5, 1. / p);
+ kappax = 0;
+ kappay = Math.min(1.3, 4. * (2 * kappal - 1) / 3.);
+ }
+ else {
+ kappax = 0;
+ kappay = 0;
+ }
+ // LoggingUtil.warning("kappax: " + kappax + " kappay: " + kappay);
+
+ SVGPath path = new SVGPath();
+ for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) {
+ v_mid[dim] += radius;
+ double[] pvp0 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] -= radius;
+ v_mid[dim] -= radius;
+ double[] pvm0 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] += radius;
+ // delta vector
+ double[] tvd0 = new double[v_mid.length];
+ tvd0[dim] = radius;
+ double[] vd0 = proj.fastProjectRelativeDataToRenderSpace(tvd0);
+ for(int dim2 = BitsUtil.nextSetBit(dims, 0); dim2 >= 0; dim2 = BitsUtil.nextSetBit(dims, dim2 + 1)) {
+ if(dim < dim2) {
+ v_mid[dim2] += radius;
+ double[] pv0p = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] -= radius;
+ v_mid[dim2] -= radius;
+ double[] pv0m = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim2] += radius;
+ // delta vector
+ double[] tv0d = new double[v_mid.length];
+ tv0d[dim2] = radius;
+ double[] v0d = proj.fastProjectRelativeDataToRenderSpace(tv0d);
+
+ if(p > 1) {
+ // p > 1
+ path.moveTo(pvp0[0], pvp0[1]);
+ // support points, p0 to 0p
+ final double s_pp1_x = pvp0[0] + v0d[0] * kappax;
+ final double s_pp1_y = pvp0[1] + v0d[1] * kappax;
+ final double s_pp2_x = pv0p[0] + vd0[0] * kappax;
+ final double s_pp2_y = pv0p[1] + vd0[1] * kappax;
+ path.cubicTo(s_pp1_x, s_pp1_y, s_pp2_x, s_pp2_y, pv0p[0], pv0p[1]);
+ // support points, 0p to m0
+ final double s_mp1_x = pv0p[0] - vd0[0] * kappax;
+ final double s_mp1_y = pv0p[1] - vd0[1] * kappax;
+ final double s_mp2_x = pvm0[0] + v0d[0] * kappax;
+ final double s_mp2_y = pvm0[1] + v0d[1] * kappax;
+ path.cubicTo(s_mp1_x, s_mp1_y, s_mp2_x, s_mp2_y, pvm0[0], pvm0[1]);
+ // support points, m0 to 0m
+ final double s_mm1_x = pvm0[0] - v0d[0] * kappax;
+ final double s_mm1_y = pvm0[1] - v0d[1] * kappax;
+ final double s_mm2_x = pv0m[0] - vd0[0] * kappax;
+ final double s_mm2_y = pv0m[1] - vd0[1] * kappax;
+ path.cubicTo(s_mm1_x, s_mm1_y, s_mm2_x, s_mm2_y, pv0m[0], pv0m[1]);
+ // support points, 0m to p0
+ final double s_pm1_x = pv0m[0] + vd0[0] * kappax;
+ final double s_pm1_y = pv0m[1] + vd0[1] * kappax;
+ final double s_pm2_x = pvp0[0] - v0d[0] * kappax;
+ final double s_pm2_y = pvp0[1] - v0d[1] * kappax;
+ path.cubicTo(s_pm1_x, s_pm1_y, s_pm2_x, s_pm2_y, pvp0[0], pvp0[1]);
+ path.close();
+ }
+ else if(p < 1) {
+ // p < 1
+ // support points, p0 to 0p
+ final double s_vp0_x = pvp0[0] - vd0[0] * kappay;
+ final double s_vp0_y = pvp0[1] - vd0[1] * kappay;
+ final double s_v0p_x = pv0p[0] - v0d[0] * kappay;
+ final double s_v0p_y = pv0p[1] - v0d[1] * kappay;
+ final double s_vm0_x = pvm0[0] + vd0[0] * kappay;
+ final double s_vm0_y = pvm0[1] + vd0[1] * kappay;
+ final double s_v0m_x = pv0m[0] + v0d[0] * kappay;
+ final double s_v0m_y = pv0m[1] + v0d[1] * kappay;
+ // Draw the star
+ path.moveTo(pvp0[0], pvp0[1]);
+ path.cubicTo(s_vp0_x, s_vp0_y, s_v0p_x, s_v0p_y, pv0p[0], pv0p[1]);
+ path.cubicTo(s_v0p_x, s_v0p_y, s_vm0_x, s_vm0_y, pvm0[0], pvm0[1]);
+ path.cubicTo(s_vm0_x, s_vm0_y, s_v0m_x, s_v0m_y, pv0m[0], pv0m[1]);
+ path.cubicTo(s_v0m_x, s_v0m_y, s_vp0_x, s_vp0_y, pvp0[0], pvp0[1]);
+ path.close();
+ }
+ else {
+ // p == 1 - Manhattan
+ path.moveTo(pvp0[0], pvp0[1]);
+ path.lineTo(pv0p[0], pv0p[1]);
+ path.lineTo(pvm0[0], pvm0[1]);
+ path.lineTo(pv0m[0], pv0m[1]);
+ path.lineTo(pvp0[0], pvp0[1]);
+ path.close();
+ }
+ }
+ }
+ }
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Wireframe "cross" hypersphere
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param mid mean vector
+ * @param radius radius
+ * @return path element
+ */
+ public static Element drawCross(SVGPlot svgp, Projection2D proj, NumberVector mid, double radius) {
+ final double[] v_mid = mid.getColumnVector().getArrayRef();
+ final long[] dims = proj.getVisibleDimensions2D();
+
+ SVGPath path = new SVGPath();
+ for(int dim = BitsUtil.nextSetBit(dims, 0); dim >= 0; dim = BitsUtil.nextSetBit(dims, dim + 1)) {
+ v_mid[dim] += radius;
+ double[] p1 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] -= radius;
+ path.moveTo(p1[0], p1[1]);
+ v_mid[dim] -= radius;
+ double[] p2 = proj.fastProjectDataToRenderSpace(v_mid);
+ v_mid[dim] += radius;
+ path.drawTo(p2[0], p2[1]);
+ path.close();
+ }
+ return path.makeElement(svgp);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java
new file mode 100644
index 00000000..948f09a7
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPath.java
@@ -0,0 +1,842 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter;
+
+/**
+ * Element used for building an SVG path using a string buffer.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Element oneway - - «create»
+ */
+public class SVGPath {
+ /**
+ * String buffer for building the path.
+ */
+ private StringBuilder buf = new StringBuilder();
+
+ /**
+ * The last action we did, to not add unnecessary commands
+ */
+ private String lastaction = null;
+
+ /**
+ * The absolute "smooth cubic to" SVG path command (missing from
+ * SVGConstants).
+ */
+ public static final String PATH_SMOOTH_CUBIC_TO = "S";
+
+ /**
+ * The lower case version (relative) line to command.
+ */
+ public static final String PATH_LINE_TO_RELATIVE = SVGConstants.PATH_LINE_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) move command.
+ */
+ public static final String PATH_MOVE_RELATIVE = SVGConstants.PATH_MOVE.toLowerCase();
+
+ /**
+ * The lower case version (relative) horizontal line to command.
+ */
+ public static final String PATH_HORIZONTAL_LINE_TO_RELATIVE = SVGConstants.PATH_HORIZONTAL_LINE_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) vertical line to command.
+ */
+ public static final String PATH_VERTICAL_LINE_TO_RELATIVE = SVGConstants.PATH_VERTICAL_LINE_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) cubic line to command.
+ */
+ public static final String PATH_CUBIC_TO_RELATIVE = SVGConstants.PATH_CUBIC_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) smooth cubic to command.
+ */
+ public static final String PATH_SMOOTH_CUBIC_TO_RELATIVE = PATH_SMOOTH_CUBIC_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) quadratic interpolation to command.
+ */
+ public static final String PATH_QUAD_TO_RELATIVE = SVGConstants.PATH_QUAD_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) smooth quadratic interpolation to
+ * command.
+ */
+ public static final String PATH_SMOOTH_QUAD_TO_RELATIVE = SVGConstants.PATH_SMOOTH_QUAD_TO.toLowerCase();
+
+ /**
+ * The lower case version (relative) path arc command.
+ */
+ public static final String PATH_ARC_RELATIVE = SVGConstants.PATH_ARC.toLowerCase();
+
+ /**
+ * Empty path constructor.
+ */
+ public SVGPath() {
+ // Nothing to do.
+ }
+
+ /**
+ * Constructor with initial point.
+ *
+ * @param x initial coordinates
+ * @param y initial coordinates
+ */
+ public SVGPath(double x, double y) {
+ this();
+ this.moveTo(x, y);
+ }
+
+ /**
+ * Constructor with initial point.
+ *
+ * @param xy initial coordinates
+ */
+ public SVGPath(double[] xy) {
+ this();
+ this.moveTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Constructor from a vector collection (e.g. a polygon)
+ *
+ * @param vectors vectors
+ */
+ public SVGPath(Polygon vectors) {
+ this();
+ for(ArrayListIter<Vector> it = vectors.iter(); it.valid(); it.advance()) {
+ Vector vec = it.get();
+ this.drawTo(vec.doubleValue(0), vec.doubleValue(1));
+ }
+ this.close();
+ }
+
+ /**
+ * Draw a line given a series of coordinates.
+ *
+ * Helper function that will use "move" for the first point, "lineto" for the
+ * remaining.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath drawTo(double x, double y) {
+ return !isStarted() ? moveTo(x, y) : lineTo(x, y);
+ }
+
+ /**
+ * Draw a line given a series of coordinates.
+ *
+ * Helper function that will use "move" for the first point, "lineto" for the
+ * remaining.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath drawTo(double[] xy) {
+ return !isStarted() ? moveTo(xy[0], xy[0]) : lineTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Draw a line given a series of coordinates.
+ *
+ * Helper function that will use "move" for the first point, "lineto" for the
+ * remaining.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath drawTo(Vector xy) {
+ return !isStarted() ? moveTo(xy.doubleValue(0), xy.doubleValue(1)) : lineTo(xy.doubleValue(0), xy.doubleValue(1));
+ }
+
+ /**
+ * Test whether the path drawing has already started.
+ *
+ * @return Path freshness
+ */
+ public boolean isStarted() {
+ return lastaction != null;
+ }
+
+ /**
+ * Draw a line to the given coordinates.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath lineTo(double x, double y) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY //
+ && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(SVGConstants.PATH_LINE_TO, x, y);
+ }
+ return this;
+ }
+
+ /**
+ * Draw a line to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath lineTo(double[] xy) {
+ return lineTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Draw a line to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath lineTo(Vector xy) {
+ return lineTo(xy.doubleValue(0), xy.doubleValue(1));
+ }
+
+ /**
+ * Draw a line to the given relative coordinates.
+ *
+ * @param x relative coordinates
+ * @param y relative coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeLineTo(double x, double y) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY //
+ && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(PATH_LINE_TO_RELATIVE, x, y);
+ }
+ return this;
+ }
+
+ /**
+ * Draw a line to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeLineTo(double[] xy) {
+ return relativeLineTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Draw a line to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeLineTo(Vector xy) {
+ return relativeLineTo(xy.doubleValue(0), xy.doubleValue(1));
+ }
+
+ /**
+ * Draw a horizontal line to the given x coordinate.
+ *
+ * @param x new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath horizontalLineTo(double x) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY) {
+ append(SVGConstants.PATH_HORIZONTAL_LINE_TO, x);
+ }
+ return this;
+ }
+
+ /**
+ * Draw a horizontal line to the given relative x coordinate.
+ *
+ * @param x new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeHorizontalLineTo(double x) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY) {
+ append(PATH_HORIZONTAL_LINE_TO_RELATIVE, x);
+ }
+ return this;
+ }
+
+ /**
+ * Draw a vertical line to the given y coordinate.
+ *
+ * @param y new coordinate
+ * @return path object, for compact syntax.
+ */
+ public SVGPath verticalLineTo(double y) {
+ if(y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(SVGConstants.PATH_VERTICAL_LINE_TO, y);
+ }
+ return this;
+ }
+
+ /**
+ * Draw a vertical line to the given relative y coordinate.
+ *
+ * @param y new coordinate
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeVerticalLineTo(double y) {
+ if(y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(PATH_VERTICAL_LINE_TO_RELATIVE, y);
+ }
+ return this;
+ }
+
+ /**
+ * Move to the given coordinates.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath moveTo(double x, double y) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY //
+ && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(SVGConstants.PATH_MOVE, x, y);
+ }
+ return this;
+ }
+
+ /**
+ * Move to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath moveTo(double[] xy) {
+ return moveTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Move to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath moveTo(Vector xy) {
+ return moveTo(xy.doubleValue(0), xy.doubleValue(1));
+ }
+
+ /**
+ * Move to the given relative coordinates.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeMoveTo(double x, double y) {
+ if(x > Double.NEGATIVE_INFINITY && x < Double.POSITIVE_INFINITY //
+ && y > Double.NEGATIVE_INFINITY && y < Double.POSITIVE_INFINITY) {
+ append(PATH_MOVE_RELATIVE, x, y);
+ }
+ return this;
+ }
+
+ /**
+ * Move to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeMoveTo(double[] xy) {
+ return relativeMoveTo(xy[0], xy[1]);
+ }
+
+ /**
+ * Move to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeMoveTo(Vector xy) {
+ return relativeMoveTo(xy.doubleValue(0), xy.doubleValue(1));
+ }
+
+ /**
+ * Cubic Bezier line to the given coordinates.
+ *
+ * @param c1x first control point x
+ * @param c1y first control point y
+ * @param c2x second control point x
+ * @param c2y second control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath cubicTo(double c1x, double c1y, double c2x, double c2y, double x, double y) {
+ append(SVGConstants.PATH_CUBIC_TO, c1x, c1y, c2x, c2y, x, y);
+ return this;
+ }
+
+ /**
+ * Cubic Bezier line to the given coordinates.
+ *
+ * @param c1xy first control point
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath cubicTo(double[] c1xy, double[] c2xy, double[] xy) {
+ append(SVGConstants.PATH_CUBIC_TO, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Cubic Bezier line to the given coordinates.
+ *
+ * @param c1xy first control point
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath cubicTo(Vector c1xy, Vector c2xy, Vector xy) {
+ append(SVGConstants.PATH_CUBIC_TO, c1xy.doubleValue(0), c1xy.doubleValue(1), c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c1x first control point x
+ * @param c1y first control point y
+ * @param c2x second control point x
+ * @param c2y second control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeCubicTo(double c1x, double c1y, double c2x, double c2y, double x, double y) {
+ append(PATH_CUBIC_TO_RELATIVE, c1x, c1y, c2x, c2y, x, y);
+ return this;
+ }
+
+ /**
+ * Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c1xy first control point
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeCubicTo(double[] c1xy, double[] c2xy, double[] xy) {
+ append(PATH_CUBIC_TO_RELATIVE, c1xy[0], c1xy[1], c2xy[0], c2xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c1xy first control point
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeCubicTo(Vector c1xy, Vector c2xy, Vector xy) {
+ append(PATH_CUBIC_TO_RELATIVE, c1xy.doubleValue(0), c1xy.doubleValue(1), c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given coordinates.
+ *
+ * @param c2x second control point x
+ * @param c2y second control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothCubicTo(double c2x, double c2y, double x, double y) {
+ append(PATH_SMOOTH_CUBIC_TO, c2x, c2y, x, y);
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given coordinates.
+ *
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothCubicTo(double[] c2xy, double[] xy) {
+ append(PATH_SMOOTH_CUBIC_TO, c2xy[0], c2xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given coordinates.
+ *
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothCubicTo(Vector c2xy, Vector xy) {
+ append(PATH_SMOOTH_CUBIC_TO, c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c2x second control point x
+ * @param c2y second control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothCubicTo(double c2x, double c2y, double x, double y) {
+ append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2x, c2y, x, y);
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothCubicTo(double[] c2xy, double[] xy) {
+ append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy[0], c2xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Smooth Cubic Bezier line to the given relative coordinates.
+ *
+ * @param c2xy second control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothCubicTo(Vector c2xy, Vector xy) {
+ append(PATH_SMOOTH_CUBIC_TO_RELATIVE, c2xy.doubleValue(0), c2xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given coordinates.
+ *
+ * @param c1x first control point x
+ * @param c1y first control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath quadTo(double c1x, double c1y, double x, double y) {
+ append(SVGConstants.PATH_QUAD_TO, c1x, c1y, x, y);
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given coordinates.
+ *
+ * @param c1xy first control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath quadTo(double[] c1xy, double[] xy) {
+ append(SVGConstants.PATH_QUAD_TO, c1xy[0], c1xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given coordinates.
+ *
+ * @param c1xy first control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath quadTo(Vector c1xy, Vector xy) {
+ append(SVGConstants.PATH_QUAD_TO, c1xy.doubleValue(0), c1xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given relative coordinates.
+ *
+ * @param c1x first control point x
+ * @param c1y first control point y
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeQuadTo(double c1x, double c1y, double x, double y) {
+ append(PATH_QUAD_TO_RELATIVE, c1x, c1y, x, y);
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given relative coordinates.
+ *
+ * @param c1xy first control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeQuadTo(double[] c1xy, double[] xy) {
+ append(PATH_QUAD_TO_RELATIVE, c1xy[0], c1xy[1], xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Quadratic Bezier line to the given relative coordinates.
+ *
+ * @param c1xy first control point
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeQuadTo(Vector c1xy, Vector xy) {
+ append(PATH_QUAD_TO_RELATIVE, c1xy.doubleValue(0), c1xy.doubleValue(1), xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given coordinates.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothQuadTo(double x, double y) {
+ append(SVGConstants.PATH_SMOOTH_QUAD_TO, x, y);
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothQuadTo(double[] xy) {
+ append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath smoothQuadTo(Vector xy) {
+ append(SVGConstants.PATH_SMOOTH_QUAD_TO, xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given relative coordinates.
+ *
+ * @param x new coordinates
+ * @param y new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothQuadTo(double x, double y) {
+ append(PATH_SMOOTH_QUAD_TO_RELATIVE, x, y);
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothQuadTo(double[] xy) {
+ append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Smooth quadratic Bezier line to the given relative coordinates.
+ *
+ * @param xy new coordinates
+ * @return path object, for compact syntax.
+ */
+ public SVGPath relativeSmoothQuadTo(Vector xy) {
+ append(PATH_SMOOTH_QUAD_TO_RELATIVE, xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given coordinates.
+ *
+ * @param rx x radius
+ * @param ry y radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param x new coordinates
+ * @param y new coordinates
+ */
+ public SVGPath ellipticalArc(double rx, double ry, double ar, double la, double sp, double x, double y) {
+ append(SVGConstants.PATH_ARC, rx, ry, ar, la, sp, x, y);
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given coordinates.
+ *
+ * @param rx x radius
+ * @param ry y radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param xy new coordinates
+ */
+ public SVGPath ellipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) {
+ append(SVGConstants.PATH_ARC, rx, ry, ar, la, sp, xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given coordinates.
+ *
+ * @param rxy radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param xy new coordinates
+ */
+ public SVGPath ellipticalArc(Vector rxy, double ar, double la, double sp, Vector xy) {
+ append(SVGConstants.PATH_ARC, rxy.doubleValue(0), rxy.doubleValue(1), ar, la, sp, xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given relative coordinates.
+ *
+ * @param rx x radius
+ * @param ry y radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param x new coordinates
+ * @param y new coordinates
+ */
+ public SVGPath relativeEllipticalArc(double rx, double ry, double ar, double la, double sp, double x, double y) {
+ append(PATH_ARC_RELATIVE, rx, ry, ar, la, sp, x, y);
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given relative coordinates.
+ *
+ * @param rx x radius
+ * @param ry y radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param xy new coordinates
+ */
+ public SVGPath relativeEllipticalArc(double rx, double ry, double ar, double la, double sp, double[] xy) {
+ append(PATH_ARC_RELATIVE, rx, ry, ar, la, sp, xy[0], xy[1]);
+ return this;
+ }
+
+ /**
+ * Elliptical arc curve to the given relative coordinates.
+ *
+ * @param rxy radius
+ * @param ar x-axis-rotation
+ * @param la large arc flag, if angle >= 180 deg
+ * @param sp sweep flag, if arc will be drawn in positive-angle direction
+ * @param xy new coordinates
+ */
+ public SVGPath relativeEllipticalArc(Vector rxy, double ar, double la, double sp, Vector xy) {
+ append(PATH_ARC_RELATIVE, rxy.doubleValue(0), rxy.doubleValue(1), ar, la, sp, xy.doubleValue(0), xy.doubleValue(1));
+ return this;
+ }
+
+ /**
+ * Append an action to the current path.
+ *
+ * @param action Current action
+ * @param ds coordinates.
+ */
+ private void append(String action, double... ds) {
+ if(lastaction != action) {
+ buf.append(action);
+ lastaction = action;
+ }
+ for(double d : ds) {
+ buf.append(SVGUtil.FMT.format(d));
+ buf.append(' ');
+ }
+ }
+
+ /**
+ * Close the path.
+ *
+ * @return path object, for compact syntax.
+ */
+ public SVGPath close() {
+ if(lastaction != SVGConstants.PATH_CLOSE) {
+ buf.append(SVGConstants.PATH_CLOSE);
+ lastaction = SVGConstants.PATH_CLOSE;
+ }
+ return this;
+ }
+
+ /**
+ * Turn the path buffer into an SVG element.
+ *
+ * @param document Document context (= element factory)
+ * @return SVG Element
+ */
+ public Element makeElement(Document document) {
+ Element elem = SVGUtil.svgElement(document, SVGConstants.SVG_PATH_TAG);
+ elem.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, buf.toString());
+ return elem;
+ }
+
+ /**
+ * Turn the path buffer into an SVG element.
+ *
+ * @param plot Plot context (= element factory)
+ * @return SVG Element
+ */
+ public Element makeElement(SVGPlot plot) {
+ Element elem = plot.svgElement(SVGConstants.SVG_PATH_TAG);
+ elem.setAttribute(SVGConstants.SVG_D_ATTRIBUTE, buf.toString());
+ return elem;
+ }
+
+ /**
+ * Return the SVG serialization of the path.
+ */
+ @Override
+ public String toString() {
+ return buf.toString();
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java
new file mode 100755
index 00000000..7eb51819
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGPlot.java
@@ -0,0 +1,720 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.BufferedImage;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.HashMap;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.apache.batik.transcoder.Transcoder;
+import org.apache.batik.transcoder.TranscoderException;
+import org.apache.batik.transcoder.TranscoderInput;
+import org.apache.batik.transcoder.TranscoderOutput;
+import org.apache.batik.transcoder.XMLAbstractTranscoder;
+import org.apache.batik.transcoder.image.JPEGTranscoder;
+import org.apache.batik.transcoder.image.PNGTranscoder;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGDocument;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.utilities.FileUtil;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.CloneInlineImages;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailTranscoder;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+
+/**
+ * Base class for SVG plots. Provides some basic functionality such as element
+ * creation, axis plotting, markers and number formatting for SVG.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.composedOf CSSClassManager
+ * @apiviz.composedOf UpdateRunner
+ * @apiviz.composedOf SVGDocument
+ * @apiviz.has Element oneway - - contains
+ * @apiviz.has UpdateSynchronizer oneway - - synchronizesWith
+ */
+public class SVGPlot {
+ /**
+ * Default JPEG quality setting
+ */
+ public static final double DEFAULT_QUALITY = 0.85;
+
+ /**
+ * Attribute to block export of element.
+ */
+ public static final String NO_EXPORT_ATTRIBUTE = "noexport";
+
+ /**
+ * Batik DOM implementation.
+ */
+ private static final DOMImplementation BATIK_DOM;
+
+ /**
+ * DOM implementations to try.
+ */
+ private static final String[] BATIK_DOMS = { //
+ "org.apache.batik.anim.dom.SVGDOMImplementation", // Batik 1.8
+ "org.apache.batik.dom.svg.SVGDOMImplementation", // Batik 1.7
+ "com.sun.org.apache.xerces.internal.dom.DOMImplementationImpl", // Untested
+ };
+
+ // Locate a usable DOM implementation.
+ static {
+ DOMImplementation dom = null;
+ for(String s : BATIK_DOMS) {
+ try {
+ Class<?> c = Class.forName(s);
+ Method m = c.getDeclaredMethod("getDOMImplementation");
+ DOMImplementation ret = DOMImplementation.class.cast(m.invoke(null));
+ if(ret != null) {
+ dom = ret;
+ break;
+ }
+ }
+ catch(Exception e) {
+ continue;
+ }
+ }
+ BATIK_DOM = dom;
+ }
+
+ /**
+ * SVG document we plot to.
+ */
+ private SVGDocument document;
+
+ /**
+ * Root element of the document.
+ */
+ private Element root;
+
+ /**
+ * Definitions element of the document.
+ */
+ private Element defs;
+
+ /**
+ * Primary style information
+ */
+ private Element style;
+
+ /**
+ * CSS class manager
+ */
+ private CSSClassManager cssman;
+
+ /**
+ * Manage objects with an id.
+ */
+ private HashMap<String, WeakReference<Element>> objWithId = new HashMap<>();
+
+ /**
+ * Registers changes of this SVGPlot.
+ */
+ private UpdateRunner runner = new UpdateRunner(this);
+
+ /**
+ * Flag whether Batik interactions should be disabled.
+ */
+ private boolean disableInteractions = false;
+
+ /**
+ * Create a new plotting document.
+ */
+ public SVGPlot() {
+ super();
+ // Get a DOMImplementation.
+ DOMImplementation domImpl = getDomImpl();
+ DocumentType dt = domImpl.createDocumentType(SVGConstants.SVG_SVG_TAG, SVGConstants.SVG_PUBLIC_ID, SVGConstants.SVG_SYSTEM_ID);
+ // Workaround: sometimes DocumentType doesn't work right, which
+ // causes problems with
+ // serialization...
+ if(dt.getName() == null) {
+ dt = null;
+ }
+
+ document = (SVGDocument) domImpl.createDocument(SVGConstants.SVG_NAMESPACE_URI, SVGConstants.SVG_SVG_TAG, dt);
+
+ root = document.getDocumentElement();
+ // setup common SVG namespaces
+ root.setAttribute(SVGConstants.XMLNS_PREFIX, SVGConstants.SVG_NAMESPACE_URI);
+ root.setAttributeNS(SVGConstants.XMLNS_NAMESPACE_URI, SVGConstants.XMLNS_PREFIX + ":" + SVGConstants.XLINK_PREFIX, SVGConstants.XLINK_NAMESPACE_URI);
+
+ // create element for SVG definitions
+ defs = svgElement(SVGConstants.SVG_DEFS_TAG);
+ root.appendChild(defs);
+
+ // create element for Stylesheet information.
+ style = SVGUtil.makeStyleElement(document);
+ root.appendChild(style);
+
+ // create a CSS class manager.
+ cssman = new CSSClassManager();
+ }
+
+ /**
+ * Get a suitable SVG DOM implementation from Batik 1.7 or 1.8.
+ *
+ * @return DOM implementation
+ */
+ public static DOMImplementation getDomImpl() {
+ if(BATIK_DOM == null) {
+ throw new AbortException("No usable Apache Batik SVG DOM could be located.");
+ }
+ return BATIK_DOM;
+ }
+
+ /**
+ * Clean up the plot.
+ */
+ public void dispose() {
+ runner.clear();
+ }
+
+ /**
+ * Create a SVG element in the SVG namespace. Non-static version.
+ *
+ * @param name node name
+ * @return new SVG element.
+ */
+ public Element svgElement(String name) {
+ return SVGUtil.svgElement(document, name);
+ }
+
+ /**
+ * Create a SVG rectangle
+ *
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param w Width
+ * @param h Height
+ * @return new element
+ */
+ public Element svgRect(double x, double y, double w, double h) {
+ return SVGUtil.svgRect(document, x, y, w, h);
+ }
+
+ /**
+ * Create a SVG circle
+ *
+ * @param cx center X
+ * @param cy center Y
+ * @param r radius
+ * @return new element
+ */
+ public Element svgCircle(double cx, double cy, double r) {
+ return SVGUtil.svgCircle(document, cx, cy, r);
+ }
+
+ /**
+ * Create a SVG line element
+ *
+ * @param x1 first point x
+ * @param y1 first point y
+ * @param x2 second point x
+ * @param y2 second point y
+ * @return new element
+ */
+ public Element svgLine(double x1, double y1, double x2, double y2) {
+ return SVGUtil.svgLine(document, x1, y1, x2, y2);
+ }
+
+ /**
+ * Create a SVG text element.
+ *
+ * @param x first point x
+ * @param y first point y
+ * @param text Content of text element.
+ * @return New text element.
+ */
+ public Element svgText(double x, double y, String text) {
+ return SVGUtil.svgText(document, x, y, text);
+ }
+
+ /**
+ * Convert screen coordinates to element coordinates.
+ *
+ * @param tag Element to convert the coordinates for
+ * @param evt Event object
+ * @return Coordinates
+ */
+ public SVGPoint elementCoordinatesFromEvent(Element tag, Event evt) {
+ return SVGUtil.elementCoordinatesFromEvent(document, tag, evt);
+ }
+
+ /**
+ * Retrieve the SVG document.
+ *
+ * @return resulting document.
+ */
+ public SVGDocument getDocument() {
+ return document;
+ }
+
+ /**
+ * Getter for root element.
+ *
+ * @return DOM element
+ */
+ public Element getRoot() {
+ return root;
+ }
+
+ /**
+ * Getter for definitions section
+ *
+ * @return DOM element
+ */
+ public Element getDefs() {
+ return defs;
+ }
+
+ /**
+ * Getter for style element.
+ *
+ * @return stylesheet DOM element
+ * @deprecated Contents will be overwritten by CSS class manager!
+ */
+ @Deprecated
+ public Element getStyle() {
+ return style;
+ }
+
+ /**
+ * Get the plots CSS class manager.
+ *
+ * Note that you need to invoke {@link #updateStyleElement()} to make changes
+ * take effect.
+ *
+ * @return CSS class manager.
+ */
+ public CSSClassManager getCSSClassManager() {
+ return cssman;
+ }
+
+ /**
+ * Convenience method to add a CSS class or log an error.
+ *
+ * @param cls CSS class to add.
+ */
+ public void addCSSClassOrLogError(CSSClass cls) {
+ try {
+ cssman.addClass(cls);
+ }
+ catch(CSSNamingConflict e) {
+ de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e);
+ }
+ }
+
+ /**
+ * Update style element - invoke this appropriately after any change to the
+ * CSS styles.
+ */
+ public void updateStyleElement() {
+ // TODO: this should be sufficient - why does Batik occasionally not pick up
+ // the changes unless we actually replace the style element itself?
+ // cssman.updateStyleElement(document, style);
+ Element newstyle = cssman.makeStyleElement(document);
+ style.getParentNode().replaceChild(newstyle, style);
+ style = newstyle;
+ }
+
+ /**
+ * Save document into a SVG file.
+ *
+ * References PNG images from the temporary files will be inlined
+ * automatically.
+ *
+ * @param file Output filename
+ * @throws IOException On write errors
+ * @throws TransformerFactoryConfigurationError Transformation error
+ * @throws TransformerException Transformation error
+ */
+ public void saveAsSVG(File file) throws IOException, TransformerFactoryConfigurationError, TransformerException {
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
+ // TODO embed linked images.
+ javax.xml.transform.Result result = new StreamResult(out);
+ SVGDocument doc = cloneDocument();
+ // Use a transformer for pretty printing
+ Transformer xformer = TransformerFactory.newInstance().newTransformer();
+ xformer.setOutputProperty(OutputKeys.INDENT, "yes");
+ xformer.transform(new DOMSource(doc), result);
+ out.flush();
+ out.close();
+ }
+
+ /**
+ * Transcode a document into a file using the given transcoder.
+ *
+ * @param file Output file
+ * @param transcoder Transcoder to use
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors
+ */
+ protected void transcode(File file, Transcoder transcoder) throws IOException, TranscoderException {
+ // Disable validation, performance is more important here (thumbnails!)
+ transcoder.addTranscodingHint(XMLAbstractTranscoder.KEY_XML_PARSER_VALIDATING, Boolean.FALSE);
+ SVGDocument doc = cloneDocument();
+ TranscoderInput input = new TranscoderInput(doc);
+ OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
+ TranscoderOutput output = new TranscoderOutput(out);
+ transcoder.transcode(input, output);
+ out.flush();
+ out.close();
+ }
+
+ /**
+ * Clone the SVGPlot document for transcoding.
+ *
+ * This will usually be necessary for exporting the SVG document if it is
+ * currently being displayed: otherwise, we break the Batik rendering trees.
+ * (Discovered by Simon).
+ *
+ * @return cloned document
+ */
+ protected SVGDocument cloneDocument() {
+ return (SVGDocument) new CloneNoExport().cloneDocument(getDomImpl(), document);
+ }
+
+ /**
+ * Transcode file to PDF.
+ *
+ * @param file Output filename
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ * @throws ClassNotFoundException PDF transcoder not installed
+ */
+ public void saveAsPDF(File file) throws IOException, TranscoderException, ClassNotFoundException {
+ try {
+ Object t = Class.forName("org.apache.fop.svg.PDFTranscoder").newInstance();
+ transcode(file, (Transcoder) t);
+ }
+ catch(InstantiationException e) {
+ throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e);
+ }
+ catch(IllegalAccessException e) {
+ throw new ClassNotFoundException("Could not instantiate PDF transcoder - is Apache FOP installed?", e);
+ }
+ }
+
+ /**
+ * Transcode file to PS.
+ *
+ * @param file Output filename
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ * @throws ClassNotFoundException PS transcoder not installed
+ */
+ public void saveAsPS(File file) throws IOException, TranscoderException, ClassNotFoundException {
+ try {
+ Object t = Class.forName("org.apache.fop.render.ps.PSTranscoder").newInstance();
+ transcode(file, (Transcoder) t);
+ }
+ catch(InstantiationException e) {
+ throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e);
+ }
+ catch(IllegalAccessException e) {
+ throw new ClassNotFoundException("Could not instantiate PS transcoder - is Apache FOP installed?", e);
+ }
+ }
+
+ /**
+ * Transcode file to EPS.
+ *
+ * @param file Output filename
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ * @throws ClassNotFoundException EPS transcoder not installed
+ */
+ public void saveAsEPS(File file) throws IOException, TranscoderException, ClassNotFoundException {
+ try {
+ Object t = Class.forName("org.apache.fop.render.ps.EPSTranscoder").newInstance();
+ transcode(file, (Transcoder) t);
+ }
+ catch(InstantiationException e) {
+ throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e);
+ }
+ catch(IllegalAccessException e) {
+ throw new ClassNotFoundException("Could not instantiate EPS transcoder - is Apache FOP installed?", e);
+ }
+ }
+
+ /**
+ * Test whether FOP were installed (for PDF, PS and EPS output support).
+ *
+ * @return true when FOP is available.
+ */
+ public static boolean hasFOPInstalled() {
+ try {
+ Class<?> c1 = Class.forName("org.apache.fop.svg.PDFTranscoder");
+ Class<?> c2 = Class.forName("org.apache.fop.render.ps.PSTranscoder");
+ Class<?> c3 = Class.forName("org.apache.fop.render.ps.EPSTranscoder");
+ return (c1 != null) && (c2 != null) && (c3 != null);
+ }
+ catch(ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Transcode file to PNG.
+ *
+ * @param file Output filename
+ * @param width Width
+ * @param height Height
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ */
+ public void saveAsPNG(File file, int width, int height) throws IOException, TranscoderException {
+ PNGTranscoder t = new PNGTranscoder();
+ t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width));
+ t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height));
+ transcode(file, t);
+ }
+
+ /**
+ * Transcode file to JPEG.
+ *
+ * @param file Output filename
+ * @param width Width
+ * @param height Height
+ * @param quality JPEG quality setting, between 0.0 and 1.0
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ */
+ public void saveAsJPEG(File file, int width, int height, double quality) throws IOException, TranscoderException {
+ JPEGTranscoder t = new JPEGTranscoder();
+ t.addTranscodingHint(JPEGTranscoder.KEY_WIDTH, new Float(width));
+ t.addTranscodingHint(JPEGTranscoder.KEY_HEIGHT, new Float(height));
+ t.addTranscodingHint(JPEGTranscoder.KEY_QUALITY, new Float(quality));
+ transcode(file, t);
+ }
+
+ /**
+ * Transcode file to JPEG.
+ *
+ * @param file Output filename
+ * @param width Width
+ * @param height Height
+ * @throws IOException On write errors
+ * @throws TranscoderException On input/parsing errors.
+ */
+ public void saveAsJPEG(File file, int width, int height) throws IOException, TranscoderException {
+ saveAsJPEG(file, width, height, DEFAULT_QUALITY);
+ }
+
+ /**
+ * Save a file trying to auto-guess the file type.
+ *
+ * @param file File name
+ * @param width Width (for pixel formats)
+ * @param height Height (for pixel formats)
+ * @param quality Quality (for lossy compression)
+ * @throws IOException on file write errors or unrecognized file extensions
+ * @throws TranscoderException on transcoding errors
+ * @throws TransformerFactoryConfigurationError on transcoding errors
+ * @throws TransformerException on transcoding errors
+ * @throws ClassNotFoundException when the transcoder was not installed
+ */
+ public void saveAsANY(File file, int width, int height, double quality) throws IOException, TranscoderException, TransformerFactoryConfigurationError, TransformerException, ClassNotFoundException {
+ String extension = FileUtil.getFilenameExtension(file);
+ if(extension.equals("svg")) {
+ saveAsSVG(file);
+ }
+ else if(extension.equals("pdf")) {
+ saveAsPDF(file);
+ }
+ else if(extension.equals("ps")) {
+ saveAsPS(file);
+ }
+ else if(extension.equals("eps")) {
+ saveAsEPS(file);
+ }
+ else if(extension.equals("png")) {
+ saveAsPNG(file, width, height);
+ }
+ else if(extension.equals("jpg") || extension.equals("jpeg")) {
+ saveAsJPEG(file, width, height, quality);
+ }
+ else {
+ throw new IOException("Unknown file extension: " + extension);
+ }
+ }
+
+ /**
+ * Convert the SVG to a thumbnail image.
+ *
+ * @param width Width of thumbnail
+ * @param height Height of thumbnail
+ * @return Buffered image
+ */
+ public BufferedImage makeAWTImage(int width, int height) throws TranscoderException {
+ ThumbnailTranscoder t = new ThumbnailTranscoder();
+ t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, new Float(width));
+ t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, new Float(height));
+ // Don't clone. Assume this is used safely.
+ TranscoderInput input = new TranscoderInput(document);
+ t.transcode(input, null);
+ return t.getLastImage();
+ }
+
+ /**
+ * Dump the SVG plot to a debug file.
+ */
+ public void dumpDebugFile() {
+ try {
+ File f = File.createTempFile("elki-debug", ".svg");
+ f.deleteOnExit();
+ this.saveAsSVG(f);
+ LoggingUtil.warning("Saved debug file to: " + f.getAbsolutePath());
+ }
+ catch(Throwable err) {
+ // Ignore.
+ }
+ }
+
+ /**
+ * Add an object id.
+ *
+ * @param id ID
+ * @param obj Element
+ */
+ public void putIdElement(String id, Element obj) {
+ objWithId.put(id, new WeakReference<>(obj));
+ }
+
+ /**
+ * Get an element by its id.
+ *
+ * @param id ID
+ * @return Element
+ */
+ public Element getIdElement(String id) {
+ WeakReference<Element> ref = objWithId.get(id);
+ return (ref != null) ? ref.get() : null;
+ }
+
+ /**
+ * Get all used DOM Ids in this plot.
+ *
+ * @return Collection of DOM element IDs.
+ */
+ protected Collection<String> getAllIds() {
+ return objWithId.keySet();
+ }
+
+ /**
+ * Schedule an update.
+ *
+ * @param runnable Runnable to schedule
+ */
+ public void scheduleUpdate(Runnable runnable) {
+ runner.invokeLater(runnable);
+ }
+
+ /**
+ * Assign an update synchronizer.
+ *
+ * @param sync Update synchronizer
+ */
+ public void synchronizeWith(UpdateSynchronizer sync) {
+ runner.synchronizeWith(sync);
+ }
+
+ /**
+ * Detach from synchronization.
+ *
+ * @param sync Update synchronizer to detach from.
+ */
+ public void unsynchronizeWith(UpdateSynchronizer sync) {
+ runner.unsynchronizeWith(sync);
+ }
+
+ /**
+ * Get Batik disable default interactions flag.
+ *
+ * @return true when Batik default interactions are disabled
+ */
+ public boolean getDisableInteractions() {
+ return disableInteractions;
+ }
+
+ /**
+ * Disable Batik predefined interactions.
+ *
+ * @param disable Flag
+ */
+ public void setDisableInteractions(boolean disable) {
+ disableInteractions = disable;
+ }
+
+ /**
+ * Class to skip nodes during cloning that have the "noexport" attribute set.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ protected class CloneNoExport extends CloneInlineImages {
+ @Override
+ public Node cloneNode(Document doc, Node eold) {
+ // Skip elements with noexport attribute set
+ if(eold instanceof Element) {
+ Element eeold = (Element) eold;
+ String vis = eeold.getAttribute(NO_EXPORT_ATTRIBUTE);
+ if(vis != null && vis.length() > 0) {
+ return null;
+ }
+ }
+ return super.cloneNode(doc, eold);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java
new file mode 100644
index 00000000..dd16070d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGScoreBar.java
@@ -0,0 +1,158 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.text.NumberFormat;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+/**
+ * Draw a score bar. Essentially like a progress bar, left-to-right, displaying
+ * a relative score.
+ *
+ * @author Sascha Goldhofer
+ */
+// TODO: refactor to get a progress bar?
+public class SVGScoreBar {
+ /**
+ * Value, minimum and maximum values
+ */
+ protected double val, min = 0., max = 1.;
+
+ /**
+ * Reversed flag.
+ */
+ protected boolean reversed = false;
+
+ /**
+ * Label (on the right)
+ */
+ protected String label = null;
+
+ /**
+ * Number format, set to print the actual score
+ */
+ private NumberFormat format = null;
+
+ /**
+ * Constructor.
+ */
+ public SVGScoreBar() {
+ // Nothing to do here.
+ }
+
+ /**
+ * Set the fill of the score bar.
+ *
+ * @param val Value
+ * @param min Minimum value
+ * @param max Maximum value
+ */
+ public void setFill(double val, double min, double max) {
+ this.val = val;
+ this.min = min;
+ this.max = max;
+ }
+
+ /**
+ * Set the reversed flag.
+ *
+ * @param reversed Reversed flag.
+ */
+ public void setReversed(boolean reversed) {
+ this.reversed = reversed;
+ }
+
+ /**
+ * Set label (right of the bar)
+ *
+ * @param text Label text
+ */
+ public void addLabel(String text) {
+ this.label = text;
+ }
+
+ /**
+ * To show score values, set a number format
+ *
+ * @param format Number format
+ */
+ public void showValues(NumberFormat format) {
+ this.format = format;
+ }
+
+ /**
+ * Build the actual element
+ *
+ * @param svgp Plot to draw to
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param width Width
+ * @param height Height
+ * @return new element
+ */
+ public Element build(SVGPlot svgp, double x, double y, double width, double height) {
+ Element barchart = svgp.svgElement(SVGConstants.SVG_G_TAG);
+
+ // TODO: use style library for colors!
+ Element bar = svgp.svgRect(x, y, width, height);
+ bar.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#a0a0a0");
+ bar.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0");
+ bar.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, String.valueOf(height * 0.01));
+ barchart.appendChild(bar);
+
+ if(val >= min && val <= max && min < max) {
+ final double frame = 0.02 * height;
+ double fpos = (val - min) / (max - min) * (width - 2 * frame);
+ Element chart;
+ if(reversed) {
+ chart = svgp.svgRect(x + frame + fpos, y + frame, width - fpos - 2 * frame, height - 2 * frame);
+ }
+ else {
+ chart = svgp.svgRect(x + frame, y + frame, fpos, height - 2 * frame);
+ }
+ chart.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1");
+ chart.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0");
+ chart.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, String.valueOf(height * 0.01));
+ barchart.appendChild(chart);
+ }
+
+ // Draw the values:
+ if(format != null) {
+ String num = Double.isNaN(val) ? "NaN" : format.format(val);
+ Element lbl = svgp.svgText(x + 0.05 * width, y + 0.75 * height, num);
+ lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: bold");
+ barchart.appendChild(lbl);
+ }
+
+ // Draw the label
+ if(label != null) {
+ Element lbl = svgp.svgText(x + 1.05 * width, y + 0.75 * height, label);
+ lbl.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + 0.75 * height + "; font-weight: normal");
+ barchart.appendChild(lbl);
+ }
+ return barchart;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java
new file mode 100755
index 00000000..5559e602
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGSimpleLinearAxis.java
@@ -0,0 +1,263 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+
+/**
+ * Class to draw a simple axis with tick marks on the plot.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses CSSClass
+ * @apiviz.uses CSSClassManager
+ * @apiviz.uses LinearScale
+ * @apiviz.uses StyleLibrary
+ * @apiviz.uses Element oneway - - «create»
+ */
+public class SVGSimpleLinearAxis {
+ /**
+ * Flag for axis label position. First char: right-hand or left-hand side of
+ * line. Second char: text alignment
+ *
+ * @apiviz.exclude
+ */
+ private enum Alignment {
+ LL, RL, LC, RC, LR, RR
+ }
+
+ /**
+ * Labeling style: left-handed, right-handed, no ticks, labels at ends
+ *
+ * @apiviz.exclude
+ */
+ public enum LabelStyle {
+ LEFTHAND, RIGHTHAND, NOLABELS, NOTHING, ENDLABEL
+ }
+
+ /**
+ * CSS class name for the axes
+ */
+ private static final String CSS_AXIS = "axis";
+
+ /**
+ * CSS class name for the axes
+ */
+ private static final String CSS_AXIS_TICK = "axis-tick";
+
+ /**
+ * CSS class name for the axes
+ */
+ private static final String CSS_AXIS_LABEL = "axis-label";
+
+ /**
+ * Register CSS classes with a {@link CSSClassManager}
+ *
+ * @param owner Owner of the CSS classes
+ * @param manager Manager to register the classes with
+ * @throws CSSNamingConflict when a name clash occurs
+ */
+ private static void setupCSSClasses(Object owner, CSSClassManager manager, StyleLibrary style) throws CSSNamingConflict {
+ if(!manager.contains(CSS_AXIS)) {
+ CSSClass axis = new CSSClass(owner, CSS_AXIS);
+ axis.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.AXIS));
+ axis.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.AXIS));
+ manager.addClass(axis);
+ }
+ if(!manager.contains(CSS_AXIS_TICK)) {
+ CSSClass tick = new CSSClass(owner, CSS_AXIS_TICK);
+ tick.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.AXIS_TICK));
+ tick.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.AXIS_TICK));
+ manager.addClass(tick);
+ }
+ if(!manager.contains(CSS_AXIS_LABEL)) {
+ CSSClass label = new CSSClass(owner, CSS_AXIS_LABEL);
+ label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL));
+ label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL));
+ label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL));
+ manager.addClass(label);
+ }
+ }
+
+ /**
+ * Plot an axis with appropriate scales
+ *
+ * @param plot Plot object
+ * @param parent Containing element
+ * @param scale axis scale information
+ * @param x1 starting coordinate
+ * @param y1 starting coordinate
+ * @param x2 ending coordinate
+ * @param y2 ending coordinate
+ * @param labelstyle Style for placing the labels
+ * @param style Style library
+ * @throws CSSNamingConflict when a conflict occurs in CSS
+ */
+ public static void drawAxis(SVGPlot plot, Element parent, LinearScale scale, double x1, double y1, double x2, double y2, LabelStyle labelstyle, StyleLibrary style) throws CSSNamingConflict {
+ assert (parent != null);
+ Element line = plot.svgLine(x1, y1, x2, y2);
+ SVGUtil.setCSSClass(line, CSS_AXIS);
+ parent.appendChild(line);
+
+ final double tx = x2 - x1;
+ final double ty = y2 - y1;
+ // ticks are orthogonal
+ final double tw = ty * 0.01;
+ final double th = -tx * 0.01;
+
+ // choose where to print labels.
+ final boolean labels, ticks;
+ switch(labelstyle){
+ case LEFTHAND:
+ case RIGHTHAND:
+ labels = true;
+ ticks = true;
+ break;
+ case NOLABELS:
+ labels = false;
+ ticks = true;
+ break;
+ case ENDLABEL: // end labels are handle specially
+ case NOTHING:
+ default:
+ labels = false;
+ ticks = false;
+ }
+ Alignment pos = Alignment.LL;
+ if(labels) {
+ double angle = Math.atan2(ty, tx);
+ // System.err.println(tx + " " + (-ty) + " " + angle);
+ if(angle > 2.6) { // pi .. 2.6 = 180 .. 150
+ pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC;
+ }
+ else if(angle > 0.5) { // 2.3 .. 0.7 = 130 .. 40
+ pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RR : Alignment.LL;
+ }
+ else if(angle > -0.5) { // 0.5 .. -0.5 = 30 .. -30
+ pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC;
+ }
+ else if(angle > -2.6) { // -0.5 .. -2.6 = -30 .. -150
+ pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RL : Alignment.LR;
+ }
+ else { // -2.6 .. -pi = -150 .. -180
+ pos = labelstyle == LabelStyle.RIGHTHAND ? Alignment.RC : Alignment.LC;
+ }
+ }
+ // vertical text offset; align approximately with middle instead of
+ // baseline.
+ double textvoff = style.getTextSize(StyleLibrary.AXIS_LABEL) * .35;
+
+ // draw ticks on x axis
+ if(ticks || labels) {
+ int sw = 1;
+ { // Compute how many ticks to draw
+ int numticks = (int) ((scale.getMax() - scale.getMin()) / scale.getRes());
+ double tlen = Math.sqrt(tx * tx + ty * ty);
+ double minl = 10 * style.getLineWidth(StyleLibrary.AXIS_TICK);
+ // Try proper divisors first.
+ if(sw * tlen / numticks < minl) {
+ for(int i = 2; i <= (numticks >> 1); i++) {
+ if(numticks % i == 0) {
+ if(i * tlen / numticks >= minl) {
+ sw = i;
+ break;
+ }
+ }
+ }
+ }
+ // Otherwise, also allow non-divisors.
+ if(sw * tlen / numticks < minl) {
+ sw = (int)Math.floor(minl * numticks / tlen);
+ }
+ }
+ for(double tick = scale.getMin(); tick <= scale.getMax() + scale.getRes() / 10; tick += sw * scale.getRes()) {
+ double x = x1 + tx * scale.getScaled(tick);
+ double y = y1 + ty * scale.getScaled(tick);
+ if(ticks) {
+ // This is correct. Vectors: (vec - tvec) to (vec + tvec)
+ Element tickline = plot.svgLine(x - tw, y - th, x + tw, y + th);
+ SVGUtil.setAtt(tickline, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_TICK);
+ parent.appendChild(tickline);
+ }
+ // draw labels
+ if(labels) {
+ double tex = x;
+ double tey = y;
+ switch(pos){
+ case LL:
+ case LC:
+ case LR:
+ tex = x + tw * 2.5;
+ tey = y + th * 2.5 + textvoff;
+ break;
+ case RL:
+ case RC:
+ case RR:
+ tex = x - tw * 2.5;
+ tey = y - th * 2.5 + textvoff;
+ }
+ Element text = plot.svgText(tex, tey, scale.formatValue(tick));
+ text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL);
+ switch(pos){
+ case LL:
+ case RL:
+ text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_START_VALUE);
+ break;
+ case LC:
+ case RC:
+ text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE);
+ break;
+ case LR:
+ case RR:
+ text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE);
+ break;
+ }
+ parent.appendChild(text);
+ }
+ }
+ }
+ if(labelstyle == LabelStyle.ENDLABEL) {
+ {
+ Element text = plot.svgText(x1 - tx * 0.02, y1 - ty * 0.02 + textvoff, scale.formatValue(scale.getMin()));
+ text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL);
+ text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE);
+ parent.appendChild(text);
+ }
+ {
+ Element text = plot.svgText(x2 + tx * 0.02, y2 + ty * 0.02 + textvoff, scale.formatValue(scale.getMax()));
+ text.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_AXIS_LABEL);
+ text.setAttribute(SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE);
+ parent.appendChild(text);
+ }
+ }
+ setupCSSClasses(plot, plot.getCSSClassManager(), style);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java
new file mode 100755
index 00000000..b85af07e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/SVGUtil.java
@@ -0,0 +1,703 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.Color;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import javax.swing.text.html.StyleSheet;
+
+import org.apache.batik.dom.events.DOMMouseEvent;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGDocument;
+import org.w3c.dom.svg.SVGLocatable;
+import org.w3c.dom.svg.SVGMatrix;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import gnu.trove.map.hash.TObjectIntHashMap;
+
+/**
+ * Utility class for SVG processing.
+ *
+ * Much of the classes are to allow easier attribute setting (conversion to
+ * string) and Namespace handling
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Element oneway - - «create»
+ */
+public final class SVGUtil {
+ /**
+ * Formatter to output numbers in a valid SVG number format.
+ */
+ public static final NumberFormat FMT = NumberFormat.getInstance(Locale.ROOT);
+
+ static {
+ FMT.setMaximumFractionDigits(10);
+ FMT.setGroupingUsed(false);
+ }
+
+ /**
+ * Hourglass object.
+ */
+ final public static String HOURGLASS_PATH = "M.35 .2 L.65 .2 L.65 .3 L.35 .7 L.35 .8 L.65 .8 L.65 .7 L.35 .3 Z";
+
+ /**
+ * Hourglass style.
+ */
+ final public static String HOURGLASS_STYLE = "stroke: black; stroke-width: .01; fill: grey; opacity: .2";
+
+ /**
+ * Throbber path.
+ */
+ final public static String THROBBER_PATH = "M.5,.25 a.25,.25 0 0 1 .1766,.42635 l-.0589 -.0589 a-.1766 -.1766 0 0 0 -.1178,-.2835 z";
+
+ /**
+ * Throbber style.
+ */
+ final public static String THROBBER_STYLE = "fill: #3d7fe6; opacity: .2";
+
+ /**
+ * SVG color names conversion.
+ */
+ final private static TObjectIntHashMap<String> SVG_COLOR_NAMES;
+
+ /**
+ * Key not found value. Not a reasonable color, fully transparent!
+ */
+ final private static int NO_VALUE = 0x00123456;
+
+ static {
+ // Build a reasonably sized hashmap. Use 0
+ SVG_COLOR_NAMES = new TObjectIntHashMap<>(90, .8f, NO_VALUE);
+ // List taken from SVG specification:
+ // http://www.w3.org/TR/SVG/types.html#ColorKeywords
+ SVG_COLOR_NAMES.put("aliceblue", 0xFFF0F8FF);
+ SVG_COLOR_NAMES.put("antiquewhite", 0xFFFAEBD7);
+ SVG_COLOR_NAMES.put("aqua", 0xFF00FFFF);
+ SVG_COLOR_NAMES.put("aquamarine", 0xFF7FFFD4);
+ SVG_COLOR_NAMES.put("azure", 0xFFF0FFFF);
+ SVG_COLOR_NAMES.put("beige", 0xFFF5F5DC);
+ SVG_COLOR_NAMES.put("bisque", 0xFFFFE4C4);
+ SVG_COLOR_NAMES.put("black", 0xFF000000);
+ SVG_COLOR_NAMES.put("blanchedalmond", 0xFFFFEBCD);
+ SVG_COLOR_NAMES.put("blue", 0xFF0000FF);
+ SVG_COLOR_NAMES.put("blueviolet", 0xFF8A2BE2);
+ SVG_COLOR_NAMES.put("brown", 0xFFA52A2A);
+ SVG_COLOR_NAMES.put("burlywood", 0xFFDEB887);
+ SVG_COLOR_NAMES.put("cadetblue", 0xFF5F9EA0);
+ SVG_COLOR_NAMES.put("chartreuse", 0xFF7FFF00);
+ SVG_COLOR_NAMES.put("chocolate", 0xFFD2691E);
+ SVG_COLOR_NAMES.put("coral", 0xFFFF7F50);
+ SVG_COLOR_NAMES.put("cornflowerblue", 0xFF6495ED);
+ SVG_COLOR_NAMES.put("cornsilk", 0xFFFFF8DC);
+ SVG_COLOR_NAMES.put("crimson", 0xFFDC143C);
+ SVG_COLOR_NAMES.put("cyan", 0xFF00FFFF);
+ SVG_COLOR_NAMES.put("darkblue", 0xFF00008B);
+ SVG_COLOR_NAMES.put("darkcyan", 0xFF008B8B);
+ SVG_COLOR_NAMES.put("darkgoldenrod", 0xFFB8860B);
+ SVG_COLOR_NAMES.put("darkgray", 0xFFA9A9A9);
+ SVG_COLOR_NAMES.put("darkgreen", 0xFF006400);
+ SVG_COLOR_NAMES.put("darkgrey", 0xFFA9A9A9);
+ SVG_COLOR_NAMES.put("darkkhaki", 0xFFBDB76B);
+ SVG_COLOR_NAMES.put("darkmagenta", 0xFF8B008B);
+ SVG_COLOR_NAMES.put("darkolivegreen", 0xFF556B2F);
+ SVG_COLOR_NAMES.put("darkorange", 0xFFFF8C00);
+ SVG_COLOR_NAMES.put("darkorchid", 0xFF9932CC);
+ SVG_COLOR_NAMES.put("darkred", 0xFF8B0000);
+ SVG_COLOR_NAMES.put("darksalmon", 0xFFE9967A);
+ SVG_COLOR_NAMES.put("darkseagreen", 0xFF8FBC8F);
+ SVG_COLOR_NAMES.put("darkslateblue", 0xFF483D8B);
+ SVG_COLOR_NAMES.put("darkslategray", 0xFF2F4F4F);
+ SVG_COLOR_NAMES.put("darkslategrey", 0xFF2F4F4F);
+ SVG_COLOR_NAMES.put("darkturquoise", 0xFF00CED1);
+ SVG_COLOR_NAMES.put("darkviolet", 0xFF9400D3);
+ SVG_COLOR_NAMES.put("deeppink", 0xFFFF1493);
+ SVG_COLOR_NAMES.put("deepskyblue", 0xFF00BFFF);
+ SVG_COLOR_NAMES.put("dimgray", 0xFF696969);
+ SVG_COLOR_NAMES.put("dimgrey", 0xFF696969);
+ SVG_COLOR_NAMES.put("dodgerblue", 0xFF1E90FF);
+ SVG_COLOR_NAMES.put("firebrick", 0xFFB22222);
+ SVG_COLOR_NAMES.put("floralwhite", 0xFFFFFAF0);
+ SVG_COLOR_NAMES.put("forestgreen", 0xFF228B22);
+ SVG_COLOR_NAMES.put("fuchsia", 0xFFFF00FF);
+ SVG_COLOR_NAMES.put("gainsboro", 0xFFDCDCDC);
+ SVG_COLOR_NAMES.put("ghostwhite", 0xFFF8F8FF);
+ SVG_COLOR_NAMES.put("gold", 0xFFFFD700);
+ SVG_COLOR_NAMES.put("goldenrod", 0xFFDAA520);
+ SVG_COLOR_NAMES.put("gray", 0xFF808080);
+ SVG_COLOR_NAMES.put("grey", 0xFF808080);
+ SVG_COLOR_NAMES.put("green", 0xFF008000);
+ SVG_COLOR_NAMES.put("greenyellow", 0xFFADFF2F);
+ SVG_COLOR_NAMES.put("honeydew", 0xFFF0FFF0);
+ SVG_COLOR_NAMES.put("hotpink", 0xFFFF69B4);
+ SVG_COLOR_NAMES.put("indianred", 0xFFCD5C5C);
+ SVG_COLOR_NAMES.put("indigo", 0xFF4B0082);
+ SVG_COLOR_NAMES.put("ivory", 0xFFFFFFF0);
+ SVG_COLOR_NAMES.put("khaki", 0xFFF0E68C);
+ SVG_COLOR_NAMES.put("lavender", 0xFFE6E6FA);
+ SVG_COLOR_NAMES.put("lavenderblush", 0xFFFFF0F5);
+ SVG_COLOR_NAMES.put("lawngreen", 0xFF7CFC00);
+ SVG_COLOR_NAMES.put("lemonchiffon", 0xFFFFFACD);
+ SVG_COLOR_NAMES.put("lightblue", 0xFFADD8E6);
+ SVG_COLOR_NAMES.put("lightcoral", 0xFFF08080);
+ SVG_COLOR_NAMES.put("lightcyan", 0xFFE0FFFF);
+ SVG_COLOR_NAMES.put("lightgoldenrodyellow", 0xFFFAFAD2);
+ SVG_COLOR_NAMES.put("lightgray", 0xFFD3D3D3);
+ SVG_COLOR_NAMES.put("lightgreen", 0xFF90EE90);
+ SVG_COLOR_NAMES.put("lightgrey", 0xFFD3D3D3);
+ SVG_COLOR_NAMES.put("lightpink", 0xFFFFB6C1);
+ SVG_COLOR_NAMES.put("lightsalmon", 0xFFFFA07A);
+ SVG_COLOR_NAMES.put("lightseagreen", 0xFF20B2AA);
+ SVG_COLOR_NAMES.put("lightskyblue", 0xFF87CEFA);
+ SVG_COLOR_NAMES.put("lightslategray", 0xFF778899);
+ SVG_COLOR_NAMES.put("lightslategrey", 0xFF778899);
+ SVG_COLOR_NAMES.put("lightsteelblue", 0xFFB0C4DE);
+ SVG_COLOR_NAMES.put("lightyellow", 0xFFFFFFE0);
+ SVG_COLOR_NAMES.put("lime", 0xFF00FF00);
+ SVG_COLOR_NAMES.put("limegreen", 0xFF32CD32);
+ SVG_COLOR_NAMES.put("linen", 0xFFFAF0E6);
+ SVG_COLOR_NAMES.put("magenta", 0xFFFF00FF);
+ SVG_COLOR_NAMES.put("maroon", 0xFF800000);
+ SVG_COLOR_NAMES.put("mediumaquamarine", 0xFF66CDAA);
+ SVG_COLOR_NAMES.put("mediumblue", 0xFF0000CD);
+ SVG_COLOR_NAMES.put("mediumorchid", 0xFFBA55D3);
+ SVG_COLOR_NAMES.put("mediumpurple", 0xFF9370DB);
+ SVG_COLOR_NAMES.put("mediumseagreen", 0xFF3CB371);
+ SVG_COLOR_NAMES.put("mediumslateblue", 0xFF7B68EE);
+ SVG_COLOR_NAMES.put("mediumspringgreen", 0xFF00FA9A);
+ SVG_COLOR_NAMES.put("mediumturquoise", 0xFF48D1CC);
+ SVG_COLOR_NAMES.put("mediumvioletred", 0xFFC71585);
+ SVG_COLOR_NAMES.put("midnightblue", 0xFF191970);
+ SVG_COLOR_NAMES.put("mintcream", 0xFFF5FFFA);
+ SVG_COLOR_NAMES.put("mistyrose", 0xFFFFE4E1);
+ SVG_COLOR_NAMES.put("moccasin", 0xFFFFE4B5);
+ SVG_COLOR_NAMES.put("navajowhite", 0xFFFFDEAD);
+ SVG_COLOR_NAMES.put("navy", 0xFF000080);
+ SVG_COLOR_NAMES.put("oldlace", 0xFFFDF5E6);
+ SVG_COLOR_NAMES.put("olive", 0xFF808000);
+ SVG_COLOR_NAMES.put("olivedrab", 0xFF6B8E23);
+ SVG_COLOR_NAMES.put("orange", 0xFFFFA500);
+ SVG_COLOR_NAMES.put("orangered", 0xFFFF4500);
+ SVG_COLOR_NAMES.put("orchid", 0xFFDA70D6);
+ SVG_COLOR_NAMES.put("palegoldenrod", 0xFFEEE8AA);
+ SVG_COLOR_NAMES.put("palegreen", 0xFF98FB98);
+ SVG_COLOR_NAMES.put("paleturquoise", 0xFFAFEEEE);
+ SVG_COLOR_NAMES.put("palevioletred", 0xFFDB7093);
+ SVG_COLOR_NAMES.put("papayawhip", 0xFFFFEFD5);
+ SVG_COLOR_NAMES.put("peachpuff", 0xFFFFDAB9);
+ SVG_COLOR_NAMES.put("peru", 0xFFCD853F);
+ SVG_COLOR_NAMES.put("pink", 0xFFFFC0CB);
+ SVG_COLOR_NAMES.put("plum", 0xFFDDA0DD);
+ SVG_COLOR_NAMES.put("powderblue", 0xFFB0E0E6);
+ SVG_COLOR_NAMES.put("purple", 0xFF800080);
+ SVG_COLOR_NAMES.put("red", 0xFFFF0000);
+ SVG_COLOR_NAMES.put("rosybrown", 0xFFBC8F8F);
+ SVG_COLOR_NAMES.put("royalblue", 0xFF4169E1);
+ SVG_COLOR_NAMES.put("saddlebrown", 0xFF8B4513);
+ SVG_COLOR_NAMES.put("salmon", 0xFFFA8072);
+ SVG_COLOR_NAMES.put("sandybrown", 0xFFF4A460);
+ SVG_COLOR_NAMES.put("seagreen", 0xFF2E8B57);
+ SVG_COLOR_NAMES.put("seashell", 0xFFFFF5EE);
+ SVG_COLOR_NAMES.put("sienna", 0xFFA0522D);
+ SVG_COLOR_NAMES.put("silver", 0xFFC0C0C0);
+ SVG_COLOR_NAMES.put("skyblue", 0xFF87CEEB);
+ SVG_COLOR_NAMES.put("slateblue", 0xFF6A5ACD);
+ SVG_COLOR_NAMES.put("slategray", 0xFF708090);
+ SVG_COLOR_NAMES.put("slategrey", 0xFF708090);
+ SVG_COLOR_NAMES.put("snow", 0xFFFFFAFA);
+ SVG_COLOR_NAMES.put("springgreen", 0xFF00FF7F);
+ SVG_COLOR_NAMES.put("steelblue", 0xFF4682B4);
+ SVG_COLOR_NAMES.put("tan", 0xFFD2B48C);
+ SVG_COLOR_NAMES.put("teal", 0xFF008080);
+ SVG_COLOR_NAMES.put("thistle", 0xFFD8BFD8);
+ SVG_COLOR_NAMES.put("tomato", 0xFFFF6347);
+ SVG_COLOR_NAMES.put("turquoise", 0xFF40E0D0);
+ SVG_COLOR_NAMES.put("violet", 0xFFEE82EE);
+ SVG_COLOR_NAMES.put("wheat", 0xFFF5DEB3);
+ SVG_COLOR_NAMES.put("white", 0xFFFFFFFF);
+ SVG_COLOR_NAMES.put("whitesmoke", 0xFFF5F5F5);
+ SVG_COLOR_NAMES.put("yellow", 0xFFFFFF00);
+ SVG_COLOR_NAMES.put("yellowgreen", 0xFF9ACD32);
+ // Nonstandard:
+ SVG_COLOR_NAMES.put("transparent", 0xFFFFFFFF);
+ }
+
+ /**
+ * CSS Stylesheet from Javax, to parse color values.
+ */
+ private static final StyleSheet colorLookupStylesheet = new StyleSheet();
+
+ /**
+ * Format a double according to the SVG specs.
+ *
+ * @param x number to format
+ * @return String representation
+ */
+ public static String fmt(double x) {
+ return FMT.format(x);
+ }
+
+ /**
+ * Create a SVG element in appropriate namespace
+ *
+ * @param document containing document
+ * @param name node name
+ * @return new SVG element.
+ */
+ public static Element svgElement(Document document, String name) {
+ return document.createElementNS(SVGConstants.SVG_NAMESPACE_URI, name);
+ }
+
+ /**
+ * Set a SVG attribute
+ *
+ * @param el element
+ * @param name attribute name
+ * @param d double value
+ */
+ public static void setAtt(Element el, String name, double d) {
+ el.setAttribute(name, fmt(d));
+ }
+
+ /**
+ * Set a SVG attribute
+ *
+ * @param el element
+ * @param name attribute name
+ * @param d integer value
+ */
+ public static void setAtt(Element el, String name, int d) {
+ el.setAttribute(name, Integer.toString(d));
+ }
+
+ /**
+ * Set a SVG attribute
+ *
+ * @param el element
+ * @param name attribute name
+ * @param d string value
+ */
+ public static void setAtt(Element el, String name, String d) {
+ el.setAttribute(name, d);
+ }
+
+ /**
+ * Set a SVG style attribute
+ *
+ * @param el element
+ * @param d style value
+ */
+ public static void setStyle(Element el, String d) {
+ el.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, d);
+ }
+
+ /**
+ * Set the CSS class of an Element. See also {@link #addCSSClass} and
+ * {@link #removeCSSClass}.
+ *
+ * @param e Element
+ * @param cssclass class to set.
+ */
+ public static void setCSSClass(Element e, String cssclass) {
+ setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, cssclass);
+ }
+
+ /**
+ * Add a CSS class to an Element.
+ *
+ * @param e Element
+ * @param cssclass class to add.
+ */
+ public static void addCSSClass(Element e, String cssclass) {
+ String oldval = e.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE);
+ if(oldval == null || oldval.length() == 0) {
+ setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, cssclass);
+ return;
+ }
+ String[] classes = oldval.split(" ");
+ for(String c : classes) {
+ if(c.equals(cssclass)) {
+ return;
+ }
+ }
+ setAtt(e, SVGConstants.SVG_CLASS_ATTRIBUTE, oldval + " " + cssclass);
+ }
+
+ /**
+ * Remove a CSS class from an Element.
+ *
+ * @param e Element
+ * @param cssclass class to remove.
+ */
+ public static void removeCSSClass(Element e, String cssclass) {
+ String oldval = e.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE);
+ if(oldval == null) {
+ return;
+ }
+ String[] classes = oldval.split(" ");
+ if(classes.length == 1) {
+ if(cssclass.equals(classes[0])) {
+ e.removeAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE);
+ }
+ }
+ else if(classes.length == 2) {
+ if(cssclass.equals(classes[0])) {
+ if(cssclass.equals(classes[1])) {
+ e.removeAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE);
+ }
+ else {
+ e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, classes[1]);
+ }
+ }
+ else if(cssclass.equals(classes[1])) {
+ e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, classes[0]);
+ }
+ }
+ else {
+ StringBuilder joined = new StringBuilder();
+ for(String c : classes) {
+ if(!c.equals(cssclass)) {
+ if(joined.length() > 0) {
+ joined.append(' ');
+ }
+ joined.append(c);
+ }
+ }
+ e.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, joined.toString());
+ }
+ }
+
+ /**
+ * Make a new CSS style element for the given Document.
+ *
+ * @param document document (factory)
+ * @return new CSS style element.
+ */
+ public static Element makeStyleElement(Document document) {
+ Element style = SVGUtil.svgElement(document, SVGConstants.SVG_STYLE_TAG);
+ style.setAttribute(SVGConstants.SVG_TYPE_ATTRIBUTE, SVGConstants.CSS_MIME_TYPE);
+ return style;
+ }
+
+ /**
+ * Create a SVG rectangle element.
+ *
+ * @param document document to create in (factory)
+ * @param x X coordinate
+ * @param y Y coordinate
+ * @param w Width
+ * @param h Height
+ * @return new element
+ */
+ public static Element svgRect(Document document, double x, double y, double w, double h) {
+ Element rect = SVGUtil.svgElement(document, SVGConstants.SVG_RECT_TAG);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_X_ATTRIBUTE, x);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_Y_ATTRIBUTE, y);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_WIDTH_ATTRIBUTE, w);
+ SVGUtil.setAtt(rect, SVGConstants.SVG_HEIGHT_ATTRIBUTE, h);
+ return rect;
+ }
+
+ /**
+ * Create a SVG circle element.
+ *
+ * @param document document to create in (factory)
+ * @param cx center X
+ * @param cy center Y
+ * @param r radius
+ * @return new element
+ */
+ public static Element svgCircle(Document document, double cx, double cy, double r) {
+ Element circ = SVGUtil.svgElement(document, SVGConstants.SVG_CIRCLE_TAG);
+ SVGUtil.setAtt(circ, SVGConstants.SVG_CX_ATTRIBUTE, cx);
+ SVGUtil.setAtt(circ, SVGConstants.SVG_CY_ATTRIBUTE, cy);
+ SVGUtil.setAtt(circ, SVGConstants.SVG_R_ATTRIBUTE, r);
+ return circ;
+ }
+
+ /**
+ * Create a SVG line element. Do not confuse this with path elements.
+ *
+ * @param document document to create in (factory)
+ * @param x1 first point x
+ * @param y1 first point y
+ * @param x2 second point x
+ * @param y2 second point y
+ * @return new element
+ */
+ public static Element svgLine(Document document, double x1, double y1, double x2, double y2) {
+ Element line = SVGUtil.svgElement(document, SVGConstants.SVG_LINE_TAG);
+ SVGUtil.setAtt(line, SVGConstants.SVG_X1_ATTRIBUTE, x1);
+ SVGUtil.setAtt(line, SVGConstants.SVG_Y1_ATTRIBUTE, y1);
+ SVGUtil.setAtt(line, SVGConstants.SVG_X2_ATTRIBUTE, x2);
+ SVGUtil.setAtt(line, SVGConstants.SVG_Y2_ATTRIBUTE, y2);
+ return line;
+ }
+
+ /**
+ * Create a SVG text element.
+ *
+ * @param document document to create in (factory)
+ * @param x first point x
+ * @param y first point y
+ * @param text Content of text element.
+ * @return New text element.
+ */
+ public static Element svgText(Document document, double x, double y, String text) {
+ Element elem = SVGUtil.svgElement(document, SVGConstants.SVG_TEXT_TAG);
+ SVGUtil.setAtt(elem, SVGConstants.SVG_X_ATTRIBUTE, x);
+ SVGUtil.setAtt(elem, SVGConstants.SVG_Y_ATTRIBUTE, y);
+ elem.setTextContent(text);
+ return elem;
+ }
+
+ /**
+ * Draw a simple "please wait" icon (in-progress) as placeholder for running
+ * renderings.
+ *
+ * @param document Document.
+ * @param x Left
+ * @param y Top
+ * @param w Width
+ * @param h Height
+ * @return New element (currently a {@link SVGConstants#SVG_PATH_TAG})
+ */
+ public static Element svgWaitIcon(Document document, double x, double y, double w, double h) {
+ Element g = SVGUtil.svgElement(document, SVGConstants.SVG_G_TAG);
+ setAtt(g, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "translate(" + x + " " + y + ") scale(" + w + " " + h + ")");
+ Element thro = SVGUtil.svgElement(document, SVGConstants.SVG_PATH_TAG);
+ setAtt(thro, SVGConstants.SVG_D_ATTRIBUTE, THROBBER_PATH);
+ setStyle(thro, THROBBER_STYLE);
+ Element anim = SVGUtil.svgElement(document, SVGConstants.SVG_ANIMATE_TRANSFORM_TAG);
+ setAtt(anim, SVGConstants.SVG_ATTRIBUTE_NAME_ATTRIBUTE, SVGConstants.SVG_TRANSFORM_ATTRIBUTE);
+ setAtt(anim, SVGConstants.SVG_ATTRIBUTE_TYPE_ATTRIBUTE, "XML");
+ setAtt(anim, SVGConstants.SVG_TYPE_ATTRIBUTE, SVGConstants.SVG_ROTATE_ATTRIBUTE);
+ setAtt(anim, SVGConstants.SVG_FROM_ATTRIBUTE, "0 .5 .5");
+ setAtt(anim, SVGConstants.SVG_TO_ATTRIBUTE, "360 .5 .5");
+ setAtt(anim, SVGConstants.SVG_BEGIN_ATTRIBUTE, fmt(Math.random() * 2) + "s");
+ setAtt(anim, SVGConstants.SVG_DUR_ATTRIBUTE, "2s");
+ setAtt(anim, SVGConstants.SVG_REPEAT_COUNT_ATTRIBUTE, "indefinite");
+ setAtt(anim, SVGConstants.SVG_FILL_ATTRIBUTE, "freeze");
+ thro.appendChild(anim);
+ g.appendChild(thro);
+ return g;
+ }
+
+ /**
+ * Convert a color name from SVG syntax to an AWT color object.
+ *
+ * @param str Color name
+ * @return Color value
+ */
+ public static Color stringToColor(String str) {
+ int icol = SVG_COLOR_NAMES.get(str.toLowerCase());
+ if(icol != NO_VALUE) {
+ return new Color(icol, false);
+ }
+ return colorLookupStylesheet.stringToColor(str);
+ }
+
+ /**
+ * Convert a color name from an AWT color object to CSS syntax
+ *
+ * Note: currently only RGB (from ARGB order) are supported.
+ *
+ * @param col Color value
+ * @return Color string
+ */
+ public static String colorToString(Color col) {
+ return colorToString(col.getRGB());
+ }
+
+ /**
+ * Convert a color name from an integer RGB color to CSS syntax
+ *
+ * Note: currently only RGB (from ARGB order) are supported. The alpha channel
+ * will be ignored.
+ *
+ * @param col Color value
+ * @return Color string
+ */
+ public static String colorToString(int col) {
+ final char[] buf = new char[] { '#', 'X', 'X', 'X', 'X', 'X', 'X' };
+ for(int i = 6; i > 0; i--) {
+ final int v = (col & 0xF);
+ buf[i] = (char) ((v < 10) ? ('0' + v) : ('a' + v - 10));
+ col >>>= 4;
+ }
+ return new String(buf);
+ }
+
+ /**
+ * Make a transform string to add margins
+ *
+ * @param owidth Width of outer (embedding) canvas
+ * @param oheight Height of outer (embedding) canvas
+ * @param iwidth Width of inner (embedded) canvas
+ * @param iheight Height of inner (embedded) canvas
+ * @param lmargin Left margin (in inner canvas' units)
+ * @param tmargin Top margin (in inner canvas' units)
+ * @param rmargin Right margin (in inner canvas' units)
+ * @param bmargin Bottom margin (in inner canvas' units)
+ * @return Transform string
+ */
+ public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double lmargin, double tmargin, double rmargin, double bmargin) {
+ double swidth = iwidth + lmargin + rmargin;
+ double sheight = iheight + tmargin + bmargin;
+ double scale = Math.max(swidth / owidth, sheight / oheight);
+ double offx = (scale * owidth - swidth) * .5 + lmargin;
+ double offy = (scale * oheight - sheight) * .5 + tmargin;
+ return "scale(" + fmt(1 / scale) + ") translate(" + fmt(offx) + " " + fmt(offy) + ")";
+ }
+
+ /**
+ * Make a transform string to add margins
+ *
+ * @param owidth Width of outer (embedding) canvas
+ * @param oheight Height of outer (embedding) canvas
+ * @param iwidth Width of inner (embedded) canvas
+ * @param iheight Height of inner (embedded) canvas
+ * @param xmargin Left and right margin (in inner canvas' units)
+ * @param ymargin Top and bottom margin (in inner canvas' units)
+ * @return Transform string
+ */
+ public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double xmargin, double ymargin) {
+ return makeMarginTransform(owidth, oheight, iwidth, iheight, xmargin, ymargin, xmargin, ymargin);
+ }
+
+ /**
+ * Make a transform string to add margins
+ *
+ * @param owidth Width of outer (embedding) canvas
+ * @param oheight Height of outer (embedding) canvas
+ * @param iwidth Width of inner (embedded) canvas
+ * @param iheight Height of inner (embedded) canvas
+ * @param margin Margin (in inner canvas' units)
+ * @return Transform string
+ */
+ public static String makeMarginTransform(double owidth, double oheight, double iwidth, double iheight, double margin) {
+ return makeMarginTransform(owidth, oheight, iwidth, iheight, margin, margin, margin, margin);
+ }
+
+ /**
+ * Convert the coordinates of an DOM Event from screen into element
+ * coordinates.
+ *
+ * @param doc Document context
+ * @param tag Element containing the coordinate system
+ * @param evt Event to interpret
+ * @return coordinates
+ */
+ public static SVGPoint elementCoordinatesFromEvent(Document doc, Element tag, Event evt) {
+ try {
+ DOMMouseEvent gnme = (DOMMouseEvent) evt;
+ SVGMatrix mat = ((SVGLocatable) tag).getScreenCTM();
+ SVGMatrix imat = mat.inverse();
+ SVGPoint cPt = ((SVGDocument) doc).getRootElement().createSVGPoint();
+ cPt.setX(gnme.getClientX());
+ cPt.setY(gnme.getClientY());
+ return cPt.matrixTransform(imat);
+ }
+ catch(Exception e) {
+ LoggingUtil.warning("Error getting coordinates from SVG event.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Remove last child of an element, when present
+ *
+ * @param tag Parent
+ */
+ public static void removeLastChild(Element tag) {
+ final Node last = tag.getLastChild();
+ if(last != null) {
+ tag.removeChild(last);
+ }
+ }
+
+ /**
+ * Remove an element from its parent, if defined.
+ *
+ * @param elem Element to remove
+ */
+ public static void removeFromParent(Element elem) {
+ if(elem != null && elem.getParentNode() != null) {
+ elem.getParentNode().removeChild(elem);
+ }
+ }
+
+ /**
+ * Create a circle segment.
+ *
+ * @param svgp Plot to draw to
+ * @param centerx Center X position
+ * @param centery Center Y position
+ * @param angleStart Starting angle
+ * @param angleDelta Angle delta
+ * @param innerRadius inner radius
+ * @param outerRadius outer radius
+ * @return SVG element representing this circle segment
+ */
+ public static Element svgCircleSegment(SVGPlot svgp, double centerx, double centery, double angleStart, double angleDelta, double innerRadius, double outerRadius) {
+ double sin1st = Math.sin(angleStart);
+ double cos1st = MathUtil.sinToCos(angleStart, sin1st);
+
+ double sin2nd = Math.sin(angleStart + angleDelta);
+ double cos2nd = MathUtil.sinToCos(angleStart + angleDelta, sin2nd);
+
+ double inner1stx = centerx + (innerRadius * sin1st);
+ double inner1sty = centery - (innerRadius * cos1st);
+ double outer1stx = centerx + (outerRadius * sin1st);
+ double outer1sty = centery - (outerRadius * cos1st);
+
+ double inner2ndx = centerx + (innerRadius * sin2nd);
+ double inner2ndy = centery - (innerRadius * cos2nd);
+ double outer2ndx = centerx + (outerRadius * sin2nd);
+ double outer2ndy = centery - (outerRadius * cos2nd);
+
+ double largeArc = 0;
+ if(angleDelta >= Math.PI) {
+ largeArc = 1;
+ }
+
+ SVGPath path = new SVGPath(inner1stx, inner1sty);
+ path.lineTo(outer1stx, outer1sty);
+ path.ellipticalArc(outerRadius, outerRadius, 0, largeArc, 1, outer2ndx, outer2ndy);
+ path.lineTo(inner2ndx, inner2ndy);
+ if(innerRadius > 0) {
+ path.ellipticalArc(innerRadius, innerRadius, 0, largeArc, 0, inner1stx, inner1sty);
+ }
+
+ return path.makeElement(svgp);
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java
new file mode 100644
index 00000000..17033ef8
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateRunner.java
@@ -0,0 +1,161 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+
+/**
+ * Class to handle updates to an SVG plot, in particular when used in an Apache
+ * Batik UI.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has Runnable
+ * @apiviz.uses UpdateSynchronizer
+ */
+public class UpdateRunner {
+ /**
+ * Owner/Synchronization object
+ */
+ private Object sync;
+
+ /**
+ * The queue of pending updates
+ */
+ final private ConcurrentLinkedQueue<Runnable> queue = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Synchronizer that can block events from being executed right away.
+ */
+ private UpdateSynchronizer synchronizer = null;
+
+ /**
+ * Construct a new update handler
+ *
+ * @param sync Object to synchronize on
+ */
+ protected UpdateRunner(Object sync) {
+ this.sync = sync;
+ }
+
+ /**
+ * Add a new update to run at any appropriate time.
+ *
+ * @param r New runnable to perform the update
+ */
+ public void invokeLater(Runnable r) {
+ queue.add(r);
+ synchronized(this) {
+ if(synchronizer == null) {
+ runQueue();
+ }
+ else {
+ synchronizer.activate();
+ }
+ }
+ }
+
+ /**
+ * Run the processing queue now. This should usually be only invoked by the
+ * UpdateSynchronizer
+ */
+ public void runQueue() {
+ synchronized(sync) {
+ while(!queue.isEmpty()) {
+ Runnable r = queue.poll();
+ if(r != null) {
+ try {
+ r.run();
+ }
+ catch(Exception e) {
+ // Alternatively, we could allow the specification of exception
+ // handlers for each runnable in the API. For now we'll just log.
+ // TODO: handle exceptions here better!
+ LoggingUtil.exception(e);
+ }
+ }
+ else {
+ LoggingUtil.warning("Tried to run a 'null' Object.");
+ }
+ }
+ }
+ }
+
+ /**
+ * Clear queue. For shutdown!
+ */
+ public synchronized void clear() {
+ queue.clear();
+ }
+
+ /**
+ * Check whether the queue is empty.
+ *
+ * @return queue status
+ */
+ public boolean isEmpty() {
+ return queue.isEmpty();
+ }
+
+ /**
+ * Set a new update synchronizer.
+ *
+ * @param newsync Update synchronizer
+ */
+ public synchronized void synchronizeWith(UpdateSynchronizer newsync) {
+ // LoggingUtil.warning("Synchronizing: " + sync + " " + newsync, new
+ // Throwable());
+ if(synchronizer == newsync) {
+ LoggingUtil.warning("Double-synced to the same plot!", new Throwable());
+ return;
+ }
+ if(synchronizer != null) {
+ LoggingUtil.warning("Attempting to synchronize to more than one synchronizer.");
+ return;
+ }
+ synchronizer = newsync;
+ newsync.addUpdateRunner(this);
+ }
+
+ /**
+ * Remove an update synchronizer
+ *
+ * @param oldsync Update synchronizer to remove
+ */
+ public synchronized void unsynchronizeWith(UpdateSynchronizer oldsync) {
+ if(synchronizer == null) {
+ LoggingUtil.warning("Warning: was not synchronized.");
+ return;
+ }
+ if(synchronizer != oldsync) {
+ LoggingUtil.warning("Warning: was synchronized differently!");
+ return;
+ }
+ // LoggingUtil.warning("Unsynchronizing: " + sync + " " + oldsync);
+ synchronizer = null;
+ runQueue();
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java
new file mode 100644
index 00000000..30eaa304
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/UpdateSynchronizer.java
@@ -0,0 +1,46 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+/**
+ * API to synchronize updates
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has UpdateRunner
+ */
+public interface UpdateSynchronizer {
+ /**
+ * This method is called whenever a new pending event was added.
+ */
+ void activate();
+
+ /**
+ * Set an update runner to use.
+ *
+ * @param updateRunner
+ */
+ void addUpdateRunner(UpdateRunner updateRunner);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java
new file mode 100644
index 00000000..434bf240
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/VoronoiDraw.java
@@ -0,0 +1,155 @@
+package de.lmu.ifi.dbs.elki.visualization.svg;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.List;
+
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D;
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+
+/**
+ * Draw the Voronoi cells
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.uses de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle
+ * @apiviz.uses Projection2D
+ */
+public class VoronoiDraw {
+ /**
+ * Draw the Delaunay triangulation.
+ *
+ * @param proj Projection
+ * @param delaunay Triangulation
+ * @param means Means
+ * @return Path
+ */
+ public static SVGPath drawDelaunay(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) {
+ final SVGPath path = new SVGPath();
+ for(SweepHullDelaunay2D.Triangle del : delaunay) {
+ path.moveTo(proj.fastProjectDataToRenderSpace(means.get(del.a)));
+ path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.b)));
+ path.drawTo(proj.fastProjectDataToRenderSpace(means.get(del.c)));
+ path.close();
+ }
+ return path;
+ }
+
+ /**
+ * Draw a Voronoi diagram
+ *
+ * @param proj Projection
+ * @param delaunay Delaunay triangulation
+ * @param means Cluster means
+ * @return SVG path
+ */
+ public static SVGPath drawVoronoi(Projection2D proj, List<SweepHullDelaunay2D.Triangle> delaunay, List<double[]> means) {
+ final SVGPath path = new SVGPath();
+ CanvasSize viewport = proj.estimateViewport();
+ for(int i = 0; i < delaunay.size(); i++) {
+ SweepHullDelaunay2D.Triangle del = delaunay.get(i);
+ final double[] projcx = proj.fastProjectDataToRenderSpace(del.m.getArrayRef());
+ if(del.ab > i) {
+ Triangle oth = delaunay.get(del.ab);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.ab < 0) {
+ double[] dirv = VMath.minus(means.get(del.a), means.get(del.b));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+
+ if(del.bc > i) {
+ Triangle oth = delaunay.get(del.bc);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.bc < 0) {
+ double[] dirv = VMath.minus(means.get(del.b), means.get(del.c));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+
+ if(del.ca > i) {
+ Triangle oth = delaunay.get(del.ca);
+ path.moveTo(projcx);
+ path.drawTo(proj.fastProjectDataToRenderSpace(oth.m.getArrayRef()));
+ }
+ else if(del.ca < 0) {
+ double[] dirv = VMath.minus(means.get(del.c), means.get(del.a));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ final double factor = viewport.continueToMargin(projcx, dir);
+ if(factor > 0) {
+ path.moveTo(projcx);
+ path.relativeLineTo(factor * dir[0], factor * dir[1]);
+ }
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Fake Voronoi diagram. For two means only
+ *
+ * @param proj Projection
+ * @param means Mean vectors
+ * @return SVG path
+ */
+ public static SVGPath drawFakeVoronoi(Projection2D proj, List<double[]> means) {
+ CanvasSize viewport = proj.estimateViewport();
+ final SVGPath path = new SVGPath();
+ // Difference
+ final double[] dirv = VMath.minus(means.get(1), means.get(0));
+ VMath.rotate90Equals(dirv);
+ double[] dir = proj.fastProjectRelativeDataToRenderSpace(dirv);
+ // Mean
+ final double[] mean = VMath.plus(means.get(0), means.get(1));
+ VMath.timesEquals(mean, 0.5);
+ double[] projmean = proj.fastProjectDataToRenderSpace(mean);
+
+ double factor = viewport.continueToMargin(projmean, dir);
+ path.moveTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]);
+ // Inverse direction:
+ dir[0] *= -1;
+ dir[1] *= -1;
+ factor = viewport.continueToMargin(projmean, dir);
+ path.drawTo(projmean[0] + factor * dir[0], projmean[1] + factor * dir[1]);
+ return path;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java
new file mode 100755
index 00000000..2ea11699
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/svg/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Base SVG functionality (generation, markers, thumbnails, export, ...).</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.svg; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java
new file mode 100644
index 00000000..841ea83f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisFactory.java
@@ -0,0 +1,73 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs.ThumbnailVisualization;
+
+/**
+ * Abstract superclass for Visualizers (aka: Visualization Factories).
+ *
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.uses ThumbnailVisualization oneway - - «create»
+ * @apiviz.excludeSubtypes
+ */
+public abstract class AbstractVisFactory implements VisFactory {
+ /**
+ * Constructor.
+ */
+ protected AbstractVisFactory() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualizationOrThumbnail(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize) {
+ if(width <= 0 || height <= 0) {
+ LoggingUtil.warning("Cannot generate visualization of 0 size.", new Throwable());
+ return null;
+ }
+ if(allowThumbnails(task)) {
+ return new ThumbnailVisualization(this, task, plot, width, height, proj, thumbsize);
+ }
+ return makeVisualization(task, plot, width, height, proj);
+ }
+
+ @Override
+ abstract public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj);
+
+ /**
+ * Test whether to do a thumbnail or a full rendering.
+ *
+ * Override this with "false" to disable thumbnails!
+ *
+ * @param task Task requested
+ */
+ public boolean allowThumbnails(VisualizationTask task) {
+ return true;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java
new file mode 100644
index 00000000..49e749c0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/AbstractVisualization.java
@@ -0,0 +1,214 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreEvent;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.result.SelectionResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationListener;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+
+/**
+ * Abstract base class for visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.excludeSubtypes
+ */
+public abstract class AbstractVisualization implements Visualization, ResultListener, VisualizationListener, DataStoreListener {
+ /**
+ * The visualization task we do.
+ */
+ protected final VisualizationTask task;
+
+ /**
+ * Our context
+ */
+ protected final VisualizerContext context;
+
+ /**
+ * The plot we are attached to
+ */
+ protected final VisualizationPlot svgp;
+
+ /**
+ * Layer storage
+ */
+ protected Element layer;
+
+ /**
+ * Width
+ */
+ private double width;
+
+ /**
+ * Height
+ */
+ private double height;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ */
+ public AbstractVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height) {
+ super();
+ this.task = task;
+ this.context = task.getContext();
+ this.svgp = plot;
+ this.width = width;
+ this.height = height;
+ this.layer = null;
+ // Note: we do not auto-add listeners, as we don't know what kind of
+ // listeners a visualizer needs, and the visualizer might need to do some
+ // initialization first
+ }
+
+ /**
+ * Add the listeners according to the mask.
+ */
+ protected void addListeners() {
+ // Listen for result changes, including the one we monitor
+ context.addResultListener(this);
+ context.addVisualizationListener(this);
+ // Listen for database events only when needed.
+ if(task.updateOnAny(VisualizationTask.ON_DATA)) {
+ context.addDataStoreListener(this);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ // Always unregister listeners, as this is easy to forget otherwise
+ // TODO: remove destroy() overrides that are redundant?
+ context.removeResultListener(this);
+ context.removeVisualizationListener(this);
+ context.removeDataStoreListener((DataStoreListener) this);
+ }
+
+ @Override
+ public Element getLayer() {
+ if(layer == null) {
+ incrementalRedraw();
+ }
+ return layer;
+ }
+
+ /**
+ * Get the width
+ *
+ * @return the width
+ */
+ protected double getWidth() {
+ return width;
+ }
+
+ /**
+ * Get the height
+ *
+ * @return the height
+ */
+ protected double getHeight() {
+ return height;
+ }
+
+ /**
+ * Redraw the visualization (maybe incremental).
+ *
+ * Optional - by default, it will do a full redraw, which often is faster!
+ */
+ @Override
+ public void incrementalRedraw() {
+ Element oldcontainer = null;
+ if(layer != null && layer.hasChildNodes()) {
+ oldcontainer = layer;
+ // Shallow clone:
+ layer = (Element) layer.cloneNode(false);
+ }
+ fullRedraw();
+ if(oldcontainer != null && oldcontainer.getParentNode() != null) {
+ oldcontainer.getParentNode().replaceChild(layer, oldcontainer);
+ }
+ }
+
+ @Override
+ public abstract void fullRedraw();
+
+ @Override
+ public void resultAdded(Result child, Result parent) {
+ // Ignore by default
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ // Default is to redraw when the result we are attached to changed.
+ if(task.getResult() == current) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ if(task.updateOnAny(VisualizationTask.ON_SELECTION) && current instanceof SelectionResult) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ if(task.updateOnAny(VisualizationTask.ON_SAMPLE) && current instanceof SamplingResult) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ }
+
+ @Override
+ public void resultRemoved(Result child, Result parent) {
+ // Ignore by default.
+ // TODO: auto-remove if parent result is removed?
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem item) {
+ if(task.getResult() == item) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ if(task.updateOnAny(VisualizationTask.ON_STYLEPOLICY) && item instanceof StylingPolicy) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ }
+
+ @Override
+ public void contentChanged(DataStoreEvent e) {
+ svgp.requestRedraw(this.task, this);
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java
new file mode 100644
index 00000000..b89720fe
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/StaticVisualizationInstance.java
@@ -0,0 +1,60 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+
+/**
+ * Static visualization
+ *
+ * @author Erich Schubert
+ */
+public class StaticVisualizationInstance extends AbstractVisualization {
+ /**
+ * Unchanging precomputed visualization.
+ *
+ * @param task Task to visualize
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param element Element containing the resulting visualization
+ */
+ public StaticVisualizationInstance(VisualizationTask task, VisualizationPlot plot, double width, double height, Element element) {
+ super(task, plot, width, height);
+ this.layer = element;
+ }
+
+ @Override
+ public void incrementalRedraw() {
+ // Do nothing - we keep our static layer
+ }
+
+ @Override
+ public void fullRedraw() {
+ // Do nothing - we keep our static layer
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java
new file mode 100644
index 00000000..bf30fe8f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/VisFactory.java
@@ -0,0 +1,77 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+
+/**
+ * Defines the requirements for a visualizer. <br>
+ * Note: Any implementation is supposed to provide a constructor without
+ * parameters (default constructor) to be used for parameterization.
+ *
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.landmark
+ * @apiviz.stereotype factory
+ * @apiviz.uses Visualization - - «create»
+ * @apiviz.uses VisualizationTask - - «create»
+ */
+public interface VisFactory extends VisualizationProcessor {
+ /**
+ * Add visualizers for the given result (tree) to the context.
+ *
+ * @param context Visualization context
+ * @param start Result to process
+ */
+ @Override
+ void processNewResult(VisualizerContext context, Object start);
+
+ /**
+ * Produce a visualization instance for the given task
+ *
+ * @param task Visualization task
+ * @param plot Plot
+ * @param width Width
+ * @param height Height
+ * @param proj Projection
+ * @return Visualization
+ */
+ Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj);
+
+ /**
+ * Produce a visualization instance for the given task that may use thumbnails
+ *
+ * @param task Visualization task
+ * @param plot Plot
+ * @param width Width
+ * @param height Height
+ * @param proj Projection
+ * @param thumbsize Thumbnail size
+ * @return Visualization
+ */
+ Visualization makeVisualizationOrThumbnail(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java
new file mode 100644
index 00000000..ee355e4a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/Visualization.java
@@ -0,0 +1,61 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+/**
+ * Base class for a materialized Visualization.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.has Element oneway
+ */
+public interface Visualization {
+ /**
+ * Get the SVG layer of the given visualization.
+ *
+ * @return layer
+ */
+ Element getLayer();
+
+ /**
+ * Request an update of the visualization.
+ */
+ void incrementalRedraw();
+
+ /**
+ * Request a full redrawing of the visualization.
+ */
+ void fullRedraw();
+
+ /**
+ * Destroy the visualization. Called after the elements have been removed from
+ * the document.
+ *
+ * Implementations should remove their listeners etc.
+ */
+ void destroy();
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java
new file mode 100644
index 00000000..7923509e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/ClusterStyleAction.java
@@ -0,0 +1,116 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.actions;
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationMenuAction;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Actions to use clusterings for styling.
+ *
+ * @author Erich Schubert
+ */
+public class ClusterStyleAction extends AbstractVisFactory {
+ /**
+ * Constructor.
+ */
+ public ClusterStyleAction() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(final VisualizerContext context, Object start) {
+ Hierarchy.Iter<Clustering<?>> it = VisualizationTree.filterResults(context, start, Clustering.class);
+ for(; it.valid(); it.advance()) {
+ final Clustering<?> c = it.get();
+ Hierarchy.Iter<SetStyleAction> it2 = VisualizationTree.filter(context, c, SetStyleAction.class);
+ if(it2.valid()) {
+ continue; // There already is a style button.
+ }
+ context.addVis(c, new SetStyleAction(c, context));
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ throw new AbortException("Should never be called.");
+ }
+
+ /**
+ * Action to use a clustering as {@link ClusterStylingPolicy}.
+ *
+ * @author Erich Schubert
+ */
+ private static final class SetStyleAction implements VisualizationMenuAction {
+ /**
+ * Clustering to use
+ */
+ private final Clustering<?> c;
+
+ /**
+ * Visualization context.
+ */
+ private final VisualizerContext context;
+
+ /**
+ * Constructor.
+ *
+ * @param c Clustering
+ * @param context Context
+ */
+ private SetStyleAction(Clustering<?> c, VisualizerContext context) {
+ this.c = c;
+ this.context = context;
+ }
+
+ @Override
+ public void activate() {
+ context.setStylingPolicy(new ClusterStylingPolicy(c, context.getStyleLibrary()));
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Use as Styling Policy";
+ }
+
+ @Override
+ public boolean enabled() {
+ StylingPolicy sp = context.getStylingPolicy();
+ if(!(sp instanceof ClusterStylingPolicy)) {
+ return true;
+ }
+ return ((ClusterStylingPolicy) sp).getClustering() != c;
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java
new file mode 100644
index 00000000..3d709ac1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/actions/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * Action-only "visualizers" that only produce menu entries.
+ */
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.actions; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java
new file mode 100644
index 00000000..34c648ba
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/AbstractHistogramVisualization.java
@@ -0,0 +1,70 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection1D;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+
+/**
+ * One-dimensional projected visualization.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.has Projection1D
+ */
+public abstract class AbstractHistogramVisualization extends AbstractVisualization {
+ /**
+ * The current projection
+ */
+ final protected Projection1D proj;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public AbstractHistogramVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height);
+ assert(proj instanceof Projection1D) : "Visualizer attached to wrong projection!";
+ this.proj = (Projection1D) proj;
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ super.resultChanged(current);
+ if(proj != null && current == proj) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java
new file mode 100644
index 00000000..80bbfeeb
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/ColoredHistogramVisualizer.java
@@ -0,0 +1,437 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram;
+import java.util.Arrays;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.DoubleVector;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.histogram.DoubleArrayStaticHistogram;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.HistogramProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Generates a SVG-Element containing a histogram representing the distribution
+ * of the database's objects.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ColoredHistogramVisualizer extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String CNAME = "Histograms";
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Number of bins to use in histogram.
+ */
+ private static final int DEFAULT_BINS = 80;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public ColoredHistogramVisualizer(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance<DoubleVector>(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNew(context, start, HistogramProjector.class, //
+ new VisualizationTree.Handler1<HistogramProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, HistogramProjector<?> p) {
+ // register self
+ final VisualizationTask task = new VisualizationTask(CNAME, context, p, p.getRelation(), ColoredHistogramVisualizer.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance
+ *
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.has NumberVector oneway - - visualizes
+ *
+ * @param <NV> Type of the DatabaseObject being visualized.
+ */
+ // FIXME: cache histogram instead of recomputing it?
+ public class Instance<NV extends NumberVector> extends AbstractHistogramVisualization {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String BIN = "bin";
+
+ /**
+ * The database we visualize
+ */
+ private Relation<NV> relation;
+
+ /**
+ * Sampling result
+ */
+ private SamplingResult sample;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.relation = task.getRelation();
+ this.sample = ResultUtil.getSamplingResult(relation);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ StyleLibrary style = context.getStyleLibrary();
+ double margin = style.getSize(StyleLibrary.MARGIN);
+ layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ double xsize = Projection.SCALE * getWidth() / getHeight();
+ double ysize = Projection.SCALE;
+
+ final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), xsize, ysize, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ // Styling policy
+ final StylingPolicy spol = context.getStylingPolicy();
+ final ClassStylingPolicy cspol;
+ if(spol instanceof ClassStylingPolicy) {
+ cspol = (ClassStylingPolicy) spol;
+ }
+ else {
+ cspol = null;
+ }
+ // TODO also use min style?
+ setupCSS(svgp, (cspol != null) ? cspol.getMaxStyle() : 0);
+
+ // Create histograms
+ final int off = (cspol != null) ? cspol.getMinStyle() : 0;
+ final int numc = (cspol != null) ? (cspol.getMaxStyle() - cspol.getMinStyle()) : 0;
+ DoubleMinMax minmax = new DoubleMinMax();
+ final double frac = 1. / relation.size(); // TODO: sampling?
+ final int cols = numc + 1;
+ DoubleArrayStaticHistogram histogram = new DoubleArrayStaticHistogram(settings.bins, -.5, .5, cols);
+
+ if(cspol != null) {
+ for(int snum = 0; snum < numc; snum++) {
+ double[] inc = new double[cols];
+ inc[0] = frac;
+ inc[snum + 1] = frac;
+ for(DBIDIter iter = cspol.iterateClass(snum + off); iter.valid(); iter.advance()) {
+ if(!sample.getSample().contains(iter)) {
+ continue; // TODO: can we test more efficiently than this?
+ }
+ try {
+ double pos = proj.fastProjectDataToRenderSpace(relation.get(iter)) * Projection.INVSCALE;
+ histogram.increment(pos, inc);
+ }
+ catch(ObjectNotFoundException e) {
+ // Ignore. The object was probably deleted from the database
+ }
+ }
+ }
+ }
+ else {
+ // Actual data distribution.
+ double[] inc = new double[cols];
+ inc[0] = frac;
+ for(DBIDIter iditer = relation.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ double pos = proj.fastProjectDataToRenderSpace(relation.get(iditer)) * Projection.INVSCALE;
+ histogram.increment(pos, inc);
+ }
+ }
+ // for scaling, get the maximum occurring value in the bins:
+ for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) {
+ for(double val : iter.getValue()) {
+ minmax.put(val);
+ }
+ }
+
+ LinearScale yscale = new LinearScale(0, minmax.getMax());
+ LinearScale xscale = new LinearScale(histogram.getCoverMinimum(), histogram.getCoverMaximum());
+
+ // Axis. TODO: Add an AxisVisualizer for this?
+ try {
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, yscale, 0, ysize, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+
+ // draw axes that are non-trivial
+ final int dimensionality = RelationUtil.dimensionality(relation);
+ final double[] vec = new double[dimensionality];
+ double orig = proj.fastProjectScaledToRender(vec);
+ for(int d = 0; d < dimensionality; d++) {
+ Arrays.fill(vec, 0.);
+ vec[d] = 1;
+ // projected endpoint of axis
+ double ax = proj.fastProjectScaledToRender(vec);
+ if(ax < orig || ax > orig) {
+ final double left = (orig / Projection.SCALE + 0.5) * xsize;
+ final double right = (ax / Projection.SCALE + 0.5) * xsize;
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getScale(d), left, ysize, right, ysize, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style);
+ }
+ }
+ }
+ catch(CSSNamingConflict e) {
+ LoggingUtil.exception("CSS class exception in axis class.", e);
+ }
+
+ // Visualizing
+ if(!settings.curves) {
+ for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) {
+ double lpos = xscale.getScaled(iter.getLeft());
+ double rpos = xscale.getScaled(iter.getRight());
+ double stack = 0.0;
+ final int start = numc > 0 ? 1 : 0;
+ for(int key = start; key < cols; key++) {
+ double val = yscale.getScaled(iter.getValue()[key]);
+ Element row = SVGUtil.svgRect(svgp.getDocument(), xsize * lpos, ysize * (1 - (val + stack)), xsize * (rpos - lpos), ysize * val);
+ stack = stack + val;
+ SVGUtil.addCSSClass(row, BIN + (off + key - 1));
+ layer.appendChild(row);
+ }
+ }
+ }
+ else {
+ double left = xscale.getScaled(histogram.getCoverMinimum());
+ double right = left;
+
+ SVGPath[] paths = new SVGPath[cols];
+ double[] lasty = new double[cols];
+ for(int i = 0; i < cols; i++) {
+ paths[i] = new SVGPath(xsize * left, ysize * 1);
+ lasty[i] = 0;
+ }
+
+ // draw histogram lines
+ for(DoubleArrayStaticHistogram.Iter iter = histogram.iter(); iter.valid(); iter.advance()) {
+ left = xscale.getScaled(iter.getLeft());
+ right = xscale.getScaled(iter.getRight());
+ for(int i = 0; i < cols; i++) {
+ double val = yscale.getScaled(iter.getValue()[i]);
+ if(lasty[i] > val || lasty[i] < val) {
+ paths[i].lineTo(xsize * left, ysize * (1 - lasty[i]));
+ paths[i].lineTo(xsize * left, ysize * (1 - val));
+ paths[i].lineTo(xsize * right, ysize * (1 - val));
+ lasty[i] = val;
+ }
+ }
+ }
+ // close and insert all lines.
+ for(int i = 0; i < cols; i++) {
+ if(lasty[i] != 0) {
+ paths[i].lineTo(xsize * right, ysize * (1 - lasty[i]));
+ }
+ paths[i].lineTo(xsize * right, ysize * 1);
+ Element elem = paths[i].makeElement(svgp);
+ SVGUtil.addCSSClass(elem, BIN + (off + i - 1));
+ layer.appendChild(elem);
+ }
+ }
+ svgp.updateStyleElement();
+ }
+
+ /**
+ * Generate the needed CSS classes.
+ *
+ * @param svgp Plot context
+ * @param numc Number of classes we need.
+ */
+ private void setupCSS(SVGPlot svgp, int numc) {
+ final StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ CSSClass allInOne = new CSSClass(svgp, BIN + -1);
+ if(!settings.curves) {
+ allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ allInOne.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 1.0);
+ }
+ else {
+ allInOne.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ allInOne.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ allInOne.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+ svgp.addCSSClassOrLogError(allInOne);
+
+ for(int clusterID = 0; clusterID < numc; clusterID++) {
+ CSSClass bin = new CSSClass(svgp, BIN + clusterID);
+
+ if(!settings.curves) {
+ bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(clusterID));
+ }
+ else {
+ bin.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(clusterID));
+ bin.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ bin.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+
+ svgp.addCSSClassOrLogError(bin);
+ }
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Flag to specify the "curves" rendering style.
+ *
+ * <p>
+ * Key: {@code -histogram.curves}
+ * </p>
+ */
+ public static final OptionID STYLE_CURVES_ID = new OptionID("projhistogram.curves", "Use curves instead of the stacked histogram style.");
+
+ /**
+ * Parameter to specify the number of bins to use in histogram.
+ *
+ * <p>
+ * Key: {@code -projhistogram.bins} Default: 80
+ * </p>
+ */
+ public static final OptionID HISTOGRAM_BINS_ID = new OptionID("projhistogram.bins", "Number of bins in the distribution histogram");
+
+ /**
+ * Internal storage of the curves flag.
+ */
+ protected boolean curves = false;
+
+ /**
+ * Number of bins to use in the histogram.
+ */
+ protected int bins = DEFAULT_BINS;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag curvesF = new Flag(STYLE_CURVES_ID);
+ if(config.grab(curvesF)) {
+ curves = curvesF.isTrue();
+ }
+ IntParameter binsP = new IntParameter(HISTOGRAM_BINS_ID, DEFAULT_BINS);
+ binsP.addConstraint(CommonConstraints.GREATER_THAN_ONE_INT);
+ if(config.grab(binsP)) {
+ bins = binsP.intValue();
+ }
+ }
+
+ @Override
+ protected ColoredHistogramVisualizer makeInstance() {
+ return new ColoredHistogramVisualizer(this);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java
new file mode 100755
index 00000000..d8ed4874
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/histogram/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Visualizers based on 1D projected histograms.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.histogram; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java
new file mode 100644
index 00000000..401e05e6
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/AbstractOPTICSVisualization.java
@@ -0,0 +1,94 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.OPTICSProjection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+
+/**
+ * Abstract base class for OPTICS visualizer
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses OPTICSProjection
+ */
+public abstract class AbstractOPTICSVisualization extends AbstractVisualization {
+ /**
+ * The plot
+ */
+ final protected OPTICSProjection optics;
+
+ /**
+ * Width of plot (in display units)
+ */
+ protected double plotwidth;
+
+ /**
+ * Height of plot (in display units)
+ */
+ protected double plotheight;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task.
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public AbstractOPTICSVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height);
+ this.optics = (OPTICSProjection) proj;
+ }
+
+ /**
+ * Produce a new layer element.
+ */
+ protected void makeLayerElement() {
+ plotwidth = StyleLibrary.SCALE;
+ plotheight = StyleLibrary.SCALE / optics.getOPTICSPlot(context).getRatio();
+ final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN);
+ layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), plotwidth, plotheight, margin * .5, margin * .5, margin * 1.5, margin * .5);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+ }
+
+ /**
+ * Access the raw cluster order
+ *
+ * @return Cluster order
+ */
+ protected ClusterOrder getClusterOrder() {
+ return optics.getResult();
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java
new file mode 100644
index 00000000..e1cd5c66
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSClusterVisualization.java
@@ -0,0 +1,233 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.OPTICSModel;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize the clusters and cluster hierarchy found by OPTICS on the OPTICS
+ * Plot.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class OPTICSClusterVisualization extends AbstractVisFactory {
+ /**
+ * The logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(OPTICSClusterVisualization.class);
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "OPTICS Cluster Ranges";
+
+ /**
+ * Constructor.
+ */
+ public OPTICSClusterVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class);
+ for(; it.valid(); it.advance()) {
+ OPTICSProjector p = it.get();
+ final Clustering<OPTICSModel> ocl = findOPTICSClustering(context, p.getResult());
+ if(ocl != null) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, ocl, null, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ context.addVis(p, task);
+ // TODO: use and react to style policy!
+ }
+ }
+ // TODO: also run when a new clustering is added, instead of just new
+ // projections?
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Find the first OPTICS clustering child of a result.
+ *
+ * @param context Result context
+ * @param start Result to start searching at
+ * @return OPTICS clustering
+ */
+ @SuppressWarnings("unchecked")
+ protected static Clustering<OPTICSModel> findOPTICSClustering(VisualizerContext context, Result start) {
+ Hierarchy.Iter<Clustering<?>> it1 = VisualizationTree.filterResults(context, start, Clustering.class);
+ for(; it1.valid(); it1.advance()) {
+ Clustering<?> clus = it1.get();
+ if(clus.getToplevelClusters().size() == 0) {
+ continue;
+ }
+ try {
+ Cluster<?> firstcluster = clus.getToplevelClusters().iterator().next();
+ if(firstcluster.getModel() instanceof OPTICSModel) {
+ return (Clustering<OPTICSModel>) clus;
+ }
+ }
+ catch(Exception e) {
+ // Empty clustering? Shouldn't happen.
+ LOG.warning("Clustering with no cluster detected.", e);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Clustering oneway - - «visualizes»
+ */
+ public class Instance extends AbstractOPTICSVisualization {
+ /**
+ * CSS class for markers
+ */
+ protected static final String CSS_BRACKET = "opticsBracket";
+
+ /**
+ * Our clustering
+ */
+ Clustering<OPTICSModel> clus;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.clus = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ makeLayerElement();
+ addCSSClasses();
+
+ ColorLibrary colors = context.getStyleLibrary().getColorSet(StyleLibrary.PLOT);
+ HashMap<Cluster<?>, String> colormap = new HashMap<>();
+ int cnum = 0;
+ for(Cluster<?> c : clus.getAllClusters()) {
+ colormap.put(c, colors.getColor(cnum));
+ cnum++;
+ }
+ drawClusters(clus, clus.iterToplevelClusters(), 1, colormap);
+ }
+
+ /**
+ * Recursively draw clusters
+ *
+ * @param clusters Current set of clusters
+ * @param depth Recursion depth
+ * @param colormap Color mapping
+ */
+ private void drawClusters(Clustering<OPTICSModel> clustering, Hierarchy.Iter<Cluster<OPTICSModel>> clusters, int depth, Map<Cluster<?>, String> colormap) {
+ final double scale = StyleLibrary.SCALE;
+
+ for(; clusters.valid(); clusters.advance()) {
+ Cluster<OPTICSModel> cluster = clusters.get();
+ try {
+ OPTICSModel model = cluster.getModel();
+ final double x1 = plotwidth * ((model.getStartIndex() + .25) / this.optics.getResult().size());
+ final double x2 = plotwidth * ((model.getEndIndex() + .75) / this.optics.getResult().size());
+ final double y = plotheight + depth * scale * 0.01;
+ Element e = svgp.svgLine(x1, y, x2, y);
+ SVGUtil.addCSSClass(e, CSS_BRACKET);
+ String color = colormap.get(cluster);
+ if(color != null) {
+ SVGUtil.setAtt(e, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_STROKE_PROPERTY + ":" + color);
+ }
+ layer.appendChild(e);
+ }
+ catch(ClassCastException e) {
+ LOG.warning("Expected OPTICSModel, got: " + cluster.getModel().getClass().getSimpleName());
+ }
+ // Descend
+ final Hierarchy.Iter<Cluster<OPTICSModel>> children = clustering.getClusterHierarchy().iterChildren(cluster);
+ if(children != null) {
+ drawClusters(clustering, children, depth + 1, colormap);
+ }
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ */
+ private void addCSSClasses() {
+ // Class for the markers
+ if(!svgp.getCSSClassManager().contains(CSS_BRACKET)) {
+ final CSSClass cls = new CSSClass(this, CSS_BRACKET);
+ final StyleLibrary style = context.getStyleLibrary();
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java
new file mode 100644
index 00000000..65e00430
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotCutVisualization.java
@@ -0,0 +1,298 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVG12Constants;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSCut;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizes a cut in an OPTICS Plot to select an Epsilon value and generate a
+ * new clustering result.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class OPTICSPlotCutVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "OPTICS Cut";
+
+ /**
+ * Constructor.
+ */
+ public OPTICSPlotCutVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class);
+ for(; it.valid(); it.advance()) {
+ OPTICSProjector p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener {
+ /**
+ * CSS-Styles
+ */
+ protected static final String CSS_LINE = "opticsPlotLine";
+
+ /**
+ * CSS-Styles
+ */
+ protected static final String CSS_EPSILON = "opticsPlotEpsilonValue";
+
+ /**
+ * The current epsilon value.
+ */
+ private double epsilon = 0.0;
+
+ /**
+ * Sensitive (clickable) area
+ */
+ private DragableArea eventarea = null;
+
+ /**
+ * The label element
+ */
+ private Element elemText = null;
+
+ /**
+ * The line element
+ */
+ private Element elementLine = null;
+
+ /**
+ * The drag handle element
+ */
+ private Element elementPoint = null;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void fullRedraw() {
+ incrementalRedraw();
+ }
+
+ @Override
+ public void incrementalRedraw() {
+ if(layer == null) {
+ makeLayerElement();
+ addCSSClasses();
+ }
+
+ // TODO make the number of digits configurable
+ final String label = (epsilon > 0.0) ? FormatUtil.NF4.format(epsilon) : "";
+ // compute absolute y-value of bar
+ final double yAct = getYFromEpsilon(epsilon);
+
+ if(elemText == null) {
+ elemText = svgp.svgText(StyleLibrary.SCALE * 1.05, yAct, label);
+ SVGUtil.setAtt(elemText, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_EPSILON);
+ layer.appendChild(elemText);
+ }
+ else {
+ elemText.setTextContent(label);
+ SVGUtil.setAtt(elemText, SVGConstants.SVG_Y_ATTRIBUTE, yAct);
+ }
+
+ // line and handle
+ if(elementLine == null) {
+ elementLine = svgp.svgLine(0, yAct, StyleLibrary.SCALE * 1.04, yAct);
+ SVGUtil.addCSSClass(elementLine, CSS_LINE);
+ layer.appendChild(elementLine);
+ }
+ else {
+ SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y1_ATTRIBUTE, yAct);
+ SVGUtil.setAtt(elementLine, SVG12Constants.SVG_Y2_ATTRIBUTE, yAct);
+ }
+ if(elementPoint == null) {
+ elementPoint = svgp.svgCircle(StyleLibrary.SCALE * 1.04, yAct, StyleLibrary.SCALE * 0.004);
+ SVGUtil.addCSSClass(elementPoint, CSS_LINE);
+ layer.appendChild(elementPoint);
+ }
+ else {
+ SVGUtil.setAtt(elementPoint, SVG12Constants.SVG_CY_ATTRIBUTE, yAct);
+ }
+
+ if(eventarea == null) {
+ eventarea = new DragableArea(svgp, StyleLibrary.SCALE, -StyleLibrary.SCALE * 0.01, //
+ StyleLibrary.SCALE * 0.1, plotheight + StyleLibrary.SCALE * 0.02, this);
+ layer.appendChild(eventarea.getElement());
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ eventarea.destroy();
+ }
+
+ /**
+ * Get epsilon from y-value
+ *
+ * @param y y-Value
+ * @return epsilon
+ */
+ protected double getEpsilonFromY(double y) {
+ OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
+ y = (y < 0) ? 0 : (y > plotheight) ? 1. : y / plotheight;
+ return optics.getOPTICSPlot(context).scaleFromPixel(y * opticsplot.getHeight());
+ }
+
+ /**
+ * Get y-value from epsilon
+ *
+ * @param epsilon epsilon
+ * @return y-Value
+ */
+ protected double getYFromEpsilon(double epsilon) {
+ OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
+ int h = opticsplot.getHeight();
+ double y = opticsplot.getScale().getScaled(epsilon, h - .5, .5) / (double) h * plotheight;
+ return (y < 0.) ? 0. : (y > plotheight) ? plotheight : y;
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint start, Event evt) {
+ epsilon = getEpsilonFromY(plotheight - start.getY());
+ // opvis.unsetEpsilonExcept(this);
+ svgp.requestRedraw(this.task, this);
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) {
+ if(inside) {
+ epsilon = getEpsilonFromY(plotheight - end.getY());
+ }
+ // opvis.unsetEpsilonExcept(this);
+ svgp.requestRedraw(this.task, this);
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint start, SVGPoint end, Event evt, boolean inside) {
+ if(inside) {
+ epsilon = getEpsilonFromY(plotheight - end.getY());
+ // opvis.unsetEpsilonExcept(this);
+
+ // FIXME: replace an existing optics cut result!
+ final ClusterOrder order = optics.getResult();
+ Clustering<Model> cl = OPTICSCut.makeOPTICSCut(order, epsilon);
+ order.addChildResult(cl);
+ }
+ svgp.requestRedraw(this.task, this);
+ return true;
+ }
+
+ /**
+ * Reset the epsilon value.
+ */
+ public void unsetEpsilon() {
+ epsilon = 0.0;
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ */
+ private void addCSSClasses() {
+ // Class for the epsilon-value
+ final StyleLibrary style = context.getStyleLibrary();
+ if(!svgp.getCSSClassManager().contains(CSS_EPSILON)) {
+ final CSSClass label = new CSSClass(svgp, CSS_EPSILON);
+ label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL));
+ label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL));
+ label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL));
+ svgp.addCSSClassOrLogError(label);
+ }
+ // Class for the epsilon cut line
+ if(!svgp.getCSSClassManager().contains(CSS_LINE)) {
+ final CSSClass lcls = new CSSClass(svgp, CSS_LINE);
+ lcls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.PLOT));
+ lcls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, 0.5 * style.getLineWidth(StyleLibrary.PLOT));
+ svgp.addCSSClassOrLogError(lcls);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java
new file mode 100644
index 00000000..7abc5609
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotSelectionVisualization.java
@@ -0,0 +1,360 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.dom.events.DOMMouseEvent;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Handle the marker in an OPTICS plot.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class OPTICSPlotSelectionVisualization extends AbstractVisFactory {
+ /**
+ * The logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(OPTICSPlotSelectionVisualization.class);
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "OPTICS Selection";
+
+ /**
+ * Input modes
+ *
+ * @apiviz.exclude
+ */
+ // TODO: Refactor all Mode copies into a shared class?
+ private enum Mode {
+ REPLACE, ADD, INVERT
+ }
+
+ /**
+ * Constructor.
+ */
+ public OPTICSPlotSelectionVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class);
+ for(; it.valid(); it.advance()) {
+ OPTICSProjector p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ *
+ * @apiviz.uses DBIDSelection oneway - 1 visualizes
+ */
+ public class Instance extends AbstractOPTICSVisualization implements DragableArea.DragListener {
+ /**
+ * CSS class for markers
+ */
+ protected static final String CSS_MARKER = "opticsPlotMarker";
+
+ /**
+ * CSS class for markers
+ */
+ protected static final String CSS_RANGEMARKER = "opticsPlotRangeMarker";
+
+ /**
+ * Element for the events
+ */
+ private Element etag;
+
+ /**
+ * Element for the marker
+ */
+ private Element mtag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ makeLayerElement();
+ addCSSClasses();
+
+ mtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ addMarker();
+
+ DragableArea drag = new DragableArea(svgp, 0 - plotwidth * 0.1, 0, plotwidth * 1.1, plotheight, this);
+ etag = drag.getElement();
+ // mtag first, etag must be the top Element
+ layer.appendChild(mtag);
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Add marker for the selected IDs to mtag
+ */
+ public void addMarker() {
+ ClusterOrder order = getClusterOrder();
+ // TODO: replace mtag!
+ DBIDSelection selContext = context.getSelection();
+ if(selContext != null) {
+ DBIDs selection = DBIDUtil.ensureSet(selContext.getSelectedIds());
+
+ final double width = plotwidth / order.size();
+ int begin = -1, j = 0;
+ for(DBIDIter it = order.iter(); it.valid(); it.advance(), j++) {
+ if(selection.contains(it)) {
+ if(begin == -1) {
+ begin = j;
+ }
+ }
+ else {
+ if(begin != -1) {
+ Element marker = addMarkerRect(begin * width, (j - begin) * width);
+ SVGUtil.addCSSClass(marker, CSS_MARKER);
+ mtag.appendChild(marker);
+ begin = -1;
+ }
+ }
+ }
+ // tail
+ if(begin != -1) {
+ Element marker = addMarkerRect(begin * width, (order.size() - begin) * width);
+ SVGUtil.addCSSClass(marker, CSS_MARKER);
+ mtag.appendChild(marker);
+ }
+ }
+ }
+
+ /**
+ * Create a rectangle as marker (Marker higher than plot!)
+ *
+ * @param x1 X-Value for the marker
+ * @param width Width of an entry
+ * @return SVG-Element svg-rectangle
+ */
+ public Element addMarkerRect(double x1, double width) {
+ return svgp.svgRect(x1, 0, width, plotheight);
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ ClusterOrder order = getClusterOrder();
+ int mouseActIndex = getSelectedIndex(order, startPoint);
+ if(mouseActIndex >= 0 && mouseActIndex < order.size()) {
+ double width = plotwidth / order.size();
+ double x1 = mouseActIndex * width;
+ Element marker = addMarkerRect(x1, width);
+ SVGUtil.setCSSClass(marker, CSS_RANGEMARKER);
+ mtag.appendChild(marker);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ ClusterOrder order = getClusterOrder();
+ int mouseDownIndex = getSelectedIndex(order, startPoint);
+ int mouseActIndex = getSelectedIndex(order, dragPoint);
+ final int begin = Math.max(Math.min(mouseDownIndex, mouseActIndex), 0);
+ final int end = Math.min(Math.max(mouseDownIndex, mouseActIndex), order.size());
+ double width = plotwidth / order.size();
+ double x1 = begin * width;
+ double x2 = (end * width) + width;
+ mtag.removeChild(mtag.getLastChild());
+ Element marker = addMarkerRect(x1, x2 - x1);
+ SVGUtil.setCSSClass(marker, CSS_RANGEMARKER);
+ mtag.appendChild(marker);
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ ClusterOrder order = getClusterOrder();
+ int mouseDownIndex = getSelectedIndex(order, startPoint);
+ int mouseActIndex = getSelectedIndex(order, dragPoint);
+ Mode mode = getInputMode(evt);
+ final int begin = Math.max(Math.min(mouseDownIndex, mouseActIndex), 0);
+ final int end = Math.min(Math.max(mouseDownIndex, mouseActIndex), order.size());
+ updateSelection(mode, begin, end);
+ return true;
+ }
+
+ /**
+ * Get the current input mode, on each mouse event.
+ *
+ * @param evt Mouse event.
+ * @return Input mode
+ */
+ private Mode getInputMode(Event evt) {
+ if(evt instanceof DOMMouseEvent) {
+ DOMMouseEvent domme = (DOMMouseEvent) evt;
+ // TODO: visual indication of mode possible?
+ if(domme.getShiftKey()) {
+ return Mode.ADD;
+ }
+ else if(domme.getCtrlKey()) {
+ return Mode.INVERT;
+ }
+ else {
+ return Mode.REPLACE;
+ }
+ }
+ // Default mode is replace.
+ return Mode.REPLACE;
+ }
+
+ /**
+ * Gets the Index of the ClusterOrderEntry where the event occurred
+ *
+ * @param order List of ClusterOrderEntries
+ * @param cPt clicked point
+ * @return Index of the object
+ */
+ private int getSelectedIndex(ClusterOrder order, SVGPoint cPt) {
+ int mouseActIndex = (int) ((cPt.getX() / plotwidth) * order.size());
+ return mouseActIndex;
+ }
+
+ /**
+ * Updates the selection for the given ClusterOrderEntry.
+ *
+ * @param mode Input mode
+ * @param begin first index to select
+ * @param end last index to select
+ */
+ protected void updateSelection(Mode mode, int begin, int end) {
+ ClusterOrder order = getClusterOrder();
+ if(begin < 0 || begin > end || end >= order.size()) {
+ LOG.warning("Invalid range in updateSelection: " + begin + " .. " + end);
+ return;
+ }
+
+ DBIDSelection selContext = context.getSelection();
+ HashSetModifiableDBIDs selection;
+ if(selContext == null || mode == Mode.REPLACE) {
+ selection = DBIDUtil.newHashSet();
+ }
+ else {
+ selection = DBIDUtil.newHashSet(selContext.getSelectedIds());
+ }
+
+ for(DBIDArrayIter it = order.iter().seek(begin); it.getOffset() <= end; it.advance()) {
+ if(mode == Mode.INVERT) {
+ if(!selection.contains(it)) {
+ selection.add(it);
+ }
+ else {
+ selection.remove(it);
+ }
+ }
+ else {
+ // In REPLACE and ADD, add objects.
+ // The difference was done before by not re-using the selection.
+ // Since we are using a set, we can just add in any case.
+ selection.add(it);
+ }
+ }
+ context.setSelection(new DBIDSelection(selection));
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ */
+ private void addCSSClasses() {
+ // Class for the markers
+ if(!svgp.getCSSClassManager().contains(CSS_MARKER)) {
+ final CSSClass cls = new CSSClass(this, CSS_MARKER);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.2");
+ svgp.addCSSClassOrLogError(cls);
+ }
+
+ // Class for the range marking
+ if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) {
+ final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER);
+ rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.2");
+ svgp.addCSSClassOrLogError(rcls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java
new file mode 100644
index 00000000..65d8ecf3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSPlotVisualizer.java
@@ -0,0 +1,143 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize an OPTICS result by constructing an OPTICS plot for it.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class OPTICSPlotVisualizer extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "OPTICS Plot";
+
+ /**
+ * Constructor.
+ */
+ public OPTICSPlotVisualizer() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class);
+ for(; it.valid(); it.advance()) {
+ OPTICSProjector p = it.get();
+ // Add plots, attach visualizer
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ // FIXME: task.setUpdates(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractOPTICSVisualization {
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ makeLayerElement();
+ // addCSSClasses();
+
+ OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
+ String ploturi = opticsplot.getSVGPlotURI();
+
+ Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, 0);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, 0);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, plotwidth);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, plotheight);
+ itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, ploturi);
+
+ layer.appendChild(itag);
+
+ LinearScale scale = opticsplot.getScale();
+ double y1 = plotheight * opticsplot.scaleToPixel(scale.getMin()) / opticsplot.getHeight();
+ double y2 = plotheight * opticsplot.scaleToPixel(scale.getMax()) / opticsplot.getHeight();
+ try {
+ final StyleLibrary style = context.getStyleLibrary();
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, scale, 0, y1, 0, y2, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, scale, plotwidth, y1, plotwidth, y2, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style);
+ }
+ catch(CSSNamingConflict e) {
+ LoggingUtil.exception("CSS naming conflict for axes on OPTICS plot", e);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java
new file mode 100644
index 00000000..ef8f6198
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/OPTICSSteepAreaVisualization.java
@@ -0,0 +1,211 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.Color;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.OPTICSXi;
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.OPTICSXi.SteepAreaResult;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDArrayIter;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.opticsplot.OPTICSPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize the steep areas found in an OPTICS plot
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class OPTICSSteepAreaVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "OPTICS Steep Areas";
+
+ /**
+ * Constructor.
+ */
+ public OPTICSSteepAreaVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ Hierarchy.Iter<OPTICSProjector> it = VisualizationTree.filter(context, result, OPTICSProjector.class);
+ for(; it.valid(); it.advance()) {
+ OPTICSProjector p = it.get();
+ final SteepAreaResult steep = findSteepAreaResult(p.getResult());
+ if(steep != null) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getResult(), null, this);
+ task.level = VisualizationTask.LEVEL_DATA + 1;
+ context.addVis(p, task);
+ context.addVis(steep, task);
+ }
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Find the OPTICS clustering child of a cluster order.
+ *
+ * @param co Cluster order
+ * @return OPTICS clustering
+ */
+ protected static OPTICSXi.SteepAreaResult findSteepAreaResult(ClusterOrder co) {
+ for(Hierarchy.Iter<Result> r = co.getHierarchy().iterChildren(co); r.valid(); r.advance()) {
+ if(OPTICSXi.SteepAreaResult.class.isInstance(r.get())) {
+ return (OPTICSXi.SteepAreaResult) r.get();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Instance
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses SteepAreaResult
+ */
+ public class Instance extends AbstractOPTICSVisualization {
+ /**
+ * CSS class for markers
+ */
+ protected static final String CSS_STEEP_UP = "opticsSteepUp";
+
+ /**
+ * CSS class for markers
+ */
+ protected static final String CSS_STEEP_DOWN = "opticsSteepDown";
+
+ /**
+ * Our clustering
+ */
+ OPTICSXi.SteepAreaResult areas;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.areas = findSteepAreaResult(this.optics.getResult());
+ }
+
+ @Override
+ public void fullRedraw() {
+ makeLayerElement();
+ addCSSClasses();
+
+ final OPTICSPlot opticsplot = optics.getOPTICSPlot(context);
+ final ClusterOrder co = getClusterOrder();
+ final int oheight = opticsplot.getHeight();
+ final double xscale = plotwidth / (double) co.size();
+ final double yscale = plotheight / (double) oheight;
+
+ DBIDArrayIter tmp = co.iter();
+
+ for(OPTICSXi.SteepArea area : areas) {
+ final int st = area.getStartIndex();
+ final int en = area.getEndIndex();
+ // Note: make sure we are using doubles!
+ final double x1 = (st + .25);
+ final double x2 = (en + .75);
+ final double y1 = opticsplot.scaleToPixel(co.getReachability(tmp.seek(st)));
+ final double y2 = opticsplot.scaleToPixel(co.getReachability(tmp.seek(en)));
+ Element e = svgp.svgLine(x1 * xscale, y1 * yscale, x2 * xscale, y2 * yscale);
+ if(area instanceof OPTICSXi.SteepDownArea) {
+ SVGUtil.addCSSClass(e, CSS_STEEP_DOWN);
+ }
+ else {
+ SVGUtil.addCSSClass(e, CSS_STEEP_UP);
+ }
+ layer.appendChild(e);
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ */
+ private void addCSSClasses() {
+ // Class for the markers
+ final StyleLibrary style = context.getStyleLibrary();
+ if(!svgp.getCSSClassManager().contains(CSS_STEEP_DOWN)) {
+ final CSSClass cls = new CSSClass(this, CSS_STEEP_DOWN);
+ Color color = SVGUtil.stringToColor(style.getColor(StyleLibrary.PLOT));
+ if(color == null) {
+ color = Color.BLACK;
+ }
+ color = new Color((int) (color.getRed() * 0.6), (int) (color.getGreen() * 0.6 + 0.4 * 256.), (int) (color.getBlue() * 0.6));
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGUtil.colorToString(color));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(CSS_STEEP_UP)) {
+ final CSSClass cls = new CSSClass(this, CSS_STEEP_UP);
+ Color color = SVGUtil.stringToColor(style.getColor(StyleLibrary.PLOT));
+ if(color == null) {
+ color = Color.BLACK;
+ }
+ color = new Color((int) (color.getRed() * 0.6 + 0.4 * 256.), (int) (color.getGreen() * 0.6), (int) (color.getBlue() * 0.6));
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGUtil.colorToString(color));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java
new file mode 100755
index 00000000..3f6f7e60
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/optics/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers that do work on OPTICS plots</p>
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.optics; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java
new file mode 100755
index 00000000..193a5f2b
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/package-info.java
@@ -0,0 +1,30 @@
+/**
+ * <p>Visualizers for various results</p>
+ *
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.utilities.datastructures.AnyMap
+ * @apiviz.exclude Visualization.Factory
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.gui.*
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers;
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java
new file mode 100644
index 00000000..72e9ccef
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/CircleSegmentsVisualizer.java
@@ -0,0 +1,746 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+import org.w3c.dom.events.MouseEvent;
+
+import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segment;
+import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segments;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultListener;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGCheckbox;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizer to draw circle segments of clusterings and enable interactive
+ * selection of segments. For "empty" segments, all related segments are
+ * selected instead, to visualize the differences.
+ *
+ * <p>
+ * Reference:<br />
+ * Evaluation of Clusterings – Metrics and Visual Support<br />
+ * Elke Achtert, Sascha Goldhofer, Hans-Peter Kriegel, Erich Schubert, Arthur
+ * Zimek<br />
+ * In: Proc. 28th International Conference on Data Engineering (ICDE) 2012
+ * </p>
+ *
+ * <p>
+ * Details on the experimental setup can be found at:
+ * <a href="http://elki.dbs.ifi.lmu.de/wiki/Examples/ClusterEvaluation" >wiki/
+ * Examples/ClusterEvaluation</a>
+ * </p>
+ *
+ * @author Sascha Goldhofer
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+@Reference(title = "Evaluation of Clusterings – Metrics and Visual Support", //
+authors = "Elke Achtert, Sascha Goldhofer, Hans-Peter Kriegel, Erich Schubert, Arthur Zimek", //
+booktitle = "Proc. 28th International Conference on Data Engineering (ICDE) 2012", //
+url = "http://dx.doi.org/10.1109/ICDE.2012.128")
+public class CircleSegmentsVisualizer extends AbstractVisFactory {
+ /**
+ * Class logger
+ */
+ private static final Logging LOG = Logging.getLogger(CircleSegmentsVisualizer.class);
+
+ /**
+ * CircleSegments visualizer name
+ */
+ private static final String NAME = "CircleSegments";
+
+ /**
+ * Constructor
+ */
+ public CircleSegmentsVisualizer() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<Segments> it1 = VisualizationTree.filterResults(context, start, Segments.class);
+ for(; it1.valid(); it1.advance()) {
+ Segments segmentResult = it1.get();
+ SegmentsStylingPolicy policy;
+ Hierarchy.Iter<SegmentsStylingPolicy> it = VisualizationTree.filter(context, segmentResult, SegmentsStylingPolicy.class);
+ if(it.valid()) {
+ policy = it.get();
+ }
+ else {
+ policy = new SegmentsStylingPolicy(segmentResult);
+ context.addVis(segmentResult, policy);
+ }
+ // create task for visualization
+ final VisualizationTask task = new VisualizationTask(NAME, context, policy, null, this);
+ task.reqwidth = 2.0;
+ task.reqheight = 2.0;
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(segmentResult, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Sascha Goldhofer
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Segments
+ * @apiviz.has SegmentsStylingPolicy
+ *
+ */
+ public class Instance extends AbstractVisualization implements ResultListener {
+ /** Minimum width (radian) of Segment */
+ private static final double SEGMENT_MIN_ANGLE = 0.01;
+
+ /** Gap (radian) between segments */
+ private static final double SEGMENT_MIN_SEP_ANGLE = 0.005;
+
+ /** Offset from center to first ring */
+ private static final double RADIUS_INNER = 0.04 * StyleLibrary.SCALE;
+
+ /** Margin between two rings */
+ private static final double RADIUS_DISTANCE = 0.01 * StyleLibrary.SCALE;
+
+ /** Radius of whole CircleSegments except selection border */
+ private static final double RADIUS_OUTER = 0.47 * StyleLibrary.SCALE;
+
+ /** Radius of highlight selection (outer ring) */
+ private static final double RADIUS_SELECTION = 0.02 * StyleLibrary.SCALE;
+
+ /**
+ * CSS class name for the clusterings.
+ */
+ private static final String CLR_CLUSTER_CLASS_PREFIX = "clusterSegment";
+
+ /**
+ * CSS border class of a cluster
+ */
+ public static final String CLR_BORDER_CLASS = "clusterBorder";
+
+ /**
+ * CSS hover class for clusters of hovered segment
+ */
+ public static final String CLR_UNPAIRED_CLASS = "clusterUnpaired";
+
+ /**
+ * CSS hover class of a segment cluster
+ */
+ public static final String CLR_HOVER_CLASS = "clusterHover";
+
+ /**
+ * CSS class of selected Segment
+ */
+ public static final String SEG_UNPAIRED_SELECTED_CLASS = "unpairedSegmentSelected";
+
+ /**
+ * Style prefix
+ */
+ public static final String STYLE = "segments";
+
+ /**
+ * Style for border lines
+ */
+ public static final String STYLE_BORDER = STYLE + ".border";
+
+ /**
+ * Style for hover effect
+ */
+ public static final String STYLE_HOVER = STYLE + ".hover";
+
+ /**
+ * First color for producing segment-cluster colors
+ */
+ public static final String STYLE_GRADIENT_FIRST = STYLE + ".cluster.first";
+
+ /**
+ * Second color for producing segment-cluster colors
+ */
+ public static final String STYLE_GRADIENT_SECOND = STYLE + ".cluster.second";
+
+ /**
+ * Segmentation of Clusterings
+ */
+ protected final Segments segments;
+
+ /**
+ * The two main layers
+ */
+ private Element visLayer, ctrlLayer;
+
+ /**
+ * Map to connect segments to their visual elements
+ */
+ public Map<Segment, List<Element>> segmentToElements = new HashMap<>();
+
+ /**
+ * Show unclustered Pairs in CircleSegments
+ */
+ boolean showUnclusteredPairs = false;
+
+ /**
+ * Styling policy
+ */
+ protected final SegmentsStylingPolicy policy;
+
+ /**
+ * Flag to disallow an incremental redraw
+ */
+ private boolean noIncrementalRedraw = true;
+
+ /**
+ * Constructor
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height);
+ policy = task.getResult();
+ segments = policy.segments;
+ // FIXME: handle this more generally.
+ policy.setStyleLibrary(context.getStyleLibrary());
+ addListeners();
+ }
+
+ public void toggleUnclusteredPairs(boolean show) {
+ noIncrementalRedraw = true;
+ showUnclusteredPairs = show;
+ svgp.requestRedraw(this.task, this);
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ super.resultChanged(current);
+ if(current == context.getStylingPolicy()) {
+ // When switching to a different policy, unhighlight segments.
+ if(context.getStylingPolicy() != policy) {
+ policy.deselectAllSegments();
+ }
+ }
+ }
+
+ @Override
+ public void incrementalRedraw() {
+ if(noIncrementalRedraw) {
+ super.incrementalRedraw();
+ }
+ else {
+ redrawSelection();
+ }
+ }
+
+ @Override
+ public void fullRedraw() {
+ LOG.debug("Full redraw");
+ noIncrementalRedraw = false; // Done that.
+
+ // initialize css (needs clusterSize!)
+ addCSSClasses(segments.getHighestClusterCount());
+
+ layer = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ visLayer = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ // Setup scaling for canvas: 0 to StyleLibrary.SCALE (usually 100 to avoid
+ // a
+ // Java drawing bug!)
+ String transform = SVGUtil.makeMarginTransform(task.reqwidth, task.reqheight, StyleLibrary.SCALE, StyleLibrary.SCALE, 0) + " translate(" + (StyleLibrary.SCALE * .5) + " " + (StyleLibrary.SCALE * .5) + ")";
+ visLayer.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+ ctrlLayer = svgp.svgElement(SVGConstants.SVG_G_TAG);
+
+ // and create svg elements
+ drawSegments();
+
+ //
+ // Build Interface
+ //
+ SVGCheckbox checkbox = new SVGCheckbox(showUnclusteredPairs, "Show unclustered pairs");
+ checkbox.addCheckBoxListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ toggleUnclusteredPairs(((SVGCheckbox) e.getSource()).isChecked());
+ }
+ });
+
+ // Add ring:clustering info
+ Element clrInfo = drawClusteringInfo();
+ Element c = checkbox.renderCheckBox(svgp, 1., 5. + FormatUtil.parseDouble(clrInfo.getAttribute(SVGConstants.SVG_HEIGHT_ATTRIBUTE)), 11);
+ ctrlLayer.appendChild(clrInfo);
+ ctrlLayer.appendChild(c);
+
+ ctrlLayer.setAttribute(SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "scale(" + (0.25 / StyleLibrary.SCALE) + ")");
+
+ layer.appendChild(visLayer);
+ layer.appendChild(ctrlLayer);
+ }
+
+ /**
+ * Define and add required CSS classes
+ */
+ protected void addCSSClasses(int maxClusterSize) {
+ StyleLibrary style = context.getStyleLibrary();
+
+ // Cluster separation lines
+ CSSClass cssReferenceBorder = new CSSClass(this.getClass(), CLR_BORDER_CLASS);
+ cssReferenceBorder.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_BORDER));
+ svgp.addCSSClassOrLogError(cssReferenceBorder);
+
+ // Hover effect for clusters
+ CSSClass cluster_hover = new CSSClass(this.getClass(), CLR_HOVER_CLASS);
+ // Note: !important is needed to override the regular color assignment
+ cluster_hover.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_HOVER) + " !important");
+ cluster_hover.setStatement(SVGConstants.SVG_CURSOR_TAG, SVGConstants.SVG_POINTER_VALUE);
+ svgp.addCSSClassOrLogError(cluster_hover);
+
+ // Unpaired cluster segment
+ CSSClass cluster_unpaired = new CSSClass(this.getClass(), CLR_UNPAIRED_CLASS);
+ cluster_unpaired.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getBackgroundColor(STYLE));
+ cluster_unpaired.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(cluster_unpaired);
+
+ // Selected unpaired cluster segment
+ CSSClass cluster_unpaired_s = new CSSClass(this.getClass(), SEG_UNPAIRED_SELECTED_CLASS);
+ cluster_unpaired_s.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, style.getColor(STYLE_HOVER) + " !important");
+ svgp.addCSSClassOrLogError(cluster_unpaired_s);
+
+ // create Color shades for clusters
+ String firstcol = style.getColor(STYLE_GRADIENT_FIRST);
+ String secondcol = style.getColor(STYLE_GRADIENT_SECOND);
+ String[] clusterColorShades = makeGradient(maxClusterSize, new String[] { firstcol, secondcol });
+
+ for(int i = 0; i < maxClusterSize; i++) {
+ CSSClass clusterClasses = new CSSClass(CircleSegmentsVisualizer.class, CLR_CLUSTER_CLASS_PREFIX + "_" + i);
+ clusterClasses.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, clusterColorShades[i]);
+ clusterClasses.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
+ svgp.addCSSClassOrLogError(clusterClasses);
+ }
+ }
+
+ /**
+ * Create the segments
+ */
+ private void drawSegments() {
+ final StyleLibrary style = context.getStyleLibrary();
+ final int clusterings = segments.getClusterings();
+
+ // Reinitialize
+ this.segmentToElements.clear();
+
+ double angle_pair = (MathUtil.TWOPI - (SEGMENT_MIN_SEP_ANGLE * segments.size())) / segments.getPairCount(showUnclusteredPairs);
+ final int pair_min_count = (int) Math.ceil(SEGMENT_MIN_ANGLE / angle_pair);
+
+ // number of segments needed to be resized
+ int cluster_min_count = 0;
+ for(Segment segment : segments) {
+ if(segment.getPairCount() <= pair_min_count) {
+ cluster_min_count++;
+ }
+ }
+
+ // update width of a pair
+ angle_pair = (MathUtil.TWOPI - (SEGMENT_MIN_SEP_ANGLE * segments.size() + cluster_min_count * SEGMENT_MIN_ANGLE)) / (segments.getPairCount(showUnclusteredPairs) - cluster_min_count);
+ double radius_delta = (RADIUS_OUTER - RADIUS_INNER - clusterings * RADIUS_DISTANCE) / clusterings;
+ double border_width = SEGMENT_MIN_SEP_ANGLE;
+
+ int refClustering = 0;
+ int refSegment = Segment.UNCLUSTERED;
+ double offsetAngle = 0.0;
+
+ for(final Segment segment : segments) {
+ long currentPairCount = segment.getPairCount();
+
+ // resize small segments if below minimum
+ double alpha = SEGMENT_MIN_ANGLE;
+ if(currentPairCount > pair_min_count) {
+ alpha = angle_pair * currentPairCount;
+ }
+
+ // ITERATE OVER ALL SEGMENT-CLUSTERS
+
+ ArrayList<Element> elems = new ArrayList<>(clusterings);
+ segmentToElements.put(segment, elems);
+ // draw segment for every clustering
+
+ for(int i = 0; i < clusterings; i++) {
+ double currentRadius = i * (radius_delta + RADIUS_DISTANCE) + RADIUS_INNER;
+
+ // Add border if the next segment is a different cluster in the
+ // reference clustering
+ if((refSegment != segment.get(refClustering)) && refClustering == i) {
+ Element border = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle - SEGMENT_MIN_SEP_ANGLE, border_width, currentRadius, RADIUS_OUTER - RADIUS_DISTANCE);
+ border.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_BORDER_CLASS);
+ visLayer.appendChild(border);
+
+ if(segment.get(refClustering) == Segment.UNCLUSTERED) {
+ refClustering = Math.min(refClustering + 1, clusterings - 1);
+ }
+ refSegment = segment.get(refClustering);
+ }
+
+ int cluster = segment.get(i);
+
+ // create ring segment
+ Element segelement = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle, alpha, currentRadius, currentRadius + radius_delta);
+ elems.add(segelement);
+
+ // MouseEvents on segment cluster
+ EventListener listener = new SegmentListenerProxy(segment, i);
+ EventTarget targ = (EventTarget) segelement;
+ targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, listener, false);
+ targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, listener, false);
+ targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, listener, false);
+
+ // Coloring based on clusterID
+ if(cluster >= 0) {
+ segelement.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_CLUSTER_CLASS_PREFIX + "_" + cluster);
+ }
+ // if its an unpaired cluster set color to white
+ else {
+ segelement.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_UNPAIRED_CLASS);
+ }
+
+ visLayer.appendChild(segelement);
+ }
+
+ //
+ // Add a extended strip for each segment to emphasis selection
+ // (easier to track thin segments and their color coding and
+ // differentiates them from cluster border lines)
+ //
+
+ double currentRadius = clusterings * (radius_delta + RADIUS_DISTANCE) + RADIUS_INNER;
+ Element extension = SVGUtil.svgCircleSegment(svgp, 0, 0, offsetAngle, alpha, currentRadius, currentRadius + RADIUS_SELECTION);
+ extension.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, CLR_UNPAIRED_CLASS);
+ elems.add(extension);
+
+ if(segment.isUnpaired()) {
+ if(policy.isSelected(segment)) {
+ SVGUtil.addCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS);
+ }
+ else {
+ // Remove highlight
+ SVGUtil.removeCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS);
+ }
+ }
+ else {
+ int idx = policy.indexOfSegment(segment);
+ if(idx >= 0) {
+ String color = style.getColorSet(StyleLibrary.PLOT).getColor(idx);
+ extension.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + color);
+ }
+ else {
+ // Remove styling
+ extension.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE);
+ }
+ }
+
+ visLayer.appendChild(extension);
+
+ // calculate angle for next segment
+ offsetAngle += alpha + SEGMENT_MIN_SEP_ANGLE;
+ }
+ }
+
+ private void redrawSelection() {
+ final StyleLibrary style = context.getStyleLibrary();
+ LOG.debug("Updating selection only.");
+ for(Entry<Segment, List<Element>> entry : segmentToElements.entrySet()) {
+ Segment segment = entry.getKey();
+ // The selection marker is the extra element in the list
+ Element extension = entry.getValue().get(segments.getClusterings());
+ if(segment.isUnpaired()) {
+ if(policy.isSelected(segment)) {
+ SVGUtil.addCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS);
+ }
+ else {
+ // Remove highlight
+ SVGUtil.removeCSSClass(extension, SEG_UNPAIRED_SELECTED_CLASS);
+ }
+ }
+ else {
+ int idx = policy.indexOfSegment(segment);
+ if(idx >= 0) {
+ String color = style.getColorSet(StyleLibrary.PLOT).getColor(idx);
+ extension.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_FILL_PROPERTY + ":" + color);
+ }
+ else {
+ // Remove styling
+ extension.removeAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a gradient over a set of colors
+ *
+ * @param shades number of colors in the gradient
+ * @param colors colors for the gradient
+ * @return array of colors for CSS
+ */
+ protected String[] makeGradient(int shades, String[] colors) {
+ if(shades <= colors.length) {
+ return colors;
+ }
+
+ // Convert SVG colors into AWT colors for math
+ Color[] cols = new Color[colors.length];
+ for(int i = 0; i < colors.length; i++) {
+ cols[i] = SVGUtil.stringToColor(colors[i]);
+ if(cols[i] == null) {
+ throw new AbortException("Error parsing color: " + colors[i]);
+ }
+ }
+
+ // Step size
+ double increment = (cols.length - 1.) / shades;
+
+ String[] colorShades = new String[shades];
+
+ for(int s = 0; s < shades; s++) {
+ final int ppos = Math.min((int) Math.floor(increment * s), cols.length);
+ final int npos = Math.min((int) Math.ceil(increment * s), cols.length);
+ if(ppos == npos) {
+ colorShades[s] = colors[ppos];
+ }
+ else {
+ Color prev = cols[ppos];
+ Color next = cols[npos];
+ final double mix = (increment * s - ppos) / (npos - ppos);
+ final int r = (int) ((1 - mix) * prev.getRed() + mix * next.getRed());
+ final int g = (int) ((1 - mix) * prev.getGreen() + mix * next.getGreen());
+ final int b = (int) ((1 - mix) * prev.getBlue() + mix * next.getBlue());
+ colorShades[s] = SVGUtil.colorToString(((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF));
+ }
+ }
+
+ return colorShades;
+ }
+
+ protected Element drawClusteringInfo() {
+ Element thumbnail = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+
+ // build thumbnail
+ int startRadius = 4;
+ int singleHeight = 12;
+ int margin = 4;
+ int radius = segments.getClusterings() * (singleHeight + margin) + startRadius;
+
+ SVGUtil.setAtt(thumbnail, SVGConstants.SVG_HEIGHT_ATTRIBUTE, radius);
+
+ for(int i = 0; i < segments.getClusterings(); i++) {
+ double innerRadius = i * singleHeight + margin * i + startRadius;
+ Element clr = SVGUtil.svgCircleSegment(svgp, radius - startRadius, radius - startRadius, Math.PI * 1.5, Math.PI * 0.5, innerRadius, innerRadius + singleHeight);
+ // FIXME: Use StyleLibrary
+ clr.setAttribute(SVGConstants.SVG_FILL_ATTRIBUTE, "#d4e4f1");
+ clr.setAttribute(SVGConstants.SVG_STROKE_ATTRIBUTE, "#a0a0a0");
+ clr.setAttribute(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "1.0");
+
+ String labelText = segments.getClusteringDescription(i);
+ Element label = svgp.svgText(radius + startRadius, radius - innerRadius - startRadius, labelText);
+ thumbnail.appendChild(label);
+
+ thumbnail.appendChild(clr);
+ }
+
+ return thumbnail;
+ }
+
+ protected void segmentHover(Segment segment, int ringid, boolean active) {
+ if(active) {
+ // abort if this are the unclustered pairs
+ if(segment.isNone()) {
+ return;
+ }
+ if(LOG.isDebugging()) {
+ LOG.debug("Hover on segment: " + segment + " unpaired: " + segment.isUnpaired());
+ }
+
+ if(!segment.isUnpaired()) {
+ //
+ // STANDARD CLUSTER SEGMENT
+ // highlight all ring segments in this clustering and this cluster
+ //
+ // highlight all corresponding ring Segments
+ for(Entry<Segment, List<Element>> entry : segmentToElements.entrySet()) {
+ Segment other = entry.getKey();
+ // Same cluster in same clustering?
+ if(other.get(ringid) != segment.get(ringid)) {
+ continue;
+ }
+ Element ringSegment = entry.getValue().get(ringid);
+ SVGUtil.addCSSClass(ringSegment, CLR_HOVER_CLASS);
+ }
+ }
+ else {
+ //
+ // UNPAIRED SEGMENT
+ // highlight all ring segments in this clustering responsible for
+ // unpaired
+ // segment
+ //
+ // get the paired segments corresponding to the unpaired segment
+ List<Segment> paired = segments.getPairedSegments(segment);
+
+ for(Segment other : paired) {
+ Element ringSegment = segmentToElements.get(other).get(ringid);
+ SVGUtil.addCSSClass(ringSegment, CLR_HOVER_CLASS);
+ }
+ }
+ }
+ else {
+ for(List<Element> elems : segmentToElements.values()) {
+ for(Element current : elems) {
+ SVGUtil.removeCSSClass(current, CLR_HOVER_CLASS);
+ }
+ }
+ }
+ }
+
+ protected void segmentClick(Segment segment, Event evt, boolean dblClick) {
+ MouseEvent mouse = (MouseEvent) evt;
+
+ // CTRL (add) pressed?
+ boolean ctrl = false;
+ if(mouse.getCtrlKey()) {
+ ctrl = true;
+ }
+
+ // Unselect others on double click
+ if(dblClick) {
+ policy.deselectAllSegments();
+ }
+ policy.select(segment, ctrl);
+ // update stylePolicy
+ context.setStylingPolicy(policy);
+ }
+
+ /**
+ * Proxy element to connect signals.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ private class SegmentListenerProxy implements EventListener {
+ /**
+ * Mouse double click time window in milliseconds
+ *
+ * TODO: does Batik have double click events?
+ */
+ public static final int EVT_DBLCLICK_DELAY = 350;
+
+ /**
+ * Segment we are attached to
+ */
+ private Segment id;
+
+ /**
+ * Segment ring we are
+ */
+ private int ringid;
+
+ /**
+ * For detecting double clicks.
+ */
+ private long lastClick = 0;
+
+ /**
+ * Constructor.
+ *
+ * @param id Segment id
+ * @param ringid Ring id
+ */
+ public SegmentListenerProxy(Segment id, int ringid) {
+ super();
+ this.id = id;
+ this.ringid = ringid;
+ }
+
+ @Override
+ public void handleEvent(Event evt) {
+ if(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE.equals(evt.getType())) {
+ segmentHover(id, ringid, true);
+ }
+ if(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE.equals(evt.getType())) {
+ segmentHover(id, ringid, false);
+ }
+ if(SVGConstants.SVG_CLICK_EVENT_TYPE.equals(evt.getType())) {
+ // Check Double Click
+ boolean dblClick = false;
+ long time = java.util.Calendar.getInstance().getTimeInMillis();
+ if(time - lastClick <= EVT_DBLCLICK_DELAY) {
+ dblClick = true;
+ }
+ lastClick = time;
+
+ segmentClick(id, evt, dblClick);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java
new file mode 100644
index 00000000..7e38d951
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/SegmentsStylingPolicy.java
@@ -0,0 +1,324 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segment;
+import de.lmu.ifi.dbs.elki.evaluation.clustering.pairsegments.Segments;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * Styling policy to communicate the segment selection to other visualizers.
+ *
+ * TODO: Remove "implements Result"
+ *
+ * @author Sascha Goldhofer
+ * @author Erich Schubert
+ */
+public class SegmentsStylingPolicy implements ClassStylingPolicy {
+ /**
+ * The segments we use for visualization
+ */
+ protected final Segments segments;
+
+ /**
+ * Selected segments
+ */
+ protected ArrayList<Segment> selectedSegments = new ArrayList<>();
+
+ /**
+ * Segments indirectly selected
+ */
+ protected TreeMap<Segment, Segment> indirectSelections = new TreeMap<>();
+
+ /**
+ * Not selected IDs that will be drawn in default colors.
+ */
+ protected ModifiableDBIDs unselectedObjects = DBIDUtil.newHashSet();
+
+ /**
+ * Color library (only used in compatibility mode)
+ */
+ // TODO: move to abstract super class?
+ ColorLibrary colorset = null;
+
+ /**
+ * Constructor.
+ *
+ * @param segments Segments
+ */
+ public SegmentsStylingPolicy(Segments segments) {
+ super();
+ this.segments = segments;
+
+ // get all selectable segments
+ for(Segment segment : segments) {
+ // store segmentID
+ if(!segment.isUnpaired()) {
+ // and store their get all objects
+ if(segment.getDBIDs() != null) {
+ unselectedObjects.addDBIDs(segment.getDBIDs());
+ }
+ }
+ }
+ }
+
+ /**
+ * Assign the style library, for compatibility color styling.
+ *
+ * FIXME: handle this more generally
+ *
+ * @param style Style library
+ */
+ public void setStyleLibrary(StyleLibrary style) {
+ this.colorset = style.getColorSet(StyleLibrary.PLOT);
+ }
+
+ /**
+ * Test whether a segment is selected.
+ *
+ * @param segment Segment to test
+ * @return true when selected
+ */
+ public boolean isSelected(Segment segment) {
+ return selectedSegments.contains(segment) || indirectSelections.containsValue(segment);
+ }
+
+ @Override
+ public int getStyleForDBID(DBIDRef id) {
+ Iterator<Segment> s = selectedSegments.iterator();
+ for(int i = 0; s.hasNext(); i++) {
+ Segment seg = s.next();
+ DBIDs ids = seg.getDBIDs();
+ if(ids != null && ids.contains(id)) {
+ return i;
+ }
+ }
+ return -2;
+ }
+
+ @Override
+ public int getColorForDBID(DBIDRef id) {
+ int style = getStyleForDBID(id);
+ if(colorset != null) {
+ // FIXME: add caching
+ return SVGUtil.stringToColor(colorset.getColor(style)).getRGB();
+ }
+ else {
+ return 0;
+ }
+ }
+
+ @Override
+ // -2=grau, -1=schwarz, 0+=farben
+ public int getMinStyle() {
+ return -2;
+ }
+
+ @Override
+ public int getMaxStyle() {
+ return selectedSegments.size();
+ }
+
+ @Override
+ public DBIDIter iterateClass(int cnum) {
+ // unselected
+ if(cnum == -2) {
+ return unselectedObjects.iter();
+ }
+ else if(cnum == -1) {
+ return DBIDUtil.EMPTYDBIDS.iter();
+ }
+ // colors
+ DBIDs ids = selectedSegments.get(cnum).getDBIDs();
+ return (ids != null) ? ids.iter() : DBIDUtil.EMPTYDBIDS.iter();
+ }
+
+ @Override
+ public int classSize(int cnum) {
+ // unselected
+ if(cnum == -2) {
+ return unselectedObjects.size();
+ }
+ else if(cnum == -1) {
+ return 0;
+ }
+ // colors
+ DBIDs ids = selectedSegments.get(cnum).getDBIDs();
+ return (ids != null) ? ids.size() : 0;
+ }
+
+ /**
+ * Adds or removes the given segment to the selection. Depending on the
+ * clustering and cluster selected and the addToSelection option given, the
+ * current selection will be modified. This method is called by clicking on a
+ * segment and ring and the CTRL-button status.
+ *
+ * Adding selections does only work on the same clustering and cluster, else a
+ * new selection will be added.
+ *
+ * @param segment the selected element representing a segment ring (specific
+ * clustering)
+ * @param addToSelection flag for adding segment to current selection
+ */
+ public void select(Segment segment, boolean addToSelection) {
+ // abort if segment represents pairs inNone. Would select all segments...
+ if(segment.isNone()) {
+ return;
+ }
+ if(!addToSelection) {
+ deselectAllSegments();
+ }
+
+ // get selected segments
+ if(segment.isUnpaired()) {
+ // check if all segments are selected
+ if(addToSelection) {
+ boolean allSegmentsSelected = true;
+ for(Segment other : segments.getPairedSegments(segment)) {
+ if(!isSelected(other)) {
+ allSegmentsSelected = false;
+ break;
+ }
+ }
+
+ // if all are selected, deselect all
+ if(allSegmentsSelected) {
+ deselectSegment(segment);
+ return;
+ }
+ }
+ if(isSelected(segment)) {
+ deselectSegment(segment);
+ }
+ else {
+ selectSegment(segment);
+ }
+ }
+ else {
+ // an object segment was selected
+ if(isSelected(segment)) {
+ deselectSegment(segment);
+ }
+ else {
+ selectSegment(segment);
+ }
+ }
+ }
+
+ /**
+ * Deselect all currently selected segments
+ */
+ public void deselectAllSegments() {
+ while(selectedSegments.size() > 0) {
+ deselectSegment(selectedSegments.get(selectedSegments.size() - 1));
+ }
+ }
+
+ /**
+ * Deselect a segment
+ *
+ * @param segment Segment to deselect
+ */
+ protected void deselectSegment(Segment segment) {
+ if(segment.isUnpaired()) {
+ ArrayList<Segment> remove = new ArrayList<>();
+ // remove all object segments associated with unpaired segment from
+ // selection list
+ for(Entry<Segment, Segment> entry : indirectSelections.entrySet()) {
+ if(entry.getValue() == segment) {
+ remove.add(entry.getKey());
+ }
+ }
+
+ for(Segment other : remove) {
+ indirectSelections.remove(other);
+ deselectSegment(other);
+ }
+ }
+ else {
+ // check if deselected object Segment has a unpaired segment highlighted
+ Segment unpaired = indirectSelections.get(segment);
+ if(unpaired != null) {
+ // remove highlight
+ deselectSegment(unpaired);
+ }
+ if(selectedSegments.remove(segment)) {
+ if(segment.getDBIDs() != null) {
+ unselectedObjects.addDBIDs(segment.getDBIDs());
+ }
+ }
+ }
+ }
+
+ /**
+ * Select a segment
+ *
+ * @param segment Segment to select
+ */
+ protected void selectSegment(Segment segment) {
+ if(segment.isUnpaired()) {
+ // remember selected unpaired segment
+ for(Segment other : segments.getPairedSegments(segment)) {
+ indirectSelections.put(other, segment);
+ selectSegment(other);
+ }
+ }
+ else {
+ if(!selectedSegments.contains(segment)) {
+ selectedSegments.add(segment);
+ if(segment.getDBIDs() != null) {
+ unselectedObjects.removeDBIDs(segment.getDBIDs());
+ }
+ }
+ }
+ }
+
+ /**
+ * Get the index of a selected segment.
+ *
+ * @param segment Segment to find
+ * @return Index, or -1
+ */
+ public int indexOfSegment(Segment segment) {
+ return selectedSegments.indexOf(segment);
+ }
+
+ @Override
+ public String getMenuName() {
+ return "Pair segments styling policy";
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java
new file mode 100755
index 00000000..8fb5e781
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/pairsegments/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for inspecting cluster differences using pair counting segments.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java
new file mode 100644
index 00000000..3ac02de2
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AbstractParallelVisualization.java
@@ -0,0 +1,170 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+
+/**
+ * Abstract base class for parallel visualizations.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @param <NV> Data type in relation
+ */
+public abstract class AbstractParallelVisualization<NV> extends AbstractVisualization {
+ /**
+ * The current projection
+ */
+ final protected ProjectionParallel proj;
+
+ /**
+ * The representation we visualize
+ */
+ final protected Relation<NV> relation;
+
+ /**
+ * margin
+ */
+ final double[] margins;
+
+ /**
+ * Space between two axes
+ */
+ protected double axsep;
+
+ /**
+ * viewbox size
+ */
+ final double[] size;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public AbstractParallelVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height);
+ this.proj = (ProjectionParallel) proj;
+ this.relation = task.getRelation();
+ margins = new double[] { 0.05 * StyleLibrary.SCALE, 0.1 * StyleLibrary.SCALE, 0.05 * StyleLibrary.SCALE, 0.1 * StyleLibrary.SCALE };
+ double ratio = (width * StyleLibrary.SCALE - margins[0] - margins[2]) / (height * StyleLibrary.SCALE - margins[1] - margins[3]);
+ size = new double[] { ratio * StyleLibrary.SCALE, StyleLibrary.SCALE };
+ recalcAxisPositions();
+ }
+
+ @Override
+ public void fullRedraw() {
+ this.layer = setupCanvas(svgp, this.proj, getWidth(), getHeight());
+ }
+
+ /**
+ * Utility function to setup a canvas element for the visualization.
+ *
+ * @param svgp Plot element
+ * @param proj Projection to use
+ * @param width Width
+ * @param height Height
+ * @return wrapper element with appropriate view box.
+ */
+ public Element setupCanvas(SVGPlot svgp, ProjectionParallel proj, double width, double height) {
+ Element layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(width, height, size[0], size[1], margins[0], margins[1], margins[2], margins[3]);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+ return layer;
+ }
+
+ /**
+ * Get width of main canvas.
+ *
+ * @return Width
+ */
+ protected double getSizeX() {
+ return size[0];
+ }
+
+ protected double getSizeY() {
+ return size[1];
+ }
+
+ protected double getMarginLeft() {
+ return margins[0];
+ }
+
+ protected double getMarginTop() {
+ return margins[1];
+ }
+
+ /**
+ * Distance between axes.
+ *
+ * @return Axis separation
+ */
+ protected double getAxisSep() {
+ return axsep;
+ }
+
+ /**
+ * Recalculate axis positions, in particular after projection changes.
+ */
+ private void recalcAxisPositions() {
+ axsep = size[0] / (proj.getVisibleDimensions() - 1.);
+ }
+
+ /**
+ * Get the position of visible axis d
+ *
+ * @param d Visible axis number
+ * @return Position
+ */
+ protected double getVisibleAxisX(double d) {
+ return d * axsep;
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem item) {
+ super.visualizationChanged(item);
+ if(item == proj) {
+ recalcAxisPositions();
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java
new file mode 100644
index 00000000..ce24ad2a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisReorderVisualization.java
@@ -0,0 +1,300 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGArrow;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Interactive SVG-Elements for reordering the axes.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class AxisReorderVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Dimension Ordering Tool";
+
+ /**
+ * Constructor, adhering to
+ */
+ public AxisReorderVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisReorderVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance for a particular plot.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector> {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String SELECTDIMENSIONORDER = "SelectDimensionOrder";
+
+ /**
+ * CSS class for a tool button
+ */
+ public static final String SDO_BUTTON = "DObutton";
+
+ /**
+ * CSS class for a button border
+ */
+ public static final String SDO_BORDER = "DOborder";
+
+ /**
+ * CSS class for a button cross
+ */
+ public static final String SDO_ARROW = "DOarrow";
+
+ /**
+ * Currently selected dimension. Use -1 to not have a dimension selected.
+ */
+ private int selecteddim = -1;
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+ final int dim = proj.getVisibleDimensions();
+
+ final double controlsize = 0.025 * getSizeY();
+ final double buttonsize = 0.75 * controlsize;
+ final double padding = 0.125 * controlsize;
+ final double arrowsize = .75 * buttonsize;
+ final double ypos = getSizeY() + getMarginTop() * .5 + controlsize;
+ final double spacing = 0.9 * controlsize;
+
+ Element back = svgp.svgRect(-controlsize * .5, ypos, getSizeX() + controlsize, controlsize);
+ SVGUtil.addCSSClass(back, SELECTDIMENSIONORDER);
+ layer.appendChild(back);
+
+ if(selecteddim < 0) {
+ // Nothing selected
+ for(int i = 0; i < dim; i++) {
+ final double xpos = getVisibleAxisX(i);
+ if(i > 0) {
+ Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.LEFT, xpos - spacing, ypos + controlsize * .5, arrowsize);
+ SVGUtil.addCSSClass(arrow, SDO_ARROW);
+ layer.appendChild(arrow);
+ Element button = svgp.svgRect(xpos - spacing - buttonsize * .5, ypos + padding, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(button, SDO_BUTTON);
+ addEventListener(button, i, SVGArrow.LEFT);
+ layer.appendChild(button);
+ }
+ {
+ Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.DOWN, xpos, ypos + controlsize * .5, arrowsize);
+ SVGUtil.addCSSClass(arrow, SDO_ARROW);
+ layer.appendChild(arrow);
+ Element button = svgp.svgRect(xpos - buttonsize * .5, ypos + padding, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(button, SDO_BUTTON);
+ addEventListener(button, i, SVGArrow.DOWN);
+ layer.appendChild(button);
+ }
+ if(i < dim - 1) {
+ Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.RIGHT, xpos + spacing, ypos + controlsize * .5, arrowsize);
+ SVGUtil.addCSSClass(arrow, SDO_ARROW);
+ layer.appendChild(arrow);
+ Element button = svgp.svgRect(xpos + spacing - buttonsize * .5, ypos + padding, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(button, SDO_BUTTON);
+ addEventListener(button, i, SVGArrow.RIGHT);
+ layer.appendChild(button);
+ }
+ }
+ }
+ else {
+ for(int i = 0; i < dim; i++) {
+ {
+ Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.DOWN, getVisibleAxisX(i), ypos + controlsize * .5, arrowsize);
+ SVGUtil.addCSSClass(arrow, SDO_ARROW);
+ layer.appendChild(arrow);
+ Element button = svgp.svgRect(getVisibleAxisX(i) - buttonsize * .5, ypos + padding, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(button, SDO_BUTTON);
+ addEventListener(button, i, SVGArrow.DOWN);
+ layer.appendChild(button);
+ }
+ if(i > 0.) {
+ Element arrow = SVGArrow.makeArrow(svgp, SVGArrow.UP, getVisibleAxisX(i - .5), ypos + controlsize * .5, arrowsize);
+ SVGUtil.addCSSClass(arrow, SDO_ARROW);
+ layer.appendChild(arrow);
+ Element button = svgp.svgRect(getVisibleAxisX(i - .5) - buttonsize * .5, ypos + padding, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(button, SDO_BUTTON);
+ addEventListener(button, i, SVGArrow.UP);
+ layer.appendChild(button);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add an event listener to the Element
+ *
+ * @param tag Element to add the listener
+ * @param i represented axis
+ */
+ private void addEventListener(final Element tag, final int i, final SVGArrow.Direction j) {
+ EventTarget targ = (EventTarget) tag;
+ targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ if(selecteddim < 0) {
+ switch(j){
+ case DOWN:
+ selecteddim = i;
+ break;
+ case LEFT:
+ int prev = i - 1;
+ while(prev >= 0 && !proj.isAxisVisible(prev)) {
+ prev -= 1;
+ }
+ proj.swapAxes(i, prev);
+ break;
+ case RIGHT:
+ int next = i + 1;
+ while(next < proj.getInputDimensionality() - 1 && !proj.isAxisVisible(next)) {
+ next += 1;
+ }
+ proj.swapAxes(i, next);
+ break;
+ default:
+ break;
+ }
+ }
+ else {
+ switch(j){
+ case DOWN:
+ proj.swapAxes(selecteddim, i);
+ selecteddim = -1;
+ break;
+ case UP:
+ if(selecteddim != i) {
+ proj.moveAxis(selecteddim, i);
+ }
+ selecteddim = -1;
+ break;
+ default:
+ break;
+ }
+ }
+ // Notify
+ context.visChanged(proj);
+ }
+ }, false);
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ if(!svgp.getCSSClassManager().contains(SELECTDIMENSIONORDER)) {
+ CSSClass cls = new CSSClass(this, SELECTDIMENSIONORDER);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SDO_BORDER)) {
+ CSSClass cls = new CSSClass(this, SDO_BORDER);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_GREY_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 3.0);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SDO_BUTTON)) {
+ CSSClass cls = new CSSClass(this, SDO_BUTTON);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.01);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE);
+ cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SDO_ARROW)) {
+ CSSClass cls = new CSSClass(this, SDO_ARROW);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_DARKGREY_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 3);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java
new file mode 100644
index 00000000..1e12f87c
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/AxisVisibilityVisualization.java
@@ -0,0 +1,298 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Layer for controlling axis visbility in parallel coordinates.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class AxisVisibilityVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Axis Visibility";
+
+ /**
+ * Constructor, adhering to
+ */
+ public AxisVisibilityVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisVisibilityVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance for a particular data set.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector> {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String SELECTAXISVISIBILITY = "SelectAxisVisibility";
+
+ /**
+ * CSS class for a tool button
+ */
+ public static final String SAV_BUTTON = "SAVbutton";
+
+ /**
+ * CSS class for a button border
+ */
+ public static final String SAV_BORDER = "SAVborder";
+
+ /**
+ * CSS class for a button cross
+ */
+ public static final String SAV_CROSS = "SAVbuttoncross";
+
+ /**
+ * Active area size
+ */
+ double controlsize;
+
+ /**
+ * Button size
+ */
+ double buttonsize;
+
+ /**
+ * Vertical position
+ */
+ double ypos;
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final int dim = proj.getInputDimensionality();
+ addCSSClasses(svgp);
+ controlsize = 0.025 * getSizeY();
+ buttonsize = 0.75 * controlsize;
+ ypos = getSizeY() + getMarginTop() * .5;
+
+ // Background
+ Element back = svgp.svgRect(-controlsize * .5, ypos - controlsize * .5 + buttonsize * .5, getSizeX() + controlsize, controlsize);
+ SVGUtil.addCSSClass(back, SELECTAXISVISIBILITY);
+ layer.appendChild(back);
+
+ // Previous visible dimension.
+ for(int i = 0, hidden = 0, vax = 0; i <= dim; i++) {
+ if(i < dim && !proj.isAxisVisible(i)) {
+ hidden += 1;
+ continue;
+ }
+ // Add button for showing hidden dimensions:
+ if(hidden > 0) {
+ makeButtonsForHidden(vax, i - hidden, hidden, dim);
+ hidden = 0;
+ }
+ // Add buttons for current dimension
+ if(i < dim) {
+ makeButtonForVisible(i, vax);
+ vax++;
+ }
+ }
+ }
+
+ /**
+ * Make a button for a visible axis
+ *
+ * @param anum Axis number
+ * @param apos Axis position in plot
+ */
+ protected void makeButtonForVisible(int anum, int apos) {
+ final double xpos = getVisibleAxisX(apos) - buttonsize * .5;
+
+ Element border = svgp.svgRect(xpos, ypos, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(border, SAV_BORDER);
+ layer.appendChild(border);
+
+ SVGPath path = new SVGPath();
+ final double qs = controlsize * .5;
+ final double cs = controlsize * .125;
+ path.moveTo(xpos + cs, ypos + cs);
+ path.relativeLineTo(qs, qs);
+ path.relativeMoveTo(0, -qs);
+ path.relativeLineTo(-qs, qs);
+ Element cross = path.makeElement(svgp);
+ SVGUtil.addCSSClass(cross, SAV_CROSS);
+ layer.appendChild(cross);
+
+ Element rect = svgp.svgRect(xpos, ypos, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(rect, SAV_BUTTON);
+ addEventListener(rect, anum);
+ layer.appendChild(rect);
+ }
+
+ /**
+ * Insert buttons for hidden dimensions.
+ *
+ * @param vnum Column number (= next visible axis number)
+ * @param first First invisible axis
+ * @param count Number of invisible axes
+ * @param dim Number of total dimensions
+ */
+ private void makeButtonsForHidden(final int vnum, final int first, final int count, final int dim) {
+ final double lpos, rpos;
+ if(vnum == 0) {
+ lpos = -getMarginLeft();
+ }
+ else {
+ lpos = getVisibleAxisX(vnum - 1);
+ }
+ if(first + count + 1 >= dim) {
+ rpos = getWidth() + getMarginLeft();
+ }
+ else {
+ rpos = getVisibleAxisX(vnum);
+ }
+ final double step = (rpos - lpos) / (count + 1.0);
+ for(int j = 0; j < count; j++) {
+ final double apos = lpos + (j + 1) * step - buttonsize * .5;
+ Element border = svgp.svgRect(apos, ypos, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(border, SAV_BORDER);
+ layer.appendChild(border);
+
+ Element rect = svgp.svgRect(apos, ypos, buttonsize, buttonsize);
+ SVGUtil.addCSSClass(rect, SAV_BUTTON);
+ addEventListener(rect, first + j);
+ layer.appendChild(rect);
+ }
+ }
+
+ /**
+ * Add an event listener to the Element
+ *
+ * @param tag Element to add the listener
+ * @param axis Axis number (including hidden axes)
+ */
+ private void addEventListener(final Element tag, final int axis) {
+ EventTarget targ = (EventTarget) tag;
+ targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ if(proj.getVisibleDimensions() > 2) {
+ proj.toggleAxisVisible(axis);
+ context.visChanged(proj);
+ }
+ }
+ }, false);
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ if(!svgp.getCSSClassManager().contains(SELECTAXISVISIBILITY)) {
+ CSSClass cls = new CSSClass(this, SELECTAXISVISIBILITY);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLUE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SAV_BORDER)) {
+ CSSClass cls = new CSSClass(this, SAV_BORDER);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_GREY_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SAV_BUTTON)) {
+ CSSClass cls = new CSSClass(this, SAV_BUTTON);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.01);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE);
+ cls.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(SAV_CROSS)) {
+ CSSClass cls = new CSSClass(this, SAV_CROSS);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .75);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java
new file mode 100644
index 00000000..6b3671d1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/BoundingBoxVisualization.java
@@ -0,0 +1,254 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Draw spatial objects (except vectors!)
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// TODO: draw filled instead?
+public class BoundingBoxVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Spatial objects";
+
+ /**
+ * Constructor.
+ */
+ public BoundingBoxVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ if(!TypeUtil.SPATIAL_OBJECT.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), BoundingBoxVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY | VisualizationTask.ON_SAMPLE);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance for a particular data set.
+ *
+ * @author Robert Rödler
+ */
+ public class Instance extends AbstractParallelVisualization<SpatialComparable>implements DataStoreListener {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String DATALINE = "Databox";
+
+ /**
+ * Sample we visualize.
+ */
+ private SamplingResult sample;
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.sample = ResultUtil.getSamplingResult(relation);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final DBIDs sam = sample.getSample();
+ StylingPolicy sp = context.getStylingPolicy();
+ final StyleLibrary style = context.getStyleLibrary();
+ final LineStyleLibrary lines = style.lines();
+ final double width = .5 * style.getLineWidth(StyleLibrary.PLOT) * MathUtil.min(.5, 2. / MathUtil.log2(sam.size()));
+ if(sp instanceof ClassStylingPolicy) {
+ ClassStylingPolicy csp = (ClassStylingPolicy) sp;
+ final int min = csp.getMinStyle();
+ String[] keys = new String[csp.getMaxStyle() - min];
+ for(int c = min; c < csp.getMaxStyle(); c++) {
+ String key = keys[c - min] = DATALINE + "_" + c;
+ if(!svgp.getCSSClassManager().contains(key)) {
+ CSSClass cls = new CSSClass(this, key);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ lines.formatCSSClass(cls, c, width);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
+ final int c = csp.getStyleForDBID(iter) + min;
+ if(c < 0) {
+ continue; // No style. Display differently?
+ }
+ Element line = drawLine(iter);
+ if(line == null) {
+ continue;
+ }
+ SVGUtil.addCSSClass(line, keys[c]);
+ layer.appendChild(line);
+ }
+ }
+ else {
+ // No classes available, but individually colored
+ if(!svgp.getCSSClassManager().contains(DATALINE)) {
+ CSSClass cls = new CSSClass(this, DATALINE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ lines.formatCSSClass(cls, -1, width);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ StringBuilder buf = new StringBuilder().append(SVGConstants.CSS_STROKE_PROPERTY).append(':');
+ final int prefix = buf.length();
+ for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
+ Element line = drawLine(iter);
+ if(line == null) {
+ continue;
+ }
+ SVGUtil.addCSSClass(line, DATALINE);
+ // assign color
+ buf.delete(prefix, buf.length());
+ buf.append(SVGUtil.colorToString(sp.getColorForDBID(iter)));
+ line.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, buf.toString());
+ layer.appendChild(line);
+ }
+ }
+ svgp.updateStyleElement();
+ }
+
+ /**
+ * Draw a single line.
+ *
+ * @param iter Object reference
+ * @return Line element
+ */
+ private Element drawLine(DBIDRef iter) {
+ SVGPath path = new SVGPath();
+ final SpatialComparable obj = relation.get(iter);
+ final int dims = proj.getVisibleDimensions();
+ boolean drawn = false;
+ int valid = 0; /* run length of valid values */
+ double prevpos = Double.NaN;
+ for(int i = 0; i < dims; i++) {
+ final int d = proj.getDimForAxis(i);
+ double minPos = proj.fastProjectDataToRenderSpace(obj.getMin(d), i);
+ // NaN handling:
+ if(minPos != minPos) {
+ valid = 0;
+ continue;
+ }
+ ++valid;
+ if(valid > 1) {
+ if(valid == 2) {
+ path.moveTo(getVisibleAxisX(d - 1), prevpos);
+ }
+ path.lineTo(getVisibleAxisX(d), minPos);
+ drawn = true;
+ }
+ prevpos = minPos;
+ }
+ valid = 0;
+ for(int i = dims - 1; i >= 0; i--) {
+ final int d = proj.getDimForAxis(i);
+ double maxPos = proj.fastProjectDataToRenderSpace(obj.getMax(d), i);
+ // NaN handling:
+ if(maxPos != maxPos) {
+ valid = 0;
+ continue;
+ }
+ ++valid;
+ if(valid > 1) {
+ if(valid == 2) {
+ path.moveTo(getVisibleAxisX(d + 1), prevpos);
+ }
+ path.lineTo(getVisibleAxisX(d), maxPos);
+ drawn = true;
+ }
+ prevpos = maxPos;
+ }
+ if(!drawn) {
+ return null; // Not enough data.
+ }
+ return path.makeElement(svgp);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java
new file mode 100644
index 00000000..a13bdb74
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/LineVisualization.java
@@ -0,0 +1,226 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.lines.LineStyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generates data lines.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class LineVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Data lines";
+
+ /**
+ * Constructor.
+ */
+ public LineVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), LineVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY | VisualizationTask.ON_SAMPLE);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance for a particular data set.
+ *
+ * @author Robert Rödler
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String DATALINE = "Dataline";
+
+ /**
+ * Sample we visualize.
+ */
+ private SamplingResult sample;
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.sample = ResultUtil.getSamplingResult(relation);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final DBIDs sam = sample.getSample();
+ StylingPolicy sp = context.getStylingPolicy();
+ final StyleLibrary style = context.getStyleLibrary();
+ final LineStyleLibrary lines = style.lines();
+ final double width = style.getLineWidth(StyleLibrary.PLOT) * MathUtil.min(.5, 2. / MathUtil.log2(sam.size()));
+ if(sp instanceof ClassStylingPolicy) {
+ ClassStylingPolicy csp = (ClassStylingPolicy) sp;
+ final int min = csp.getMinStyle();
+ String[] keys = new String[csp.getMaxStyle() - min];
+ for(int c = min; c < csp.getMaxStyle(); c++) {
+ String key = keys[c - min] = DATALINE + "_" + c;
+ if(!svgp.getCSSClassManager().contains(key)) {
+ CSSClass cls = new CSSClass(this, key);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ lines.formatCSSClass(cls, c, width);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
+ final int c = csp.getStyleForDBID(iter) + min;
+ if(c < 0) {
+ continue; // No style. Display differently?
+ }
+ Element line = drawLine(iter);
+ if(line == null) {
+ continue;
+ }
+ SVGUtil.addCSSClass(line, keys[c]);
+ layer.appendChild(line);
+ }
+ }
+ else {
+ // No classes available, but individually colored
+ if(!svgp.getCSSClassManager().contains(DATALINE)) {
+ CSSClass cls = new CSSClass(this, DATALINE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ lines.formatCSSClass(cls, -1, width);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ StringBuilder buf = new StringBuilder().append(SVGConstants.CSS_STROKE_PROPERTY).append(':');
+ final int prefix = buf.length();
+ for(DBIDIter iter = sam.iter(); iter.valid(); iter.advance()) {
+ Element line = drawLine(iter);
+ if(line == null) {
+ continue;
+ }
+ SVGUtil.addCSSClass(line, DATALINE);
+ // assign color
+ buf.delete(prefix, buf.length());
+ buf.append(SVGUtil.colorToString(sp.getColorForDBID(iter)));
+ line.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, buf.toString());
+ layer.appendChild(line);
+ }
+ }
+ svgp.updateStyleElement();
+ }
+
+ /**
+ * Draw a single line.
+ *
+ * @param iter Object reference
+ * @return Line element
+ */
+ private Element drawLine(DBIDRef iter) {
+ SVGPath path = new SVGPath();
+ double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter));
+ boolean drawn = false;
+ int valid = 0; /* run length of valid values */
+ for(int i = 0; i < yPos.length; i++) {
+ // NaN handling:
+ if(yPos[i] != yPos[i]) {
+ valid = 0;
+ continue;
+ }
+ ++valid;
+ if(valid > 1) {
+ if(valid == 2) {
+ path.moveTo(getVisibleAxisX(i - 1), yPos[i - 1]);
+ }
+ path.lineTo(getVisibleAxisX(i), yPos[i]);
+ drawn = true;
+ }
+ }
+ if(!drawn) {
+ return null; // Not enough data.
+ }
+ return path.makeElement(svgp);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java
new file mode 100644
index 00000000..4f35faf6
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/ParallelAxisVisualization.java
@@ -0,0 +1,216 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generates a SVG-Element containing axes, including labeling.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ParallelAxisVisualization extends AbstractVisFactory {
+ /**
+ * Class logger
+ */
+ private static final Logging LOG = Logging.getLogger(ParallelAxisVisualization.class);
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Parallel Axes";
+
+ /**
+ * Constructor.
+ */
+ public ParallelAxisVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), this);
+ task.level = VisualizationTask.LEVEL_BACKGROUND;
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return true;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.uses SVGSimpleLinearAxis
+ */
+ // TODO: split into interactive / non-interactive parts?
+ public class Instance extends AbstractParallelVisualization<NumberVector> {
+ /**
+ * Axis label class.
+ */
+ public static final String AXIS_LABEL = "paxis-label";
+
+ /**
+ * Clickable area for the axis.
+ */
+ public static final String INVERTEDAXIS = "paxis-button";
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final StyleLibrary style = context.getStyleLibrary();
+ addCSSClasses(svgp);
+ final int dim = proj.getInputDimensionality();
+ for(int i = 0, vdim = 0; i < dim; i++) {
+ if(!proj.isAxisVisible(i)) {
+ continue;
+ }
+ final int truedim = proj.getDimForAxis(i);
+ final double axisX = getVisibleAxisX(vdim);
+ try {
+ if(!proj.isAxisInverted(vdim)) {
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getAxisScale(i), axisX, getSizeY(), axisX, 0, SVGSimpleLinearAxis.LabelStyle.ENDLABEL, style);
+ }
+ else {
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getAxisScale(i), axisX, 0, axisX, getSizeY(), SVGSimpleLinearAxis.LabelStyle.ENDLABEL, style);
+ }
+ }
+ catch(CSSNamingConflict e) {
+ LOG.warning("Conflict in CSS naming for axes.", e);
+ continue;
+ }
+ // Get axis label
+ final String label = RelationUtil.getColumnLabel(relation, truedim);
+ // Add axis label
+ Element text = svgp.svgText(axisX, -.7 * getMarginTop(), label);
+ SVGUtil.setCSSClass(text, AXIS_LABEL);
+ // TODO: find a reliable way for sizing axis labels.
+ if(dim > 10) {
+ SVGUtil.setAtt(text, SVGConstants.SVG_TEXT_LENGTH_ATTRIBUTE, getAxisSep() * 0.95);
+ SVGUtil.setAtt(text, SVGConstants.SVG_LENGTH_ADJUST_ATTRIBUTE, SVGConstants.SVG_SPACING_AND_GLYPHS_VALUE);
+ }
+ layer.appendChild(text);
+ // TODO: Split into background + clickable layer.
+ Element button = svgp.svgRect(axisX - getAxisSep() * .475, -getMarginTop(), .95 * getAxisSep(), .5 * getMarginTop());
+ SVGUtil.setCSSClass(button, INVERTEDAXIS);
+ addEventListener(button, truedim);
+ layer.appendChild(button);
+ vdim++;
+ }
+ }
+
+ /**
+ * Add the main CSS classes.
+ *
+ * @param svgp Plot to draw to
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ if(!svgp.getCSSClassManager().contains(AXIS_LABEL)) {
+ CSSClass cls = new CSSClass(this, AXIS_LABEL);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL));
+ cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL));
+ cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.AXIS_LABEL));
+ cls.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.SVG_MIDDLE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ if(!svgp.getCSSClassManager().contains(INVERTEDAXIS)) {
+ CSSClass cls = new CSSClass(this, INVERTEDAXIS);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, 0.1);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_GREY_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+
+ /**
+ * Add an event listener to the Element.
+ *
+ * @param tag Element to add the listener
+ * @param truedim Tool number for the Element
+ */
+ private void addEventListener(final Element tag, final int truedim) {
+ EventTarget targ = (EventTarget) tag;
+ targ.addEventListener(SVGConstants.SVG_EVENT_CLICK, new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ proj.toggleDimInverted(truedim);
+ context.visChanged(proj);
+ }
+ }, false);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java
new file mode 100644
index 00000000..1a660bf9
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterOutlineVisualization.java
@@ -0,0 +1,308 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Iterator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization;
+
+/**
+ * Generates a SVG-Element that visualizes the area covered by a cluster.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// TODO: make parameterizable: rounded.
+public class ClusterOutlineVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Cluster Hull (Parallel Coordinates)";
+
+ /**
+ * Settings
+ */
+ Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public ClusterOutlineVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ // We use the style library, not individual clusterings!
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterOutlineVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.initDefaultVisibility(false);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String CLUSTERAREA = "Clusteroutline";
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ final ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol;
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) cpol.getClustering();
+
+ int dim = proj.getVisibleDimensions();
+
+ DoubleMinMax[] mms = DoubleMinMax.newArray(dim);
+ DoubleMinMax[] midmm = DoubleMinMax.newArray(dim - 1);
+
+ // Heuristic value for transparency:
+ double baseopacity = .5;
+
+ Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator();
+ for(int cnum = 0; cnum < clustering.getAllClusters().size(); cnum++) {
+ Cluster<?> clus = ci.next();
+ final DBIDs ids = clus.getIDs();
+ if(ids.size() < 1) {
+ continue;
+ }
+ for(int i = 0; i < dim; i++) {
+ mms[i].reset();
+ if(i < dim - 1) {
+ midmm[i].reset();
+ }
+ }
+
+ // Process points
+ // TODO: do this just once, cache the result somewhere appropriately?
+ for(DBIDIter id = ids.iter(); id.valid(); id.advance()) {
+ double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(id));
+ for(int i = 0; i < dim; i++) {
+ mms[i].put(yPos[i]);
+ if(i > 0) {
+ midmm[i - 1].put((yPos[i] + yPos[i - 1]) / 2.);
+ }
+ }
+ }
+
+ SVGPath path = new SVGPath();
+ if(!settings.bend) {
+ // Straight lines
+ for(int i = 0; i < dim; i++) {
+ path.drawTo(getVisibleAxisX(i), mms[i].getMax());
+ if(i < dim - 1) {
+ path.drawTo(getVisibleAxisX(i + .5), midmm[i].getMax());
+ }
+ }
+ for(int i = dim - 1; i >= 0; i--) {
+ if(i < dim - 1) {
+ path.drawTo(getVisibleAxisX(i + .5), midmm[i].getMin());
+ }
+ path.drawTo(getVisibleAxisX(i), mms[i].getMin());
+ }
+ }
+ else {
+ // Maxima
+ path.drawTo(getVisibleAxisX(0), mms[0].getMax());
+ for(int i = 1; i < dim; i++) {
+ path.quadTo(getVisibleAxisX(i - .5), midmm[i - 1].getMax(), getVisibleAxisX(i), mms[i].getMax());
+ }
+ // Minima
+ path.drawTo(getVisibleAxisX(dim - 1), mms[dim - 1].getMin());
+ for(int i = dim - 1; i > 0; i--) {
+ path.quadTo(getVisibleAxisX(i - .5), midmm[i - 1].getMin(), getVisibleAxisX(i - 1), mms[i - 1].getMin());
+ }
+ }
+ path.close();
+
+ // TODO: improve the visualization by adjusting the opacity by the
+ // cluster extends on each axis (maybe use a horizontal gradient?)
+ double weight = 0.;
+ for(int i = 0; i < dim; i++) {
+ weight += mms[i].getDiff();
+ }
+ weight = (weight > 0.) ? (dim * StyleLibrary.SCALE) / weight : 1.;
+
+ Element intervals = path.makeElement(svgp);
+ addCSSClasses(svgp, cpol.getStyleForCluster(clus), baseopacity * weight * ids.size() / relation.size());
+ SVGUtil.addCSSClass(intervals, CLUSTERAREA + cnum);
+ layer.appendChild(intervals);
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ * @param clusterID Cluster ID to style
+ * @param opac Opacity
+ */
+ private void addCSSClasses(SVGPlot svgp, int clusterID, double opac) {
+ final StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ CSSClass cls = new CSSClass(this, CLUSTERAREA + clusterID);
+ final String color = colors.getColor(clusterID);
+
+ // cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY,
+ // context.getStyleLibrary().getLineWidth(StyleLibrary.PLOT) / 2.0);
+ // cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac);
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Option string to draw straight lines for hull.
+ */
+ public static final OptionID STRAIGHT_ID = new OptionID("parallel.clusteroutline.straight", "Draw straight lines");
+
+ /**
+ * Alpha value
+ */
+ double alpha = Double.POSITIVE_INFINITY;
+
+ /**
+ * Use bend curves
+ */
+ private boolean bend = true;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ DoubleParameter alphaP = new DoubleParameter(ClusterHullVisualization.Parameterizer.ALPHA_ID, Double.POSITIVE_INFINITY);
+ if(config.grab(alphaP)) {
+ alpha = alphaP.doubleValue();
+ }
+
+ Flag bendP = new Flag(STRAIGHT_ID);
+ if(config.grab(bendP)) {
+ bend = bendP.isFalse();
+ }
+ }
+
+ @Override
+ protected ClusterOutlineVisualization makeInstance() {
+ return new ClusterOutlineVisualization(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java
new file mode 100644
index 00000000..6e0dc167
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/ClusterParallelMeanVisualization.java
@@ -0,0 +1,193 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Iterator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.model.MeanModel;
+import de.lmu.ifi.dbs.elki.data.model.MedoidModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+
+/**
+ * Generates a SVG-Element that visualizes cluster means.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ClusterParallelMeanVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Cluster Means";
+
+ /**
+ * Constructor.
+ */
+ public ClusterParallelMeanVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), ClusterParallelMeanVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA + 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ *
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String CLUSTERMEAN = "Clustermean";
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering();
+ if(clustering.getAllClusters().size() == 0) {
+ return;
+ }
+
+ final StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator();
+ for(int cnum = 0; cnum < clustering.getAllClusters().size(); cnum++) {
+ Model model = ci.next().getModel();
+ NumberVector mean = null;
+ try {
+ if(model instanceof MeanModel) {
+ mean = ((MeanModel) model).getMean();
+ }
+ else if(model instanceof MedoidModel) {
+ mean = relation.get(((MedoidModel) model).getMedoid());
+ }
+ }
+ catch(ObjectNotFoundException e) {
+ continue; // Element not found.
+ }
+ if(mean == null) {
+ continue;
+ }
+ double[] pmean = proj.fastProjectDataToRenderSpace(mean);
+
+ SVGPath path = new SVGPath();
+ for(int i = 0; i < pmean.length; i++) {
+ path.drawTo(getVisibleAxisX(i), pmean[i]);
+ }
+ Element meanline = path.makeElement(svgp);
+
+ String cnam = CLUSTERMEAN + cnum;
+ if(!svgp.getCSSClassManager().contains(cnam)) {
+ CSSClass cls = new CSSClass(this, cnam);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * 2.);
+
+ final String color = colors.getColor(cnum);
+
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+ SVGUtil.addCSSClass(meanline, cnam);
+ layer.appendChild(meanline);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java
new file mode 100755
index 00000000..d82a4e1d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/cluster/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for clustering results based on parallel coordinates.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java
new file mode 100644
index 00000000..338729f4
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/RTreeParallelVisualization.java
@@ -0,0 +1,252 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index;
+
+/*This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.SpatialEntry;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTree;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTreeNode;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.rstar.RStarTreeNode;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.ProjectionParallel;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization;
+
+/**
+ * Visualize the of an R-Tree based index.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class RTreeParallelVisualization extends AbstractVisFactory {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String INDEX = "parallelrtree";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "R-Tree Index MBRs";
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public RTreeParallelVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance<RStarTreeNode, SpatialEntry>(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, AbstractRStarTree.class, ParallelPlotProjector.class, //
+ new VisualizationTree.Handler2<AbstractRStarTree<RStarTreeNode, SpatialEntry, ?>, ParallelPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, AbstractRStarTree<RStarTreeNode, SpatialEntry, ?> tree, ParallelPlotProjector<?> p) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, (Result) tree, p.getRelation(), RTreeParallelVisualization.this);
+ task.level = VisualizationTask.LEVEL_BACKGROUND + 2;
+ task.default_visibility = false;
+ context.addVis((Result) tree, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Instance for a particular data set and tree
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has AbstractRStarTree oneway - - visualizes
+ *
+ * @param <N> Tree node type
+ * @param <E> Tree entry type
+ */
+ public class Instance<N extends AbstractRStarTreeNode<N, E>, E extends SpatialEntry> extends AbstractParallelVisualization<NumberVector>implements DataStoreListener {
+ /**
+ * The tree we visualize
+ */
+ protected AbstractRStarTree<N, E, ?> tree;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ @SuppressWarnings("unchecked")
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.tree = AbstractRStarTree.class.cast(task.getResult());
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+ E root = tree.getRootEntry();
+ visualizeRTreeEntry(svgp, layer, proj, tree, root, 0, 0);
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ final ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ for(int i = 0; i < tree.getHeight(); i++) {
+ if(!svgp.getCSSClassManager().contains(INDEX + i)) {
+ CSSClass cls = new CSSClass(this, INDEX + i);
+
+ // Relative depth of this level. 1.0 = toplevel
+ final double relDepth = 1. - (((double) i) / tree.getHeight());
+ if(settings.fill) {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.2);
+ }
+ else {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ svgp.updateStyleElement();
+ }
+
+ /**
+ * Recursively draw the MBR rectangles.
+ *
+ * @param svgp SVG Plot
+ * @param layer Layer
+ * @param proj Projection
+ * @param rtree Rtree to visualize
+ * @param entry Current entry
+ * @param depth Current depth
+ */
+ private void visualizeRTreeEntry(SVGPlot svgp, Element layer, ProjectionParallel proj, AbstractRStarTree<? extends N, E, ?> rtree, E entry, int depth, int step) {
+ final int dim = proj.getVisibleDimensions();
+ double[] min = proj.fastProjectDataToRenderSpace(SpatialUtil.getMin(entry));
+ double[] max = proj.fastProjectDataToRenderSpace(SpatialUtil.getMax(entry));
+ assert(min.length == dim && max.length == dim);
+ SVGPath path = new SVGPath();
+ for(int i = 0; i < dim; i++) {
+ path.drawTo(getVisibleAxisX(i), Math.max(min[i], max[i]));
+ }
+ for(int i = dim - 1; i >= 0; i--) {
+ path.drawTo(getVisibleAxisX(i), Math.min(min[i], max[i]));
+ }
+ path.close();
+
+ Element intervals = path.makeElement(svgp);
+
+ SVGUtil.addCSSClass(intervals, INDEX + depth);
+ layer.appendChild(intervals);
+
+ if(!entry.isLeafEntry()) {
+ N node = rtree.getNode(entry);
+ for(int i = 0; i < node.getNumEntries(); i++) {
+ E child = node.getEntry(i);
+ if(!child.isLeafEntry()) {
+ visualizeRTreeEntry(svgp, layer, proj, rtree, child, depth + 1, ++step);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ protected boolean fill = true;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag fillF = new Flag(TreeMBRVisualization.Parameterizer.FILL_ID);
+ fillF.setDefaultValue(Boolean.TRUE);
+ if(config.grab(fillF)) {
+ fill = fillF.isTrue();
+ }
+ }
+
+ @Override
+ protected RTreeParallelVisualization makeInstance() {
+ return new RTreeParallelVisualization(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java
new file mode 100755
index 00000000..b6de9cf4
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/index/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for index structure based on parallel coordinates.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java
new file mode 100755
index 00000000..b4fffa74
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Visualizers based on parallel coordinates.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java
new file mode 100644
index 00000000..5f56e774
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionAxisRangeVisualization.java
@@ -0,0 +1,180 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.HyperBoundingBox;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.RangeSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+
+/**
+ * Visualizer for generating an SVG-Element representing the selected range.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionAxisRangeVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Selection Axis Range";
+
+ /**
+ * Constructor.
+ */
+ public SelectionAxisRangeVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionAxisRangeVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has RangeSelection oneway - - visualizes
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector> {
+ /**
+ * CSS Class for the range marker
+ */
+ public static final String MARKER = "selectionAxisRange";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ // Class for the cube
+ if(!svgp.getCSSClassManager().contains(MARKER)) {
+ CSSClass cls = new CSSClass(this, MARKER);
+ cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+ DBIDSelection selContext = context.getSelection();
+ if(!(selContext instanceof RangeSelection)) {
+ return;
+ }
+ HyperBoundingBox range = ((RangeSelection) selContext).getRanges();
+ if(range == null) {
+ return;
+ }
+
+ // Project:
+ final int dims = range.getDimensionality();
+ double[] min = new double[dims];
+ double[] max = new double[dims];
+ for(int d = 0; d < dims; d++) {
+ min[d] = range.getMin(d);
+ max[d] = range.getMax(d);
+ }
+ min = proj.fastProjectDataToRenderSpace(min);
+ max = proj.fastProjectDataToRenderSpace(max);
+
+ final int vdim = proj.getVisibleDimensions();
+ for(int vd = 0; vd < vdim; vd++) {
+ final int ad = proj.getDimForVisibleAxis(vd);
+ final double amin = Math.min(min[ad], max[ad]);
+ final double amax = Math.max(min[ad], max[ad]);
+ if(amin > Double.MIN_VALUE && amax < Double.MAX_VALUE) {
+ Element rect = svgp.svgRect(getVisibleAxisX(vd) - (0.01 * StyleLibrary.SCALE), amin, 0.02 * StyleLibrary.SCALE, amax - amin);
+ SVGUtil.addCSSClass(rect, MARKER);
+ layer.appendChild(rect);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java
new file mode 100644
index 00000000..b4107147
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionLineVisualization.java
@@ -0,0 +1,198 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+
+/**
+ * Visualizer for generating SVG-Elements representing the selected objects
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionLineVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Selection Line";
+
+ /**
+ * Constructor.
+ */
+ public SelectionLineVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionLineVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has DBIDSelection oneway - - visualizes
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DataStoreListener {
+ /**
+ * CSS Class for the range marker
+ */
+ public static final String MARKER = "SelectionLine";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+ DBIDSelection selContext = context.getSelection();
+ if(selContext != null) {
+ DBIDs selection = selContext.getSelectedIds();
+
+ for(DBIDIter iter = selection.iter(); iter.valid(); iter.advance()) {
+ Element marker = drawLine(iter);
+ if(marker == null) {
+ continue;
+ }
+ SVGUtil.addCSSClass(marker, MARKER);
+ layer.appendChild(marker);
+ }
+ }
+ }
+
+ /**
+ * Draw a single line.
+ *
+ * @param iter Object reference
+ * @return SVG Element
+ */
+ private Element drawLine(DBIDRef iter) {
+ SVGPath path = new SVGPath();
+ double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter));
+ boolean draw = false, drawprev = false, drawn = false;
+ for(int i = 0; i < yPos.length; i++) {
+ // NaN handling:
+ if(yPos[i] != yPos[i]) {
+ draw = false;
+ drawprev = false;
+ continue;
+ }
+ if(draw) {
+ if(drawprev) {
+ path.moveTo(getVisibleAxisX(i - 1), yPos[i - 1]);
+ drawprev = false;
+ }
+ path.lineTo(getVisibleAxisX(i), yPos[i]);
+ drawn = true;
+ }
+ else {
+ drawprev = true;
+ }
+ draw = true;
+ }
+ if(!drawn) {
+ return null; // Not enough data.
+ }
+ return path.makeElement(svgp);
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ // Class for the cube
+ if(!svgp.getCSSClassManager().contains(MARKER)) {
+ CSSClass cls = new CSSClass(this, MARKER);
+ cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * 2.);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java
new file mode 100644
index 00000000..3dd96007
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolAxisRangeVisualization.java
@@ -0,0 +1,314 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.data.ModifiableHyperBoundingBox;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.RangeSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+
+/**
+ * Tool-Visualization for the tool to select axis ranges
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionToolAxisRangeVisualization extends AbstractVisFactory {
+ /**
+ * The logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(SelectionToolAxisRangeVisualization.class);
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Axis Range Selection";
+
+ /**
+ * Constructor.
+ */
+ public SelectionToolAxisRangeVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolAxisRangeVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ task.initDefaultVisibility(false);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has RangeSelection oneway - - updates
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DragableArea.DragListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ private static final String CSS_RANGEMARKER = "selectionAxisRangeMarker";
+
+ /**
+ * Element for selection rectangle
+ */
+ private Element rtag;
+
+ /**
+ * Element for the rectangle to add listeners
+ */
+ private Element etag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+
+ // rtag: tag for the selected rect
+ rtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER);
+ layer.appendChild(rtag);
+
+ // etag: sensitive area
+ DragableArea drag = new DragableArea(svgp, -.1 * getMarginLeft(), -.1 * getMarginTop(), getSizeX() + getMarginLeft() * .2, getSizeY() + getMarginTop() * .2, this);
+ etag = drag.getElement();
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Delete the children of the element
+ *
+ * @param container SVG-Element
+ */
+ private void deleteChildren(Element container) {
+ while(container.hasChildNodes()) {
+ container.removeChild(container.getLastChild());
+ }
+ }
+
+ /**
+ * Set the selected ranges and the mask for the actual dimensions in the
+ * context
+ *
+ * @param x1 min x-value
+ * @param x2 max x-value
+ * @param y1 min y-value
+ * @param y2 max y-value
+ */
+ private void updateSelectionRectKoordinates(double x1, double x2, double y1, double y2, ModifiableHyperBoundingBox ranges) {
+ final int dims = proj.getVisibleDimensions();
+ int minaxis = dims + 1;
+ int maxaxis = -1;
+ {
+ int i = 0;
+ while(i < dims) {
+ double axx = getVisibleAxisX(i);
+ if(x1 < axx || x2 < axx) {
+ minaxis = i;
+ break;
+ }
+ i++;
+ }
+ while(i <= dims) {
+ double axx = getVisibleAxisX(i);
+ if(x2 < axx && x1 < axx) {
+ maxaxis = i;
+ break;
+ }
+ i++;
+ }
+ }
+ double z1 = Math.max(Math.min(y1, y2), 0);
+ double z2 = Math.min(Math.max(y1, y2), getSizeY());
+ for(int i = minaxis; i < maxaxis; i++) {
+ double v1 = proj.fastProjectRenderToDataSpace(z1, i);
+ double v2 = proj.fastProjectRenderToDataSpace(z2, i);
+ final int ddim = proj.getDimForVisibleAxis(i);
+ if(LOG.isDebugging()) {
+ LOG.debug("Axis " + i + " dimension " + ddim + " " + v1 + " to " + v2);
+ }
+ ranges.setMin(ddim, Math.min(v1, v2));
+ ranges.setMax(ddim, Math.max(v1, v2));
+ }
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ double x = Math.min(startPoint.getX(), dragPoint.getX());
+ double y = Math.min(startPoint.getY(), dragPoint.getY());
+ double width = Math.abs(startPoint.getX() - dragPoint.getX());
+ double height = Math.abs(startPoint.getY() - dragPoint.getY());
+ rtag.appendChild(svgp.svgRect(x, y, width, height));
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) {
+ updateSelection(proj, startPoint, dragPoint);
+ }
+ return true;
+ }
+
+ /**
+ * Update the selection in the context.
+ *
+ * @param proj The projection
+ * @param p1 First Point of the selected rectangle
+ * @param p2 Second Point of the selected rectangle
+ */
+ private void updateSelection(Projection proj, SVGPoint p1, SVGPoint p2) {
+ DBIDSelection selContext = context.getSelection();
+ ModifiableDBIDs selection;
+ if(selContext != null) {
+ selection = DBIDUtil.newHashSet(selContext.getSelectedIds());
+ }
+ else {
+ selection = DBIDUtil.newHashSet();
+ }
+ ModifiableHyperBoundingBox ranges;
+
+ if(p1 == null || p2 == null) {
+ LOG.warning("no rect selected: p1: " + p1 + " p2: " + p2);
+ }
+ else {
+ double x1 = Math.min(p1.getX(), p2.getX());
+ double x2 = Math.max(p1.getX(), p2.getX());
+ double y1 = Math.max(p1.getY(), p2.getY());
+ double y2 = Math.min(p1.getY(), p2.getY());
+
+ int dim = proj.getInputDimensionality();
+ if(selContext instanceof RangeSelection) {
+ ranges = ((RangeSelection) selContext).getRanges();
+ }
+ else {
+ ranges = new ModifiableHyperBoundingBox(dim, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+ updateSelectionRectKoordinates(x1, x2, y1, y2, ranges);
+
+ selection.clear();
+
+ candidates: for(DBIDIter iditer = relation.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ NumberVector dbTupel = relation.get(iditer);
+ for(int d = 0; d < dim; d++) {
+ final double min = ranges.getMin(d), max = ranges.getMax(d);
+ if(max < Double.POSITIVE_INFINITY && min > Double.NEGATIVE_INFINITY) {
+ if(dbTupel.doubleValue(d) < min || dbTupel.doubleValue(d) > max) {
+ continue candidates;
+ }
+ }
+ }
+ selection.add(iditer);
+ }
+ context.setSelection(new RangeSelection(selection, ranges));
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ protected void addCSSClasses(SVGPlot svgp) {
+ // Class for the range marking
+ if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) {
+ final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER);
+ final StyleLibrary style = context.getStyleLibrary();
+ rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE));
+ rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE));
+ svgp.addCSSClassOrLogError(rcls);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java
new file mode 100644
index 00000000..aa22a267
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/SelectionToolLineVisualization.java
@@ -0,0 +1,337 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.geom.Line2D;
+
+import org.apache.batik.dom.events.DOMMouseEvent;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AbstractParallelVisualization;
+
+/**
+ * Tool-Visualization for the tool to select objects
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance - - «create»
+ */
+public class SelectionToolLineVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Object Selection";
+
+ /**
+ * Input modes
+ *
+ * @apiviz.exclude
+ */
+ private enum Mode {
+ REPLACE, ADD, INVERT
+ }
+
+ /**
+ * Constructor.
+ */
+ public SelectionToolLineVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ParallelPlotProjector<?>> it = VisualizationTree.filter(context, start, ParallelPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ParallelPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolLineVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ task.initDefaultVisibility(false);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has DBIDSelection oneway - - updates
+ */
+ public class Instance extends AbstractParallelVisualization<NumberVector>implements DragableArea.DragListener {
+ /**
+ * CSS class of the selection rectangle while selecting.
+ */
+ private static final String CSS_RANGEMARKER = "selectionRangeMarker";
+
+ /**
+ * Element for selection rectangle
+ */
+ Element rtag;
+
+ /**
+ * Element for the rectangle to add listeners
+ */
+ Element etag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ super.fullRedraw();
+ addCSSClasses(svgp);
+
+ rtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER);
+ layer.appendChild(rtag);
+
+ // etag: sensitive area
+ DragableArea drag = new DragableArea(svgp, -.1 * getMarginLeft(), -.5 * getMarginTop(), getSizeX() + .2 * getMarginLeft(), getMarginTop() * 1.5 + getSizeY(), this);
+ etag = drag.getElement();
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Delete the children of the element
+ *
+ * @param container SVG-Element
+ */
+ private void deleteChildren(Element container) {
+ while(container.hasChildNodes()) {
+ container.removeChild(container.getLastChild());
+ }
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ double x = Math.min(startPoint.getX(), dragPoint.getX());
+ double y = Math.min(startPoint.getY(), dragPoint.getY());
+ double width = Math.abs(startPoint.getX() - dragPoint.getX());
+ double height = Math.abs(startPoint.getY() - dragPoint.getY());
+ rtag.appendChild(svgp.svgRect(x, y, width, height));
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ Mode mode = getInputMode(evt);
+ deleteChildren(rtag);
+ if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) {
+ updateSelection(mode, startPoint, dragPoint);
+ }
+ return true;
+ }
+
+ /**
+ * Get the current input mode, on each mouse event.
+ *
+ * @param evt Mouse event.
+ * @return current input mode
+ */
+ private Mode getInputMode(Event evt) {
+ if(evt instanceof DOMMouseEvent) {
+ DOMMouseEvent domme = (DOMMouseEvent) evt;
+ // TODO: visual indication of mode possible?
+ if(domme.getShiftKey()) {
+ return Mode.ADD;
+ }
+ else if(domme.getCtrlKey()) {
+ return Mode.INVERT;
+ }
+ else {
+ return Mode.REPLACE;
+ }
+ }
+ // Default mode is replace.
+ return Mode.REPLACE;
+ }
+
+ /**
+ * Updates the selection in the context.<br>
+ *
+ * @param mode Input mode
+ * @param p1 first point of the selected rectangle
+ * @param p2 second point of the selected rectangle
+ */
+ private void updateSelection(Mode mode, SVGPoint p1, SVGPoint p2) {
+ DBIDSelection selContext = context.getSelection();
+ // Note: we rely on SET semantics below!
+ final HashSetModifiableDBIDs selection;
+ if(selContext == null || mode == Mode.REPLACE) {
+ selection = DBIDUtil.newHashSet();
+ }
+ else {
+ selection = DBIDUtil.newHashSet(selContext.getSelectedIds());
+ }
+ int[] axisrange = getAxisRange(Math.min(p1.getX(), p2.getX()), Math.max(p1.getX(), p2.getX()));
+ DBIDs ids = ResultUtil.getSamplingResult(relation).getSample();
+ for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) {
+ double[] yPos = proj.fastProjectDataToRenderSpace(relation.get(iter));
+ if(checkSelected(axisrange, yPos, Math.max(p1.getX(), p2.getX()), Math.min(p1.getX(), p2.getX()), Math.max(p1.getY(), p2.getY()), Math.min(p1.getY(), p2.getY()))) {
+ if(mode == Mode.INVERT) {
+ if(!selection.contains(iter)) {
+ selection.add(iter);
+ }
+ else {
+ selection.remove(iter);
+ }
+ }
+ else {
+ // In REPLACE and ADD, add objects.
+ // The difference was done before by not re-using the selection.
+ // Since we are using a set, we can just add in any case.
+ selection.add(iter);
+ }
+ }
+ }
+ context.setSelection(new DBIDSelection(selection));
+ }
+
+ private int[] getAxisRange(double x1, double x2) {
+ final int dim = proj.getVisibleDimensions();
+ int minaxis = 0;
+ int maxaxis = 0;
+ boolean minx = true;
+ boolean maxx = false;
+ int count = -1;
+ for(int i = 0; i < dim; i++) {
+ if(minx && getVisibleAxisX(i) > x1) {
+ minaxis = count;
+ minx = false;
+ maxx = true;
+ }
+ if(maxx && (getVisibleAxisX(i) > x2 || i == dim - 1)) {
+ maxaxis = count + 1;
+ if(i == dim - 1 && getVisibleAxisX(i) <= x2) {
+ maxaxis++;
+ }
+ break;
+ }
+ count = i;
+ }
+ return new int[] { minaxis, maxaxis };
+ }
+
+ private boolean checkSelected(int[] ar, double[] yPos, double x1, double x2, double y1, double y2) {
+ final int dim = proj.getVisibleDimensions();
+ if(ar[0] < 0) {
+ ar[0] = 0;
+ }
+ if(ar[1] >= dim) {
+ ar[1] = dim - 1;
+ }
+ for(int i = ar[0] + 1; i <= ar[1] - 1; i++) {
+ if(yPos[i] <= y1 && yPos[i] >= y2) {
+ return true;
+ }
+ }
+ Line2D.Double idline1 = new Line2D.Double(getVisibleAxisX(ar[0]), yPos[ar[0]], getVisibleAxisX(ar[0] + 1), yPos[ar[0] + 1]);
+ Line2D.Double idline2 = new Line2D.Double(getVisibleAxisX(ar[1] - 1), yPos[ar[1] - 1], getVisibleAxisX(ar[1]), yPos[ar[1]]);
+ Line2D.Double rectline1 = new Line2D.Double(x2, y1, x1, y1);
+ Line2D.Double rectline2 = new Line2D.Double(x2, y1, x2, y2);
+ Line2D.Double rectline3 = new Line2D.Double(x2, y2, x1, y2);
+ if(idline1.intersectsLine(rectline1) || idline1.intersectsLine(rectline2) || idline1.intersectsLine(rectline3)) {
+ return true;
+ }
+ Line2D.Double rectline4 = new Line2D.Double(x1, y1, x1, y2);
+ if(idline2.intersectsLine(rectline1) || idline2.intersectsLine(rectline4) || idline2.intersectsLine(rectline3)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ protected void addCSSClasses(SVGPlot svgp) {
+ // Class for the range marking
+ if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) {
+ final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER);
+ final StyleLibrary style = context.getStyleLibrary();
+ rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE));
+ rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE));
+ svgp.addCSSClassOrLogError(rcls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java
new file mode 100755
index 00000000..cc12b74a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/parallel/selection/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for object selection based on parallel projections.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java
new file mode 100644
index 00000000..13225426
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractScatterplotVisualization.java
@@ -0,0 +1,124 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+
+/**
+ * Default class to handle 2D projected visualizations.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.landmark
+ * @apiviz.has Projection2D
+ */
+public abstract class AbstractScatterplotVisualization extends AbstractVisualization {
+ /**
+ * The current projection
+ */
+ final protected Projection2D proj;
+
+ /**
+ * The representation we visualize
+ */
+ final protected Relation<? extends NumberVector> rel;
+
+ /**
+ * The DBID sample
+ */
+ final protected SamplingResult sample;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public AbstractScatterplotVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height);
+ this.proj = (Projection2D) proj;
+ this.rel = task.getRelation();
+ this.sample = task.updateOnAny(VisualizationTask.ON_SAMPLE) ? ResultUtil.getSamplingResult(rel) : null;
+ }
+
+ /**
+ * Setup our canvas.
+ *
+ * @return Canvas
+ */
+ protected Element setupCanvas() {
+ final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN);
+ this.layer = setupCanvas(svgp, this.proj, margin, getWidth(), getHeight());
+ return layer;
+ }
+
+ /**
+ * Utility function to setup a canvas element for the visualization.
+ *
+ * @param svgp Plot element
+ * @param proj Projection to use
+ * @param margin Margin to use
+ * @param width Width
+ * @param height Height
+ * @return wrapper element with appropriate view box.
+ */
+ public static Element setupCanvas(SVGPlot svgp, Projection2D proj, double margin, double width, double height) {
+ final CanvasSize canvas = proj.estimateViewport();
+ final double sizex = canvas.getDiffX();
+ final double sizey = canvas.getDiffY();
+ String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin) + " translate(" + SVGUtil.fmt(sizex * .5) + " " + SVGUtil.fmt(sizey * .5) + ")";
+
+ final Element layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+ return layer;
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem item) {
+ super.visualizationChanged(item);
+ if(item == proj) {
+ svgp.requestRedraw(this.task, this);
+ return;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java
new file mode 100644
index 00000000..313b2dff
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AbstractTooltipVisualization.java
@@ -0,0 +1,190 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.events.EventListener;
+import org.w3c.dom.events.EventTarget;
+
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+
+/**
+ * General base class for a tooltip visualizer.
+ *
+ * @author Erich Schubert
+ */
+// TODO: can we improve performance by not adding as many hovers?
+public abstract class AbstractTooltipVisualization extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String TOOLTIP_HIDDEN = "tooltip_hidden";
+
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String TOOLTIP_VISIBLE = "tooltip_visible";
+
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String TOOLTIP_STICKY = "tooltip_sticky";
+
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String TOOLTIP_AREA = "tooltip_area";
+
+ /**
+ * Our event listener.
+ */
+ EventListener hoverer = new EventListener() {
+ @Override
+ public void handleEvent(Event evt) {
+ handleHoverEvent(evt);
+ }
+ };
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public AbstractTooltipVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ setupCSS(svgp);
+ final StyleLibrary style = context.getStyleLibrary();
+ double dotsize = style.getLineWidth(StyleLibrary.PLOT);
+
+ for(DBIDIter id = sample.getSample().iter(); id.valid(); id.advance()) {
+ double[] v = proj.fastProjectDataToRenderSpace(rel.get(id));
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element tooltip = makeTooltip(id, v[0], v[1], dotsize);
+ SVGUtil.addCSSClass(tooltip, TOOLTIP_HIDDEN);
+
+ // sensitive area.
+ Element area = svgp.svgRect(v[0] - dotsize, v[1] - dotsize, 2 * dotsize, 2 * dotsize);
+ SVGUtil.addCSSClass(area, TOOLTIP_AREA);
+
+ EventTarget targ = (EventTarget) area;
+ targ.addEventListener(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE, hoverer, false);
+ targ.addEventListener(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE, hoverer, false);
+ targ.addEventListener(SVGConstants.SVG_CLICK_EVENT_TYPE, hoverer, false);
+
+ // NOTE: do not change the sequence in which these are inserted!
+ layer.appendChild(area);
+ layer.appendChild(tooltip);
+ }
+ }
+
+ /**
+ * Make a tooltip Element for this id.
+ *
+ * @param id Id to make a tooltip for
+ * @param x X position
+ * @param y Y position
+ * @param dotsize Size of a dot
+ * @return Element
+ */
+ protected abstract Element makeTooltip(DBIDRef id, double x, double y, double dotsize);
+
+ /**
+ * Handle the hover events.
+ *
+ * @param evt Event.
+ */
+ protected void handleHoverEvent(Event evt) {
+ if(evt.getTarget() instanceof Element) {
+ Element e = (Element) evt.getTarget();
+ Node next = e.getNextSibling();
+ if(next instanceof Element) {
+ toggleTooltip((Element) next, evt.getType());
+ }
+ else {
+ LoggingUtil.warning("Tooltip sibling not found.");
+ }
+ }
+ else {
+ LoggingUtil.warning("Got event for non-Element?!?");
+ }
+ }
+
+ /**
+ * Toggle the Tooltip of an element.
+ *
+ * @param elem Element
+ * @param type Event type
+ */
+ protected void toggleTooltip(Element elem, String type) {
+ String csscls = elem.getAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE);
+ if(SVGConstants.SVG_MOUSEOVER_EVENT_TYPE.equals(type)) {
+ if(TOOLTIP_HIDDEN.equals(csscls)) {
+ SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_VISIBLE);
+ }
+ }
+ else if(SVGConstants.SVG_MOUSEOUT_EVENT_TYPE.equals(type)) {
+ if(TOOLTIP_VISIBLE.equals(csscls)) {
+ SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_HIDDEN);
+ }
+ }
+ else if(SVGConstants.SVG_CLICK_EVENT_TYPE.equals(type)) {
+ if(TOOLTIP_STICKY.equals(csscls)) {
+ SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_HIDDEN);
+ }
+ if(TOOLTIP_HIDDEN.equals(csscls) || TOOLTIP_VISIBLE.equals(csscls)) {
+ SVGUtil.setAtt(elem, SVGConstants.SVG_CLASS_ATTRIBUTE, TOOLTIP_STICKY);
+ }
+ }
+ }
+
+ /**
+ * Registers the Tooltip-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ */
+ protected abstract void setupCSS(SVGPlot svgp);
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java
new file mode 100644
index 00000000..9fa1bd96
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/AxisVisualization.java
@@ -0,0 +1,167 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generates a SVG-Element containing axes, including labeling.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class AxisVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Axes";
+
+ /**
+ * Constructor.
+ */
+ public AxisVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), p.getRelation(), AxisVisualization.this);
+ task.level = VisualizationTask.LEVEL_BACKGROUND;
+ context.addVis(p, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ * @author Remigius Wojdanowski
+ *
+ * @apiviz.uses SVGSimpleLinearAxis
+ *
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ final int dim = RelationUtil.dimensionality(rel);
+
+ // origin
+ double[] orig = proj.fastProjectScaledToRenderSpace(new double[dim]);
+ // diagonal point opposite to origin
+ double[] diag = new double[dim];
+ for(int d2 = 0; d2 < dim; d2++) {
+ diag[d2] = 1;
+ }
+ diag = proj.fastProjectScaledToRenderSpace(diag);
+ // compute angle to diagonal line, used for axis labeling.
+ double diaga = Math.atan2(diag[1] - orig[1], diag[0] - orig[0]);
+
+ double alfontsize = 1.1 * style.getTextSize(StyleLibrary.AXIS_LABEL);
+ CSSClass alcls = new CSSClass(AxisVisualization.class, "unmanaged");
+ alcls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, SVGUtil.fmt(alfontsize));
+ alcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.AXIS_LABEL));
+ alcls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.AXIS_LABEL));
+
+ // draw axes
+ for(int d = 0; d < dim; d++) {
+ double[] v = new double[dim];
+ v[d] = 1;
+ // projected endpoint of axis
+ double[] ax = proj.fastProjectScaledToRenderSpace(v);
+ boolean righthand = false;
+ double axa = Math.atan2(ax[1] - orig[1], ax[0] - orig[0]);
+ if(axa > diaga || (diaga > 0 && axa > diaga + Math.PI)) {
+ righthand = true;
+ }
+ // System.err.println(ax.get(0) + " "+ ax.get(1)+
+ // " "+(axa*180/Math.PI)+" "+(diaga*180/Math.PI));
+ if(ax[0] != orig[0] || ax[1] != orig[1]) {
+ try {
+ SVGSimpleLinearAxis.drawAxis(svgp, layer, proj.getScale(d), orig[0], orig[1], ax[0], ax[1], righthand ? SVGSimpleLinearAxis.LabelStyle.RIGHTHAND : SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+ // TODO: move axis labeling into drawAxis function.
+ double offx = (righthand ? 1 : -1) * 0.02 * Projection.SCALE;
+ double offy = (righthand ? 1 : -1) * 0.02 * Projection.SCALE;
+ Element label = svgp.svgText(ax[0] + offx, ax[1] + offy, RelationUtil.getColumnLabel(rel, d));
+ SVGUtil.setAtt(label, SVGConstants.SVG_STYLE_ATTRIBUTE, alcls.inlineCSS());
+ SVGUtil.setAtt(label, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, righthand ? SVGConstants.SVG_START_VALUE : SVGConstants.SVG_END_VALUE);
+ layer.appendChild(label);
+ }
+ catch(CSSNamingConflict e) {
+ throw new RuntimeException("Conflict in CSS naming for axes.", e);
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java
new file mode 100644
index 00000000..6508d9ce
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/MarkerVisualization.java
@@ -0,0 +1,164 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize e.g. a clustering using different markers for different clusters.
+ * This visualizer is not constraint to clusters. It can in fact visualize any
+ * kind of result we have a style source for.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class MarkerVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Markers";
+
+ /**
+ * Constructor.
+ */
+ public MarkerVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, MarkerVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses StylingPolicy
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String DOTMARKER = "dot";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ final MarkerLibrary ml = style.markers();
+ final double marker_size = style.getSize(StyleLibrary.MARKERPLOT);
+ final StylingPolicy spol = context.getStylingPolicy();
+
+ if(spol instanceof ClassStylingPolicy) {
+ ClassStylingPolicy cspol = (ClassStylingPolicy) spol;
+ for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) {
+ try {
+ final NumberVector vec = rel.get(iter);
+ double[] v = proj.fastProjectDataToRenderSpace(vec);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ ml.useMarker(svgp, layer, v[0], v[1], cspol.getStyleForDBID(iter), marker_size);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ else {
+ final String FILL = SVGConstants.CSS_FILL_PROPERTY + ":";
+ // Color-based styling. Fall back to dots
+ for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) {
+ try {
+ double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ Element dot = svgp.svgCircle(v[0], v[1], marker_size);
+ SVGUtil.addCSSClass(dot, DOTMARKER);
+ int col = spol.getColorForDBID(iter);
+ SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col));
+ layer.appendChild(dot);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java
new file mode 100644
index 00000000..796d8f5f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/PolygonVisualization.java
@@ -0,0 +1,178 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
+import de.lmu.ifi.dbs.elki.data.spatial.PolygonsObject;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Renders PolygonsObject in the data set.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class PolygonVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Polygons";
+
+ /**
+ * Constructor
+ */
+ public PolygonVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ VisualizationTree.findNewResultVis(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, Relation<?> rel, ScatterPlotProjector<?> p) {
+ if(!TypeUtil.POLYGON_TYPE.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ if(RelationUtil.dimensionality(p.getRelation()) != 2) {
+ return;
+ }
+ // Assume that a 2d projector is using the same coordinates as the
+ // polygons.
+ final VisualizationTask task = new VisualizationTask(NAME, context, rel, rel, PolygonVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 10;
+ task.addUpdateFlags(VisualizationTask.ON_DATA);
+ context.addVis(rel, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Instance
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has PolygonsObject - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String POLYS = "polys";
+
+ /**
+ * The representation we visualize
+ */
+ final protected Relation<PolygonsObject> rep;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task to visualize
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.rep = task.getResult(); // Note: relation was used for projection
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass css = new CSSClass(svgp, POLYS);
+ // TODO: separate fill and line colors?
+ css.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.POLYGONS));
+ css.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.POLYGONS));
+ css.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(css);
+ svgp.updateStyleElement();
+
+ // draw data
+ for(DBIDIter iditer = rep.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ try {
+ PolygonsObject poly = rep.get(iditer);
+ if(poly == null) {
+ continue;
+ }
+ SVGPath path = new SVGPath();
+ for(Polygon ppoly : poly.getPolygons()) {
+ Vector first = ppoly.get(0);
+ double[] f = proj.fastProjectDataToRenderSpace(first.getArrayRef());
+ path.moveTo(f[0], f[1]);
+ for(ArrayListIter<Vector> it = ppoly.iter(); it.valid(); it.advance()) {
+ if(it.getOffset() == 0) {
+ continue;
+ }
+ double[] p = proj.fastProjectDataToRenderSpace(it.get().getArrayRef());
+ path.drawTo(p[0], p[1]);
+ }
+ // close path.
+ path.drawTo(f[0], f[1]);
+ }
+ Element e = path.makeElement(svgp);
+ SVGUtil.addCSSClass(e, POLYS);
+ layer.appendChild(e);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java
new file mode 100644
index 00000000..1f0246e0
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/ReferencePointsVisualization.java
@@ -0,0 +1,157 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Iterator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.ReferencePointsResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * The actual visualization instance, for a single projection
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ReferencePointsVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Reference Points";
+
+ /**
+ * Constructor.
+ */
+ public ReferencePointsVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ VisualizationTree.findNewResultVis(context, result, ReferencePointsResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<ReferencePointsResult<?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, ReferencePointsResult<?> rp, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, rp, rel, ReferencePointsVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ context.addVis(rp, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.has ReferencePointsResult oneway - - visualizes
+ */
+ // TODO: add a result listener for the reference points.
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String REFPOINT = "refpoint";
+
+ /**
+ * Serves reference points.
+ */
+ protected ReferencePointsResult<? extends NumberVector> result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ setupCSS(svgp);
+ Iterator<? extends NumberVector> iter = result.iterator();
+
+ final double dotsize = style.getSize(StyleLibrary.REFERENCE_POINTS);
+ while(iter.hasNext()) {
+ NumberVector v = iter.next();
+ double[] projected = proj.fastProjectDataToRenderSpace(v);
+ Element dot = svgp.svgCircle(projected[0], projected[1], dotsize);
+ SVGUtil.addCSSClass(dot, REFPOINT);
+ layer.appendChild(dot);
+ }
+ }
+
+ /**
+ * Registers the Reference-Point-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the -CSS-Class.
+ */
+ private void setupCSS(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass refpoint = new CSSClass(svgp, REFPOINT);
+ refpoint.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.REFERENCE_POINTS));
+ svgp.addCSSClassOrLogError(refpoint);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java
new file mode 100644
index 00000000..585b5405
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipScoreVisualization.java
@@ -0,0 +1,280 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.relation.DoubleRelation;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.ResultHierarchy;
+import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.constraints.CommonConstraints;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.IntParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generates a SVG-Element containing Tooltips. Tooltips remain invisible until
+ * their corresponding Marker is touched by the cursor and stay visible as long
+ * as the cursor lingers on the marker.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class TooltipScoreVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Outlier Score Tooltips";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME_GEN = " Tooltips";
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public TooltipScoreVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ final ResultHierarchy hier = context.getHierarchy();
+ // TODO: we can also visualize other scores!
+ VisualizationTree.findNewSiblings(context, result, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(o.getLongName() + NAME_GEN, context, o.getScores(), rel, TooltipScoreVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(o.getScores(), task);
+ context.addVis(p, task);
+ }
+ });
+ VisualizationTree.findNewSiblings(context, result, DoubleRelation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<DoubleRelation, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, DoubleRelation r, ScatterPlotProjector<?> p) {
+ for(Iter<Result> it = hier.iterParents(r); it.valid(); it.advance()) {
+ if(it.get() instanceof OutlierResult) {
+ return; // Handled by above case already.
+ }
+ }
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(r.getLongName() + NAME_GEN, context, r, rel, TooltipScoreVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(r, task);
+ context.addVis(p, task);
+ }
+ });
+ VisualizationTree.findNewSiblings(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, Relation<?> r, ScatterPlotProjector<?> p) {
+ if(r instanceof DoubleRelation) {
+ return; // Handled above already.
+ }
+ if(!TypeUtil.DOUBLE.isAssignableFromType(r.getDataTypeInformation()) && !TypeUtil.INTEGER.isAssignableFromType(r.getDataTypeInformation())) {
+ return;
+ }
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(r.getLongName() + NAME_GEN, context, r, rel, TooltipScoreVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(r, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Instance
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractTooltipVisualization {
+ /**
+ * Number value to visualize
+ */
+ private Relation<? extends Number> result;
+
+ /**
+ * Font size to use.
+ */
+ private double fontsize;
+
+ /**
+ * Constructor
+ *
+ * @param task Task
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ final StyleLibrary style = context.getStyleLibrary();
+ this.fontsize = 3 * style.getTextSize(StyleLibrary.PLOT);
+ addListeners();
+ }
+
+ @Override
+ protected Element makeTooltip(DBIDRef id, double x, double y, double dotsize) {
+ return svgp.svgText(x + dotsize, y + fontsize * 0.07, settings.nf.format(result.get(id).doubleValue()));
+ }
+
+ /**
+ * Registers the Tooltip-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ */
+ @Override
+ protected void setupCSS(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ final double fontsize = style.getTextSize(StyleLibrary.PLOT);
+ final String fontfamily = style.getFontFamily(StyleLibrary.PLOT);
+
+ CSSClass tooltiphidden = new CSSClass(svgp, TOOLTIP_HIDDEN);
+ tooltiphidden.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltiphidden.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ tooltiphidden.setStatement(SVGConstants.CSS_DISPLAY_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(tooltiphidden);
+
+ CSSClass tooltipvisible = new CSSClass(svgp, TOOLTIP_VISIBLE);
+ tooltipvisible.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltipvisible.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ svgp.addCSSClassOrLogError(tooltipvisible);
+
+ CSSClass tooltipsticky = new CSSClass(svgp, TOOLTIP_STICKY);
+ tooltipsticky.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltipsticky.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ svgp.addCSSClassOrLogError(tooltipsticky);
+
+ // invisible but sensitive area for the tooltip activator
+ CSSClass tooltiparea = new CSSClass(svgp, TOOLTIP_AREA);
+ tooltiparea.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ tooltiparea.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ tooltiparea.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0");
+ tooltiparea.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ svgp.addCSSClassOrLogError(tooltiparea);
+
+ svgp.updateStyleElement();
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Number formatter used for visualization
+ */
+ NumberFormat nf = null;
+
+ /**
+ * Parameter for the gamma-correction.
+ *
+ * <p>
+ * Key: {@code -tooltip.digits}
+ * </p>
+ *
+ * <p>
+ * Default value: 4
+ * </p>
+ */
+ public static final OptionID DIGITS_ID = new OptionID("tooltip.digits", "Number of digits to show (e.g. when visualizing outlier scores)");
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ IntParameter digitsP = new IntParameter(DIGITS_ID, 4);
+ digitsP.addConstraint(CommonConstraints.GREATER_EQUAL_ZERO_INT);
+
+ if(config.grab(digitsP)) {
+ int digits = digitsP.intValue();
+ nf = NumberFormat.getInstance(Locale.ROOT);
+ nf.setGroupingUsed(false);
+ nf.setMaximumFractionDigits(digits);
+ }
+ }
+
+ @Override
+ protected TooltipScoreVisualization makeInstance() {
+ return new TooltipScoreVisualization(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java
new file mode 100644
index 00000000..d367306d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/TooltipStringVisualization.java
@@ -0,0 +1,214 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.ClassLabel;
+import de.lmu.ifi.dbs.elki.data.ExternalID;
+import de.lmu.ifi.dbs.elki.data.LabelList;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBID;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Generates a SVG-Element containing Tooltips. Tooltips remain invisible until
+ * their corresponding Marker is touched by the cursor and stay visible as long
+ * as the cursor lingers on the marker.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class TooltipStringVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME_ID = "ID Tooltips";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME_LABEL = "Object Label Tooltips";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME_CLASS = "Class Label Tooltips";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME_EID = "External ID Tooltips";
+
+ /**
+ * Constructor.
+ */
+ public TooltipStringVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object result) {
+ VisualizationTree.findNewSiblings(context, result, Relation.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<Relation<?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, Relation<?> rep, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final Class<?> clz = rep.getDataTypeInformation().getRestrictionClass();
+ if(DBID.class.isAssignableFrom(clz)) {
+ final VisualizationTask task = new VisualizationTask(NAME_ID, context, rep, rel, TooltipStringVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(rep, task);
+ context.addVis(p, task);
+ }
+ if(ClassLabel.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) {
+ final VisualizationTask task = new VisualizationTask(NAME_CLASS, context, rep, rel, TooltipStringVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(rep, task);
+ context.addVis(p, task);
+ }
+ if(LabelList.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) {
+ final VisualizationTask task = new VisualizationTask(NAME_LABEL, context, rep, rel, TooltipStringVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(rep, task);
+ context.addVis(p, task);
+ }
+ if(ExternalID.class.isAssignableFrom(rep.getDataTypeInformation().getRestrictionClass())) {
+ final VisualizationTask task = new VisualizationTask(NAME_EID, context, rep, rel, TooltipStringVisualization.this);
+ task.tool = true;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ task.initDefaultVisibility(false);
+ context.addVis(rep, task);
+ context.addVis(p, task);
+ }
+ }
+ });
+ }
+
+ /**
+ * Instance
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.has Relation oneway - - visualizes
+ */
+ public class Instance extends AbstractTooltipVisualization {
+ /**
+ * Number value to visualize
+ */
+ private Relation<?> result;
+
+ /**
+ * Font size to use.
+ */
+ private double fontsize;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ final StyleLibrary style = context.getStyleLibrary();
+ this.fontsize = 3 * style.getTextSize(StyleLibrary.PLOT);
+ addListeners();
+ }
+
+ @Override
+ protected Element makeTooltip(DBIDRef id, double x, double y, double dotsize) {
+ final Object data = result.get(id);
+ String label = (data == null) ? "null" : data.toString();
+ label = (label == "" || label == null) ? "null" : label;
+ return svgp.svgText(x + dotsize, y + fontsize * 0.07, label);
+ }
+
+ /**
+ * Registers the Tooltip-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ */
+ @Override
+ protected void setupCSS(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ final double fontsize = style.getTextSize(StyleLibrary.PLOT);
+ final String fontfamily = style.getFontFamily(StyleLibrary.PLOT);
+
+ CSSClass tooltiphidden = new CSSClass(svgp, TOOLTIP_HIDDEN);
+ tooltiphidden.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltiphidden.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ tooltiphidden.setStatement(SVGConstants.CSS_DISPLAY_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(tooltiphidden);
+
+ CSSClass tooltipvisible = new CSSClass(svgp, TOOLTIP_VISIBLE);
+ tooltipvisible.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltipvisible.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ svgp.addCSSClassOrLogError(tooltipvisible);
+
+ CSSClass tooltipsticky = new CSSClass(svgp, TOOLTIP_STICKY);
+ tooltipsticky.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ tooltipsticky.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ svgp.addCSSClassOrLogError(tooltipsticky);
+
+ // invisible but sensitive area for the tooltip activator
+ CSSClass tooltiparea = new CSSClass(svgp, TOOLTIP_AREA);
+ tooltiparea.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ tooltiparea.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ tooltiparea.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, "0");
+ tooltiparea.setStatement(SVGConstants.CSS_CURSOR_PROPERTY, SVGConstants.CSS_POINTER_VALUE);
+ svgp.addCSSClassOrLogError(tooltiparea);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java
new file mode 100644
index 00000000..fb4ed69c
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterHullVisualization.java
@@ -0,0 +1,403 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.CoreObjectsModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialUtil;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.geometry.AlphaShape;
+import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.iterator.ArrayListIter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.DoubleParameter;
+import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleObjPair;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer for generating an SVG-Element containing the convex hull / alpha
+ * shape of each cluster.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ClusterHullVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Cluster Hull (Scatterplot)";
+
+ /**
+ * Settings
+ */
+ Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public ClusterHullVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ // We attach ourselves to the style library, not the clustering, so there is
+ // only one hull.
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterHullVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ task.initDefaultVisibility(false);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.has Clustering oneway - - visualizes
+ * @apiviz.uses GrahamScanConvexHull2D
+ * @apiviz.uses AlphaShape
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String CLUSTERHULL = "cluster-hull";
+
+ /**
+ * Constructor
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ final ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol;
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) cpol.getClustering();
+
+ // Viewport size, for "relative size" computations
+ final CanvasSize viewp = proj.estimateViewport();
+ double projarea = viewp.getDiffX() * viewp.getDiffY();
+
+ List<Cluster<Model>> clusters = clustering.getAllClusters();
+ List<Cluster<Model>> topc = clustering.getToplevelClusters();
+ Hierarchy<Cluster<Model>> hier = clustering.getClusterHierarchy();
+ boolean flat = (clusters.size() == topc.size());
+ // Heuristic value for transparency:
+ double baseopacity = flat ? 0.5 : 0.5;
+
+ // Convex hull mode:
+ if(settings.alpha >= Double.POSITIVE_INFINITY) {
+ // Build the convex hulls (reusing the hulls of nested clusters!)
+ Map<Object, DoubleObjPair<Polygon>> hullmap = new HashMap<>(clusters.size());
+ for(Cluster<Model> clu : topc) {
+ buildHullsRecursively(clu, hier, hullmap);
+ }
+
+ // This way, we draw each cluster only once.
+ // Unfortunately, not depth ordered (TODO!)
+ for(Cluster<Model> clu : clusters) {
+ DoubleObjPair<Polygon> pair = hullmap.get(clu),
+ mpair = hullmap.get(clu.getModel());
+ // Plot the convex hull:
+ if(pair != null && pair.second != null && pair.second.size() > 1) {
+ SVGPath path = new SVGPath(pair.second);
+ // Approximate area (using bounding box)
+ double hullarea = SpatialUtil.volume(pair.second);
+ final double relativeArea = 1 - (hullarea / projarea);
+ final double relativeSize = pair.first / rel.size();
+ final double corefact = (mpair == null) ? 1.0 : .5;
+ final double opacity = corefact * baseopacity * Math.sqrt(relativeSize * relativeArea);
+ addCSSClasses(svgp, cpol.getStyleForCluster(clu), opacity);
+
+ Element hulls = path.makeElement(svgp);
+ SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu));
+ layer.appendChild(hulls);
+ }
+ // For core density models, over-plot the core:
+ if(mpair != null && mpair.second != null && mpair.second.size() > 1) {
+ SVGPath path = new SVGPath(mpair.second);
+ // Approximate area (using bounding box)
+ double hullarea = SpatialUtil.volume(mpair.second);
+ final double relativeArea = 1 - (hullarea / projarea);
+ final double relativeSize = mpair.first / rel.size();
+ final double opacity = .5 * baseopacity * Math.sqrt(relativeSize * relativeArea);
+ addCSSClasses(svgp, cpol.getStyleForCluster(clu), opacity);
+
+ Element hulls = path.makeElement(svgp);
+ SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu));
+ layer.appendChild(hulls);
+ }
+ }
+ }
+ else {
+ // Alpha shape mode.
+ // For alpha shapes we can't use the shortcut of convex hulls,
+ // but have to revisit all child clusters.
+ for(Cluster<Model> clu : clusters) {
+ ArrayList<Vector> ps = new ArrayList<>();
+ double weight = addRecursively(ps, hier, clu);
+ List<Polygon> polys;
+ if(ps.size() < 1) {
+ continue;
+ }
+ if(ps.size() > 2) {
+ polys = (new AlphaShape(ps, settings.alpha * Projection.SCALE)).compute();
+ }
+ else {
+ // Trivial polygon. Might still degenerate to a single point though.
+ polys = new ArrayList<>(1);
+ polys.add(new Polygon(ps));
+ }
+ for(Polygon p : polys) {
+ SVGPath path = new SVGPath(p);
+ Element hulls = path.makeElement(svgp);
+ addCSSClasses(svgp, cpol.getStyleForCluster(clu), baseopacity * weight / rel.size());
+ SVGUtil.addCSSClass(hulls, CLUSTERHULL + cpol.getStyleForCluster(clu));
+ layer.appendChild(hulls);
+ }
+ }
+ }
+ }
+
+ /**
+ * Recursively step through the clusters to build the hulls.
+ *
+ * @param clu Current cluster
+ * @param hier Clustering hierarchy
+ * @param hulls Hull map
+ */
+ private DoubleObjPair<Polygon> buildHullsRecursively(Cluster<Model> clu, Hierarchy<Cluster<Model>> hier, Map<Object, DoubleObjPair<Polygon>> hulls) {
+ final Model model = clu.getModel();
+ final DBIDs ids = clu.getIDs();
+ boolean coremodel = false;
+ DBIDs cids = null;
+ if(model instanceof CoreObjectsModel) {
+ cids = ((CoreObjectsModel) model).getCoreObjects();
+ coremodel = cids.size() > 0;
+ }
+
+ GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D();
+ GrahamScanConvexHull2D hull2 = coremodel ? new GrahamScanConvexHull2D() : null;
+ for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) {
+ final double[] projv = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ if(projv[0] != projv[0] || projv[1] != projv[1]) {
+ continue; // NaN!
+ }
+ Vector projP = new Vector(projv);
+ hull.add(projP);
+ if(coremodel && cids.contains(iter)) {
+ hull2.add(projP);
+ }
+ }
+ double weight = ids.size(), cweight = coremodel ? cids.size() : 0.0;
+ if(hier != null && hulls != null) {
+ final int numc = hier.numChildren(clu);
+ if(numc > 0) {
+ for(Iter<Cluster<Model>> iter = hier.iterChildren(clu); iter.valid(); iter.advance()) {
+ final Cluster<Model> iclu = iter.get();
+ DoubleObjPair<Polygon> poly = hulls.get(iclu);
+ if(poly == null) {
+ poly = buildHullsRecursively(iclu, hier, hulls);
+ }
+ // Add inner convex hull to outer convex hull.
+ for(ArrayListIter<Vector> vi = poly.second.iter(); vi.valid(); vi.advance()) {
+ hull.add(vi.get());
+ }
+ // For a core model, include the inner core, too.
+ if(coremodel) {
+ DoubleObjPair<Polygon> ipoly = hulls.get(iclu.getModel());
+ if(ipoly != null) {
+ for(ArrayListIter<Vector> vi = ipoly.second.iter(); vi.valid(); vi.advance()) {
+ hull2.add(vi.get());
+ }
+ cweight += ipoly.first / numc;
+ }
+ }
+ weight += poly.first / numc;
+ }
+ }
+ }
+ DoubleObjPair<Polygon> pair = new DoubleObjPair<>(weight, hull.getHull());
+ hulls.put(clu, pair);
+ if(coremodel) {
+ hulls.put(model, new DoubleObjPair<>(cweight, hull2.getHull()));
+ }
+ return pair;
+ }
+
+ /**
+ * Recursively add a cluster and its children.
+ *
+ * @param hull Hull to add to
+ * @param hier Cluster hierarchy
+ * @param clus Current cluster
+ * @return Weight for visualization
+ */
+ private double addRecursively(ArrayList<Vector> hull, Hierarchy<Cluster<Model>> hier, Cluster<Model> clus) {
+ final DBIDs ids = clus.getIDs();
+ double weight = ids.size();
+ for(DBIDIter iter = ids.iter(); iter.valid(); iter.advance()) {
+ double[] projP = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ if(projP[0] != projP[0] || projP[1] != projP[1]) {
+ continue; // NaN!
+ }
+ hull.add(new Vector(projP));
+ }
+ for(Iter<Cluster<Model>> iter = hier.iterChildren(clus); iter.valid(); iter.advance()) {
+ weight += .5 * addRecursively(hull, hier, iter.get());
+ }
+ return weight;
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp, int clusterID, double opac) {
+ final StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ CSSClass cls = new CSSClass(this, CLUSTERHULL + clusterID);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, .5 * style.getLineWidth(StyleLibrary.PLOT));
+
+ final String color = colors.getColor(clusterID);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac);
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Alpha-Value for alpha-shapes
+ *
+ * <p>
+ * Key: {@code -hull.alpha}
+ * </p>
+ */
+ public static final OptionID ALPHA_ID = new OptionID("hull.alpha", "Alpha value for hull drawing (in projected space!).");
+
+ /**
+ * Alpha value
+ */
+ double alpha = Double.POSITIVE_INFINITY;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ DoubleParameter alphaP = new DoubleParameter(ALPHA_ID, Double.POSITIVE_INFINITY);
+ if(config.grab(alphaP)) {
+ alpha = alphaP.doubleValue();
+ }
+ }
+
+ @Override
+ protected ClusterHullVisualization makeInstance() {
+ return new ClusterHullVisualization(this);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java
new file mode 100644
index 00000000..bd597111
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterMeanVisualization.java
@@ -0,0 +1,207 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Iterator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.model.MeanModel;
+import de.lmu.ifi.dbs.elki.data.model.MedoidModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.database.ids.DBID;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize the mean of a KMeans-Clustering
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ClusterMeanVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Cluster Means";
+
+ /**
+ * Constructor.
+ */
+ public ClusterMeanVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(final VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), ClusterMeanVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA + 1;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has MeanModel oneway - - visualizes
+ * @apiviz.has MedoidModel oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * CSS class name for center of the means
+ */
+ private static final String CSS_MEAN_CENTER = "mean-center";
+
+ /**
+ * CSS class name for center of the means
+ */
+ private static final String CSS_MEAN = "mean-marker";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering();
+ if(clustering.getAllClusters().size() == 0) {
+ return;
+ }
+
+ StyleLibrary slib = context.getStyleLibrary();
+ MarkerLibrary ml = slib.markers();
+ double marker_size = slib.getSize(StyleLibrary.MARKERPLOT);
+
+ // Small crosses for mean:
+ if(!svgp.getCSSClassManager().contains(CSS_MEAN_CENTER)) {
+ CSSClass center = new CSSClass(this, CSS_MEAN_CENTER);
+ center.setStatement(SVGConstants.CSS_STROKE_PROPERTY, slib.getTextColor(StyleLibrary.DEFAULT));
+ center.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, slib.getLineWidth(StyleLibrary.AXIS_TICK) * .5);
+ svgp.addCSSClassOrLogError(center);
+ }
+ // Markers for the mean:
+ if(!svgp.getCSSClassManager().contains(CSS_MEAN)) {
+ CSSClass center = new CSSClass(this, CSS_MEAN);
+ center.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.7");
+ svgp.addCSSClassOrLogError(center);
+ }
+
+ Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator();
+ for(int cnum = 0; ci.hasNext(); cnum++) {
+ Cluster<Model> clus = ci.next();
+ Model model = clus.getModel();
+ double[] mean = null;
+ try {
+ if(model instanceof MeanModel) {
+ final Vector mmean = ((MeanModel) model).getMean();
+ if(mmean == null) {
+ continue;
+ }
+ mean = proj.fastProjectDataToRenderSpace(mmean);
+ }
+ else if(model instanceof MedoidModel) {
+ DBID medoid = ((MedoidModel) model).getMedoid();
+ if(medoid == null) {
+ continue;
+ }
+ NumberVector v = rel.get(medoid);
+ if(v == null) {
+ continue;
+ }
+ mean = proj.fastProjectDataToRenderSpace(v);
+ }
+ else {
+ continue;
+ }
+ }
+ catch(ObjectNotFoundException e) {
+ continue; // Element not found.
+ }
+
+ // add a greater Marker for the mean
+ Element meanMarker = ml.useMarker(svgp, layer, mean[0], mean[1], cnum, marker_size * 3);
+ SVGUtil.setAtt(meanMarker, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN);
+
+ // Add a fine cross to mark the exact location of the mean.
+ Element meanMarkerCenter = svgp.svgLine(mean[0] - .7, mean[1], mean[0] + .7, mean[1]);
+ SVGUtil.setAtt(meanMarkerCenter, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN_CENTER);
+ Element meanMarkerCenter2 = svgp.svgLine(mean[0], mean[1] - .7, mean[0], mean[1] + .7);
+ SVGUtil.setAtt(meanMarkerCenter2, SVGConstants.SVG_CLASS_ATTRIBUTE, CSS_MEAN_CENTER);
+
+ layer.appendChild(meanMarkerCenter);
+ layer.appendChild(meanMarkerCenter2);
+ }
+ svgp.updateStyleElement();
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java
new file mode 100644
index 00000000..9a029c79
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterOrderVisualization.java
@@ -0,0 +1,162 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.algorithm.clustering.optics.ClusterOrder;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDVar;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Cluster order visualizer: connect objects via the spanning tree the cluster
+ * order represents.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// TODO: draw sample only?
+public class ClusterOrderVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Predecessor Graph";
+
+ /**
+ * Constructor.
+ */
+ public ClusterOrderVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, ClusterOrder.class, ScatterPlotProjector.class, //
+ new VisualizationTree.Handler2<ClusterOrder, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, ClusterOrder co, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, co, rel, ClusterOrderVisualization.this);
+ task.initDefaultVisibility(false);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA);
+ context.addVis(co, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Instance
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has ClusterOrder oneway - - visualizes
+ */
+ // TODO: listen for CLUSTER ORDER changes.
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * CSS class name
+ */
+ private static final String CSSNAME = "predecessor";
+
+ /**
+ * The result we visualize
+ */
+ protected ClusterOrder result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task.
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass cls = new CSSClass(this, CSSNAME);
+ style.lines().formatCSSClass(cls, 0, style.getLineWidth(StyleLibrary.CLUSTERORDER));
+
+ svgp.addCSSClassOrLogError(cls);
+
+ DBIDVar prev = DBIDUtil.newVar();
+ for(DBIDIter it = result.iter(); it.valid(); it.advance()) {
+ result.getPredecessor(it, prev);
+ if(prev.isEmpty()) {
+ continue;
+ }
+ double[] thisVec = proj.fastProjectDataToRenderSpace(rel.get(it));
+ double[] prevVec = proj.fastProjectDataToRenderSpace(rel.get(prev));
+
+ if(thisVec[0] != thisVec[0] || thisVec[1] != thisVec[1]) {
+ continue; // NaN!
+ }
+ if(prevVec[0] != prevVec[0] || prevVec[1] != prevVec[1]) {
+ continue; // NaN!
+ }
+ // FIXME: add arrow decorations!
+ Element arrow = svgp.svgLine(prevVec[0], prevVec[1], thisVec[0], thisVec[1]);
+ SVGUtil.setCSSClass(arrow, cls.getName());
+
+ layer.appendChild(arrow);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java
new file mode 100644
index 00000000..c0bedc4a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/ClusterStarVisualization.java
@@ -0,0 +1,182 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Iterator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.MeanModel;
+import de.lmu.ifi.dbs.elki.data.model.MedoidModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize the mean of a KMeans-Clustering using stars.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class ClusterStarVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Cluster Stars";
+
+ /**
+ * Constructor.
+ */
+ public ClusterStarVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(final VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, rel, ClusterStarVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA + 1;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ task.initDefaultVisibility(false);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has MeanModel oneway - - visualizes
+ * @apiviz.has MedoidModel oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * CSS class name for center of the means
+ */
+ private static final String CSS_MEAN_STAR = "mean-star";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering();
+ if(clustering.getAllClusters().size() == 0) {
+ return;
+ }
+
+ StyleLibrary slib = context.getStyleLibrary();
+ ColorLibrary colors = slib.getColorSet(StyleLibrary.PLOT);
+
+ Iterator<Cluster<Model>> ci = clustering.getAllClusters().iterator();
+ for(int cnum = 0; ci.hasNext(); cnum++) {
+ Cluster<Model> clus = ci.next();
+ Model model = clus.getModel();
+ double[] mean;
+ if(model instanceof MeanModel) {
+ MeanModel mmodel = (MeanModel) model;
+ mean = proj.fastProjectDataToRenderSpace(mmodel.getMean());
+ }
+ else if(model instanceof MedoidModel) {
+ MedoidModel mmodel = (MedoidModel) model;
+ mean = proj.fastProjectDataToRenderSpace(rel.get(mmodel.getMedoid()));
+ }
+ else {
+ continue;
+ }
+
+ if(!svgp.getCSSClassManager().contains(CSS_MEAN_STAR + "_" + cnum)) {
+ CSSClass center = new CSSClass(this, CSS_MEAN_STAR + "_" + cnum);
+ center.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(cnum));
+ center.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, slib.getLineWidth(StyleLibrary.PLOT));
+ center.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, "0.7");
+ svgp.addCSSClassOrLogError(center);
+ }
+
+ SVGPath star = new SVGPath();
+ for(DBIDIter id = clus.getIDs().iter(); id.valid(); id.advance()) {
+ double[] obj = proj.fastProjectDataToRenderSpace(rel.get(id));
+ star.moveTo(obj);
+ star.drawTo(mean);
+ }
+ Element stare = star.makeElement(svgp);
+ SVGUtil.setCSSClass(stare, CSS_MEAN_STAR + "_" + cnum);
+ layer.appendChild(stare);
+ }
+ svgp.updateStyleElement();
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java
new file mode 100644
index 00000000..04a2d241
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/EMClusterVisualization.java
@@ -0,0 +1,487 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.EMModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.EigenPair;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Matrix;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.SortedEigenPairs;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.pca.PCARunner;
+import de.lmu.ifi.dbs.elki.utilities.ClassGenericsUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.EmptyParameterization;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer for generating SVG-Elements containing ellipses for first, second
+ * and third standard deviation. In more than 2-dimensional data, the class
+ * tries to approximate the cluster extends.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class EMClusterVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "EM Cluster Models";
+
+ /**
+ * Constants for quantiles of standard deviation
+ */
+ final static double[] sigma = new double[] { 0.41, 0.223, 0.047 };
+
+ /**
+ * Constructor
+ */
+ public EMClusterVisualization() {
+ super();
+ }
+
+ @Override
+ public Instance makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), EMClusterVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA + 3;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has EMModel oneway - - visualizes
+ * @apiviz.uses GrahamScanConvexHull2D
+ */
+ // TODO: nicer stacking of n-fold hulls
+ // TODO: can we find a proper sphere for 3+ dimensions?
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String EMBORDER = "EMClusterBorder";
+
+ /**
+ * Kappa constant,
+ */
+ private static final double KAPPA = SVGHyperSphere.EUCLIDEAN_KAPPA;
+
+ /**
+ * StyleParameter:
+ */
+ private int times = 3;
+
+ private int opacStyle = 1;
+
+ private int softBorder = 1;
+
+ private int drawStyle = 0;
+
+ /**
+ * Constructor
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering();
+ List<Cluster<Model>> clusters = clustering.getAllClusters();
+ if(clusters.size() <= 1) {
+ return;
+ }
+
+ StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ // PCARunner
+ PCARunner pcarun = ClassGenericsUtil.parameterizeOrAbort(PCARunner.class, new EmptyParameterization());
+
+ Iterator<Cluster<Model>> ci = clusters.iterator();
+ for(int cnum = 0; cnum < clusters.size(); cnum++) {
+ Cluster<Model> clus = ci.next();
+ DBIDs ids = clus.getIDs();
+ if(ids.size() <= 0) {
+ continue;
+ }
+ if(!(clus.getModel() instanceof EMModel)) {
+ continue;
+ }
+ EMModel model = (EMModel) clus.getModel();
+
+ // Add cluster style
+ final String sname = EMBORDER + "_" + cnum;
+ if(!svgp.getCSSClassManager().contains(sname)) {
+ CSSClass cls = new CSSClass(this, sname);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5);
+
+ String color = colors.getColor(cnum);
+ if(softBorder == 0) {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ }
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.15);
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+
+ Matrix covmat = model.getCovarianceMatrix();
+ Vector centroid = model.getMean();
+ Vector cent = new Vector(proj.fastProjectDataToRenderSpace(centroid));
+
+ // Compute the eigenvectors
+ SortedEigenPairs eps = pcarun.processCovarMatrix(covmat).getEigenPairs();
+ Vector[] pc = new Vector[eps.size()];
+ for(int i = 0; i < eps.size(); i++) {
+ EigenPair ep = eps.getEigenPair(i);
+ Vector sev = ep.getEigenvector().times(Math.sqrt(ep.getEigenvalue()));
+ pc[i] = new Vector(proj.fastProjectRelativeDataToRenderSpace(sev.getArrayRef()));
+ }
+ if(drawStyle != 0 || eps.size() == 2) {
+ drawSphere2D(sname, cent, pc);
+ }
+ else {
+ Polygon chres = makeHullComplex(pc);
+ drawHullLines(sname, cent, chres);
+ }
+ }
+ }
+
+ /**
+ * Draw by approximating a sphere via cubic splines
+ *
+ * @param sname CSS class name
+ * @param cent center
+ * @param pc Principal components
+ */
+ protected void drawSphere2D(String sname, Vector cent, Vector[] pc) {
+ CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null;
+ for(int dim1 = 0; dim1 < pc.length - 1; dim1++) {
+ for(int dim2 = dim1 + 1; dim2 < pc.length; dim2++) {
+ for(int i = 1; i <= times; i++) {
+ SVGPath path = new SVGPath();
+
+ Vector p1 = cent.plusTimes(pc[dim1], i);
+ Vector p2 = cent.plusTimes(pc[dim2], i);
+ Vector p3 = cent.minusTimes(pc[dim1], i);
+ Vector p4 = cent.minusTimes(pc[dim2], i);
+
+ path.moveTo(p1);
+ path.cubicTo(//
+ p1.plusTimes(pc[dim2], KAPPA * i), //
+ p2.plusTimes(pc[dim1], KAPPA * i), //
+ p2);
+ path.cubicTo(//
+ p2.minusTimes(pc[dim1], KAPPA * i), //
+ p3.plusTimes(pc[dim2], KAPPA * i), //
+ p3);
+ path.cubicTo(//
+ p3.minusTimes(pc[dim2], KAPPA * i), //
+ p4.minusTimes(pc[dim1], KAPPA * i), //
+ p4);
+ path.cubicTo(//
+ p4.plusTimes(pc[dim1], KAPPA * i), //
+ p1.minusTimes(pc[dim2], KAPPA * i), //
+ p1);
+ path.close();
+
+ Element ellipse = path.makeElement(svgp);
+ SVGUtil.addCSSClass(ellipse, sname);
+ if(cls != null) {
+ double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0;
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s);
+ SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ }
+ layer.appendChild(ellipse);
+ }
+ }
+ }
+ }
+
+ /**
+ * Approximate by convex hull.
+ *
+ * @param sname CSS name
+ * @param cent center
+ * @param chres Polygon around center
+ */
+ protected void drawHullLines(String sname, Vector cent, Polygon chres) {
+ if(chres.size() <= 1) {
+ return;
+ }
+ CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null;
+ for(int i = 1; i <= times; i++) {
+ SVGPath path = new SVGPath();
+ for(int p = 0; p < chres.size(); p++) {
+ path.drawTo(cent.plusTimes(chres.get(p), i));
+ }
+ path.close();
+ Element ellipse = path.makeElement(svgp);
+ SVGUtil.addCSSClass(ellipse, sname);
+ if(cls != null) {
+ double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0;
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s);
+ SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ }
+ layer.appendChild(ellipse);
+ }
+ }
+
+ /**
+ * Build a convex hull to approximate the sphere.
+ *
+ * @param pc Principal components
+ * @return Polygon
+ */
+ protected Polygon makeHull(Vector[] pc) {
+ GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D();
+
+ Vector diag = new Vector(0, 0);
+ for(int j = 0; j < pc.length; j++) {
+ hull.add(pc[j]);
+ hull.add(pc[j].times(-1));
+ for(int k = j + 1; k < pc.length; k++) {
+ Vector q = pc[k];
+ Vector ppq = pc[j].plus(q).timesEquals(MathUtil.SQRTHALF);
+ Vector pmq = pc[j].minus(q).timesEquals(MathUtil.SQRTHALF);
+ hull.add(ppq);
+ hull.add(ppq.times(-1));
+ hull.add(pmq);
+ hull.add(pmq.times(-1));
+ }
+ diag.plusEquals(pc[j]);
+ }
+ diag.timesEquals(1.0 / Math.sqrt(pc.length));
+ hull.add(diag);
+ hull.add(diag.times(-1));
+
+ return hull.getHull();
+ }
+
+ /**
+ * Build a convex hull to approximate the sphere.
+ *
+ * @param pc Principal components
+ * @return Polygon
+ */
+ protected Polygon makeHullComplex(Vector[] pc) {
+ GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D();
+
+ Vector diag = new Vector(0, 0);
+ for(int j = 0; j < pc.length; j++) {
+ hull.add(pc[j]);
+ hull.add(pc[j].times(-1));
+ for(int k = j + 1; k < pc.length; k++) {
+ Vector q = pc[k];
+ Vector ppq = pc[j].plus(q).timesEquals(MathUtil.SQRTHALF);
+ Vector pmq = pc[j].minus(q).timesEquals(MathUtil.SQRTHALF);
+ hull.add(ppq);
+ hull.add(ppq.times(-1));
+ hull.add(pmq);
+ hull.add(pmq.times(-1));
+ for(int l = k + 1; l < pc.length; l++) {
+ Vector r = pc[k];
+ Vector ppqpr = ppq.plus(r).timesEquals(Math.sqrt(1 / 3.));
+ Vector pmqpr = pmq.plus(r).timesEquals(Math.sqrt(1 / 3.));
+ Vector ppqmr = ppq.minus(r).timesEquals(Math.sqrt(1 / 3.));
+ Vector pmqmr = pmq.minus(r).timesEquals(Math.sqrt(1 / 3.));
+ hull.add(ppqpr);
+ hull.add(ppqpr.times(-1));
+ hull.add(pmqpr);
+ hull.add(pmqpr.times(-1));
+ hull.add(ppqmr);
+ hull.add(ppqmr.times(-1));
+ hull.add(pmqmr);
+ hull.add(pmqmr.times(-1));
+ }
+ }
+ diag.plusEquals(pc[j]);
+ }
+ diag.timesEquals(1.0 / Math.sqrt(pc.length));
+ hull.add(diag);
+ hull.add(diag.times(-1));
+ return hull.getHull();
+ }
+
+ /**
+ * Approximate the hull using arcs.
+ *
+ * @param sname CSS name
+ * @param cent Center
+ * @param chres Polygon
+ */
+ protected void drawHullArc(String sname, Vector cent, Polygon chres) {
+ if(chres.size() <= 1) {
+ return;
+ }
+ CSSClass cls = opacStyle == 1 ? new CSSClass(null, "temp") : null;
+ for(int i = 1; i <= times; i++) {
+ SVGPath path = new SVGPath();
+
+ ArrayList<Vector> delta = new ArrayList<>(chres.size());
+ for(int p = 0; p < chres.size(); p++) {
+ Vector prev = chres.get((p - 1 + chres.size()) % chres.size());
+ Vector curr = chres.get(p);
+ Vector next = chres.get((p + 1) % chres.size());
+ Vector d1 = next.minus(curr).normalize();
+ Vector d2 = curr.minus(prev).normalize();
+ delta.add(d1.plus(d2));
+ // delta.add(next.minus(prev));
+ }
+
+ for(int p = 0; p < chres.size(); p++) {
+ Vector cur = cent.plus(chres.get(p));
+ Vector nex = cent.plus(chres.get((p + 1) % chres.size()));
+ Vector dcur = delta.get(p);
+ Vector dnex = delta.get((p + 1) % chres.size());
+ drawArc(path, cent, cur, nex, dcur, dnex, i);
+ }
+ path.close();
+
+ Element ellipse = path.makeElement(svgp);
+
+ SVGUtil.addCSSClass(ellipse, sname);
+ if(cls != null) {
+ double s = (i >= 1 && i <= sigma.length) ? sigma[i - 1] : 0.0;
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, s);
+ SVGUtil.setAtt(ellipse, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ }
+ layer.appendChild(ellipse);
+ }
+ }
+
+ /**
+ * Draw an arc to simulate the hyper ellipse.
+ *
+ * @param path Path to draw to
+ * @param cent Center
+ * @param pre Previous point
+ * @param nex Next point
+ * @param scale Scaling factor
+ */
+ private void drawArc(SVGPath path, Vector cent, Vector pre, Vector nex, Vector oPrev, Vector oNext, double scale) {
+ // Delta vectors
+ final Vector rPrev = pre.minus(cent);
+ final Vector rNext = nex.minus(cent);
+ final Vector rPrNe = pre.minus(nex);
+ // Scaled fix points
+ final Vector sPrev = cent.plusTimes(rPrev, scale);
+ final Vector sNext = cent.plusTimes(rNext, scale);
+ // Orthogonal vectors to the relative vectors
+ // final Vector oPrev = new Vector(rPrev.get(1), -rPrev.get(0));
+ // final Vector oNext = new Vector(-rNext.get(1), rNext.get(0));
+
+ // Compute the intersection of rPrev+tp*oPrev and rNext+tn*oNext
+ // rPrNe == rPrev - rNext
+ final double zp = rPrNe.get(0) * oNext.get(1) - rPrNe.get(1) * oNext.get(0);
+ final double zn = rPrNe.get(0) * oPrev.get(1) - rPrNe.get(1) * oPrev.get(0);
+ final double n = oPrev.get(1) * oNext.get(0) - oPrev.get(0) * oNext.get(1);
+ if(n == 0) {
+ LoggingUtil.warning("Parallel?!?");
+ path.drawTo(sNext.get(0), sNext.get(1));
+ return;
+ }
+ final double tp = Math.abs(zp / n);
+ final double tn = Math.abs(zn / n);
+ // LoggingUtil.warning("tp: "+tp+" tn: "+tn);
+
+ // Guide points
+ final Vector gPrev = sPrev.plusTimes(oPrev, KAPPA * scale * tp);
+ final Vector gNext = sNext.minusTimes(oNext, KAPPA * scale * tn);
+
+ if(!path.isStarted()) {
+ path.moveTo(sPrev);
+ }
+ // path.drawTo(sPrev);
+ // path.drawTo(gPrev);
+ // path.drawTo(gNext);
+ // path.drawTo(sNext));
+ // path.moveTo(sPrev);
+ // if(tp < 0 || tn < 0) {
+ // path.drawTo(sNext);
+ // }
+ // else {
+ path.cubicTo(gPrev, gNext, sNext);
+ // }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java
new file mode 100644
index 00000000..f1d9051b
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/VoronoiVisualization.java
@@ -0,0 +1,323 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.model.KMeansModel;
+import de.lmu.ifi.dbs.elki.data.model.MedoidModel;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.database.ids.DBID;
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D;
+import de.lmu.ifi.dbs.elki.math.geometry.SweepHullDelaunay2D.Triangle;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.EnumParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.svg.VoronoiDraw;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer drawing Voronoi cells for k-means clusterings.
+ *
+ * See also: {@link de.lmu.ifi.dbs.elki.algorithm.clustering.kmeans.KMeansLloyd
+ * KMeans clustering}
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class VoronoiVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "k-means Voronoi cells";
+
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ private static final String KMEANSBORDER = "kmeans-border";
+
+ /**
+ * Visualization mode.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static enum Mode {
+ /**
+ * Draw Voronoi cells.
+ */
+ VORONOI, //
+ /**
+ * Draw Delaunay triangulation.
+ */
+ DELAUNAY, //
+ /**
+ * Draw both Delaunay and Voronoi.
+ */
+ V_AND_D
+ }
+
+ /**
+ * Visualization mode.
+ */
+ private Mode mode;
+
+ /**
+ * Constructor.
+ *
+ * @param mode Visualization mod
+ */
+ public VoronoiVisualization(Mode mode) {
+ super();
+ this.mode = mode;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, p.getRelation(), VoronoiVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA + 3;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.has KMeansModel oneway - - visualizes
+ * @apiviz.has MedoidModel oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * The Voronoi diagram.
+ */
+ Element voronoi;
+
+ /**
+ * Constructor.
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StylingPolicy spol = context.getStylingPolicy();
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) spol).getClustering();
+ if(clustering.getAllClusters().size() <= 1) {
+ return;
+ }
+
+ final int dim = proj.getInputDimensionality();
+ if(dim != 2) {
+ return;
+ }
+
+ addCSSClasses(svgp);
+ final List<Cluster<Model>> clusters = clustering.getAllClusters();
+
+ // Collect cluster means
+ ArrayList<Vector> vmeans = new ArrayList<>(clusters.size());
+ ArrayList<double[]> means = new ArrayList<>(clusters.size());
+ {
+ for(Cluster<Model> clus : clusters) {
+ Model model = clus.getModel();
+ Vector mean;
+ try {
+ if(model instanceof KMeansModel) {
+ Vector mmean = ((KMeansModel) model).getMean();
+ if(mmean == null) {
+ continue;
+ }
+ mean = mmean.getColumnVector();
+ if(mean.getDimensionality() != dim) {
+ continue;
+ }
+ }
+ else if(model instanceof MedoidModel) {
+ DBID medoid = ((MedoidModel) model).getMedoid();
+ if(medoid == null) {
+ continue;
+ }
+ NumberVector v = rel.get(medoid);
+ if(v == null) {
+ continue;
+ }
+ mean = v.getColumnVector();
+ if(mean.getDimensionality() != dim) {
+ continue;
+ }
+ }
+ else {
+ continue;
+ }
+ }
+ catch(ObjectNotFoundException e) {
+ continue; // Element not found.
+ }
+ vmeans.add(mean);
+ means.add(mean.getArrayRef());
+ }
+ }
+
+ if(means.size() < 2) {
+ return; // Cannot visualize
+ }
+ if(means.size() == 2) {
+ if(mode == Mode.VORONOI || mode == Mode.V_AND_D) {
+ Element path = VoronoiDraw.drawFakeVoronoi(proj, means).makeElement(svgp);
+ SVGUtil.addCSSClass(path, KMEANSBORDER);
+ layer.appendChild(path);
+ }
+ if(mode == Mode.DELAUNAY || mode == Mode.V_AND_D) {
+ Element path = new SVGPath(proj.fastProjectDataToRenderSpace(means.get(0)))//
+ .drawTo(proj.fastProjectDataToRenderSpace(means.get(1))).makeElement(svgp);
+ SVGUtil.addCSSClass(path, KMEANSBORDER);
+ layer.appendChild(path);
+ }
+ }
+ else {
+ // Compute Delaunay Triangulation
+ ArrayList<Triangle> delaunay = new SweepHullDelaunay2D(vmeans).getDelaunay();
+ if(mode == Mode.VORONOI || mode == Mode.V_AND_D) {
+ Element path = VoronoiDraw.drawVoronoi(proj, delaunay, means).makeElement(svgp);
+ SVGUtil.addCSSClass(path, KMEANSBORDER);
+ layer.appendChild(path);
+ }
+ if(mode == Mode.DELAUNAY || mode == Mode.V_AND_D) {
+ Element path = VoronoiDraw.drawDelaunay(proj, delaunay, means).makeElement(svgp);
+ SVGUtil.addCSSClass(path, KMEANSBORDER);
+ layer.appendChild(path);
+ }
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes.
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ // Class for the distance markers
+ if(!svgp.getCSSClassManager().contains(KMEANSBORDER)) {
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass cls = new CSSClass(this, KMEANSBORDER);
+ cls = new CSSClass(this, KMEANSBORDER);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) * .5);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Mode for drawing: Voronoi, Delaunay, both.
+ *
+ * <p>
+ * Key: {@code -voronoi.mode}
+ * </p>
+ */
+ public static final OptionID MODE_ID = new OptionID("voronoi.mode", "Mode for drawing the voronoi cells (and/or delaunay triangulation)");
+
+ /**
+ * Drawing mode.
+ */
+ protected Mode mode;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ EnumParameter<Mode> modeP = new EnumParameter<>(MODE_ID, Mode.class, Mode.VORONOI);
+ if(config.grab(modeP)) {
+ mode = modeP.getValue();
+ }
+ }
+
+ @Override
+ protected VoronoiVisualization makeInstance() {
+ return new VoronoiVisualization(mode);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java
new file mode 100755
index 00000000..84f9f745
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/cluster/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for clustering results based on 2D projections.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java
new file mode 100644
index 00000000..95239509
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/DensityEstimationOverlay.java
@@ -0,0 +1,254 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+import java.awt.image.BufferedImage;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.math.MeanVariance;
+import de.lmu.ifi.dbs.elki.result.KMLOutputHandler;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * A simple density estimation visualization, based on a simple kernel-density
+ * <em>in the projection, not the actual data!</em>
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// TODO: Use sample only
+public class DensityEstimationOverlay extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Density estimation overlay";
+
+ /**
+ * Constructor.
+ */
+ public DensityEstimationOverlay() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, rel, p.getRelation(), DensityEstimationOverlay.this);
+ task.level = VisualizationTask.LEVEL_DATA + 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA);
+ task.initDefaultVisibility(false);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance for a particular data set.
+ *
+ * @author Erich Schubert
+ */
+ // TODO: make parameterizable, in particular color map, kernel bandwidth and
+ // kernel function
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Density map resolution
+ */
+ private int resolution = 500;
+
+ /**
+ * The actual image
+ */
+ private BufferedImage img = null;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ if(img == null) {
+ renderImage();
+ }
+
+ CanvasSize canvas = proj.estimateViewport();
+ String imguri = ThumbnailRegistryEntry.INTERNAL_PREFIX + ThumbnailRegistryEntry.registerImage(img);
+ Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, canvas.minx);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, canvas.miny);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, canvas.maxx - canvas.minx);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, canvas.maxy - canvas.miny);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_STYLE_ATTRIBUTE, SVGConstants.CSS_OPACITY_PROPERTY + ": .5");
+ itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, imguri);
+
+ layer.appendChild(itag);
+ }
+
+ @Reference(authors = "D. W. Scott", title = "Multivariate density estimation: Theory, Practice, and Visualization", //
+ booktitle = "Multivariate Density Estimation: Theory, Practice, and Visualization", //
+ url = "http://dx.doi.org/10.1002/9780470316849")
+ private double[] initializeBandwidth(double[][] data) {
+ MeanVariance mv0 = new MeanVariance();
+ MeanVariance mv1 = new MeanVariance();
+ // For Kernel bandwidth.
+ for(double[] projected : data) {
+ mv0.put(projected[0]);
+ mv1.put(projected[1]);
+ }
+ // Set bandwidths according to Scott's rule:
+ // Note: in projected space, d=2.
+ double[] bandwidth = new double[2];
+ bandwidth[0] = MathUtil.SQRT5 * mv0.getSampleStddev() * Math.pow(rel.size(), -1 / 6.);
+ bandwidth[1] = MathUtil.SQRT5 * mv1.getSampleStddev() * Math.pow(rel.size(), -1 / 6.);
+ return bandwidth;
+ }
+
+ private void renderImage() {
+ // TODO: SAMPLE? Do region queries?
+ // Project the data just once, keep a copy.
+ double[][] data = new double[rel.size()][];
+ {
+ int i = 0;
+ for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ data[i] = proj.fastProjectDataToRenderSpace(rel.get(iditer));
+ i++;
+ }
+ }
+ double[] bandwidth = initializeBandwidth(data);
+ // Compare by first component
+ Comparator<double[]> comp0 = new Comparator<double[]>() {
+ @Override
+ public int compare(double[] o1, double[] o2) {
+ return Double.compare(o1[0], o2[0]);
+ }
+ };
+ // Compare by second component
+ Comparator<double[]> comp1 = new Comparator<double[]>() {
+ @Override
+ public int compare(double[] o1, double[] o2) {
+ return Double.compare(o1[1], o2[1]);
+ }
+ };
+ // TODO: choose comparator order based on smaller bandwidth?
+ Arrays.sort(data, comp0);
+
+ CanvasSize canvas = proj.estimateViewport();
+ double min0 = canvas.minx, max0 = canvas.maxx,
+ ste0 = (max0 - min0) / resolution;
+ double min1 = canvas.miny, max1 = canvas.maxy,
+ ste1 = (max1 - min1) / resolution;
+
+ double kernf = 9. / (16 * bandwidth[0] * bandwidth[1]);
+ double maxdens = 0.0;
+ double[][] dens = new double[resolution][resolution];
+ {
+ // TODO: incrementally update the loff/roff values?
+ for(int x = 0; x < resolution; x++) {
+ double xlow = min0 + ste0 * x, xhig = xlow + ste0;
+ int loff = unflip(Arrays.binarySearch(data, new double[] { xlow - bandwidth[0] }, comp0));
+ int roff = unflip(Arrays.binarySearch(data, new double[] { xhig + bandwidth[0] }, comp0));
+ // Resort by second component
+ Arrays.sort(data, loff, roff, comp1);
+ for(int y = 0; y < resolution; y++) {
+ double ylow = min1 + ste1 * y, yhig = ylow + ste1;
+ int boff = unflip(Arrays.binarySearch(data, loff, roff, new double[] { 0, ylow - bandwidth[1] }, comp1));
+ int toff = unflip(Arrays.binarySearch(data, loff, roff, new double[] { 0, yhig + bandwidth[1] }, comp1));
+ for(int pos = boff; pos < toff; pos++) {
+ double[] val = data[pos];
+ double d0 = (val[0] < xlow) ? (xlow - val[0]) : (val[0] > xhig) ? (val[0] - xhig) : 0;
+ double d1 = (val[1] < ylow) ? (ylow - val[1]) : (val[1] > yhig) ? (val[1] - yhig) : 0;
+ d0 = d0 / bandwidth[0];
+ d1 = d1 / bandwidth[1];
+ dens[x][y] += kernf * (1 - d0 * d0) * (1 - d1 * d1);
+ }
+ maxdens = Math.max(maxdens, dens[x][y]);
+ }
+ // Restore original sorting, as the intervals overlap
+ Arrays.sort(data, loff, roff, comp0);
+ }
+ }
+ img = new BufferedImage(resolution, resolution, BufferedImage.TYPE_INT_ARGB);
+ {
+ for(int x = 0; x < resolution; x++) {
+ for(int y = 0; y < resolution; y++) {
+ int rgb = KMLOutputHandler.getColorForValue(dens[x][y] / maxdens).getRGB();
+ img.setRGB(x, y, rgb);
+ }
+ }
+ }
+ }
+
+ private int unflip(int binarySearch) {
+ if(binarySearch < 0) {
+ return (-binarySearch) - 1;
+ }
+ else {
+ return binarySearch;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java
new file mode 100755
index 00000000..ab8cc128
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/density/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for data set density in a scatterplot projection.</p>
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java
new file mode 100644
index 00000000..67fd97a7
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeMBRVisualization.java
@@ -0,0 +1,252 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.spatial.SpatialComparable;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.SpatialEntry;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTree;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.AbstractRStarTreeNode;
+import de.lmu.ifi.dbs.elki.index.tree.spatial.rstarvariants.rstar.RStarTreeNode;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize the bounding rectangles of an R-Tree based index.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class TreeMBRVisualization extends AbstractVisFactory {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String INDEX = "index";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Index MBRs";
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public TreeMBRVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance<RStarTreeNode, SpatialEntry>(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, AbstractRStarTree.class, ScatterPlotProjector.class, //
+ new VisualizationTree.Handler2<AbstractRStarTree<RStarTreeNode, SpatialEntry, ?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, AbstractRStarTree<RStarTreeNode, SpatialEntry, ?> tree, ScatterPlotProjector<?> p) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, (Result) tree, p.getRelation(), TreeMBRVisualization.this);
+ task.level = VisualizationTask.LEVEL_BACKGROUND + 1;
+ task.initDefaultVisibility(false);
+ context.addVis((Result) tree, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Instance for a particular tree
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has AbstractRStarTree oneway - - visualizes
+ * @apiviz.uses SVGHyperCube
+ *
+ * @param <N> Tree node type
+ * @param <E> Tree entry type
+ */
+ // TODO: listen for tree changes instead of data changes?
+ public class Instance<N extends AbstractRStarTreeNode<N, E>, E extends SpatialEntry> extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * The tree we visualize
+ */
+ protected AbstractRStarTree<N, E, ?> tree;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ @SuppressWarnings("unchecked")
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.tree = AbstractRStarTree.class.cast(task.getResult());
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ int projdim = BitsUtil.cardinality(proj.getVisibleDimensions2D());
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ if(tree != null) {
+ E root = tree.getRootEntry();
+ for(int i = 0; i < tree.getHeight(); i++) {
+ CSSClass cls = new CSSClass(this, INDEX + i);
+ // Relative depth of this level. 1.0 = toplevel
+ final double relDepth = 1. - (((double) i) / tree.getHeight());
+ if(settings.fill) {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.1 / (projdim - 1));
+ }
+ else {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ visualizeRTreeEntry(svgp, layer, proj, tree, root, 0);
+ }
+ }
+
+ /**
+ * Recursively draw the MBR rectangles.
+ *
+ * @param svgp SVG Plot
+ * @param layer Layer
+ * @param proj Projection
+ * @param rtree Rtree to visualize
+ * @param entry Current entry
+ * @param depth Current depth
+ */
+ private void visualizeRTreeEntry(SVGPlot svgp, Element layer, Projection2D proj, AbstractRStarTree<? extends N, E, ?> rtree, E entry, int depth) {
+ SpatialComparable mbr = entry;
+
+ if(settings.fill) {
+ Element r = SVGHyperCube.drawFilled(svgp, INDEX + depth, proj, mbr);
+ layer.appendChild(r);
+ }
+ else {
+ Element r = SVGHyperCube.drawFrame(svgp, proj, mbr);
+ SVGUtil.setCSSClass(r, INDEX + depth);
+ layer.appendChild(r);
+ }
+
+ if(!entry.isLeafEntry()) {
+ N node = rtree.getNode(entry);
+ for(int i = 0; i < node.getNumEntries(); i++) {
+ E child = node.getEntry(i);
+ if(!child.isLeafEntry()) {
+ visualizeRTreeEntry(svgp, layer, proj, rtree, child, depth + 1);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ context.removeDataStoreListener(this);
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Flag for half-transparent filling of bubbles.
+ *
+ * <p>
+ * Key: {@code -index.fill}
+ * </p>
+ */
+ public static final OptionID FILL_ID = new OptionID("index.fill", "Partially transparent filling of index pages.");
+
+ protected boolean fill = false;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag fillF = new Flag(FILL_ID);
+ if(config.grab(fillF)) {
+ fill = fillF.isTrue();
+ }
+ }
+
+ @Override
+ protected TreeMBRVisualization makeInstance() {
+ return new TreeMBRVisualization(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java
new file mode 100644
index 00000000..f4eb6a95
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/TreeSphereVisualization.java
@@ -0,0 +1,321 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBID;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.DistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.EuclideanDistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.LPNormDistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.ManhattanDistanceFunction;
+import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.AbstractMTree;
+import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.AbstractMTreeNode;
+import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.MTreeEntry;
+import de.lmu.ifi.dbs.elki.index.tree.metrical.mtreevariants.mtree.MTreeNode;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize the bounding sphere of a metric index.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class TreeSphereVisualization extends AbstractVisFactory {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String INDEX = "index";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Index Spheres";
+
+ /**
+ * Drawing modes.
+ *
+ * @apiviz.exclude
+ */
+ private enum Modus {
+ MANHATTAN, EUCLIDEAN, LPCROSS
+ }
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public TreeSphereVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, AbstractMTree.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<AbstractMTree<?, ?, ?, ?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, AbstractMTree<?, ?, ?, ?> tree, ScatterPlotProjector<?> p) {
+ Relation<?> rel = p.getRelation();
+ if(!canVisualize(rel, tree)) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, tree, rel, TreeSphereVisualization.this);
+ task.level = VisualizationTask.LEVEL_BACKGROUND + 1;
+ task.initDefaultVisibility(false);
+ context.addVis((Result) tree, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance<MTreeNode<Object>, MTreeEntry>(task, plot, width, height, proj);
+ }
+
+ /**
+ * Get the "p" value of an Lp norm.
+ *
+ * @param tree Tree to visualize
+ * @return p value
+ */
+ public static double getLPNormP(AbstractMTree<?, ?, ?, ?> tree) {
+ // Note: we deliberately lose generics here, so the compilers complain
+ // less on the next typecheck and cast!
+ DistanceFunction<?> distanceFunction = tree.getDistanceFunction();
+ if(LPNormDistanceFunction.class.isInstance(distanceFunction)) {
+ return ((LPNormDistanceFunction) distanceFunction).getP();
+ }
+ return 0;
+ }
+
+ /**
+ * Test for a visualizable index in the context's database.
+ *
+ * @param rel Vector relation
+ * @param tree Tree to visualize
+ * @return whether the tree is visualizable
+ */
+ public static boolean canVisualize(Relation<?> rel, AbstractMTree<?, ?, ?, ?> tree) {
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return false;
+ }
+ return getLPNormP(tree) > 0;
+ }
+
+ /**
+ * Instance for a particular tree.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has AbstractMTree oneway - - visualizes
+ * @apiviz.uses SVGHyperSphere
+ *
+ * @param <N> Tree node type
+ * @param <E> Tree entry type
+ */
+ // TODO: listen for tree changes!
+ public class Instance<N extends AbstractMTreeNode<?, N, E>, E extends MTreeEntry> extends AbstractScatterplotVisualization implements DataStoreListener {
+ protected double p;
+
+ /**
+ * Drawing mode (distance) to use
+ */
+ protected Modus dist = Modus.LPCROSS;
+
+ /**
+ * The tree we visualize
+ */
+ protected AbstractMTree<?, N, E, ?> tree;
+
+ /**
+ * Constructor
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ @SuppressWarnings("unchecked")
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.tree = AbstractMTree.class.cast(task.getResult());
+ this.p = getLPNormP(this.tree);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ int projdim = BitsUtil.cardinality(proj.getVisibleDimensions2D());
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ p = getLPNormP(tree);
+ if(tree != null) {
+ if(ManhattanDistanceFunction.class.isInstance(tree.getDistanceFunction())) {
+ dist = Modus.MANHATTAN;
+ }
+ else if(EuclideanDistanceFunction.class.isInstance(tree.getDistanceFunction())) {
+ dist = Modus.EUCLIDEAN;
+ }
+ else {
+ dist = Modus.LPCROSS;
+ }
+ E root = tree.getRootEntry();
+ final int mtheight = tree.getHeight();
+ for(int i = 0; i < mtheight; i++) {
+ CSSClass cls = new CSSClass(this, INDEX + i);
+ // Relative depth of this level. 1.0 = toplevel
+ final double relDepth = 1. - (((double) i) / mtheight);
+ if(settings.fill) {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.1 / (projdim - 1));
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ }
+ else {
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, colors.getColor(i));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, relDepth * style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ }
+ svgp.addCSSClassOrLogError(cls);
+ }
+ visualizeMTreeEntry(svgp, this.layer, proj, tree, root, 0);
+ }
+ }
+
+ /**
+ * Recursively draw the MBR rectangles.
+ *
+ * @param svgp SVG Plot
+ * @param layer Layer
+ * @param proj Projection
+ * @param mtree Mtree to visualize
+ * @param entry Current entry
+ * @param depth Current depth
+ */
+ private void visualizeMTreeEntry(SVGPlot svgp, Element layer, Projection2D proj, AbstractMTree<?, N, E, ?> mtree, E entry, int depth) {
+ DBID roid = entry.getRoutingObjectID();
+ if(roid != null) {
+ NumberVector ro = rel.get(roid);
+ double rad = entry.getCoveringRadius();
+
+ final Element r;
+ if(dist == Modus.MANHATTAN) {
+ r = SVGHyperSphere.drawManhattan(svgp, proj, ro, rad);
+ }
+ else if(dist == Modus.EUCLIDEAN) {
+ r = SVGHyperSphere.drawEuclidean(svgp, proj, ro, rad);
+ }
+ // TODO: add visualizer for infinity norm?
+ else {
+ // r = SVGHyperSphere.drawCross(svgp, proj, ro, rad);
+ r = SVGHyperSphere.drawLp(svgp, proj, ro, rad, p);
+ }
+ SVGUtil.setCSSClass(r, INDEX + (depth - 1));
+ layer.appendChild(r);
+ }
+
+ if(!entry.isLeafEntry()) {
+ N node = mtree.getNode(entry);
+ for(int i = 0; i < node.getNumEntries(); i++) {
+ E child = node.getEntry(i);
+ if(!child.isLeafEntry()) {
+ visualizeMTreeEntry(svgp, layer, proj, mtree, child, depth + 1);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void destroy() {
+ super.destroy();
+ context.removeDataStoreListener(this);
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ protected boolean fill = false;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag fillF = new Flag(TreeMBRVisualization.Parameterizer.FILL_ID);
+ if(config.grab(fillF)) {
+ fill = fillF.isTrue();
+ }
+ }
+
+ @Override
+ protected TreeSphereVisualization makeInstance() {
+ return new TreeSphereVisualization(this);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java
new file mode 100755
index 00000000..7da4da26
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/index/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for index structures based on 2D projections.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java
new file mode 100644
index 00000000..f99b52f4
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/BubbleVisualization.java
@@ -0,0 +1,333 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDRef;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter;
+import de.lmu.ifi.dbs.elki.utilities.scaling.ScalingFunction;
+import de.lmu.ifi.dbs.elki.utilities.scaling.outlier.OutlierScalingFunction;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Generates a SVG-Element containing bubbles. A Bubble is a circle visualizing
+ * an outlierness-score, with its center at the position of the visualized
+ * object and its radius depending on the objects score.
+ *
+ * @author Remigius Wojdanowski
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+@Reference(authors = "E. Achtert, H.-P. Kriegel, L. Reichert, E. Schubert, R. Wojdanowski, A. Zimek", //
+title = "Visual Evaluation of Outlier Detection Models", //
+booktitle = "Proceedings of the 15th International Conference on Database Systems for Advanced Applications (DASFAA), Tsukuba, Japan, 2010", //
+url = "http://dx.doi.org/10.1007/978-3-642-12098-5_34")
+public class BubbleVisualization extends AbstractVisFactory {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes etc.
+ */
+ public static final String BUBBLE = "bubble";
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Outlier Bubbles";
+
+ /**
+ * Current settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public BubbleVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ if(settings.scaling != null && settings.scaling instanceof OutlierScalingFunction) {
+ final OutlierResult outlierResult = task.getResult();
+ ((OutlierScalingFunction) settings.scaling).prepare(outlierResult);
+ }
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ boolean vis = true;
+ // Quick and dirty hack: hide if parent result is also an outlier result
+ // Since that probably is already visible and we're redundant.
+ for(Hierarchy.Iter<Result> r = o.getHierarchy().iterParents(o); r.valid(); r.advance()) {
+ if(r.get() instanceof OutlierResult) {
+ vis = false;
+ break;
+ }
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, o, rel, BubbleVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ task.initDefaultVisibility(vis);
+ context.addVis(o, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Factory for producing bubble visualizations
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has OutlierResult oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * The outlier result to visualize
+ */
+ protected OutlierResult result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ StyleLibrary style = context.getStyleLibrary();
+ StylingPolicy stylepolicy = context.getStylingPolicy();
+ // bubble size
+ final double bubble_size = style.getSize(StyleLibrary.BUBBLEPLOT);
+ if(stylepolicy instanceof ClassStylingPolicy) {
+ ClassStylingPolicy colors = (ClassStylingPolicy) stylepolicy;
+ setupCSS(svgp, colors);
+ // draw data
+ for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) {
+ final double radius = getScaledForId(objId);
+ if(radius > 0.01 && !Double.isInfinite(radius)) {
+ final NumberVector vec = rel.get(objId);
+ if(vec != null) {
+ double[] v = proj.fastProjectDataToRenderSpace(vec);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element circle = svgp.svgCircle(v[0], v[1], radius * bubble_size);
+ SVGUtil.addCSSClass(circle, BUBBLE + colors.getStyleForDBID(objId));
+ layer.appendChild(circle);
+ }
+ }
+ }
+ }
+ else {
+ // draw data
+ for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) {
+ final double radius = getScaledForId(objId);
+ if(radius > 0.01 && !Double.isInfinite(radius)) {
+ final NumberVector vec = rel.get(objId);
+ if(vec != null) {
+ double[] v = proj.fastProjectDataToRenderSpace(vec);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element circle = svgp.svgCircle(v[0], v[1], radius * bubble_size);
+ int color = stylepolicy.getColorForDBID(objId);
+ final StringBuilder cssstyle = new StringBuilder();
+ if(settings.fill) {
+ cssstyle.append(SVGConstants.CSS_FILL_PROPERTY).append(':').append(SVGUtil.colorToString(color));
+ cssstyle.append(SVGConstants.CSS_FILL_OPACITY_PROPERTY).append(":0.5");
+ }
+ else {
+ cssstyle.append(SVGConstants.CSS_STROKE_VALUE).append(':').append(SVGUtil.colorToString(color));
+ cssstyle.append(SVGConstants.CSS_FILL_PROPERTY).append(':').append(SVGConstants.CSS_NONE_VALUE);
+ }
+ SVGUtil.setAtt(circle, SVGConstants.SVG_STYLE_ATTRIBUTE, cssstyle.toString());
+ layer.appendChild(circle);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Registers the Bubble-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ * @param policy Clustering to use
+ */
+ private void setupCSS(SVGPlot svgp, ClassStylingPolicy policy) {
+ final StyleLibrary style = context.getStyleLibrary();
+ ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ // creating IDs manually because cluster often return a null-ID.
+ for(int clusterID = policy.getMinStyle(); clusterID < policy.getMaxStyle(); clusterID++) {
+ CSSClass bubble = new CSSClass(svgp, BUBBLE + clusterID);
+ bubble.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+
+ String color = colors.getColor(clusterID);
+
+ if(settings.fill) {
+ bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ bubble.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, 0.5);
+ }
+ else {
+ // for diamond-shaped strokes, see bugs.sun.com, bug ID 6294396
+ bubble.setStatement(SVGConstants.CSS_STROKE_VALUE, color);
+ bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+
+ svgp.addCSSClassOrLogError(bubble);
+ }
+ }
+
+ /**
+ * Convenience method to apply scalings in the right order.
+ *
+ * @param id object ID to get scaled score for
+ * @return a Double representing a outlierness-score, after it has modified
+ * by the given scales.
+ */
+ protected double getScaledForId(DBIDRef id) {
+ double d = result.getScores().doubleValue(id);
+ if(Double.isNaN(d) || Double.isInfinite(d)) {
+ return 0.0;
+ }
+ if(settings.scaling == null) {
+ return result.getOutlierMeta().normalizeScore(d);
+ }
+ else {
+ return settings.scaling.getScaled(d);
+ }
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Flag for half-transparent filling of bubbles.
+ *
+ * <p>
+ * Key: {@code -bubble.fill}
+ * </p>
+ */
+ public static final OptionID FILL_ID = new OptionID("bubble.fill", "Half-transparent filling of bubbles.");
+
+ /**
+ * Parameter for scaling functions
+ *
+ * <p>
+ * Key: {@code -bubble.scaling}
+ * </p>
+ */
+ public static final OptionID SCALING_ID = new OptionID("bubble.scaling", "Additional scaling function for bubbles.");
+
+ /**
+ * Fill parameter.
+ */
+ protected boolean fill;
+
+ /**
+ * Scaling function to use for Bubbles
+ */
+ protected ScalingFunction scaling;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag fillF = new Flag(FILL_ID);
+ if(config.grab(fillF)) {
+ fill = fillF.isTrue();
+ }
+
+ ObjectParameter<ScalingFunction> scalingP = new ObjectParameter<>(SCALING_ID, OutlierScalingFunction.class, true);
+ if(config.grab(scalingP)) {
+ scaling = scalingP.instantiateClass(config);
+ }
+ }
+
+ @Override
+ protected BubbleVisualization makeInstance() {
+ return new BubbleVisualization(this);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java
new file mode 100644
index 00000000..b2abeb9f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/COPVectorVisualization.java
@@ -0,0 +1,189 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.algorithm.outlier.COP;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.result.outlier.OutlierResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
+import de.lmu.ifi.dbs.elki.utilities.documentation.Title;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize error vectors as produced by COP.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ * @apiviz.has OutlierResult oneway - - visualizes
+ */
+@Title("COP: Correlation Outlier Probability")
+@Reference(authors = "Hans-Peter Kriegel, Peer Kröger, Erich Schubert, Arthur Zimek", //
+title = "Outlier Detection in Arbitrarily Oriented Subspaces", //
+booktitle = "Proc. IEEE International Conference on Data Mining (ICDM 2012)", //
+url = "http://dx.doi.org/10.1109/ICDM.2012.21")
+public class COPVectorVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "Error Vectors";
+
+ /**
+ * Constructor.
+ */
+ public COPVectorVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, OutlierResult.class, ScatterPlotProjector.class, new VisualizationTree.Handler2<OutlierResult, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, OutlierResult o, ScatterPlotProjector<?> p) {
+ final Relation<?> rel2 = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel2.getDataTypeInformation())) {
+ return;
+ }
+ Hierarchy.Iter<Relation<?>> it1 = VisualizationTree.filterResults(context, o, Relation.class);
+ for(; it1.valid(); it1.advance()) {
+ Relation<?> rel = it1.get();
+ if(!rel.getShortName().equals(COP.COP_ERRORVEC)) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, rel, rel2, COPVectorVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE);
+ context.addVis(o, task);
+ context.addVis(p, task);
+ }
+ }
+ });
+ }
+
+ /**
+ * Visualize error vectors as produced by COP.
+ *
+ * @author Erich Schubert
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String VEC = "copvec";
+
+ /**
+ * The outlier result to visualize
+ */
+ protected Relation<Vector> result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ setupCSS(svgp);
+ for(DBIDIter objId = sample.getSample().iter(); objId.valid(); objId.advance()) {
+ Vector evec = result.get(objId);
+ if(evec == null) {
+ continue;
+ }
+ double[] ev = proj.fastProjectRelativeDataToRenderSpace(evec);
+ // TODO: avoid hard-coded plot threshold
+ if(VMath.euclideanLength(ev) < 0.01) {
+ continue;
+ }
+ final NumberVector vec = rel.get(objId);
+ if(vec == null) {
+ continue;
+ }
+ double[] v = proj.fastProjectDataToRenderSpace(vec);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element arrow = svgp.svgLine(v[0], v[1], v[0] + ev[0], v[1] + ev[1]);
+ SVGUtil.addCSSClass(arrow, VEC);
+ layer.appendChild(arrow);
+ }
+ }
+
+ /**
+ * Registers the COP error vector-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ */
+ private void setupCSS(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass bubble = new CSSClass(svgp, VEC);
+ bubble.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT) / 2);
+
+ // ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+ String color = "red"; // TODO: use style library
+ bubble.setStatement(SVGConstants.CSS_STROKE_VALUE, color);
+ bubble.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ svgp.addCSSClassOrLogError(bubble);
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java
new file mode 100755
index 00000000..0800548e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/outlier/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for outlier scores based on 2D projections.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java
new file mode 100755
index 00000000..4f5ed7c4
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/package-info.java
@@ -0,0 +1,33 @@
+/**
+ * <p>Visualizers based on scatterplots.</p>
+ *
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.batikutil.*
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.visualization.svg.*
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.result.*
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.index.*
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.data.*
+ * @apiviz.exclude de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java
new file mode 100644
index 00000000..4751605f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/DistanceFunctionVisualization.java
@@ -0,0 +1,368 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.VectorUtil;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.ids.DoubleDBIDListIter;
+import de.lmu.ifi.dbs.elki.database.ids.DoubleDBIDPair;
+import de.lmu.ifi.dbs.elki.database.ids.KNNList;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.ArcCosineDistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.CosineDistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.DistanceFunction;
+import de.lmu.ifi.dbs.elki.distance.distancefunction.minkowski.LPNormDistanceFunction;
+import de.lmu.ifi.dbs.elki.index.preprocessed.knn.AbstractMaterializeKNNPreprocessor;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.MathUtil;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.VMath;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.CanvasSize;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperSphere;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Factory for visualizers to generate an SVG-Element containing dots as markers
+ * representing the kNN of the selected Database objects.
+ *
+ * To use this, add a kNN preprocessor index to your database!
+ *
+ * @author Erich Schubert
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// FIXME: for >2 dimensions, cosine doesn't seem to be correct yet.
+public class DistanceFunctionVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ public static final String NAME = "k Nearest Neighbor Visualization";
+
+ /**
+ * Constructor
+ */
+ public DistanceFunctionVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ VisualizationTree.findNewSiblings(context, start, AbstractMaterializeKNNPreprocessor.class, ScatterPlotProjector.class, //
+ new VisualizationTree.Handler2<AbstractMaterializeKNNPreprocessor<?>, ScatterPlotProjector<?>>() {
+ @Override
+ public void process(VisualizerContext context, AbstractMaterializeKNNPreprocessor<?> kNN, ScatterPlotProjector<?> p) {
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ return;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, kNN, rel, DistanceFunctionVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_SELECTION);
+ context.addVis(kNN, task);
+ context.addVis(p, task);
+ }
+ });
+ }
+
+ /**
+ * Get the "p" value of an Lp norm.
+ *
+ * @param kNN kNN preprocessor
+ * @return p of LP norm, or NaN
+ */
+ public static double getLPNormP(AbstractMaterializeKNNPreprocessor<?> kNN) {
+ DistanceFunction<?> distanceFunction = kNN.getDistanceQuery().getDistanceFunction();
+ if(LPNormDistanceFunction.class.isInstance(distanceFunction)) {
+ return ((LPNormDistanceFunction) distanceFunction).getP();
+ }
+ return Double.NaN;
+ }
+
+ /**
+ * Test whether the given preprocessor used an angular distance function
+ *
+ * @param kNN kNN preprocessor
+ * @return true when angular
+ */
+ public static boolean isAngularDistance(AbstractMaterializeKNNPreprocessor<?> kNN) {
+ DistanceFunction<?> distanceFunction = kNN.getDistanceQuery().getDistanceFunction();
+ if(CosineDistanceFunction.class.isInstance(distanceFunction)) {
+ return true;
+ }
+ if(ArcCosineDistanceFunction.class.isInstance(distanceFunction)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Visualizes Cosine and ArcCosine distance functions
+ *
+ * @param svgp SVG Plot
+ * @param proj Visualization projection
+ * @param mid mean vector
+ * @param angle Opening angle in radians
+ * @return path element
+ */
+ public static Element drawCosine(SVGPlot svgp, Projection2D proj, NumberVector mid, double angle) {
+ // Project origin
+ double[] pointOfOrigin = proj.fastProjectDataToRenderSpace(new double[proj.getInputDimensionality()]);
+
+ // direction of the selected Point
+ double[] selPoint = proj.fastProjectDataToRenderSpace(mid);
+
+ double[] range1, range2;
+ {
+ // Rotation plane:
+ double[] p1 = proj.fastProjectRenderToDataSpace(selPoint[0] + 10, selPoint[1]);
+ double[] p2 = proj.fastProjectRenderToDataSpace(selPoint[0], selPoint[1] + 10);
+ double[] pm = mid.getColumnVector().getArrayRef();
+ // Compute relative vectors
+ VMath.minusEquals(p1, pm);
+ VMath.minusEquals(p2, pm);
+ // Scale p1 and p2 to unit length:
+ VMath.timesEquals(p1, 1. / VMath.euclideanLength(p1));
+ VMath.timesEquals(p2, 1. / VMath.euclideanLength(p2));
+ {
+ double test = VMath.scalarProduct(p1, p2);
+ if(Math.abs(test) > 1E-10) {
+ LoggingUtil.warning("Projection does not seem to be orthogonal?");
+ }
+ }
+ // Project onto p1, p2:
+ double l1 = VMath.scalarProduct(pm, p1), l2 = VMath.scalarProduct(pm, p2);
+ // Rotate projection by + and - angle
+ // Using sin(-x) = -sin(x) and cos(-x)=cos(x)
+ final double cangle = Math.cos(angle),
+ sangle = MathUtil.cosToSin(angle, cangle);
+ double r11 = +cangle * l1 - sangle * l2, r12 = +sangle * l1 + cangle * l2;
+ double r21 = +cangle * l1 + sangle * l2, r22 = -sangle * l1 + cangle * l2;
+ // Build rotated vectors - remove projected component, add rotated
+ // component:
+ double[] r1 = VMath.copy(pm), r2 = VMath.copy(pm);
+ VMath.plusTimesEquals(r1, p1, -l1 + r11);
+ VMath.plusTimesEquals(r1, p2, -l2 + r12);
+ VMath.plusTimesEquals(r2, p1, -l1 + r21);
+ VMath.plusTimesEquals(r2, p2, -l2 + r22);
+ // Project to render space:
+ range1 = proj.fastProjectDataToRenderSpace(r1);
+ range2 = proj.fastProjectDataToRenderSpace(r2);
+ }
+
+ // Continue lines to viewport.
+ {
+ CanvasSize viewport = proj.estimateViewport();
+ VMath.minusEquals(range1, pointOfOrigin);
+ VMath.minusEquals(range2, pointOfOrigin);
+ VMath.timesEquals(range1, viewport.continueToMargin(pointOfOrigin, range1));
+ VMath.timesEquals(range2, viewport.continueToMargin(pointOfOrigin, range2));
+ VMath.plusEquals(range1, pointOfOrigin);
+ VMath.plusEquals(range2, pointOfOrigin);
+ // Go backwards into the other direction - the origin might not be in the
+ // viewport!
+ double[] start1 = VMath.minus(pointOfOrigin, range1);
+ double[] start2 = VMath.minus(pointOfOrigin, range2);
+ VMath.timesEquals(start1, viewport.continueToMargin(range1, start1));
+ VMath.timesEquals(start2, viewport.continueToMargin(range2, start2));
+ VMath.plusEquals(start1, range1);
+ VMath.plusEquals(start2, range2);
+
+ // TODO: add filled variant?
+ SVGPath path = new SVGPath();
+ path.moveTo(start1);
+ path.lineTo(range1);
+ path.moveTo(start2);
+ path.lineTo(range2);
+ return path.makeElement(svgp);
+ }
+ }
+
+ /**
+ * Instance, visualizing a particular set of kNNs
+ *
+ * @author Robert Rödler
+ * @author Erich Schubert
+ *
+ * @apiviz.has DBIDSelection oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tags to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String KNNMARKER = "kNNMarker";
+
+ public static final String KNNDIST = "kNNDist";
+
+ public static final String DISTANCEFUNCTION = "distancefunction";
+
+ /**
+ * The selection result we work on
+ */
+ private AbstractMaterializeKNNPreprocessor<? extends NumberVector> result;
+
+ /**
+ * Constructor
+ *
+ * @param task VisualizationTask
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ addCSSClasses(svgp);
+ final double p = getLPNormP(result);
+ final boolean angular = isAngularDistance(result);
+
+ final double size = style.getSize(StyleLibrary.SELECTION);
+ DBIDSelection selContext = context.getSelection();
+ if(selContext != null) {
+ DBIDs selection = selContext.getSelectedIds();
+
+ for(DBIDIter i = selection.iter(); i.valid(); i.advance()) {
+ final KNNList knn = result.get(i);
+ for(DoubleDBIDListIter iter = knn.iter(); iter.valid(); iter.advance()) {
+ try {
+ double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element dot = svgp.svgCircle(v[0], v[1], size);
+ SVGUtil.addCSSClass(dot, KNNMARKER);
+ layer.appendChild(dot);
+
+ Element lbl = svgp.svgText(v[0] + size, v[1] + size, Double.toString(iter.doubleValue()));
+ SVGUtil.addCSSClass(lbl, KNNDIST);
+ layer.appendChild(lbl);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore
+ }
+ }
+ // Last element
+ DoubleDBIDPair last = knn.get(knn.size() - 1);
+ // Draw hypersphere if possible
+ {
+ final Element dist;
+ if(p == 1.0) {
+ dist = SVGHyperSphere.drawManhattan(svgp, proj, rel.get(i), last.doubleValue());
+ }
+ else if(p == 2.0) {
+ dist = SVGHyperSphere.drawEuclidean(svgp, proj, rel.get(i), last.doubleValue());
+ }
+ else if(!Double.isNaN(p)) {
+ dist = SVGHyperSphere.drawLp(svgp, proj, rel.get(i), last.doubleValue(), p);
+ }
+ else if(angular) {
+ final NumberVector refvec = rel.get(i);
+ // Recompute the angle - it could be cosine or arccosine distance
+ double maxangle = Math.acos(VectorUtil.cosAngle(refvec, rel.get(last)));
+ dist = drawCosine(svgp, proj, refvec, maxangle);
+ }
+ else {
+ dist = null;
+ }
+ if(dist != null) {
+ SVGUtil.addCSSClass(dist, DISTANCEFUNCTION);
+ layer.appendChild(dist);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ // Class for the distance markers
+ if(!svgp.getCSSClassManager().contains(KNNMARKER)) {
+ CSSClass cls = new CSSClass(this, KNNMARKER);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_DARKGREEN_VALUE);
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ svgp.addCSSClassOrLogError(cls);
+ }
+ // Class for the distance function
+ if(!svgp.getCSSClassManager().contains(DISTANCEFUNCTION)) {
+ CSSClass cls = new CSSClass(this, DISTANCEFUNCTION);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, SVGConstants.CSS_RED_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ // Class for the distance label
+ if(!svgp.getCSSClassManager().contains(KNNDIST)) {
+ CSSClass cls = new CSSClass(this, KNNDIST);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_BLACK_VALUE);
+ cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.PLOT));
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java
new file mode 100644
index 00000000..c594d500
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/MoveObjectsToolVisualization.java
@@ -0,0 +1,239 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.UpdatableDatabase;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.AbortException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea.DragListener;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Tool to move the currently selected objects.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class MoveObjectsToolVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Move Objects";
+
+ /**
+ * Constructor
+ */
+ public MoveObjectsToolVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Database db = ResultUtil.findDatabase(context.getHierarchy());
+ if(!(db instanceof UpdatableDatabase)) {
+ return;
+ }
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p.getRelation(), rel, MoveObjectsToolVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.tool = true;
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_SELECTION);
+ task.initDefaultVisibility(false);
+ // baseResult.getHierarchy().add(p.getRelation(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ * @author Erich Schubert
+ *
+ * @apiviz.has de.lmu.ifi.dbs.elki.data.NumberVector oneway - - edits
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DragListener {
+ /**
+ * CSS tag for our event rectangle
+ */
+ protected static final String CSS_ARROW = "moveArrow";
+
+ /**
+ * Element for the rectangle to add listeners
+ */
+ private Element etag;
+
+ /**
+ * Element to contain the drag arrow
+ */
+ private Element rtag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ addCSSClasses(svgp);
+
+ rtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.addCSSClass(rtag, CSS_ARROW);
+ layer.appendChild(rtag);
+
+ DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this);
+ etag = drag.getElement();
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Updates the objects with the given DBIDs It will be moved depending on
+ * the given Vector
+ *
+ * @param dbids - DBIDs of the objects to move
+ * @param movingVector - Vector for moving object
+ */
+ // TODO: move to DatabaseUtil?
+ private void updateDB(DBIDs dbids, Vector movingVector) {
+ throw new AbortException("FIXME: INCOMPLETE TRANSITION");
+ /*
+ * NumberVector nv = null; database.accumulateDataStoreEvents();
+ * Representation<DatabaseObjectMetadata> mrep =
+ * database.getMetadataQuery(); for(DBID dbid : dbids) { NV obj =
+ * database.get(dbid); // Copy metadata to keep DatabaseObjectMetadata
+ * meta = mrep.get(dbid);
+ *
+ * Vector v = proj.projectDataToRenderSpace(obj); v.set(0, v.get(0) +
+ * movingVector.get(0)); v.set(1, v.get(1) + movingVector.get(1)); NV nv =
+ * proj.projectRenderToDataSpace(v, obj); nv.setID(obj.getID());
+ *
+ * try { database.delete(dbid); database.insert(new Pair<NV,
+ * DatabaseObjectMetadata>(nv, meta)); } catch(UnableToComplyException e)
+ * { de.lmu.ifi.dbs.elki.logging.LoggingUtil.exception(e); } }
+ * database.flushDataStoreEvents();
+ */
+ }
+
+ /**
+ * Delete the children of the element
+ *
+ * @param container SVG-Element
+ */
+ private void deleteChildren(Element container) {
+ while(container.hasChildNodes()) {
+ container.removeChild(container.getLastChild());
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVGPlot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ // Class for the rectangle to add eventListeners
+ if(!svgp.getCSSClassManager().contains(CSS_ARROW)) {
+ final CSSClass acls = new CSSClass(this, CSS_ARROW);
+ final StyleLibrary style = context.getStyleLibrary();
+ acls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE));
+ acls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION_ACTIVE));
+ acls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(acls);
+ }
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ rtag.appendChild(svgp.svgLine(startPoint.getX(), startPoint.getY(), dragPoint.getX(), dragPoint.getY()));
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ Vector movingVector = new Vector(2);
+ movingVector.set(0, dragPoint.getX() - startPoint.getX());
+ movingVector.set(1, dragPoint.getY() - startPoint.getY());
+ if(context.getSelection() != null) {
+ updateDB(context.getSelection().getSelectedIds(), movingVector);
+ }
+ deleteChildren(rtag);
+ return true;
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java
new file mode 100644
index 00000000..add2ab8d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionConvexHullVisualization.java
@@ -0,0 +1,181 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.spatial.Polygon;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.geometry.GrahamScanConvexHull2D;
+import de.lmu.ifi.dbs.elki.math.linearalgebra.Vector;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer for generating an SVG-Element containing the convex hull of the
+ * selected points
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionConvexHullVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Selection Hull";
+
+ /**
+ * Constructor
+ */
+ public SelectionConvexHullVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionConvexHullVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 2;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Robert Rödler
+ *
+ * @apiviz.has DBIDSelection oneway - - visualizes
+ * @apiviz.uses GrahamScanConvexHull2D
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String SELECTEDHULL = "selectionConvexHull";
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ addCSSClasses(svgp);
+ DBIDSelection selContext = context.getSelection();
+ if(selContext != null) {
+ DBIDs selection = selContext.getSelectedIds();
+ GrahamScanConvexHull2D hull = new GrahamScanConvexHull2D();
+ for(DBIDIter iter = selection.iter(); iter.valid(); iter.advance()) {
+ try {
+ final double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ hull.add(new Vector(v));
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore
+ }
+ }
+ Polygon chres = hull.getHull();
+ if(chres != null && chres.size() >= 3) {
+ SVGPath path = new SVGPath(chres);
+
+ Element selHull = path.makeElement(svgp);
+ SVGUtil.addCSSClass(selHull, SELECTEDHULL);
+ // TODO: use relative selection size for opacity?
+ layer.appendChild(selHull);
+ }
+ }
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ // Class for the dot markers
+ if(!svgp.getCSSClassManager().contains(SELECTEDHULL)) {
+ final StyleLibrary style = context.getStyleLibrary();
+ CSSClass cls = new CSSClass(this, SELECTEDHULL);
+ // cls = new CSSClass(this, CONVEXHULL);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, ".25");
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java
new file mode 100644
index 00000000..7ad7b32b
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionCubeVisualization.java
@@ -0,0 +1,253 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.HyperBoundingBox;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.RangeSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.AbstractParameterizer;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.OptionID;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.Parameterization;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.Flag;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer for generating an SVG-Element containing a cube as marker
+ * representing the selected range for each dimension
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+// TODO: Does not use the relation. Always enable, but hide in the menu?
+public class SelectionCubeVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Selection Range";
+
+ /**
+ * Settings
+ */
+ protected Parameterizer settings;
+
+ /**
+ * Constructor.
+ *
+ * @param settings Settings
+ */
+ public SelectionCubeVisualization(Parameterizer settings) {
+ super();
+ this.settings = settings;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionCubeVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 2;
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has RangeSelection oneway - - visualizes
+ * @apiviz.uses SVGHyperCube
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String MARKER = "selectionCubeMarker";
+
+ /**
+ * CSS class for the filled cube
+ */
+ public static final String CSS_CUBE = "selectionCube";
+
+ /**
+ * CSS class for the cube frame
+ */
+ public static final String CSS_CUBEFRAME = "selectionCubeFrame";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ private void addCSSClasses(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ // Class for the cube
+ if(!svgp.getCSSClassManager().contains(CSS_CUBE)) {
+ CSSClass cls = new CSSClass(this, CSS_CUBE);
+ cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.PLOT));
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINEJOIN_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ if(settings.nofill) {
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, SVGConstants.CSS_NONE_VALUE);
+ }
+ else {
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ }
+ svgp.addCSSClassOrLogError(cls);
+ }
+ // Class for the cube frame
+ if(!svgp.getCSSClassManager().contains(CSS_CUBEFRAME)) {
+ CSSClass cls = new CSSClass(this, CSS_CUBEFRAME);
+ cls.setStatement(SVGConstants.CSS_STROKE_VALUE, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth(StyleLibrary.SELECTION));
+
+ svgp.addCSSClassOrLogError(cls);
+ }
+ }
+
+ /**
+ * Generates a cube and a frame depending on the selection stored in the
+ * context
+ *
+ * @param svgp The plot
+ * @param proj The projection
+ */
+ private void setSVGRect(SVGPlot svgp, Projection2D proj) {
+ DBIDSelection selContext = context.getSelection();
+ if(selContext instanceof RangeSelection) {
+ HyperBoundingBox ranges = ((RangeSelection) selContext).getRanges();
+ if(settings.nofill) {
+ Element r = SVGHyperCube.drawFrame(svgp, proj, ranges);
+ SVGUtil.setCSSClass(r, CSS_CUBEFRAME);
+ layer.appendChild(r);
+ }
+ else {
+ Element r = SVGHyperCube.drawFilled(svgp, CSS_CUBE, proj, ranges);
+ layer.appendChild(r);
+ }
+
+ }
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ addCSSClasses(svgp);
+ DBIDSelection selContext = context.getSelection();
+ if(selContext instanceof RangeSelection) {
+ setSVGRect(svgp, proj);
+ }
+ }
+ }
+
+ /**
+ * Parameterization class.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Parameterizer extends AbstractParameterizer {
+ /**
+ * Flag for half-transparent filling of selection cubes.
+ *
+ * <p>
+ * Key: {@code -selectionrange.nofill}
+ * </p>
+ */
+ public static final OptionID NOFILL_ID = new OptionID("selectionrange.nofill", "Use wireframe style for selection ranges.");
+
+ /**
+ * Fill parameter.
+ */
+ protected boolean nofill;
+
+ @Override
+ protected void makeOptions(Parameterization config) {
+ super.makeOptions(config);
+ Flag nofillF = new Flag(NOFILL_ID);
+ if(config.grab(nofillF)) {
+ nofill = nofillF.isTrue();
+ }
+ }
+
+ @Override
+ protected SelectionCubeVisualization makeInstance() {
+ return new SelectionCubeVisualization(this);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java
new file mode 100644
index 00000000..c9aed72f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionDotVisualization.java
@@ -0,0 +1,153 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualizer for generating an SVG-Element containing dots as markers
+ * representing the selected Database's objects.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionDotVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Selection Markers";
+
+ /**
+ * Constructor
+ */
+ public SelectionDotVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionDotVisualization.this);
+ task.level = VisualizationTask.LEVEL_DATA - 1;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has DBIDSelection oneway - - visualizes
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DataStoreListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ public static final String MARKER = "selectionDotMarker";
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ DBIDSelection selContext = context.getSelection();
+ if(selContext == null) {
+ return;
+ }
+ final StyleLibrary style = context.getStyleLibrary();
+ // Class for the dot markers
+ if(!svgp.getCSSClassManager().contains(MARKER)) {
+ CSSClass cls = new CSSClass(this, MARKER);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION));
+ cls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION));
+ svgp.addCSSClassOrLogError(cls);
+ }
+ final double size = style.getSize(StyleLibrary.SELECTION);
+ for(DBIDIter iter = selContext.getSelectedIds().iter(); iter.valid(); iter.advance()) {
+ try {
+ double[] v = proj.fastProjectDataToRenderSpace(rel.get(iter));
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ Element dot = svgp.svgCircle(v[0], v[1], size);
+ SVGUtil.addCSSClass(dot, MARKER);
+ layer.appendChild(dot);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java
new file mode 100644
index 00000000..0cc56e08
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolCubeVisualization.java
@@ -0,0 +1,298 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.data.ModifiableHyperBoundingBox;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.ModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.result.RangeSelection;
+import de.lmu.ifi.dbs.elki.utilities.BitsUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Tool-Visualization for the tool to select ranges.
+ *
+ * TODO: support non-point spatial data
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SelectionToolCubeVisualization extends AbstractVisFactory {
+ /**
+ * The logger for this class.
+ */
+ private static final Logging LOG = Logging.getLogger(SelectionToolCubeVisualization.class);
+
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Range Selection";
+
+ /**
+ * Constructor.
+ */
+ public SelectionToolCubeVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolCubeVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.tool = true;
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ task.addUpdateFlags(VisualizationTask.ON_SELECTION);
+ task.initDefaultVisibility(false);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has RangeSelection oneway - - updates
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DragableArea.DragListener {
+ /**
+ * Generic tag to indicate the type of element. Used in IDs, CSS-Classes
+ * etc.
+ */
+ private static final String CSS_RANGEMARKER = "selectionRangeMarker";
+
+ /**
+ * Dimension.
+ */
+ private int dim;
+
+ /**
+ * Element for selection rectangle.
+ */
+ private Element rtag;
+
+ /**
+ * Element for the rectangle to add listeners.
+ */
+ private Element etag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ this.dim = RelationUtil.dimensionality(rel);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ addCSSClasses(svgp);
+
+ // rtag: tag for the selected rect
+ rtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER);
+ layer.appendChild(rtag);
+
+ // etag: sensitive area
+ DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this);
+ etag = drag.getElement();
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Delete the children of the element.
+ *
+ * @param container SVG-Element
+ */
+ private void deleteChildren(Element container) {
+ while(container.hasChildNodes()) {
+ container.removeChild(container.getLastChild());
+ }
+ }
+
+ /**
+ * Set the selected ranges and the mask for the actual dimensions in the
+ * context.
+ *
+ * @param x1 x-value of the first dimension
+ * @param x2 x-value of the second dimension
+ * @param y1 y-value of the first dimension
+ * @param y2 y-value of the second dimension
+ * @param ranges Ranges to update
+ */
+ private void updateSelectionRectKoordinates(double x1, double x2, double y1, double y2, ModifiableHyperBoundingBox ranges) {
+ double[] nv1 = proj.fastProjectRenderToDataSpace(x1, y1);
+ double[] nv2 = proj.fastProjectRenderToDataSpace(x2, y2);
+
+ long[] actDim = proj.getVisibleDimensions2D();
+ for(int d = BitsUtil.nextSetBit(actDim, 0); d >= 0; d = BitsUtil.nextSetBit(actDim, d + 1)) {
+ ranges.setMin(d, Math.min(nv1[d], nv2[d]));
+ ranges.setMax(d, Math.max(nv1[d], nv2[d]));
+ }
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ double x = Math.min(startPoint.getX(), dragPoint.getX());
+ double y = Math.min(startPoint.getY(), dragPoint.getY());
+ double width = Math.abs(startPoint.getX() - dragPoint.getX());
+ double height = Math.abs(startPoint.getY() - dragPoint.getY());
+ rtag.appendChild(svgp.svgRect(x, y, width, height));
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) {
+ updateSelection(proj, startPoint, dragPoint);
+ }
+ return true;
+ }
+
+ /**
+ * Update the selection in the context.
+ *
+ * @param proj The projection
+ * @param p1 First Point of the selected rectangle
+ * @param p2 Second Point of the selected rectangle
+ */
+ private void updateSelection(Projection proj, SVGPoint p1, SVGPoint p2) {
+ if(p1 == null || p2 == null) {
+ LOG.warning("no rect selected: p1: " + p1 + " p2: " + p2);
+ return;
+ }
+
+ DBIDSelection selContext = context.getSelection();
+ ModifiableDBIDs selection;
+ if(selContext != null) {
+ selection = DBIDUtil.newHashSet(selContext.getSelectedIds());
+ }
+ else {
+ selection = DBIDUtil.newHashSet();
+ }
+ ModifiableHyperBoundingBox ranges;
+
+ double x1 = Math.min(p1.getX(), p2.getX());
+ double x2 = Math.max(p1.getX(), p2.getX());
+ double y1 = Math.max(p1.getY(), p2.getY());
+ double y2 = Math.min(p1.getY(), p2.getY());
+
+ if(selContext instanceof RangeSelection) {
+ ranges = ((RangeSelection) selContext).getRanges();
+ }
+ else {
+ ranges = new ModifiableHyperBoundingBox(dim, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
+ }
+ updateSelectionRectKoordinates(x1, x2, y1, y2, ranges);
+
+ selection.clear();
+ candidates: for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ NumberVector dbTupel = rel.get(iditer);
+ for(int i = 0; i < dim; i++) {
+ final double min = ranges.getMin(i), max = ranges.getMax(i);
+ if(max < Double.POSITIVE_INFINITY || min > Double.NEGATIVE_INFINITY) {
+ final double v = dbTupel.doubleValue(i);
+ if(v < min || v > max) {
+ continue candidates;
+ }
+ }
+ }
+ selection.add(iditer);
+ }
+ context.setSelection(new RangeSelection(selection, ranges));
+ }
+
+ /**
+ * Adds the required CSS-Classes.
+ *
+ * @param svgp SVG-Plot
+ */
+ protected void addCSSClasses(SVGPlot svgp) {
+ // Class for the range marking
+ if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) {
+ final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER);
+ final StyleLibrary style = context.getStyleLibrary();
+ rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE));
+ rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE));
+ svgp.addCSSClassOrLogError(rcls);
+ }
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java
new file mode 100644
index 00000000..04a092b5
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/SelectionToolDotVisualization.java
@@ -0,0 +1,280 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.dom.events.DOMMouseEvent;
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+import org.w3c.dom.events.Event;
+import org.w3c.dom.svg.SVGPoint;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDUtil;
+import de.lmu.ifi.dbs.elki.database.ids.HashSetModifiableDBIDs;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.result.DBIDSelection;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.DragableArea;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection2D;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Tool-Visualization for the tool to select objects
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance - - «create»
+ */
+public class SelectionToolDotVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Object Selection";
+
+ /**
+ * Input modes
+ *
+ * @apiviz.exclude
+ */
+ private enum Mode {
+ REPLACE, ADD, INVERT
+ }
+
+ /**
+ * Constructor.
+ */
+ public SelectionToolDotVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ final Relation<?> rel = p.getRelation();
+ if(!TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(rel.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getSelectionResult(), rel, SelectionToolDotVisualization.this);
+ task.level = VisualizationTask.LEVEL_INTERACTIVE;
+ task.tool = true;
+ task.addFlags(VisualizationTask.FLAG_NO_THUMBNAIL | VisualizationTask.FLAG_NO_EXPORT);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SELECTION);
+ task.initDefaultVisibility(false);
+ context.addVis(context.getSelectionResult(), task);
+ context.addVis(p, task);
+ }
+ }
+
+ /**
+ * Instance
+ *
+ * @author Heidi Kolb
+ *
+ * @apiviz.has DBIDSelection oneway - - updates
+ */
+ public class Instance extends AbstractScatterplotVisualization implements DragableArea.DragListener {
+ /**
+ * CSS class of the selection rectangle while selecting.
+ */
+ private static final String CSS_RANGEMARKER = "selectionRangeMarker";
+
+ /**
+ * Element for selection rectangle
+ */
+ Element rtag;
+
+ /**
+ * Element for the rectangle to add listeners
+ */
+ Element etag;
+
+ /**
+ * Constructor.
+ *
+ * @param task Task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ addCSSClasses(svgp);
+
+ //
+ rtag = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ SVGUtil.addCSSClass(rtag, CSS_RANGEMARKER);
+ layer.appendChild(rtag);
+
+ // etag: sensitive area
+ DragableArea drag = new DragableArea(svgp, -0.6 * StyleLibrary.SCALE, -0.7 * StyleLibrary.SCALE, 1.3 * StyleLibrary.SCALE, 1.4 * StyleLibrary.SCALE, this);
+ etag = drag.getElement();
+ layer.appendChild(etag);
+ }
+
+ /**
+ * Delete the children of the element
+ *
+ * @param container SVG-Element
+ */
+ private void deleteChildren(Element container) {
+ while(container.hasChildNodes()) {
+ container.removeChild(container.getLastChild());
+ }
+ }
+
+ @Override
+ public boolean startDrag(SVGPoint startPoint, Event evt) {
+ return true;
+ }
+
+ @Override
+ public boolean duringDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ deleteChildren(rtag);
+ double x = Math.min(startPoint.getX(), dragPoint.getX());
+ double y = Math.min(startPoint.getY(), dragPoint.getY());
+ double width = Math.abs(startPoint.getX() - dragPoint.getX());
+ double height = Math.abs(startPoint.getY() - dragPoint.getY());
+ rtag.appendChild(svgp.svgRect(x, y, width, height));
+ return true;
+ }
+
+ @Override
+ public boolean endDrag(SVGPoint startPoint, SVGPoint dragPoint, Event evt, boolean inside) {
+ Mode mode = getInputMode(evt);
+ deleteChildren(rtag);
+ if(startPoint.getX() != dragPoint.getX() || startPoint.getY() != dragPoint.getY()) {
+ updateSelection(mode, proj, startPoint, dragPoint);
+ }
+ return true;
+ }
+
+ /**
+ * Get the current input mode, on each mouse event.
+ *
+ * @param evt Mouse event.
+ * @return current input mode
+ */
+ private Mode getInputMode(Event evt) {
+ if(evt instanceof DOMMouseEvent) {
+ DOMMouseEvent domme = (DOMMouseEvent) evt;
+ // TODO: visual indication of mode possible?
+ if(domme.getShiftKey()) {
+ return Mode.ADD;
+ }
+ else if(domme.getCtrlKey()) {
+ return Mode.INVERT;
+ }
+ else {
+ return Mode.REPLACE;
+ }
+ }
+ // Default mode is replace.
+ return Mode.REPLACE;
+ }
+
+ /**
+ * Updates the selection in the context.<br>
+ *
+ * @param mode Input mode
+ * @param proj
+ * @param p1 first point of the selected rectangle
+ * @param p2 second point of the selected rectangle
+ */
+ private void updateSelection(Mode mode, Projection2D proj, SVGPoint p1, SVGPoint p2) {
+ DBIDSelection selContext = context.getSelection();
+ // Note: we rely on SET semantics below!
+ HashSetModifiableDBIDs selection;
+ if(selContext == null || mode == Mode.REPLACE) {
+ selection = DBIDUtil.newHashSet();
+ }
+ else {
+ selection = DBIDUtil.newHashSet(selContext.getSelectedIds());
+ }
+ for(DBIDIter iditer = rel.iterDBIDs(); iditer.valid(); iditer.advance()) {
+ double[] vec = proj.fastProjectDataToRenderSpace(rel.get(iditer));
+ if(vec[0] >= Math.min(p1.getX(), p2.getX()) && vec[0] <= Math.max(p1.getX(), p2.getX()) && vec[1] >= Math.min(p1.getY(), p2.getY()) && vec[1] <= Math.max(p1.getY(), p2.getY())) {
+ if(mode == Mode.INVERT) {
+ if(!selection.contains(iditer)) {
+ selection.add(iditer);
+ }
+ else {
+ selection.remove(iditer);
+ }
+ }
+ else {
+ // In REPLACE and ADD, add objects.
+ // The difference was done before by not re-using the selection.
+ // Since we are using a set, we can just add in any case.
+ selection.add(iditer);
+ }
+ }
+ }
+ context.setSelection(new DBIDSelection(selection));
+ }
+
+ /**
+ * Adds the required CSS-Classes
+ *
+ * @param svgp SVG-Plot
+ */
+ protected void addCSSClasses(SVGPlot svgp) {
+ // Class for the range marking
+ if(!svgp.getCSSClassManager().contains(CSS_RANGEMARKER)) {
+ final CSSClass rcls = new CSSClass(this, CSS_RANGEMARKER);
+ final StyleLibrary style = context.getStyleLibrary();
+ rcls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getColor(StyleLibrary.SELECTION_ACTIVE));
+ rcls.setStatement(SVGConstants.CSS_OPACITY_PROPERTY, style.getOpacity(StyleLibrary.SELECTION_ACTIVE));
+ svgp.addCSSClassOrLogError(rcls);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java
new file mode 100755
index 00000000..5a8bf99e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/selection/package-info.java
@@ -0,0 +1,26 @@
+/**
+ * <p>Visualizers for object selection based on 2D projections.</p>
+ */
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java
new file mode 100644
index 00000000..4e4cca3a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainBoundingBoxVisualization.java
@@ -0,0 +1,191 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGHyperCube;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize uncertain objects by their bounding box.
+ *
+ * Note: this is currently a hack. Our projection only applies to vector field
+ * relations currently, and this visualizer activates if such a relation (e.g. a
+ * sample, or the center of mass) has a parent relation of type UncertainObject.
+ * But it serves the purpose.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class UncertainBoundingBoxVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Uncertain Bounding Boxes";
+
+ /**
+ * Constructor.
+ */
+ public UncertainBoundingBoxVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ Relation<?> r = p.getRelation();
+ if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ // task.initDefaultVisibility(false);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ continue;
+ }
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses StylingPolicy
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * CSS class for uncertain bounding boxes.
+ */
+ public static final String CSS_CLASS = "uncertainbb";
+
+ /**
+ * The representation we visualize
+ */
+ final protected Relation<? extends UncertainObject> rel;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ this.rel = task.getRelation();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final double opac = .1; // Opacity
+ final StyleLibrary style = context.getStyleLibrary();
+ final double lw = .25 * style.getLineWidth(StyleLibrary.PLOT);
+ final StylingPolicy spol = context.getStylingPolicy();
+ final ColorLibrary colors = style.getColorSet(StyleLibrary.PLOT);
+
+ if(spol instanceof ClassStylingPolicy) {
+ ClassStylingPolicy cspol = (ClassStylingPolicy) spol;
+ for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) {
+ String css = CSS_CLASS + "_" + cnum;
+ final String color = colors.getColor(cnum);
+ CSSClass cls = new CSSClass(this, css);
+ cls.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, lw);
+ cls.setStatement(SVGConstants.CSS_STROKE_LINECAP_PROPERTY, SVGConstants.CSS_ROUND_VALUE);
+ cls.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ cls.setStatement(SVGConstants.CSS_FILL_OPACITY_PROPERTY, opac);
+
+ svgp.addCSSClassOrLogError(cls);
+
+ for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) {
+ if(!sample.getSample().contains(iter)) {
+ continue; // TODO: can we test more efficiently than this?
+ }
+ try {
+ final UncertainObject mbr = rel.get(iter);
+ Element r = SVGHyperCube.drawFrame(svgp, proj, mbr);
+ SVGUtil.addCSSClass(r, css);
+ layer.appendChild(r);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+ else {
+ final String STROKE = SVGConstants.CSS_STROKE_PROPERTY + ":";
+ // Color-based styling.
+ for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) {
+ try {
+ final UncertainObject mbr = rel.get(iter);
+ Element r = SVGHyperCube.drawFrame(svgp, proj, mbr);
+ SVGUtil.addCSSClass(r, CSS_CLASS);
+ int col = spol.getColorForDBID(iter);
+ SVGUtil.setAtt(r, SVGConstants.SVG_STYLE_ATTRIBUTE, STROKE + SVGUtil.colorToString(col));
+ layer.appendChild(r);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java
new file mode 100644
index 00000000..c66d88e3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainInstancesVisualization.java
@@ -0,0 +1,190 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.SimpleTypeInformation;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.database.relation.RelationUtil;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize a single derived sample from an uncertain database.
+ *
+ * Note: this is currently a hack. Our projection only applies to vector field
+ * relations currently, and this visualizer activates if such a relation (e.g. a
+ * sample, or the center of mass) has a parent relation of type UncertainObject.
+ * But it serves the purpose.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class UncertainInstancesVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Uncertain Instance";
+
+ /**
+ * Constructor.
+ */
+ public UncertainInstancesVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ // Find a scatter plot visualizing uncertain objects:
+ ScatterPlotProjector<?> p = it.get();
+ Relation<?> r = p.getRelation();
+ if(!TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) {
+ continue;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ continue;
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses StylingPolicy
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * CSS class for uncertain bounding boxes.
+ */
+ public static final String CSS_CLASS = "uncertain-instances";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ final StylingPolicy spol = context.getStylingPolicy();
+ final double size = style.getSize(StyleLibrary.MARKERPLOT);
+ final MarkerLibrary ml = style.markers();
+
+ // Only visualize cluster-based policies
+ if(!(spol instanceof ClusterStylingPolicy)) {
+ return;
+ }
+ ClusterStylingPolicy cspol = (ClusterStylingPolicy) spol;
+ Clustering<?> c = cspol.getClustering();
+ // If this is a sample from the uncertain database, it must have a parent
+ // relation containing vectors, which is a child to the uncertain
+ // database.
+ Hierarchy.Iter<Result> it = context.getHierarchy().iterAncestors(c);
+ Relation<? extends NumberVector> srel = null;
+ boolean isChild = false;
+ for(; it.valid(); it.advance()) {
+ Result r = it.get();
+ if(r == this.rel) {
+ isChild = true;
+ }
+ else if(r instanceof Relation) {
+ final SimpleTypeInformation<?> type = ((Relation<?>) r).getDataTypeInformation();
+ if(TypeUtil.NUMBER_VECTOR_FIELD.isAssignableFromType(type)) {
+ @SuppressWarnings("unchecked")
+ Relation<? extends NumberVector> vr = (Relation<? extends NumberVector>) r;
+ int dim = RelationUtil.dimensionality(vr);
+ if(dim == RelationUtil.dimensionality(this.rel)) {
+ srel = vr;
+ }
+ }
+ }
+ if(isChild && srel != null) {
+ break;
+ }
+ }
+ // Nothing found, probably in a different subtree.
+ if(!isChild || srel == null) {
+ return;
+ }
+ for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) {
+ for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) {
+ if(!sample.getSample().contains(iter)) {
+ continue; // TODO: can we test more efficiently than this?
+ }
+ try {
+ final NumberVector vec = srel.get(iter);
+ double[] v = proj.fastProjectDataToRenderSpace(vec);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ ml.useMarker(svgp, layer, v[0], v[1], cnum, size);
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java
new file mode 100644
index 00000000..a186d881
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/UncertainSamplesVisualization.java
@@ -0,0 +1,301 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Random;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.data.type.TypeUtil;
+import de.lmu.ifi.dbs.elki.data.uncertain.DiscreteUncertainObject;
+import de.lmu.ifi.dbs.elki.data.uncertain.UncertainObject;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.math.random.RandomFactory;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.FilteredIter;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.exceptions.ObjectNotFoundException;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotProjector;
+import de.lmu.ifi.dbs.elki.visualization.style.ClassStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AbstractScatterplotVisualization;
+
+/**
+ * Visualize uncertain objects by multiple samples.
+ *
+ * Note: this is currently a hack. Our projection only applies to vector field
+ * relations currently, and this visualizer activates if such a relation (e.g. a
+ * sample, or the center of mass) has a parent relation of type UncertainObject.
+ * But it serves the purpose.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class UncertainSamplesVisualization extends AbstractVisFactory {
+ /**
+ * A short name characterizing this Visualizer.
+ */
+ private static final String NAME = "Uncertain Samples";
+
+ /**
+ * Number of samples to draw for uncertain objects.
+ */
+ protected int samples = 10;
+
+ /**
+ * Constructor.
+ */
+ public UncertainSamplesVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height, proj);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<ScatterPlotProjector<?>> it = VisualizationTree.filter(context, start, ScatterPlotProjector.class);
+ for(; it.valid(); it.advance()) {
+ ScatterPlotProjector<?> p = it.get();
+ Relation<?> r = p.getRelation();
+ if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r.getDataTypeInformation())) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, r, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.initDefaultVisibility(false);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ continue;
+ }
+ Hierarchy.Iter<Relation<?>> it2 = new FilteredIter<Relation<?>>(context.getHierarchy().iterParents(r), Relation.class);
+ for(; it2.valid(); it2.advance()) {
+ Relation<?> r2 = it2.get();
+ if(TypeUtil.UNCERTAIN_OBJECT_FIELD.isAssignableFromType(r2.getDataTypeInformation())) {
+ final VisualizationTask task = new VisualizationTask(NAME, context, p, r2, this);
+ task.level = VisualizationTask.LEVEL_DATA;
+ task.initDefaultVisibility(false);
+ task.addUpdateFlags(VisualizationTask.ON_DATA | VisualizationTask.ON_SAMPLE | VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(p, task);
+ continue;
+ }
+ }
+ }
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses StylingPolicy
+ */
+ public class Instance extends AbstractScatterplotVisualization {
+ /**
+ * CSS class for uncertain bounding boxes.
+ */
+ public static final String CSS_CLASS = "uncertain-sample";
+
+ /**
+ * The representation we visualize
+ */
+ final protected Relation<? extends UncertainObject> rel;
+
+ /**
+ * Random factory.
+ */
+ final protected RandomFactory random = RandomFactory.DEFAULT;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ super(task, plot, width, height, proj);
+ addListeners();
+ this.rel = task.getRelation();
+ }
+
+ @Override
+ public void fullRedraw() {
+ setupCanvas();
+ final StyleLibrary style = context.getStyleLibrary();
+ final StylingPolicy spol = context.getStylingPolicy();
+ final double size = style.getSize(StyleLibrary.MARKERPLOT);
+ final double ssize = size / Math.sqrt(samples);
+ final MarkerLibrary ml = style.markers();
+
+ Random rand = random.getSingleThreadedRandom();
+
+ if(spol instanceof ClassStylingPolicy) {
+ ClassStylingPolicy cspol = (ClassStylingPolicy) spol;
+ for(int cnum = cspol.getMinStyle(); cnum < cspol.getMaxStyle(); cnum++) {
+ for(DBIDIter iter = cspol.iterateClass(cnum); iter.valid(); iter.advance()) {
+ if(!sample.getSample().contains(iter)) {
+ continue; // TODO: can we test more efficiently than this?
+ }
+ try {
+ final UncertainObject uo = rel.get(iter);
+ if(uo instanceof DiscreteUncertainObject) {
+ drawDiscete((DiscreteUncertainObject) uo, ml, cnum, size);
+ }
+ else {
+ drawContinuous(uo, ml, cnum, ssize, rand);
+ }
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+ else {
+ // Color-based styling.
+ for(DBIDIter iter = sample.getSample().iter(); iter.valid(); iter.advance()) {
+ try {
+ final int col = spol.getColorForDBID(iter);
+ final UncertainObject uo = rel.get(iter);
+ if(uo instanceof DiscreteUncertainObject) {
+ drawDiscreteDefault((DiscreteUncertainObject) uo, col, size);
+ }
+ else {
+ drawContinuousDefault(uo, col, size, rand);
+ }
+ }
+ catch(ObjectNotFoundException e) {
+ // ignore.
+ }
+ }
+ }
+ }
+
+ /**
+ * Visualize a discrete uncertain object
+ *
+ * @param uo Uncertain object
+ * @param ml Marker library
+ * @param cnum Cluster number
+ * @param size Size
+ */
+ private void drawDiscete(DiscreteUncertainObject uo, MarkerLibrary ml, int cnum, double size) {
+ final int e = uo.getNumberSamples();
+ final double ssize = size * Math.sqrt(e);
+ for(int i = 0; i < e; i++) {
+ final NumberVector s = uo.getSample(i);
+ if(s == null) {
+ continue;
+ }
+ double[] v = proj.fastProjectDataToRenderSpace(s);
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ ml.useMarker(svgp, layer, v[0], v[1], cnum, uo.getWeight(i) * ssize);
+ }
+ }
+
+ /**
+ * Visualize random samples
+ *
+ * @param uo Uncertain object
+ * @param ml Marker library
+ * @param cnum Cluster number
+ * @param size Marker size
+ * @param rand Random generator
+ */
+ private void drawContinuous(UncertainObject uo, MarkerLibrary ml, int cnum, double size, Random rand) {
+ for(int i = 0; i < samples; i++) {
+ double[] v = proj.fastProjectDataToRenderSpace(uo.drawSample(rand));
+ if(v[0] != v[0] || v[1] != v[1]) {
+ continue; // NaN!
+ }
+ ml.useMarker(svgp, layer, v[0], v[1], cnum, size);
+ }
+ }
+
+ /**
+ * String constant.
+ */
+ private final static String FILL = SVGConstants.CSS_FILL_PROPERTY + ":";
+
+ /**
+ * Visualize discrete object
+ *
+ * @param uo Uncertain object
+ * @param col Color
+ * @param size Size
+ */
+ private void drawDiscreteDefault(DiscreteUncertainObject uo, int col, double size) {
+ final int e = uo.getNumberSamples();
+ final double ssize = size * Math.sqrt(e);
+ for(int i = 0; i < e; i++) {
+ final NumberVector s = uo.getSample(i);
+ if(s == null) {
+ continue;
+ }
+ double[] v = proj.fastProjectDataToRenderSpace(s);
+ Element dot = svgp.svgCircle(v[0], v[1], ssize * uo.getWeight(i));
+ SVGUtil.addCSSClass(dot, CSS_CLASS);
+ SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col));
+ layer.appendChild(dot);
+ }
+ }
+
+ /**
+ * Visualize random samples
+ *
+ * @param uo Uncertain object
+ * @param col Color
+ * @param size Size
+ * @param rand Random generator
+ */
+ private void drawContinuousDefault(UncertainObject uo, int col, double size, Random rand) {
+ for(int i = 0; i < samples; i++) {
+ double[] v = proj.fastProjectDataToRenderSpace(uo.drawSample(rand));
+ Element dot = svgp.svgCircle(v[0], v[1], size);
+ SVGUtil.addCSSClass(dot, CSS_CLASS);
+ SVGUtil.setAtt(dot, SVGConstants.SVG_STYLE_ATTRIBUTE, FILL + SVGUtil.colorToString(col));
+ layer.appendChild(dot);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java
new file mode 100644
index 00000000..f30516f9
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/scatterplot/uncertain/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * Visualizers for uncertain data.
+ */
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java
new file mode 100644
index 00000000..e43656cf
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailThread.java
@@ -0,0 +1,162 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+/**
+ * Thread to render thumbnails in the background.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses Listener oneway - - signals
+ */
+public class ThumbnailThread extends Thread {
+ /**
+ * Queue of thumbnails to generate.
+ */
+ private Queue<Task> queue = new ConcurrentLinkedQueue<>();
+
+ /**
+ * Flag to signal shutdown.
+ */
+ private boolean shutdown = false;
+
+ /**
+ * The static thumbnail thread.
+ */
+ private static ThumbnailThread THREAD = null;
+
+ /**
+ * Queue a thumbnail task in a global thumbnail thread.
+ *
+ * @param callback Callback
+ */
+ public synchronized static Task QUEUE(Listener callback) {
+ final Task task = new Task(callback);
+ if(THREAD != null) {
+ // TODO: synchronization?
+ if(THREAD.isAlive()) {
+ THREAD.queue(task);
+ return task;
+ }
+ }
+ THREAD = new ThumbnailThread();
+ THREAD.queue(task);
+ THREAD.start();
+ return task;
+ }
+
+ /**
+ * Remove a pending task from the queue.
+ *
+ * @param task Task to remove.
+ */
+ public static void UNQUEUE(Task task) {
+ if(THREAD != null) {
+ synchronized(THREAD) {
+ THREAD.queue.remove(task);
+ }
+ }
+ }
+
+ /**
+ * Shutdown the thumbnailer thread.
+ */
+ public static synchronized void SHUTDOWN() {
+ if(THREAD != null && THREAD.isAlive()) {
+ THREAD.shutdown();
+ }
+ }
+
+ /**
+ * Queue a new thumbnail task.
+ *
+ * @param task Thumbnail task
+ */
+ private void queue(Task task) {
+ this.queue.add(task);
+ }
+
+ /**
+ * Generate a single Thumbnail.
+ *
+ * @param ti Visualization task
+ */
+ private void generateThumbnail(Task ti) {
+ ti.callback.doThumbnail();
+ }
+
+ @Override
+ public void run() {
+ while(!queue.isEmpty() && !shutdown) {
+ generateThumbnail(queue.poll());
+ }
+ }
+
+ /**
+ * Set the shutdown flag.
+ */
+ private void shutdown() {
+ this.shutdown = true;
+ queue.clear();
+ }
+
+ /**
+ * A single thumbnailer task.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.exclude
+ */
+ public static class Task {
+ /**
+ * Runnable to call back
+ */
+ Listener callback;
+
+ /**
+ * Constructor.
+ *
+ * @param callback Callback when complete
+ */
+ public Task(Listener callback) {
+ super();
+ this.callback = callback;
+ }
+ }
+
+ /**
+ * Listener interface for completed thumbnails.
+ *
+ * @author Erich Schubert
+ */
+ public interface Listener {
+ /**
+ * Callback when to (re-)compute the thumbnail.
+ */
+ public void doThumbnail();
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java
new file mode 100644
index 00000000..1c0dd760
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/ThumbnailVisualization.java
@@ -0,0 +1,224 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.BufferedImage;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.database.datastore.DataStoreListener;
+import de.lmu.ifi.dbs.elki.logging.Logging;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.result.SamplingResult;
+import de.lmu.ifi.dbs.elki.result.SelectionResult;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationItem;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationListener;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Thumbnail visualization.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.uses ThumbnailThread
+ */
+public class ThumbnailVisualization extends AbstractVisualization implements ThumbnailThread.Listener, DataStoreListener, VisualizationListener {
+ /**
+ * Visualizer factory
+ */
+ protected final VisFactory visFactory;
+
+ /**
+ * The thumbnail id.
+ */
+ protected int thumbid = -1;
+
+ /**
+ * Pending redraw
+ */
+ protected ThumbnailThread.Task pendingThumbnail = null;
+
+ /**
+ * Thumbnail resolution
+ */
+ protected int tresolution;
+
+ /**
+ * Our thumbnail (keep a reference to prevent garbage collection!)
+ */
+ private BufferedImage thumb;
+
+ /**
+ * Plot the thumbnail is in.
+ */
+ private SVGPlot plot;
+
+ /**
+ * Projection.
+ */
+ private Projection proj;
+
+ /**
+ * Constructor.
+ *
+ * @param visFactory Visualizer Factory to use
+ * @param task Task to use
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ * @param proj Projection
+ * @param thumbsize Thumbnail size
+ */
+ public ThumbnailVisualization(VisFactory visFactory, VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj, int thumbsize) {
+ super(task, plot, width, height);
+ this.visFactory = visFactory;
+ this.plot = plot;
+ this.proj = proj;
+ this.tresolution = thumbsize;
+ this.layer = plot.svgElement(SVGConstants.SVG_G_TAG);
+ this.thumbid = -1;
+ this.thumb = null;
+ addListeners();
+ }
+
+ @Override
+ public void destroy() {
+ if(pendingThumbnail != null) {
+ ThumbnailThread.UNQUEUE(pendingThumbnail);
+ }
+ // TODO: remove image from registry?
+ super.destroy();
+ }
+
+ @Override
+ public Element getLayer() {
+ if(thumbid < 0) {
+ svgp.requestRedraw(this.task, this);
+ }
+ return layer;
+ }
+
+ /**
+ * Perform a full redraw.
+ */
+ @Override
+ public void fullRedraw() {
+ if(!(getWidth() > 0 && getHeight() > 0)) {
+ LoggingUtil.warning("Thumbnail of zero size requested: " + visFactory);
+ return;
+ }
+ if(thumbid < 0) {
+ // LoggingUtil.warning("Generating new thumbnail " + this);
+ layer.appendChild(SVGUtil.svgWaitIcon(plot.getDocument(), 0, 0, getWidth(), getHeight()));
+ if(pendingThumbnail == null) {
+ pendingThumbnail = ThumbnailThread.QUEUE(this);
+ }
+ return;
+ }
+ // LoggingUtil.warning("Injecting Thumbnail " + this);
+ Element i = plot.svgElement(SVGConstants.SVG_IMAGE_TAG);
+ SVGUtil.setAtt(i, SVGConstants.SVG_X_ATTRIBUTE, 0);
+ SVGUtil.setAtt(i, SVGConstants.SVG_Y_ATTRIBUTE, 0);
+ SVGUtil.setAtt(i, SVGConstants.SVG_WIDTH_ATTRIBUTE, getWidth());
+ SVGUtil.setAtt(i, SVGConstants.SVG_HEIGHT_ATTRIBUTE, getHeight());
+ i.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, ThumbnailRegistryEntry.INTERNAL_PROTOCOL + ":" + thumbid);
+ layer.appendChild(i);
+ }
+
+ @Override
+ public synchronized void doThumbnail() {
+ pendingThumbnail = null;
+ try {
+ VisualizationPlot plot = new VisualizationPlot();
+ plot.getRoot().setAttribute(SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + getWidth() + " " + getHeight());
+
+ // Work on a clone
+ Visualization vis = visFactory.makeVisualization(task, plot, getWidth(), getHeight(), proj);
+
+ plot.getRoot().appendChild(vis.getLayer());
+ plot.updateStyleElement();
+ final int tw = (int) (getWidth() * tresolution);
+ final int th = (int) (getHeight() * tresolution);
+ thumb = plot.makeAWTImage(tw, th);
+ thumbid = ThumbnailRegistryEntry.registerImage(thumb);
+ // The visualization will not be used anymore.
+ vis.destroy();
+ svgp.requestRedraw(this.task, this);
+ }
+ catch(Exception e) {
+ final Logging logger = Logging.getLogger(task.getFactory().getClass());
+ if(logger != null && logger.isDebugging()) {
+ logger.exception("Thumbnail for " + task.getFactory() + " failed.", e);
+ }
+ else {
+ LoggingUtil.warning("Thumbnail for " + task.getFactory() + " failed - enable debugging to see details.");
+ }
+ // TODO: hide the failed image?
+ }
+ }
+
+ private void refreshThumbnail() {
+ // Discard an existing thumbnail
+ thumbid = -1;
+ thumb = null;
+ // TODO: also purge from ThumbnailRegistryEntry?
+ svgp.requestRedraw(this.task, this);
+ }
+
+ @Override
+ public void resultChanged(Result current) {
+ // Default is to redraw when the result we are attached to changed.
+ if(task.getResult() == current) {
+ refreshThumbnail();
+ return;
+ }
+ if(task.updateOnAny(VisualizationTask.ON_SELECTION) && current instanceof SelectionResult) {
+ refreshThumbnail();
+ return;
+ }
+ if(task.updateOnAny(VisualizationTask.ON_SAMPLE) && current instanceof SamplingResult) {
+ refreshThumbnail();
+ return;
+ }
+ }
+
+ @Override
+ public void visualizationChanged(VisualizationItem item) {
+ if(task.updateOnAny(VisualizationTask.ON_STYLEPOLICY) && item instanceof StylingPolicy) {
+ refreshThumbnail();
+ return;
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java
new file mode 100755
index 00000000..2031e44e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/thumbs/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Thumbnail "Visualizers" (that take care of refreshing thumbnails)</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.thumbs; \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java
new file mode 100644
index 00000000..b378c71a
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/EvaluationVisualization.java
@@ -0,0 +1,216 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.result.EvaluationResult;
+import de.lmu.ifi.dbs.elki.result.Result;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGScoreBar;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Pseudo-Visualizer, that lists the cluster evaluation results found.
+ *
+ * TODO: add indication/warning when values are out-of-bounds.
+ *
+ * TODO: Find a nicer solution than the current hack to only display the
+ * evaluation results for the currently active clustering.
+ *
+ * @author Erich Schubert
+ * @author Sascha Goldhofer
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ * @apiviz.has EvaluationResult oneway - - visualizes
+ */
+public class EvaluationVisualization extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "Evaluation Bar Chart";
+
+ /**
+ * Constant: width of score bars
+ */
+ private static final double BARLENGTH = 5;
+
+ /**
+ * Constant: height of score bars
+ */
+ private static final double BARHEIGHT = 0.7;
+
+ /**
+ * Constructor.
+ */
+ public EvaluationVisualization() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<EvaluationResult> it = VisualizationTree.filterResults(context, start, EvaluationResult.class);
+ candidate: for(; it.valid(); it.advance()) {
+ EvaluationResult sr = it.get();
+ // Avoid duplicates:
+ Hierarchy.Iter<VisualizationTask> it2 = VisualizationTree.filter(context, sr, VisualizationTask.class);
+ for(; it2.valid(); it2.advance()) {
+ if(it2.get().getFactory() instanceof EvaluationVisualization) {
+ continue candidate;
+ }
+ }
+ // Hack: for clusterings, only show the currently visible clustering.
+ if(sr.visualizeSingleton()) {
+ Class<? extends EvaluationResult> c = sr.getClass();
+ // Ensure singleton.
+ Hierarchy.Iter<?> it3 = context.getVisHierarchy().iterChildren(context.getBaseResult());
+ for(; it3.valid(); it3.advance()) {
+ Object o = it3.get();
+ if(!(o instanceof VisualizationTask)) {
+ continue;
+ }
+ final VisualizationTask otask = (VisualizationTask) o;
+ if(otask.getFactory() instanceof EvaluationVisualization && otask.getResult() == c) {
+ continue candidate;
+ }
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, c, null, EvaluationVisualization.this);
+ task.reqwidth = .5;
+ task.reqheight = sr.numLines() * .05;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ context.addVis(context.getBaseResult(), task);
+ continue candidate;
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, sr, null, EvaluationVisualization.this);
+ task.reqwidth = .5;
+ task.reqheight = sr.numLines() * .05;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(sr, task);
+ }
+ }
+
+ private double addBarChart(SVGPlot svgp, Element parent, double ypos, String label, double value, double minValue, double maxValue, double baseValue, boolean reversed) {
+ SVGScoreBar barchart = new SVGScoreBar();
+ barchart.setFill(value, baseValue == baseValue ? baseValue : minValue, maxValue);
+ barchart.setReversed(reversed);
+ barchart.showValues(FormatUtil.NF4);
+ barchart.addLabel(label);
+ parent.appendChild(barchart.build(svgp, 0.0, ypos, BARLENGTH, BARHEIGHT));
+ ypos += 1;
+ return ypos;
+ }
+
+ private double addHeader(SVGPlot svgp, Element parent, double ypos, String text) {
+ ypos += .5;
+ Element object = svgp.svgText(0, ypos + BARHEIGHT * 0.5, text);
+ object.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6; font-weight: bold");
+ parent.appendChild(object);
+ ypos += 1;
+ return ypos;
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ // TODO: make a utility class to wrap SVGPlot + parent layer + ypos.
+ // TODO: use CSSClass and StyleLibrary
+
+ double ypos = -.5; // Skip space before first header
+ Element parent = plot.svgElement(SVGConstants.SVG_G_TAG);
+ Object o = task.getResult();
+ EvaluationResult sr = null;
+ if(o instanceof EvaluationResult) {
+ sr = (EvaluationResult) o;
+ }
+ else if(o instanceof Class) {
+ // Use cluster evaluation of current style instead.
+ VisualizerContext context = task.getContext();
+ StylingPolicy spol = context.getStylingPolicy();
+ if(spol instanceof ClusterStylingPolicy) {
+ ClusterStylingPolicy cpol = (ClusterStylingPolicy) spol;
+ @SuppressWarnings("unchecked")
+ final Class<Object> c = (Class<Object>) o;
+ Iter<?> it = VisualizationTree.filterResults(context, cpol.getClustering(), c);
+ candidates: for(; it.valid(); it.advance()) {
+ // This could be attached to a child clustering, in which case we
+ // may end up displaying the wrong evaluation.
+ Iter<Result> it2 = context.getHierarchy().iterAncestors((EvaluationResult) it.get());
+ for(; it2.valid(); it2.advance()) {
+ if(it2.get() instanceof Clustering && it2.get() != cpol.getClustering()) {
+ continue candidates;
+ }
+ }
+ sr = (EvaluationResult) it.get();
+ break;
+ }
+ }
+ }
+ if(sr == null) {
+ return new StaticVisualizationInstance(task, plot, width, height, parent); // Failed.
+ }
+
+ for(String header : sr.getHeaderLines()) {
+ ypos = addHeader(plot, parent, ypos, header);
+ }
+
+ for(EvaluationResult.MeasurementGroup g : sr) {
+ ypos = addHeader(plot, parent, ypos, g.getName());
+ for(EvaluationResult.Measurement m : g) {
+ ypos = addBarChart(plot, parent, ypos, m.getName(), m.getVal(), m.getMin(), m.getMax(), m.getExp(), m.lowerIsBetter());
+ }
+ }
+
+ // scale vis
+ double cols = 10;
+ final StyleLibrary style = task.getContext().getStyleLibrary();
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ final String transform = SVGUtil.makeMarginTransform(width, height, cols, ypos, margin / StyleLibrary.SCALE);
+ SVGUtil.setAtt(parent, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ return new StaticVisualizationInstance(task, plot, width, height, parent);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java
new file mode 100644
index 00000000..1d3e8771
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/HistogramVisualization.java
@@ -0,0 +1,181 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.NumberVector;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.result.HistogramResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.colors.ColorLibrary;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizer to draw histograms.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ * @apiviz.has HistogramResult oneway - - visualizes
+ */
+public class HistogramVisualization extends AbstractVisFactory {
+ /**
+ * Histogram visualizer name
+ */
+ private static final String NAME = "Histogram";
+
+ /**
+ * CSS class name for the series.
+ */
+ private static final String SERIESID = "series";
+
+ /**
+ * Constructor.
+ */
+ public HistogramVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ VisualizerContext context = task.getContext();
+ HistogramResult<? extends NumberVector> curve = task.getResult();
+
+ final StyleLibrary style = context.getStyleLibrary();
+ final double sizex = StyleLibrary.SCALE;
+ final double sizey = StyleLibrary.SCALE * height / width;
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ // find maximum, determine step size
+ int dim = -1;
+ DoubleMinMax xminmax = new DoubleMinMax();
+ DoubleMinMax yminmax = new DoubleMinMax();
+ for(NumberVector vec : curve) {
+ xminmax.put(vec.doubleValue(0));
+ if(dim < 0) {
+ dim = vec.getDimensionality();
+ }
+ else {
+ // TODO: test and throw always
+ assert(dim == vec.getDimensionality());
+ }
+ for(int i = 1; i < dim; i++) {
+ yminmax.put(vec.doubleValue(i));
+ }
+ }
+ // Minimum should always start at 0 for histograms
+ yminmax.put(0.0);
+ // remove one dimension which are the x values.
+ dim = dim - 1;
+
+ int size = curve.size();
+ double range = xminmax.getMax() - xminmax.getMin();
+ double binwidth = range / (size - 1);
+
+ LinearScale xscale = new LinearScale(xminmax.getMin() - binwidth * .49999, xminmax.getMax() + binwidth * .49999);
+ LinearScale yscale = new LinearScale(yminmax.getMin(), yminmax.getMax());
+
+ SVGPath[] path = new SVGPath[dim];
+ for(int i = 0; i < dim; i++) {
+ path[i] = new SVGPath(sizex * xscale.getScaled(xminmax.getMin() - binwidth * .5), sizey);
+ }
+
+ // draw curves.
+ for(NumberVector vec : curve) {
+ for(int d = 0; d < dim; d++) {
+ path[d].lineTo(sizex * (xscale.getScaled(vec.doubleValue(0) - binwidth * .5)), sizey * (1 - yscale.getScaled(vec.doubleValue(d + 1))));
+ path[d].lineTo(sizex * (xscale.getScaled(vec.doubleValue(0) + binwidth * .5)), sizey * (1 - yscale.getScaled(vec.doubleValue(d + 1))));
+ }
+ }
+
+ // close all histograms
+ for(int i = 0; i < dim; i++) {
+ path[i].lineTo(sizex * xscale.getScaled(xminmax.getMax() + binwidth * .5), sizey);
+ }
+
+ // add axes
+ try {
+ SVGSimpleLinearAxis.drawAxis(plot, layer, yscale, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+ SVGSimpleLinearAxis.drawAxis(plot, layer, xscale, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style);
+ }
+ catch(CSSNamingConflict e) {
+ LoggingUtil.exception(e);
+ }
+ // Setup line styles and insert lines.
+ ColorLibrary cl = style.getColorSet(StyleLibrary.PLOT);
+ for(int d = 0; d < dim; d++) {
+ CSSClass csscls = new CSSClass(this, SERIESID + "_" + d);
+ csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
+ csscls.setStatement(SVGConstants.SVG_STROKE_ATTRIBUTE, cl.getColor(d));
+ csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, style.getLineWidth(StyleLibrary.PLOT));
+ plot.addCSSClassOrLogError(csscls);
+
+ Element line = path[d].makeElement(plot);
+ line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, csscls.getName());
+ layer.appendChild(line);
+ }
+
+ return new StaticVisualizationInstance(task, plot, width, height, layer);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<HistogramResult<?>> it = VisualizationTree.filterResults(context, start, HistogramResult.class);
+ for(; it.valid(); it.advance()) {
+ HistogramResult<?> histogram = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, histogram, null, HistogramVisualization.this);
+ task.reqwidth = 2.0;
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(histogram, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // TODO: depending on the histogram complexity?
+ return false;
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java
new file mode 100644
index 00000000..7361bd4e
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/KeyVisualization.java
@@ -0,0 +1,334 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import gnu.trove.map.TObjectIntMap;
+import gnu.trove.map.hash.TObjectIntHashMap;
+
+import java.util.List;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.data.Cluster;
+import de.lmu.ifi.dbs.elki.data.Clustering;
+import de.lmu.ifi.dbs.elki.data.model.Model;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy.Iter;
+import de.lmu.ifi.dbs.elki.utilities.pairs.DoubleDoublePair;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.ClusterStylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.style.StylingPolicy;
+import de.lmu.ifi.dbs.elki.visualization.style.marker.MarkerLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizer, displaying the key for a clustering.
+ *
+ * TODO: re-add automatic sizing depending on the number of clusters.
+ *
+ * TODO: also show in scatter plot detail view.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class KeyVisualization extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "Cluster Key";
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ // Ensure there is a clustering result:
+ Hierarchy.Iter<Clustering<?>> it = VisualizationTree.filterResults(context, start, Clustering.class);
+ if(!it.valid()) {
+ return;
+ }
+ Hierarchy.Iter<VisualizationTask> i2 = VisualizationTree.filter(context, VisualizationTask.class);
+ for(; i2.valid(); i2.advance()) {
+ if(i2.get().getFactory() instanceof KeyVisualization) {
+ return; // At most one key per plot.
+ }
+ }
+ final VisualizationTask task = new VisualizationTask(NAME, context, context.getStylingPolicy(), null, this);
+ task.level = VisualizationTask.LEVEL_STATIC;
+ task.addUpdateFlags(VisualizationTask.ON_STYLEPOLICY);
+ task.reqwidth = 1.;
+ task.reqheight = 1.;
+ context.addVis(context.getStylingPolicy(), task);
+ }
+
+ /**
+ * Compute the size of the clustering.
+ *
+ * @param c Clustering
+ * @return Array storing the depth and the number of leaf nodes.
+ */
+ protected static <M extends Model> int[] findDepth(Clustering<M> c) {
+ final Hierarchy<Cluster<M>> hier = c.getClusterHierarchy();
+ int[] size = { 0, 0 };
+ for(Iter<Cluster<M>> iter = c.iterToplevelClusters(); iter.valid(); iter.advance()) {
+ findDepth(hier, iter.get(), size);
+ }
+ return size;
+ }
+
+ /**
+ * Recursive depth computation.
+ *
+ * @param hier Hierarchy
+ * @param cluster Current cluster
+ * @param size Counting array.
+ */
+ private static <M extends Model> void findDepth(Hierarchy<Cluster<M>> hier, Cluster<M> cluster, int[] size) {
+ if(hier.numChildren(cluster) > 0) {
+ for(Iter<Cluster<M>> iter = hier.iterChildren(cluster); iter.valid(); iter.advance()) {
+ findDepth(hier, iter.get(), size);
+ }
+ size[0] += 1; // Depth
+ }
+ else {
+ size[1] += 1; // Leaves
+ }
+ }
+
+ /**
+ * Compute the preferred number of columns.
+ *
+ * @param width Target width
+ * @param height Target height
+ * @param numc Number of clusters
+ * @param maxwidth Max width of entries
+ * @return Preferred number of columns
+ */
+ protected static int getPreferredColumns(double width, double height, int numc, double maxwidth) {
+ // Maximum width (compared to height) of labels - guess.
+ // FIXME: do we really need to do this three-step computation?
+ // Number of rows we'd use in a squared layout:
+ final double rows = Math.ceil(Math.pow(numc * maxwidth, height / (width + height)));
+ // Given this number of rows (plus one for header), use this many columns:
+ return (int) Math.ceil(numc / (rows + 1));
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ return false;
+ }
+
+ /**
+ * Instance
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has Clustering oneway - - visualizes
+ */
+ public class Instance extends AbstractVisualization {
+ /**
+ * CSS class for key captions.
+ */
+ private static final String KEY_CAPTION = "key-caption";
+
+ /**
+ * CSS class for key entries.
+ */
+ private static final String KEY_ENTRY = "key-entry";
+
+ /**
+ * CSS class for hierarchy plot lines
+ */
+ private static final String KEY_HIERLINE = "key-hierarchy";
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) {
+ super(task, plot, width, height);
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ StylingPolicy pol = context.getStylingPolicy();
+ if(!(pol instanceof ClusterStylingPolicy)) {
+ Element label = svgp.svgText(0.1, 0.7, "No clustering selected.");
+ SVGUtil.setCSSClass(label, KEY_CAPTION);
+ layer.appendChild(label);
+ return;
+ }
+ @SuppressWarnings("unchecked")
+ Clustering<Model> clustering = (Clustering<Model>) ((ClusterStylingPolicy) pol).getClustering();
+
+ StyleLibrary style = context.getStyleLibrary();
+ MarkerLibrary ml = style.markers();
+ final List<Cluster<Model>> allcs = clustering.getAllClusters();
+ final List<Cluster<Model>> topcs = clustering.getToplevelClusters();
+
+ setupCSS(svgp);
+ layer = svgp.svgElement(SVGConstants.SVG_G_TAG);
+ // Add a label for the clustering.
+ {
+ Element label = svgp.svgText(0.1, 0.7, clustering.getLongName());
+ SVGUtil.setCSSClass(label, KEY_CAPTION);
+ layer.appendChild(label);
+ }
+
+ double kwi, khe;
+ if(allcs.size() == topcs.size()) {
+ // Maximum width (compared to height) of labels - guess.
+ // FIXME: compute from labels?
+ final double maxwidth = 10.;
+
+ // Flat clustering. Use multiple columns.
+ final int numc = allcs.size();
+ final int cols = getPreferredColumns(getWidth(), getHeight(), numc, maxwidth);
+ final int rows = (int) Math.ceil(numc / (double) cols);
+ // We use a coordinate system based on rows, so columns are at
+ // c*maxwidth
+
+ int i = 0;
+ for(Cluster<Model> c : allcs) {
+ final int col = i / rows;
+ final int row = i % rows;
+ ml.useMarker(svgp, layer, 0.3 + maxwidth * col, row + 1.5, i, 0.3);
+ Element label = svgp.svgText(0.7 + maxwidth * col, row + 1.7, c.getNameAutomatic());
+ SVGUtil.setCSSClass(label, KEY_ENTRY);
+ layer.appendChild(label);
+ i++;
+ }
+ kwi = cols * maxwidth;
+ khe = rows;
+ }
+ else {
+ // For consistent keying:
+ TObjectIntMap<Cluster<Model>> cnum = new TObjectIntHashMap<>(allcs.size());
+ int i = 0;
+ for(Cluster<Model> c : allcs) {
+ cnum.put(c, i);
+ i++;
+ }
+ // Hierarchical clustering. Draw recursively.
+ DoubleDoublePair size = new DoubleDoublePair(0., 1.), pos = new DoubleDoublePair(0., 1.);
+ Hierarchy<Cluster<Model>> hier = clustering.getClusterHierarchy();
+ for(Cluster<Model> cluster : topcs) {
+ drawHierarchy(svgp, ml, size, pos, 0, cluster, cnum, hier);
+ }
+ kwi = size.first;
+ khe = size.second;
+ }
+
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), kwi, khe, margin / StyleLibrary.SCALE);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+ }
+
+ private double drawHierarchy(SVGPlot svgp, MarkerLibrary ml, DoubleDoublePair size, DoubleDoublePair pos, int depth, Cluster<Model> cluster, TObjectIntMap<Cluster<Model>> cnum, Hierarchy<Cluster<Model>> hier) {
+ final double maxwidth = 8.;
+ DoubleDoublePair subpos = new DoubleDoublePair(pos.first + maxwidth, pos.second);
+ int numc = hier.numChildren(cluster);
+ double posy;
+ if(numc > 0) {
+ double[] mids = new double[numc];
+ Iter<Cluster<Model>> iter = hier.iterChildren(cluster);
+ for(int i = 0; iter.valid(); iter.advance(), i++) {
+ mids[i] = drawHierarchy(svgp, ml, size, subpos, depth, iter.get(), cnum, hier);
+ }
+ // Center:
+ posy = (pos.second + subpos.second) * .5;
+ for(int i = 0; i < numc; i++) {
+ Element line = svgp.svgLine(pos.first + maxwidth - 1., posy + .5, pos.first + maxwidth, mids[i] + .5);
+ SVGUtil.setCSSClass(line, KEY_HIERLINE);
+ layer.appendChild(line);
+ }
+ // Use vertical extends of children:
+ pos.second = subpos.second;
+ }
+ else {
+ posy = pos.second + .5;
+ pos.second += 1.;
+ }
+ ml.useMarker(svgp, layer, 0.3 + pos.first, posy + 0.5, cnum.get(cluster), 0.3);
+ Element label = svgp.svgText(0.7 + pos.first, posy + 0.7, cluster.getNameAutomatic());
+ SVGUtil.setCSSClass(label, KEY_ENTRY);
+ layer.appendChild(label);
+ size.first = Math.max(size.first, pos.first + maxwidth);
+ size.second = Math.max(size.second, pos.second);
+ return posy;
+ }
+
+ /**
+ * Registers the Tooltip-CSS-Class at a SVGPlot.
+ *
+ * @param svgp the SVGPlot to register the Tooltip-CSS-Class.
+ */
+ protected void setupCSS(SVGPlot svgp) {
+ final StyleLibrary style = context.getStyleLibrary();
+ final double fontsize = style.getTextSize(StyleLibrary.KEY);
+ final String fontfamily = style.getFontFamily(StyleLibrary.KEY);
+ final String color = style.getColor(StyleLibrary.KEY);
+
+ CSSClass keycaption = new CSSClass(svgp, KEY_CAPTION);
+ keycaption.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ keycaption.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ keycaption.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ keycaption.setStatement(SVGConstants.CSS_FONT_WEIGHT_PROPERTY, SVGConstants.CSS_BOLD_VALUE);
+ svgp.addCSSClassOrLogError(keycaption);
+
+ CSSClass keyentry = new CSSClass(svgp, KEY_ENTRY);
+ keyentry.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, fontsize);
+ keyentry.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, fontfamily);
+ keyentry.setStatement(SVGConstants.CSS_FILL_PROPERTY, color);
+ svgp.addCSSClassOrLogError(keyentry);
+
+ CSSClass hierline = new CSSClass(svgp, KEY_HIERLINE);
+ hierline.setStatement(SVGConstants.CSS_STROKE_PROPERTY, color);
+ hierline.setStatement(SVGConstants.CSS_STROKE_WIDTH_PROPERTY, style.getLineWidth("key.hierarchy") / StyleLibrary.SCALE);
+ svgp.addCSSClassOrLogError(hierline);
+
+ svgp.updateStyleElement();
+ }
+ }
+}
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java
new file mode 100644
index 00000000..c89d9c75
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/LabelVisualization.java
@@ -0,0 +1,122 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Trivial "visualizer" that displays a static label. The visualizer is meant to
+ * be used for dimension labels in the overview.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ */
+public class LabelVisualization extends AbstractVisFactory {
+ /**
+ * The label to render
+ */
+ private String label = "undefined";
+
+ /**
+ * Flag to indicate rotated labels (90 deg to the left)
+ */
+ private boolean rotated = false;
+
+ /**
+ * Constructor. Solely for API purposes (Parameterizable!)
+ */
+ public LabelVisualization() {
+ super();
+ }
+
+ /**
+ * The actually used constructor - with a static label.
+ *
+ * @param label Label to use
+ */
+ public LabelVisualization(String label) {
+ this(label, false);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param label Label to use
+ * @param rotated Rotated 90 deg to the left
+ */
+ public LabelVisualization(String label, boolean rotated) {
+ super();
+ this.label = label;
+ this.rotated = rotated;
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ // No auto discovery supported.
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ VisualizerContext context = task.getContext();
+ CSSClass cls = new CSSClass(plot, "unmanaged");
+ StyleLibrary style = context.getStyleLibrary();
+ double fontsize = style.getTextSize("overview.labels") / StyleLibrary.SCALE;
+ cls.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, SVGUtil.fmt(fontsize));
+ cls.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor("overview.labels"));
+ cls.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily("overview.labels"));
+
+ Element layer;
+ if(!rotated) {
+ layer = plot.svgText(width * .5, height * .5 + .35 * fontsize, this.label);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE);
+ }
+ else {
+ layer = plot.svgText(height * -.5, width * .5 + .35 * fontsize, this.label);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_STYLE_ATTRIBUTE, cls.inlineCSS());
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_MIDDLE_VALUE);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90)");
+ }
+ return new StaticVisualizationInstance(task, plot, width, height, layer);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ return false;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java
new file mode 100644
index 00000000..b6fcdd57
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/PixmapVisualizer.java
@@ -0,0 +1,146 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.RenderedImage;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.result.PixmapResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize an arbitrary pixmap result.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class PixmapVisualizer extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "Pixmap Visualizer";
+
+ /**
+ * Constructor.
+ */
+ public PixmapVisualizer() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<PixmapResult> it = VisualizationTree.filterResults(context, start, PixmapResult.class);
+ for(; it.valid(); it.advance()) {
+ PixmapResult pr = it.get();
+ // Add plots, attach visualizer
+ final VisualizationTask task = new VisualizationTask(NAME, context, pr, null, PixmapVisualizer.this);
+ task.reqwidth = pr.getImage().getWidth() / (double) pr.getImage().getHeight();
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(pr, task);
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has PixmapResult oneway - 1 visualizes
+ */
+ public class Instance extends AbstractVisualization {
+ /**
+ * The actual pixmap result.
+ */
+ private PixmapResult result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) {
+ super(task, plot, width, height);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ final double scale = StyleLibrary.SCALE;
+ final double sizex = scale;
+ final double sizey = scale * getHeight() / getWidth();
+ final double margin = 0.0; // context.getStyleLibrary().getSize(StyleLibrary.MARGIN);
+ layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), sizex, sizey, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ RenderedImage img = result.getImage();
+ // is ratio, target ratio
+ double iratio = img.getHeight() / img.getWidth();
+ double tratio = getHeight() / getWidth();
+ // We want to place a (iratio, 1.0) object on a (tratio, 1.0) screen.
+ // Both dimensions must fit:
+ double zoom = (iratio >= tratio) ? Math.min(tratio / iratio, 1.0) : Math.max(iratio / tratio, 1.0);
+
+ Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, 0);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, 0);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, scale * zoom * iratio);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, scale * zoom);
+ itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, result.getAsFile().toURI().toString());
+
+ layer.appendChild(itag);
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java
new file mode 100644
index 00000000..3efa04d1
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SettingsVisualization.java
@@ -0,0 +1,154 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.util.Collection;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.result.SettingsResult;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackedParameter;
+import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ClassParameter;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Pseudo-Visualizer, that lists the settings of the algorithm-
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ * @apiviz.has SettingsResult oneway - - visualizes
+ */
+// TODO: make this a menu item instead of a "visualization"?
+public class SettingsVisualization extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "Settings";
+
+ /**
+ * Constructor.
+ */
+ public SettingsVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ SettingsResult sr = task.getResult();
+ VisualizerContext context = task.getContext();
+
+ Collection<TrackedParameter> settings = sr.getSettings();
+
+ Element layer = plot.svgElement(SVGConstants.SVG_G_TAG);
+
+ // FIXME: use CSSClass and StyleLibrary
+
+ int i = 0;
+ Object last = null;
+ for(TrackedParameter setting : settings) {
+ if(setting.getOwner() != last && setting.getOwner() != null) {
+ String name;
+ try {
+ if(setting.getOwner() instanceof Class) {
+ name = ((Class<?>) setting.getOwner()).getName();
+ }
+ else {
+ name = setting.getOwner().getClass().getName();
+ }
+ if(ClassParameter.class.isInstance(setting.getOwner())) {
+ name = ((ClassParameter<?>) setting.getOwner()).getValue().getName();
+ }
+ }
+ catch(NullPointerException e) {
+ name = "[null]";
+ }
+ Element object = plot.svgText(0, i + 0.7, name);
+ object.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6; font-weight: bold");
+ layer.appendChild(object);
+ i++;
+ last = setting.getOwner();
+ }
+ // get name and value
+ String name = setting.getParameter().getOptionID().getName();
+ String value = "[unset]";
+ try {
+ if(setting.getParameter().isDefined()) {
+ value = setting.getParameter().getValueAsString();
+ }
+ }
+ catch(NullPointerException e) {
+ value = "[null]";
+ }
+
+ Element label = plot.svgText(0, i + 0.7, name);
+ label.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6");
+ layer.appendChild(label);
+ Element vale = plot.svgText(7.5, i + 0.7, value);
+ vale.setAttribute(SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: 0.6");
+ layer.appendChild(vale);
+ // only advance once, since we want these two to be in the same line.
+ i++;
+ }
+
+ int cols = Math.max(30, (int) (i * height / width));
+ int rows = i;
+ final double margin = context.getStyleLibrary().getSize(StyleLibrary.MARGIN);
+ final String transform = SVGUtil.makeMarginTransform(width, height, cols, rows, margin / StyleLibrary.SCALE);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ return new StaticVisualizationInstance(task, plot, width, height, layer);
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<SettingsResult> it = VisualizationTree.filterResults(context, start, SettingsResult.class);
+ for(; it.valid(); it.advance()) {
+ SettingsResult sr = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, sr, null, SettingsVisualization.this);
+ task.reqwidth = 1.0;
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ task.initDefaultVisibility(false);
+ context.addVis(sr, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ return false;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java
new file mode 100644
index 00000000..0b64d6c3
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/SimilarityMatrixVisualizer.java
@@ -0,0 +1,176 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import java.awt.image.RenderedImage;
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.database.Database;
+import de.lmu.ifi.dbs.elki.database.ids.DBIDIter;
+import de.lmu.ifi.dbs.elki.database.relation.Relation;
+import de.lmu.ifi.dbs.elki.evaluation.similaritymatrix.ComputeSimilarityMatrixImage.SimilarityMatrix;
+import de.lmu.ifi.dbs.elki.result.ResultUtil;
+import de.lmu.ifi.dbs.elki.utilities.DatabaseUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisualization;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualize a similarity matrix with object labels
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses Instance oneway - - «create»
+ */
+public class SimilarityMatrixVisualizer extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "Similarity Matrix Visualizer";
+
+ /**
+ * Constructor.
+ */
+ public SimilarityMatrixVisualizer() {
+ super();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<SimilarityMatrix> it = VisualizationTree.filterResults(context, start, SimilarityMatrix.class);
+ for(; it.valid(); it.advance()) {
+ SimilarityMatrix pr = it.get();
+ // Add plots, attach visualizer
+ final VisualizationTask task = new VisualizationTask(NAME, context, pr, null, SimilarityMatrixVisualizer.this);
+ task.reqwidth = 1.0;
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(pr, task);
+ }
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ return new Instance(task, plot, width, height);
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // Don't use thumbnails
+ return false;
+ }
+
+ /**
+ * Instance
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.has SimilarityMatrix oneway - 1 visualizes
+ */
+ public class Instance extends AbstractVisualization {
+ /**
+ * The actual pixmap result.
+ */
+ private SimilarityMatrix result;
+
+ /**
+ * Constructor.
+ *
+ * @param task Visualization task
+ * @param plot Plot to draw to
+ * @param width Embedding width
+ * @param height Embedding height
+ */
+ public Instance(VisualizationTask task, VisualizationPlot plot, double width, double height) {
+ super(task, plot, width, height);
+ this.result = task.getResult();
+ addListeners();
+ }
+
+ @Override
+ public void fullRedraw() {
+ final StyleLibrary style = context.getStyleLibrary();
+ final double sizex = StyleLibrary.SCALE;
+ final double sizey = StyleLibrary.SCALE * getHeight() / getWidth();
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ layer = SVGUtil.svgElement(svgp.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(getWidth(), getHeight(), sizex, sizey, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ RenderedImage img = result.getImage();
+ // is ratio, target ratio
+ double iratio = img.getHeight() / img.getWidth();
+ double tratio = getHeight() / getWidth();
+ // We want to place a (iratio, 1.0) object on a (tratio, 1.0) screen.
+ // Both dimensions must fit:
+ double zoom = (iratio >= tratio) ? Math.min(tratio / iratio, 1.0) : Math.max(iratio / tratio, 1.0);
+
+ Element itag = svgp.svgElement(SVGConstants.SVG_IMAGE_TAG);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_IMAGE_RENDERING_ATTRIBUTE, SVGConstants.SVG_OPTIMIZE_SPEED_VALUE);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_X_ATTRIBUTE, margin * 0.75);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_Y_ATTRIBUTE, margin * 0.75);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_WIDTH_ATTRIBUTE, StyleLibrary.SCALE * zoom * iratio);
+ SVGUtil.setAtt(itag, SVGConstants.SVG_HEIGHT_ATTRIBUTE, StyleLibrary.SCALE * zoom);
+ itag.setAttributeNS(SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.XLINK_HREF_QNAME, result.getAsFile().toURI().toString());
+ layer.appendChild(itag);
+
+ // Add object labels
+ final int size = result.getIDs().size();
+ final double hlsize = StyleLibrary.SCALE * zoom * iratio / size;
+ final double vlsize = StyleLibrary.SCALE * zoom / size;
+ int i = 0;
+ Database database = ResultUtil.findDatabase(context.getHierarchy());
+ final Relation<String> lrep = DatabaseUtil.guessObjectLabelRepresentation(database);
+ for(DBIDIter id = result.getIDs().iter(); id.valid(); id.advance()) {
+ String label = lrep.get(id);
+ if(label != null) {
+ // Label on horizontal axis
+ final double hlx = margin * 0.75 + hlsize * (i + .8);
+ final double hly = margin * 0.7;
+ Element lbl = svgp.svgText(hlx, hly, label);
+ SVGUtil.setAtt(lbl, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + hlx + "," + hly + ")");
+ SVGUtil.setAtt(lbl, SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + hlsize * 0.8);
+ layer.appendChild(lbl);
+ // Label on vertical axis
+ Element lbl2 = svgp.svgText(margin * 0.7, margin * 0.75 + vlsize * (i + .8), label);
+ SVGUtil.setAtt(lbl2, SVGConstants.SVG_TEXT_ANCHOR_ATTRIBUTE, SVGConstants.SVG_END_VALUE);
+ SVGUtil.setAtt(lbl2, SVGConstants.SVG_STYLE_ATTRIBUTE, "font-size: " + vlsize * 0.8);
+ layer.appendChild(lbl2);
+ }
+ i++;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java
new file mode 100644
index 00000000..3c620a8f
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYCurveVisualization.java
@@ -0,0 +1,205 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierPrecisionRecallCurve.PRCurve;
+import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierROCCurve;
+import de.lmu.ifi.dbs.elki.evaluation.outlier.OutlierROCCurve.ROCResult;
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.geometry.XYCurve;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizer to render a simple 2D curve such as a ROC curve.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ * @apiviz.has XYCurve oneway - - visualizes
+ */
+public class XYCurveVisualization extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "XYCurve";
+
+ /**
+ * SVG class name for plot line
+ */
+ private static final String SERIESID = "series";
+
+ /**
+ * Axis labels
+ */
+ private static final String CSS_AXIS_LABEL = "xy-axis-label";
+
+ /**
+ * Constructor, Parameterizable style - does nothing.
+ */
+ public XYCurveVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ VisualizerContext context = task.getContext();
+ XYCurve curve = task.getResult();
+
+ setupCSS(context, plot);
+ final StyleLibrary style = context.getStyleLibrary();
+ final double sizex = StyleLibrary.SCALE;
+ final double sizey = StyleLibrary.SCALE * height / width;
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ // determine scaling
+ LinearScale scalex = new LinearScale(curve.getMinx(), curve.getMaxx());
+ LinearScale scaley = new LinearScale(curve.getMiny(), curve.getMaxy());
+ // plot the line
+ SVGPath path = new SVGPath();
+ for(XYCurve.Itr iterator = curve.iterator(); iterator.valid(); iterator.advance()) {
+ final double x = scalex.getScaled(iterator.getX());
+ final double y = 1 - scaley.getScaled(iterator.getY());
+ path.drawTo(sizex * x, sizey * y);
+ }
+ Element line = path.makeElement(plot);
+ line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, SERIESID);
+
+ // add axes
+ try {
+ SVGSimpleLinearAxis.drawAxis(plot, layer, scaley, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+ SVGSimpleLinearAxis.drawAxis(plot, layer, scalex, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style);
+ }
+ catch(CSSNamingConflict e) {
+ LoggingUtil.exception(e);
+ }
+ // Add axis labels
+ {
+ Element labelx = plot.svgText(sizex * .5, sizey + margin * .9, curve.getLabelx());
+ SVGUtil.setCSSClass(labelx, CSS_AXIS_LABEL);
+ layer.appendChild(labelx);
+ Element labely = plot.svgText(margin * -.8, sizey * .5, curve.getLabely());
+ SVGUtil.setCSSClass(labely, CSS_AXIS_LABEL);
+ SVGUtil.setAtt(labely, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + FormatUtil.NF6.format(margin * -.8) + "," + FormatUtil.NF6.format(sizey * .5) + ")");
+ layer.appendChild(labely);
+ }
+
+ // Add AUC value when found
+ if(curve instanceof ROCResult) {
+ double rocauc = ((ROCResult) curve).getAUC();
+ String lt = OutlierROCCurve.ROCAUC_LABEL + ": " + FormatUtil.NF.format(rocauc);
+ if(rocauc <= 0.5) {
+ Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.10, lt);
+ SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL);
+ layer.appendChild(auclbl);
+ }
+ else {
+ Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.95, lt);
+ SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL);
+ layer.appendChild(auclbl);
+ }
+ }
+ if(curve instanceof PRCurve) {
+ double prauc = ((PRCurve) curve).getAUC();
+ String lt = PRCurve.PRAUC_LABEL + ": " + FormatUtil.NF.format(prauc);
+ if(prauc <= 0.5) {
+ Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.10, lt);
+ SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL);
+ layer.appendChild(auclbl);
+ }
+ else {
+ Element auclbl = plot.svgText(sizex * 0.5, sizey * 0.95, lt);
+ SVGUtil.setCSSClass(auclbl, CSS_AXIS_LABEL);
+ layer.appendChild(auclbl);
+ }
+ }
+
+ layer.appendChild(line);
+ return new StaticVisualizationInstance(task, plot, width, height, layer);
+ }
+
+ /**
+ * Setup the CSS classes for the plot.
+ *
+ * @param svgp Plot
+ */
+ private void setupCSS(VisualizerContext context, SVGPlot svgp) {
+ StyleLibrary style = context.getStyleLibrary();
+ CSSClass csscls = new CSSClass(this, SERIESID);
+ // csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.2%");
+ csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
+ style.lines().formatCSSClass(csscls, 0, style.getLineWidth(StyleLibrary.XYCURVE));
+ svgp.addCSSClassOrLogError(csscls);
+ // Axis label
+ CSSClass label = new CSSClass(this, CSS_AXIS_LABEL);
+ label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE);
+ svgp.addCSSClassOrLogError(label);
+ svgp.updateStyleElement();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<XYCurve> it = VisualizationTree.filterResults(context, start, XYCurve.class);
+ for(; it.valid(); it.advance()) {
+ XYCurve curve = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, curve, null, XYCurveVisualization.this);
+ task.reqwidth = 1.0;
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(curve, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // TODO: depending on the curve complexity?
+ return false;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java
new file mode 100644
index 00000000..3b07860d
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/XYPlotVisualization.java
@@ -0,0 +1,178 @@
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj;
+
+/*
+ This file is part of ELKI:
+ Environment for Developing KDD-Applications Supported by Index-Structures
+
+ Copyright (C) 2015
+ Ludwig-Maximilians-Universität München
+ Lehr- und Forschungseinheit für Datenbanksysteme
+ ELKI Development Team
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import org.apache.batik.util.SVGConstants;
+import org.w3c.dom.Element;
+
+import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
+import de.lmu.ifi.dbs.elki.math.geometry.XYPlot;
+import de.lmu.ifi.dbs.elki.math.scales.LinearScale;
+import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
+import de.lmu.ifi.dbs.elki.utilities.datastructures.hierarchy.Hierarchy;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTask;
+import de.lmu.ifi.dbs.elki.visualization.VisualizationTree;
+import de.lmu.ifi.dbs.elki.visualization.VisualizerContext;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClass;
+import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
+import de.lmu.ifi.dbs.elki.visualization.gui.VisualizationPlot;
+import de.lmu.ifi.dbs.elki.visualization.projections.Projection;
+import de.lmu.ifi.dbs.elki.visualization.style.StyleLibrary;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPath;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGPlot;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGSimpleLinearAxis;
+import de.lmu.ifi.dbs.elki.visualization.svg.SVGUtil;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.AbstractVisFactory;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.StaticVisualizationInstance;
+import de.lmu.ifi.dbs.elki.visualization.visualizers.Visualization;
+
+/**
+ * Visualizer to render a simple 2D curve such as a ROC curve.
+ *
+ * @author Erich Schubert
+ *
+ * @apiviz.stereotype factory
+ * @apiviz.uses StaticVisualizationInstance oneway - - «create»
+ * @apiviz.has XYPlot oneway - - visualizes
+ */
+public class XYPlotVisualization extends AbstractVisFactory {
+ /**
+ * Name for this visualizer.
+ */
+ private static final String NAME = "XYPlot";
+
+ /**
+ * SVG class name for plot line
+ */
+ private static final String SERIESID = "series_";
+
+ /**
+ * Axis labels
+ */
+ private static final String CSS_AXIS_LABEL = "xy-axis-label";
+
+ /**
+ * Constructor, Parameterizable style - does nothing.
+ */
+ public XYPlotVisualization() {
+ super();
+ }
+
+ @Override
+ public Visualization makeVisualization(VisualizationTask task, VisualizationPlot plot, double width, double height, Projection proj) {
+ VisualizerContext context = task.getContext();
+ XYPlot xyplot = task.getResult();
+
+ setupCSS(context, plot, xyplot);
+ final StyleLibrary style = context.getStyleLibrary();
+ final double sizex = StyleLibrary.SCALE;
+ final double sizey = StyleLibrary.SCALE * height / width;
+ final double margin = style.getSize(StyleLibrary.MARGIN);
+ Element layer = SVGUtil.svgElement(plot.getDocument(), SVGConstants.SVG_G_TAG);
+ final String transform = SVGUtil.makeMarginTransform(width, height, sizex, sizey, margin);
+ SVGUtil.setAtt(layer, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, transform);
+
+ // determine scaling
+ LinearScale scalex = new LinearScale(xyplot.getMinx(), xyplot.getMaxx());
+ LinearScale scaley = new LinearScale(xyplot.getMiny(), xyplot.getMaxy());
+
+ for(XYPlot.Curve curve : xyplot) {
+ // plot the line
+ SVGPath path = new SVGPath();
+ for(XYPlot.Curve.Itr iterator = curve.iterator(); iterator.valid(); iterator.advance()) {
+ final double x = scalex.getScaled(iterator.getX());
+ final double y = 1 - scaley.getScaled(iterator.getY());
+ path.drawTo(sizex * x, sizey * y);
+ }
+ Element line = path.makeElement(plot);
+ line.setAttribute(SVGConstants.SVG_CLASS_ATTRIBUTE, SERIESID + curve.getColor());
+ layer.appendChild(line);
+ }
+
+ // add axes
+ try {
+ SVGSimpleLinearAxis.drawAxis(plot, layer, scaley, 0, sizey, 0, 0, SVGSimpleLinearAxis.LabelStyle.LEFTHAND, style);
+ SVGSimpleLinearAxis.drawAxis(plot, layer, scalex, 0, sizey, sizex, sizey, SVGSimpleLinearAxis.LabelStyle.RIGHTHAND, style);
+ }
+ catch(CSSNamingConflict e) {
+ LoggingUtil.exception(e);
+ }
+ // Add axis labels
+ {
+ Element labelx = plot.svgText(sizex * .5, sizey + margin * .9, xyplot.getLabelx());
+ SVGUtil.setCSSClass(labelx, CSS_AXIS_LABEL);
+ layer.appendChild(labelx);
+ Element labely = plot.svgText(margin * -.8, sizey * .5, xyplot.getLabely());
+ SVGUtil.setCSSClass(labely, CSS_AXIS_LABEL);
+ SVGUtil.setAtt(labely, SVGConstants.SVG_TRANSFORM_ATTRIBUTE, "rotate(-90," + FormatUtil.NF6.format(margin * -.8) + "," + FormatUtil.NF6.format(sizey * .5) + ")");
+ layer.appendChild(labely);
+ }
+
+ return new StaticVisualizationInstance(task, plot, width, height, layer);
+ }
+
+ /**
+ * Setup the CSS classes for the plot.
+ *
+ * @param svgp Plot
+ * @param plot Plot to render
+ */
+ private void setupCSS(VisualizerContext context, SVGPlot svgp, XYPlot plot) {
+ StyleLibrary style = context.getStyleLibrary();
+ for(XYPlot.Curve curve : plot) {
+ CSSClass csscls = new CSSClass(this, SERIESID + curve.getColor());
+ // csscls.setStatement(SVGConstants.SVG_STROKE_WIDTH_ATTRIBUTE, "0.2%");
+ csscls.setStatement(SVGConstants.SVG_FILL_ATTRIBUTE, SVGConstants.SVG_NONE_VALUE);
+ style.lines().formatCSSClass(csscls, curve.getColor(), style.getLineWidth(StyleLibrary.XYCURVE));
+ svgp.addCSSClassOrLogError(csscls);
+ }
+ // Axis label
+ CSSClass label = new CSSClass(this, CSS_AXIS_LABEL);
+ label.setStatement(SVGConstants.CSS_FILL_PROPERTY, style.getTextColor(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_FONT_FAMILY_PROPERTY, style.getFontFamily(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_FONT_SIZE_PROPERTY, style.getTextSize(StyleLibrary.XYCURVE));
+ label.setStatement(SVGConstants.CSS_TEXT_ANCHOR_PROPERTY, SVGConstants.CSS_MIDDLE_VALUE);
+ svgp.addCSSClassOrLogError(label);
+ svgp.updateStyleElement();
+ }
+
+ @Override
+ public void processNewResult(VisualizerContext context, Object start) {
+ Hierarchy.Iter<XYPlot> it = VisualizationTree.filterResults(context, start, XYPlot.class);
+ for(; it.valid(); it.advance()) {
+ XYPlot plot = it.get();
+ final VisualizationTask task = new VisualizationTask(NAME, context, plot, null, XYPlotVisualization.this);
+ task.reqwidth = 1.0;
+ task.reqheight = 1.0;
+ task.level = VisualizationTask.LEVEL_STATIC;
+ context.addVis(plot, task);
+ }
+ }
+
+ @Override
+ public boolean allowThumbnails(VisualizationTask task) {
+ // TODO: depending on the curve complexity?
+ return false;
+ }
+} \ No newline at end of file
diff --git a/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java
new file mode 100755
index 00000000..23e29d19
--- /dev/null
+++ b/addons/batikvis/src/main/java/de/lmu/ifi/dbs/elki/visualization/visualizers/visunproj/package-info.java
@@ -0,0 +1,27 @@
+/**
+ * <p>Visualizers that do not use a particular projection.</p>
+ *
+ */
+/*
+This file is part of ELKI:
+Environment for Developing KDD-Applications Supported by Index-Structures
+
+Copyright (C) 2015
+Ludwig-Maximilians-Universität München
+Lehr- und Forschungseinheit für Datenbanksysteme
+ELKI Development Team
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+package de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj; \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication
new file mode 100644
index 00000000..ef4a2452
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.application.AbstractApplication
@@ -0,0 +1 @@
+de.lmu.ifi.dbs.elki.application.greedyensemble.VisualizePairwiseGainMatrix
diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler
new file mode 100644
index 00000000..00032d8f
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.result.ResultHandler
@@ -0,0 +1,2 @@
+de.lmu.ifi.dbs.elki.visualization.gui.ResultVisualizer visualizer vis ResultVisualizer
+de.lmu.ifi.dbs.elki.visualization.ExportVisualizations
diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor
new file mode 100644
index 00000000..3ba543d8
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.VisualizationProcessor
@@ -0,0 +1,60 @@
+de.lmu.ifi.dbs.elki.visualization.projector.HistogramFactory
+de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotFactory
+de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotFactory
+de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjectorFactory
+de.lmu.ifi.dbs.elki.visualization.visualizers.actions.ClusterStyleAction
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AxisVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.MarkerVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.PolygonVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterMeanVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterStarVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.EMClusterVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.VoronoiVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterOrderVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeSphereVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.BubbleVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.COPVectorVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipScoreVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipStringVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.ReferencePointsVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density.DensityEstimationOverlay
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.DistanceFunctionVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.MoveObjectsToolVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionDotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionConvexHullVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionCubeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolCubeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolDotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainBoundingBoxVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainSamplesVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainInstancesVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.histogram.ColoredHistogramVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.LineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.BoundingBoxVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.ParallelAxisVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisVisibilityVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisReorderVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterParallelMeanVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterOutlineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index.RTreeParallelVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionAxisRangeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionLineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolAxisRangeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolLineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments.CircleSegmentsVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.HistogramVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.EvaluationVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYCurveVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYPlotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.PixmapVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.KeyVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SettingsVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSClusterVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotCutVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotSelectionVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSSteepAreaVisualization
diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory
new file mode 100644
index 00000000..5fc4f399
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.projector.ProjectorFactory
@@ -0,0 +1,4 @@
+de.lmu.ifi.dbs.elki.visualization.projector.HistogramFactory
+de.lmu.ifi.dbs.elki.visualization.projector.ScatterPlotFactory
+de.lmu.ifi.dbs.elki.visualization.projector.ParallelPlotFactory
+de.lmu.ifi.dbs.elki.visualization.projector.OPTICSProjectorFactory \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory
new file mode 100644
index 00000000..19c05f02
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/elki/de.lmu.ifi.dbs.elki.visualization.visualizers.VisFactory
@@ -0,0 +1,56 @@
+de.lmu.ifi.dbs.elki.visualization.visualizers.actions.ClusterStyleAction
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.AxisVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.MarkerVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.PolygonVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterMeanVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterStarVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterHullVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.EMClusterVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.VoronoiVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.cluster.ClusterOrderVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeMBRVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.index.TreeSphereVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.BubbleVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.outlier.COPVectorVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipScoreVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.TooltipStringVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.ReferencePointsVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.density.DensityEstimationOverlay
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.DistanceFunctionVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.MoveObjectsToolVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionDotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionConvexHullVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionCubeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolCubeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.selection.SelectionToolDotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainBoundingBoxVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainSamplesVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.scatterplot.uncertain.UncertainInstancesVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.histogram.ColoredHistogramVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.LineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.BoundingBoxVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.ParallelAxisVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisVisibilityVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.AxisReorderVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterParallelMeanVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.cluster.ClusterOutlineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.index.RTreeParallelVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionAxisRangeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionLineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolAxisRangeVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.parallel.selection.SelectionToolLineVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.pairsegments.CircleSegmentsVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.HistogramVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.EvaluationVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYCurveVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.XYPlotVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.LabelVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.PixmapVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SimilarityMatrixVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.KeyVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.visunproj.SettingsVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSClusterVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotCutVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotSelectionVisualization
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSPlotVisualizer
+de.lmu.ifi.dbs.elki.visualization.visualizers.optics.OPTICSSteepAreaVisualization
diff --git a/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry
new file mode 100644
index 00000000..f1252025
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.ext.awt.image.spi.RegistryEntry
@@ -0,0 +1 @@
+de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler
new file mode 100644
index 00000000..f1252025
--- /dev/null
+++ b/addons/batikvis/src/main/resources/META-INF/services/org.apache.batik.util.ParsedURLProtocolHandler
@@ -0,0 +1 @@
+de.lmu.ifi.dbs.elki.visualization.batikutil.ThumbnailRegistryEntry \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties
new file mode 100644
index 00000000..66df4720
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/classic.properties
@@ -0,0 +1,79 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.02
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.1
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.05
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=red,blue,green,orange,cyan,magenta,darkred,darkblue,darkgreen
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.005
+## Greyed out color
+plot.grey=grey
+## Dot size
+plot.dot.size=0.002
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.03
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=darkblue
+plot.selection.opacity=0.25
+plot.selection.size=0.015
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties
new file mode 100644
index 00000000..bfcaaa3a
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/default.properties
@@ -0,0 +1,81 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.02
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.1
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.1
+
+## A list of color names for data lines
+colorset=#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.005
+## Greyed out color
+plot.grey=grey
+## Dot size
+plot.dot.size=0.002
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.03
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=darkblue
+plot.selection.opacity=0.25
+plot.selection.size=0.015
+
+## Circle segment colors. Will be interpolated to produce extra classes.
+segments.border.color=#FF0073
+segments.hover.color=#73ff00
+segments.cluster.first.color=#505050
+segments.cluster.second.color=#E0E0E0 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties
new file mode 100644
index 00000000..f27a85a2
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/greyscale.properties
@@ -0,0 +1,79 @@
+## Libraries
+lines-library = DashedLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.02
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.10
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.05
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=black,grey,silver,darkgrey,dimgrey
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.005
+## Greyed out color
+plot.grey=lightgrey
+## Dot size
+plot.dot.size=0.002
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=black
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=dimgrey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.03
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=grey
+plot.selection.opacity=0.25
+plot.selection.size=0.015 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties
new file mode 100644
index 00000000..808b8161
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/kelly.properties
@@ -0,0 +1,83 @@
+#### Based on Kelly, Kenneth L. "Twenty-two colors of maximum contrast." Color Engineering 3.26 (1965): 26-27.
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.02
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.1
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.1
+
+## A list of color names for data lines
+# Yes, this is just 20. We skipped #F2F3F4,#222222.
+colorset=#F3C300,#875692,#F38400,#A1CAF1,#BE0032,#C2B280,#848482,#008856,#E68FAC,#0067A5,#F99379,#604E97,#F6A600,#B3446C,#DCD300,#882D17,#8DB600,#654522,#E25822,#2B3D26
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.005
+## Greyed out color
+plot.grey=grey
+## Dot size
+plot.dot.size=0.002
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.03
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=darkblue
+plot.selection.opacity=0.25
+plot.selection.size=0.015
+
+## Circle segment colors. Will be interpolated to produce extra classes.
+segments.border.color=#FF0073
+segments.hover.color=#73ff00
+segments.cluster.first.color=#505050
+segments.cluster.second.color=#E0E0E0 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties
new file mode 100644
index 00000000..d18c393a
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/neon.properties
@@ -0,0 +1,79 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=white
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=black
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.02
+## Text color
+text-color=white
+## Default margin (relative)
+margin.size=0.10
+
+## Named color for the page background
+page.background-color=black
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=white
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=white
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=white
+key.hierarchy.line-width=0.05
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=yellow,magenta,cyan,red,blue,green,orange
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.005
+## Greyed out color
+plot.grey=darkgrey
+## Dot size
+plot.dot.size=0.002
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.03
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=blue
+plot.selection.opacity=0.25
+plot.selection.size=0.015 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties
new file mode 100644
index 00000000..d30667b6
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/presentation.properties
@@ -0,0 +1,85 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='CompatilFact', 'Droid Sans', sans-serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.02
+## Text font size scale
+text-size=0.015
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.10
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.004
+## Axis tick width
+axis.tick.line-width=0.004
+## Axis label font size
+axis.label.text-size=0.03
+
+## Named color for a label in the key part
+key.text-size=0.01
+key.color=black
+key.hierarchy.line-width=0.1
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760
+## Line width scaling (for graphs)
+plot.line-width=0.01
+## For the cluster order
+plot.clusterorder.line-width=0.004
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.01
+## Greyed out color
+plot.grey=grey
+## Dot size
+plot.dot.size=0.004
+## Reference points
+plot.referencepoints.size=0.006
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.04
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=red
+plot.selection.opacity=0.4
+plot.selection.size=0.03
+
+## Circle segment colors. Will be interpolated to produce extra classes.
+segments.border.color=#FF0073
+segments.hover.color=#73ff00
+segments.cluster.first.color=#404050
+segments.cluster.second.color=#D0D0F0 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties
new file mode 100644
index 00000000..02c19ba1
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/print.properties
@@ -0,0 +1,85 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = PrettyMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.03
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.10
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=grey
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.004
+## Axis tick width
+axis.tick.line-width=0.004
+## Axis label font size
+axis.label.text-size=0.03
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.05
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=black,#ed420e,#fdca19,#4548a5,#7ebd3a,#a81e51,#00748b,#fa8116,#512d85,#008a7a,#fea918,#019d60,#cfde3d,#015a9c,#7b1760
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.003
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.01
+## Greyed out color
+plot.grey=grey
+## Dot size
+plot.dot.size=0.004
+## Reference points
+plot.referencepoints.size=0.006
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=grey
+
+## Curve vis (ROC curves) labels:
+xycurve.text-size=0.04
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=darkblue
+plot.selection.opacity=0.4
+plot.selection.size=0.015
+
+## Circle segment colors. Will be interpolated to produce extra classes.
+segments.border.color=#FF0073
+segments.hover.color=#73ff00
+segments.cluster.first.color=#404050
+segments.cluster.second.color=#D0D0F0 \ No newline at end of file
diff --git a/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties
new file mode 100644
index 00000000..e02ad985
--- /dev/null
+++ b/addons/batikvis/src/main/resources/de/lmu/ifi/dbs/elki/visualization/style/wikipedia.properties
@@ -0,0 +1,77 @@
+## Libraries
+lines-library = SolidLineStyleLibrary
+marker-library = CircleMarkers
+
+## Default foreground color
+color=black
+## Default font family
+font-family='Times New Roman', serif
+## Default background color
+background-color=white
+## Default line width
+line-width=0.01
+## Text font size scale
+text-size=0.03
+## Text color
+text-color=black
+## Default margin (relative)
+margin.size=0.1
+
+## Named color for the page background
+page.background-color=white
+## Background color for plot area
+plot.background-color=none
+
+## Named color for a typical axis
+axis.color=black
+## Named color for a typical axis tick mark
+axis.tick.color=gray
+## Named color for a typical axis tick mark
+axis.tick.minor.color=silver
+## Named color for a typical axis label
+axis.label.color=black
+## Axis line width
+axis.line-width=0.002
+## Axis tick width
+axis.tick.line-width=0.002
+## Axis label font size
+axis.label.text-size=0.02
+
+## Named color for a label in the key part
+key.text-size=0.007
+key.color=black
+key.hierarchy.line-width=0.05
+
+## A list of color names for data lines
+# We stick to primary colors first to have the least issues
+# The first two colors are red and blue to help red-green blind people.
+# Yellow usually offers bad contrast, therefore comes late.
+# Magenta often shows up too similar to red, cyan too similar to blue in print.
+colorset=red,blue,green,orange,cyan,magenta,darkred,darkblue,darkgreen
+## Line width scaling (for graphs)
+plot.line-width=0.005
+## For the cluster order
+plot.clusterorder.line-width=0.002
+## Bubble sizes (relative)
+plot.bubble.size=0.10
+## Marker size (relative)
+plot.marker.size=0.01
+## Dot size
+plot.dot.size=0.005
+## Reference points
+plot.referencepoints.size=0.003
+plot.referencepoints.color=red
+## Polygons
+plot.polygons.line-width=0.001
+plot.polygons.color=gray
+
+## Curve vis (ROC curves) labels:
+curve.labels.text-size=0.04
+
+# Text size in overview plot
+overview.labels.text-size=0.08
+
+## Selection colors
+plot.selection.color=darkblue
+plot.selection.opacity=0.25
+plot.selection.size=0.015