summaryrefslogtreecommitdiff
path: root/src/de/lmu/ifi/dbs/elki/gui/minigui/MiniGUI.java
blob: 36010fc5c9042d4f986139451f35ec4de08b629e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
package de.lmu.ifi.dbs.elki.gui.minigui;

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

 Copyright (C) 2012
 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.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
import java.util.logging.Level;

import javax.swing.AbstractListModel;
import javax.swing.BoxLayout;
import javax.swing.ComboBoxModel;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingWorker;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;

import de.lmu.ifi.dbs.elki.KDDTask;
import de.lmu.ifi.dbs.elki.application.AbstractApplication;
import de.lmu.ifi.dbs.elki.gui.GUIUtil;
import de.lmu.ifi.dbs.elki.gui.util.DynamicParameters;
import de.lmu.ifi.dbs.elki.gui.util.LogPanel;
import de.lmu.ifi.dbs.elki.gui.util.ParameterTable;
import de.lmu.ifi.dbs.elki.gui.util.ParametersModel;
import de.lmu.ifi.dbs.elki.gui.util.SavedSettingsFile;
import de.lmu.ifi.dbs.elki.logging.Logging;
import de.lmu.ifi.dbs.elki.utilities.FormatUtil;
import de.lmu.ifi.dbs.elki.utilities.exceptions.UnableToComplyException;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.ParameterException;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.UnspecifiedParameterException;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.SerializedParameterization;
import de.lmu.ifi.dbs.elki.utilities.optionhandling.parameterization.TrackParameters;
import de.lmu.ifi.dbs.elki.workflow.LoggingStep;
import de.lmu.ifi.dbs.elki.workflow.OutputStep;

/**
 * Minimal GUI built around a table-based parameter editor.
 * 
 * @author Erich Schubert
 * 
 * @apiviz.composedOf SettingsComboboxModel
 * @apiviz.composedOf LoggingStep
 * @apiviz.owns ParameterTable
 * @apiviz.owns DynamicParameters
 */
public class MiniGUI extends AbstractApplication {
  /**
   * Filename for saved settings.
   */
  public static final String SAVED_SETTINGS_FILENAME = "MiniGUI-saved-settings.txt";

  /**
   * Newline used in output.
   */
  public static final String NEWLINE = System.getProperty("line.separator");

  /**
   * ELKI logger for the GUI.
   */
  private static final Logging LOG = Logging.getLogger(MiniGUI.class);

  /**
   * The frame
   */
  JFrame frame;

  /**
   * The main panel.
   */
  JPanel panel;

  /**
   * Logging output area.
   */
  protected LogPanel outputArea;

  /**
   * The parameter table.
   */
  protected ParameterTable parameterTable;

  /**
   * Parameter storage.
   */
  protected DynamicParameters parameters;

  /**
   * Settings storage.
   */
  protected SavedSettingsFile store = new SavedSettingsFile(SAVED_SETTINGS_FILENAME);

  /**
   * Combo box for saved settings.
   */
  protected JComboBox savedCombo;

  /**
   * Model to link the combobox with.
   */
  protected SettingsComboboxModel savedSettingsModel;

  /**
   * The "run" button.
   */
  protected JButton runButton;

  /**
   * Constructor.
   */
  public MiniGUI() {
    super();
    // Create and set up the window.
    frame = new JFrame("ELKI MiniGUI");
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    try {
      frame.setIconImage(new ImageIcon(KDDTask.class.getResource("elki-icon.png")).getImage());
    }
    catch(Exception e) {
      // Ignore - icon not found is not fatal.
    }

    panel = new JPanel();
    panel.setOpaque(true); // content panes must be opaque
    panel.setLayout(new GridBagLayout());

    {
      // Button panel
      JPanel buttonPanel = new JPanel();
      buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));

      // Combo box for saved settings
      savedSettingsModel = new SettingsComboboxModel(store);
      savedCombo = new JComboBox(savedSettingsModel);
      savedCombo.setEditable(true);
      savedCombo.setSelectedItem("[Saved Settings]");
      buttonPanel.add(savedCombo);

      // button to load settings
      JButton loadButton = new JButton("Load");
      loadButton.setMnemonic(KeyEvent.VK_L);
      loadButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          String key = savedSettingsModel.getSelectedItem();
          ArrayList<String> settings = store.get(key);
          if(settings != null) {
            outputArea.clear();
            outputArea.publish("Parameters: " + FormatUtil.format(settings, " ") + NEWLINE, Level.INFO);
            doSetParameters(settings);
          }
        }
      });
      buttonPanel.add(loadButton);
      // button to save settings
      JButton saveButton = new JButton("Save");
      saveButton.setMnemonic(KeyEvent.VK_S);
      saveButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          String key = savedSettingsModel.getSelectedItem();
          // Stop editing the table.
          parameterTable.editCellAt(-1, -1);
          store.put(key, parameters.serializeParameters());
          try {
            store.save();
          }
          catch(IOException e1) {
            LOG.exception(e1);
          }
          savedSettingsModel.update();
        }
      });
      buttonPanel.add(saveButton);
      // button to remove saved settings
      JButton removeButton = new JButton("Remove");
      removeButton.setMnemonic(KeyEvent.VK_E);
      removeButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          String key = savedSettingsModel.getSelectedItem();
          store.remove(key);
          try {
            store.save();
          }
          catch(IOException e1) {
            LOG.exception(e1);
          }
          savedCombo.setSelectedItem("[Saved Settings]");
          savedSettingsModel.update();
        }
      });
      buttonPanel.add(removeButton);

      // button to launch the task
      runButton = new JButton("Run Task");
      runButton.setMnemonic(KeyEvent.VK_R);
      runButton.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
          startTask();
        }
      });
      buttonPanel.add(runButton);

      GridBagConstraints constraints = new GridBagConstraints();
      constraints.fill = GridBagConstraints.HORIZONTAL;
      constraints.gridx = 0;
      constraints.gridy = 1;
      constraints.weightx = 1.0;
      constraints.weighty = 0.01;
      panel.add(buttonPanel, constraints);
    }

    {
      // Setup parameter storage and table model
      this.parameters = new DynamicParameters();
      ParametersModel parameterModel = new ParametersModel(parameters);
      parameterModel.addTableModelListener(new TableModelListener() {
        @Override
        public void tableChanged(TableModelEvent e) {
          // logger.debug("Change event.");
          updateParameterTable();
        }
      });

      // Create parameter table
      parameterTable = new ParameterTable(parameterModel, parameters);
      // Create the scroll pane and add the table to it.
      JScrollPane scrollPane = new JScrollPane(parameterTable);

      // Add the scroll pane to this panel.
      GridBagConstraints constraints = new GridBagConstraints();
      constraints.fill = GridBagConstraints.BOTH;
      constraints.gridx = 0;
      constraints.gridy = 0;
      constraints.weightx = 1;
      constraints.weighty = 1;
      panel.add(scrollPane, constraints);
    }

    {
      // setup text output area
      outputArea = new LogPanel();

      // Create the scroll pane and add the table to it.
      JScrollPane outputPane = new JScrollPane(outputArea);
      outputPane.setPreferredSize(new Dimension(800, 400));

      // Add the output pane to the bottom
      GridBagConstraints constraints = new GridBagConstraints();
      constraints.fill = GridBagConstraints.BOTH;
      constraints.gridx = 0;
      constraints.gridy = 2;
      constraints.weightx = 1;
      constraints.weighty = 1;
      panel.add(outputPane, constraints);
    }

    // load saved settings (we wanted to have the logger first!)
    try {
      store.load();
      savedSettingsModel.update();
    }
    catch(FileNotFoundException e) {
      // Ignore - probably didn't save any settings yet.
    }
    catch(IOException e) {
      LOG.exception(e);
    }

    // Finalize the frame.
    frame.setContentPane(panel);
    frame.pack();
  }

  /**
   * Serialize the parameter table and run setParameters().
   */
  protected void updateParameterTable() {
    parameterTable.setEnabled(false);
    ArrayList<String> params = parameters.serializeParameters();
    outputArea.clear();
    outputArea.publish("Parameters: " + FormatUtil.format(params, " ") + NEWLINE, Level.INFO);
    doSetParameters(params);
    parameterTable.setEnabled(true);
  }

  /**
   * Do the actual setParameters invocation.
   * 
   * @param params Parameters
   */
  protected void doSetParameters(List<String> params) {
    SerializedParameterization config = new SerializedParameterization(params);
    TrackParameters track = new TrackParameters(config);
    track.tryInstantiate(LoggingStep.class);
    track.tryInstantiate(KDDTask.class);
    config.logUnusedParameters();
    // config.logAndClearReportedErrors();
    final boolean hasErrors = (config.getErrors().size() > 0);
    if(hasErrors && params.size() > 0) {
      reportErrors(config);
    }
    runButton.setEnabled(!hasErrors);

    List<String> remainingParameters = config.getRemainingParameters();

    // update table:
    parameterTable.setEnabled(false);
    parameters.updateFromTrackParameters(track);
    // Add remaining parameters
    if(remainingParameters != null && !remainingParameters.isEmpty()) {
      DynamicParameters.RemainingOptions remo = new DynamicParameters.RemainingOptions();
      try {
        remo.setValue(FormatUtil.format(remainingParameters, " "));
      }
      catch(ParameterException e) {
        LOG.exception(e);
      }
      BitSet bits = new BitSet();
      bits.set(DynamicParameters.BIT_INVALID);
      bits.set(DynamicParameters.BIT_SYNTAX_ERROR);
      parameters.addParameter(remo, remo.getValue(), bits, 0);
    }

    config.clearErrors();
    parameterTable.revalidate();
    parameterTable.setEnabled(true);
  }

  /**
   * Do a full run of the KDDTask with the specified parameters.
   */
  protected void startTask() {
    parameterTable.editCellAt(-1, -1);
    parameterTable.setEnabled(false);
    final ArrayList<String> params = parameters.serializeParameters();
    parameterTable.setEnabled(true);

    runButton.setEnabled(false);

    outputArea.clear();
    outputArea.publish("Running: " + FormatUtil.format(params, " ") + NEWLINE, Level.INFO);

    SwingWorker<Void, Void> r = new SwingWorker<Void, Void>() {
      @Override
      public Void doInBackground() {
        SerializedParameterization config = new SerializedParameterization(params);
        config.tryInstantiate(LoggingStep.class);
        KDDTask task = config.tryInstantiate(KDDTask.class);
        try {
          config.logUnusedParameters();
          if(config.getErrors().size() == 0) {
            task.run();
          }
          else {
            reportErrors(config);
          }
          LOG.debug("Task completed successfully.");
        }
        catch(Throwable e) {
          LOG.exception("Task failed", e);
        }
        return null;
      }

      @Override
      protected void done() {
        super.done();
        runButton.setEnabled(true);
      }
    };
    r.execute();
  }

  /**
   * Report errors in a single error log record.
   * 
   * @param config Parameterization
   */
  protected void reportErrors(SerializedParameterization config) {
    StringBuilder buf = new StringBuilder();
    buf.append("Task is not completely configured:" + NEWLINE + NEWLINE);
    for(ParameterException e : config.getErrors()) {
      if(e instanceof UnspecifiedParameterException) {
        buf.append("The parameter ");
        buf.append(((UnspecifiedParameterException) e).getParameterName());
        buf.append(" is required.").append(NEWLINE);
      }
      else {
        buf.append(e.getMessage() + NEWLINE);
      }
    }
    LOG.warning(buf.toString());
    config.clearErrors();
  }

  @Override
  public void run() throws UnableToComplyException {
    frame.setVisible(true);
    outputArea.becomeDefaultLogger();
  }

  /**
   * Main method that just spawns the UI.
   * 
   * @param args command line parameters
   */
  public static void main(final String[] args) {
    GUIUtil.logUncaughtExceptions(LOG);
    GUIUtil.setLookAndFeel();
    OutputStep.setDefaultHandlerVisualizer();

    javax.swing.SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        try {
          final MiniGUI gui = new MiniGUI();
          gui.run();
          if(args != null && args.length > 0) {
            gui.doSetParameters(Arrays.asList(args));
          }
          else {
            gui.doSetParameters(new ArrayList<String>());
          }
        }
        catch(UnableToComplyException e) {
          LOG.exception(e);
        }
      }
    });
  }

  /**
   * Class to interface between the saved settings list and a JComboBox.
   * 
   * @author Erich Schubert
   * 
   * @apiviz.composedOf de.lmu.ifi.dbs.elki.gui.util.SavedSettingsFile
   */
  class SettingsComboboxModel extends AbstractListModel implements ComboBoxModel {
    /**
     * Serial version.
     */
    private static final long serialVersionUID = 1L;

    /**
     * Settings storage.
     */
    protected SavedSettingsFile store;

    /**
     * Selected entry.
     */
    protected String selected = null;

    /**
     * Constructor.
     * 
     * @param store Store to access
     */
    public SettingsComboboxModel(SavedSettingsFile store) {
      super();
      this.store = store;
    }

    @Override
    public String getSelectedItem() {
      return selected;
    }

    @Override
    public void setSelectedItem(Object anItem) {
      if(anItem instanceof String) {
        selected = (String) anItem;
      }
    }

    @Override
    public Object getElementAt(int index) {
      return store.getElementAt(store.size() - 1 - index).first;
    }

    @Override
    public int getSize() {
      return store.size();
    }

    /**
     * Force an update.
     */
    public void update() {
      fireContentsChanged(this, 0, getSize() + 1);
    }
  }
}