Package de.lmu.ifi.dbs.elki.application.visualization

Source Code of de.lmu.ifi.dbs.elki.application.visualization.KNNExplorer$Parameterizer

package de.lmu.ifi.dbs.elki.application.visualization;

/*
This file is part of ELKI:
Environment for Developing KDD-Applications Supported by Index-Structures

Copyright (C) 2011
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.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;

import javax.swing.DefaultListCellRenderer;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;

import org.apache.batik.util.SVGConstants;
import org.w3c.dom.Element;

import de.lmu.ifi.dbs.elki.KDDTask;
import de.lmu.ifi.dbs.elki.application.AbstractApplication;
import de.lmu.ifi.dbs.elki.data.NumberVector;
import de.lmu.ifi.dbs.elki.data.VectorUtil;
import de.lmu.ifi.dbs.elki.database.Database;
import de.lmu.ifi.dbs.elki.database.StaticArrayDatabase;
import de.lmu.ifi.dbs.elki.database.ids.DBID;
import de.lmu.ifi.dbs.elki.database.query.DistanceResultPair;
import de.lmu.ifi.dbs.elki.database.query.distance.DistanceQuery;
import de.lmu.ifi.dbs.elki.database.query.knn.KNNQuery;
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.EuclideanDistanceFunction;
import de.lmu.ifi.dbs.elki.distance.distancevalue.NumberDistance;
import de.lmu.ifi.dbs.elki.logging.LoggingUtil;
import de.lmu.ifi.dbs.elki.math.DoubleMinMax;
import de.lmu.ifi.dbs.elki.utilities.DatabaseUtil;
import de.lmu.ifi.dbs.elki.utilities.documentation.Reference;
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.ClassParameter;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameters.ObjectParameter;
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.batikutil.NodeReplacer;
import de.lmu.ifi.dbs.elki.visualization.css.CSSClassManager.CSSNamingConflict;
import de.lmu.ifi.dbs.elki.visualization.savedialog.SVGSaveDialog;
import de.lmu.ifi.dbs.elki.visualization.scales.LinearScale;
import de.lmu.ifi.dbs.elki.visualization.style.PropertiesBasedStyleLibrary;
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;

/**
* User application to explore the k Nearest Neighbors for a given data set and
* distance function. When selecting one or more data entries, the nearest
* neighbors each are determined and visualized.
*
* <p>
* Reference:<br/>
*
* Elke Achtert, Thomas Bernecker, Hans-Peter Kriegel, Erich Schubert, Arthur
* Zimek:<br/>
*
* ELKI in Time: ELKI 0.2 for the Performance Evaluation of Distance Measures
* for Time Series.<br/>
*
* In Proc. 11th International Symposium on Spatial and Temporal Databases (SSTD
* 2009), Aalborg, Denmark, 2009.
* </p>
*
* <h3>Usage example:</h3>
*
* <p>
* Main invocation:<br/>
*
* <code>java -cp elki.jar de.lmu.ifi.dbs.elki.application.visualization.KNNExplorer</code>
* </p>
*
* <p>
* The application supports the usual parameterization, in particular parameters
* <code>-dbc.in</code> and <code>-explorer.distancefunction</code> to select an
* input file and the distance function to explore.
* </p>
*
* @author Erich Schubert
*
* @apiviz.composedOf ExplorerWindow
*
* @param <O> Object type
* @param <D> Distance type
*/
@Reference(authors = "E. Achtert, T. Bernecker, H.-P. Kriegel, E. Schubert, A. Zimek", title = "ELKI in Time: ELKI 0.2 for the Performance Evaluation of Distance Measures for Time Series", booktitle = "Proceedings of the 11th International Symposium on Spatial and Temporal Databases (SSTD), Aalborg, Denmark, 2009", url = "http://dx.doi.org/10.1007/978-3-642-02982-0_35")
public class KNNExplorer<O extends NumberVector<?, ?>, D extends NumberDistance<D, ?>> extends AbstractApplication {
  // TODO: replace by a wrapper creating appropriate visualization modules.

  /**
   * Parameter to specify the distance function to determine the distance
   * between database objects, must extend
   * {@link de.lmu.ifi.dbs.elki.distance.distancefunction.DistanceFunction}.
   * <p>
   * Key: {@code -explorer.distancefunction}
   * </p>
   * <p>
   * Default value:
   * {@link de.lmu.ifi.dbs.elki.distance.distancefunction.EuclideanDistanceFunction}
   * </p>
   */
  public static final OptionID DISTANCE_FUNCTION_ID = OptionID.getOrCreateOptionID("explorer.distancefunction", "Distance function to determine the distance between database objects.");

  /**
   * Holds the database connection to have the algorithm run with.
   */
  private Database database;

  /**
   * Holds the instance of the distance function specified by
   * {@link #DISTANCE_FUNCTION_ID}.
   */
  private DistanceFunction<O, D> distanceFunction;

  /**
   * Constructor.
   *
   * @param verbose Verbose flag
   * @param database Database
   * @param distanceFunction Distance function
   */
  public KNNExplorer(boolean verbose, Database database, DistanceFunction<O, D> distanceFunction) {
    super(verbose);
    this.database = database;
    this.distanceFunction = distanceFunction;
  }

  @Override
  public void run() throws IllegalStateException {
    database.initialize();
    Relation<O> relation = database.getRelation(distanceFunction.getInputTypeRestriction());
    DistanceQuery<O, D> distFunc = database.getDistanceQuery(relation, distanceFunction);
    (new ExplorerWindow()).run(database, distFunc);
  }

  /**
   * Main window of KNN Explorer.
   *
   * @author Erich Schubert
   */
  class ExplorerWindow {
    /**
     * Default Window Title
     */
    private static final String WINDOW_TITLE_BASE = "ELKI k Nearest Neighbors Explorer";

    /**
     * Maximum resolution for plotted lines to improve performance for long time
     * series.
     */
    private static final int MAXRESOLUTION = 1000;

    /**
     * SVG graph object ID (for replacing)
     */
    private static final String SERIESID = "series";

    // The frame.
    protected JFrame frame = new JFrame(WINDOW_TITLE_BASE);

    // The spinner
    protected JSpinner spinner;

    // The list of series
    private JList seriesList = new JList();

    // The "Quit" button, to close the application.
    protected JButton quitButton = new JButton("Quit");

    // The "Export" button, to save the image
    protected JButton saveButton = new JButton("Export");

    // The SVG canvas.
    protected JSVGSynchronizedCanvas svgCanvas = new JSVGSynchronizedCanvas();

    // The plot
    SVGPlot plot;

    // Viewport
    Element viewport;

    // Dimensionality
    protected int dim;

    // k
    protected int k = 20;

    // Scale
    protected LinearScale s;

    // The current database
    protected Relation<? extends O> data;

    // Distance cache
    protected HashMap<DBID, Double> distancecache = new HashMap<DBID, Double>();

    // Canvas scaling ratio
    protected double ratio;

    /**
     * Holds the instance of the distance function specified by
     * {@link #DISTANCE_FUNCTION_ID}.
     */
    private DistanceQuery<O, D> distanceQuery;

    /**
     * Holds the associated kNN query
     */
    private KNNQuery<O, D> knnQuery;

    /**
     * The label representation
     */
    protected Relation<String> labelRep;

    private Database db;

    /**
     * Constructor.
     */
    public ExplorerWindow() {
      super();

      frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

      // ELKI icon
      try {
        frame.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 bigpanel = new JPanel(new BorderLayout());

      // set up spinner
      SpinnerModel model = new SpinnerNumberModel(k, 1, 1000, 1);
      spinner = new JSpinner(model);
      JPanel spinnerPanel = new JPanel(new BorderLayout());
      spinnerPanel.add(BorderLayout.WEST, new JLabel("k"));
      spinnerPanel.add(BorderLayout.EAST, spinner);

      // button panel
      JPanel buttonPanel = new JPanel(new BorderLayout());
      buttonPanel.add(BorderLayout.WEST, saveButton);
      buttonPanel.add(BorderLayout.EAST, quitButton);

      // set up cell renderer
      seriesList.setCellRenderer(new SeriesLabelRenderer());

      JPanel sidepanel = new JPanel(new BorderLayout());
      sidepanel.add(BorderLayout.NORTH, spinnerPanel);
      sidepanel.add(BorderLayout.CENTER, new JScrollPane(seriesList));
      sidepanel.add(BorderLayout.SOUTH, buttonPanel);

      bigpanel.add(BorderLayout.WEST, sidepanel);
      bigpanel.add(BorderLayout.CENTER, svgCanvas);

      frame.getContentPane().add(bigpanel);

      spinner.addChangeListener(new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent e) {
          updateK((Integer) (spinner.getValue()));
          updateSelection();
        }
      });

      seriesList.addListSelectionListener(new ListSelectionListener() {
        @Override
        public void valueChanged(ListSelectionEvent e) {
          if(!e.getValueIsAdjusting()) {
            updateSelection();
          }
        }
      });

      saveButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent ae) {
          SVGSaveDialog.showSaveDialog(plot, 512, 512);
        }
      });

      quitButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          frame.setVisible(false);
          frame.dispose();
        }
      });

      // display
      frame.setSize(600, 600);

      // resize listener
      LazyCanvasResizer listener = new LazyCanvasResizer(frame) {
        @Override
        public void executeResize(double newratio) {
          ratio = newratio;
          updateSize();
          updateSelection();
        }
      };
      ratio = listener.getActiveRatio();
      frame.addComponentListener(listener);
    }

    /**
     * Update the SVG plot size.
     */
    public void updateSize() {
      SVGUtil.setAtt(plot.getRoot(), SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, "0 0 " + ratio + " 1");
      SVGUtil.setAtt(viewport, SVGConstants.SVG_WIDTH_ATTRIBUTE, ratio);
      SVGUtil.setAtt(viewport, SVGConstants.SVG_HEIGHT_ATTRIBUTE, "1");
      SVGUtil.setAtt(viewport, SVGConstants.SVG_VIEW_BOX_ATTRIBUTE, (-0.1 * StyleLibrary.SCALE) + " " + (-0.1 * StyleLibrary.SCALE) + " " + (ratio + 0.2) * StyleLibrary.SCALE + " " + 1.2 * StyleLibrary.SCALE);
    }

    /**
     * Process the given Database and distance function.
     *
     * @param db Database
     * @param distanceQuery Distance function
     */
    public void run(Database db, DistanceQuery<O, D> distanceQuery) {
      this.db = db;
      this.data = distanceQuery.getRelation();
      this.labelRep = DatabaseUtil.guessObjectLabelRepresentation(db);
      this.dim = DatabaseUtil.dimensionality(distanceQuery.getRelation());
      this.distanceQuery = distanceQuery;
      this.updateK(k);

      double min = Double.MAX_VALUE;
      double max = Double.MIN_VALUE;
      for(DBID objID : data.iterDBIDs()) {
        O vec = data.get(objID);
        DoubleMinMax mm = VectorUtil.getRangeDouble(vec);
        min = Math.min(min, mm.getMin());
        max = Math.max(max, mm.getMax());
      }
      this.s = new LinearScale(min, max);

      this.frame.setTitle(distanceQuery.getClass().getSimpleName() + " - " + WINDOW_TITLE_BASE);

      plot = new SVGPlot();
      viewport = plot.svgElement(SVGConstants.SVG_SVG_TAG);
      plot.getRoot().appendChild(viewport);
      updateSize();

      try {
        StyleLibrary style = new PropertiesBasedStyleLibrary();
        SVGSimpleLinearAxis.drawAxis(plot, viewport, this.s, 0.0, StyleLibrary.SCALE, 0.0, 0.0, true, false, style);
      }
      catch(CSSNamingConflict e) {
        LoggingUtil.exception(e);
      }
      plot.updateStyleElement();

      // insert the actual data series.
      Element egroup = plot.svgElement(SVGConstants.SVG_G_TAG);
      SVGUtil.setAtt(egroup, SVGConstants.SVG_ID_ATTRIBUTE, SERIESID);
      viewport.appendChild(egroup);
      plot.putIdElement(SERIESID, egroup);

      svgCanvas.setPlot(plot);

      DefaultListModel m = new DefaultListModel();
      for(DBID dbid : data.iterDBIDs()) {
        m.addElement(dbid);
      }
      seriesList.setModel(m);

      frame.setVisible(true);
    }

    void updateK(int k) {
      this.k = k;
      knnQuery = db.getKNNQuery(distanceQuery, k);
    }

    /**
     * Process the users new selection.
     */
    protected void updateSelection() {
      Object[] sel = seriesList.getSelectedValues();
      // prepare replacement tag.
      Element newe = plot.svgElement(SVGConstants.SVG_G_TAG);
      SVGUtil.setAtt(newe, SVGConstants.SVG_ID_ATTRIBUTE, SERIESID);

      distancecache.clear();

      for(Object o : sel) {
        DBID idx = (DBID) o;

        List<DistanceResultPair<D>> knn = knnQuery.getKNNForDBID(idx, k);

        double maxdist = knn.get(knn.size() - 1).getDistance().doubleValue();
        // avoid division by zero.
        if(maxdist == 0) {
          maxdist = 1;
        }

        for(ListIterator<DistanceResultPair<D>> iter = knn.listIterator(knn.size()); iter.hasPrevious();) {
          DistanceResultPair<D> pair = iter.previous();
          Element line = plotSeries(pair.getDBID(), MAXRESOLUTION);
          double dist = pair.getDistance().doubleValue() / maxdist;
          Color color = getColor(dist);
          String colstr = "#" + Integer.toHexString(color.getRGB()).substring(2);
          String width = (pair.getDBID() == idx) ? "0.5%" : "0.2%";
          SVGUtil.setStyle(line, "stroke: " + colstr + "; stroke-width: " + width + "; fill: none");
          newe.appendChild(line);
          // put into cache
          Double known = distancecache.get(pair.getDBID());
          if(known == null || dist < known) {
            distancecache.put(pair.getDBID(), dist);
          }
        }
      }
      plot.scheduleUpdate(new NodeReplacer(newe, plot, SERIESID));
      seriesList.repaint();
    }

    /**
     * Get the appropriate color for the given distance.
     *
     * @param dist Distance
     * @return Color
     */
    Color getColor(double dist) {
      Color color = new Color((int) (255 * dist), 0, (int) (255 * (1.0 - dist)));
      return color;
    }

    /**
     * Plot a single time series.
     *
     * @param idx Object index
     * @param resolution Maximum number of steps to plot
     * @return SVG element
     */
    private Element plotSeries(DBID idx, int resolution) {
      O series = data.get(idx);

      double step = 1.0;
      if(resolution < dim) {
        step = (double) dim / (double) resolution;
      }

      SVGPath path = new SVGPath();
      for(double id = 0; id < dim; id += step) {
        int i = (int) Math.floor(id);
        path.drawTo(StyleLibrary.SCALE * ratio * (((double) i) / (dim - 1)), StyleLibrary.SCALE * (1.0 - s.getScaled(series.doubleValue(i + 1))));
      }
      Element p = path.makeElement(plot);
      return p;
    }

    /**
     * Renderer for the labels, with coloring as in the plot.
     *
     * @author Erich Schubert
     *
     * @apiviz.exclude
     */
    private class SeriesLabelRenderer extends DefaultListCellRenderer {
      /**
       * Serial version
       */
      private static final long serialVersionUID = 1L;

      /**
       * Constructor.
       */
      public SeriesLabelRenderer() {
        super();
      }

      @Override
      public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
        String label = null;
        if(label == null || label == "") {
          label = labelRep.get((DBID) value);
        }
        if(label == null || label == "") {
          label = ((DBID) value).toString();
        }
        // setText(label);
        Component renderer = super.getListCellRendererComponent(list, label, index, isSelected, cellHasFocus);
        Double known = distancecache.get(value);
        if(known != null) {
          setBackground(getColor(known));
        }
        return renderer;
      }
    }
  }

  /**
   * Parameterization class.
   *
   * @author Erich Schubert
   *
   * @apiviz.exclude
   */
  public static class Parameterizer<O extends NumberVector<?, ?>, D extends NumberDistance<D, ?>> extends AbstractApplication.Parameterizer {
    protected Database database = null;

    protected DistanceFunction<O, D> distanceFunction = null;

    @Override
    protected void makeOptions(Parameterization config) {
      super.makeOptions(config);
      // parameter database connection
      final ObjectParameter<Database> dbpar = new ObjectParameter<Database>(OptionID.DATABASE_CONNECTION, Database.class, StaticArrayDatabase.class);
      if(config.grab(dbpar)) {
        database = dbpar.instantiateClass(config);
      }

      // Distance function
      final ClassParameter<DistanceFunction<O, D>> dfpar = new ClassParameter<DistanceFunction<O, D>>(DISTANCE_FUNCTION_ID, DistanceFunction.class, EuclideanDistanceFunction.class);
      if(config.grab(dfpar)) {
        distanceFunction = dfpar.instantiateClass(config);
      }
    }

    @Override
    protected KNNExplorer<O, D> makeInstance() {
      return new KNNExplorer<O, D>(verbose, database, distanceFunction);
    }
  }

  /**
   * Main method to run this application.
   *
   * @param args the arguments to run this application
   */
  public static void main(String[] args) {
    runCLIApplication(KNNExplorer.class, args);
  }
}
TOP

Related Classes of de.lmu.ifi.dbs.elki.application.visualization.KNNExplorer$Parameterizer

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.