001/* Extension of plot that allows interactive modification of plot data.
002
003 Copyright (c) 1998-2014 The Regents of the University of California.
004 All rights reserved.
005 Permission is hereby granted, without written agreement and without
006 license or royalty fees, to use, copy, modify, and distribute this
007 software and its documentation for any purpose, provided that the above
008 copyright notice and the following two paragraphs appear in all copies
009 of this software.
010
011 IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY
012 FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
013 ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
014 THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
015 SUCH DAMAGE.
016
017 THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
018 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
019 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
020 PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
021 CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES,
022 ENHANCEMENTS, OR MODIFICATIONS.
023
024 PT_COPYRIGHT_VERSION_2
025 COPYRIGHTENDKEY
026
027 */
028package ptolemy.plot;
029
030import java.awt.Color;
031import java.awt.Graphics;
032import java.awt.event.InputEvent;
033import java.awt.event.KeyEvent;
034import java.awt.event.KeyListener;
035import java.awt.event.MouseEvent;
036import java.awt.event.MouseListener;
037import java.awt.event.MouseMotionListener;
038import java.util.ArrayList;
039import java.util.Enumeration;
040import java.util.Stack;
041import java.util.Vector;
042
043///////////////////////////////////////////////////////////////////
044//// EditablePlot
045
046/**
047 This extension of Plot permits interactive modification of plotted
048 data, one dataset at a time.  By default, you can modify dataset
049 number zero (the first one given).  To change this default, call
050 setEditable().  To edit a plot, use the right mouse button.
051 Click and drag to the left to trace out new values for the data.
052 To read back the modified data, use getData().  To undo a change to
053 the data, type Control-Z.  To redo the change, type Control-Y.
054 The undo history is infinite.
055 <p>
056 The style of editing is very particular.  This class assumes the data
057 specify a function of <i>x</i>.  I.e., there there is exactly one
058 <i>y</i> value for every <i>x</i> value.  Thus, with the right mouse
059 button, you are allowed to trace out new <i>y</i> values
060 starting with some leftmost <i>x</i> value.  You can only trace
061 values to the right.  This feature makes it easy to trace values
062 with discontinuities.  Just start at the left, and drag to the right
063 to the point of the discontinuity, then drag to the left,
064 then right again.  You will have to try it...
065 Notice that this style of editing probably does not make sense with
066 error bars, since there is no mechanism for editing the error bars.
067 <p>
068 To be able to modify the data in a dataset, of course, there must
069 be data in the dataset.  Thus, you should create a dataset (for
070 example by calling addPoint()) before editing it.  Only the visible
071 part of the dataset can be edited (that is, the portion of the dataset
072 along the visible part of the horizontal axis).  If you zoom in, then,
073 you can edit particular points more precisely.
074 <p>
075 To be notified when the user sketches a new signal, create an
076 object that implements the EditListener interface and add that
077 listener using addEditListener().
078
079 @author Edward A. Lee
080 @version $Id$
081 @since Ptolemy II 0.4
082 @Pt.ProposedRating Red (cxh)
083 @Pt.AcceptedRating Red (cxh)
084 */
085@SuppressWarnings("serial")
086public class EditablePlot extends Plot {
087    /** Constructor.
088     */
089    public EditablePlot() {
090        super();
091        addMouseListener(new EditMouseListener());
092        addMouseMotionListener(new ModifyListener());
093        addKeyListener(new UndoListener());
094    }
095
096    ///////////////////////////////////////////////////////////////////
097    ////                         public methods                    ////
098
099    /** Add a listener to be informed when the user modifies a data set.
100     *  @param listener The listener.
101     *  @see EditListener
102     *  @see #removeEditListener(EditListener)
103     */
104    public void addEditListener(EditListener listener) {
105        if (_editListeners == null) {
106            _editListeners = new Vector();
107        } else {
108            if (_editListeners.contains(listener)) {
109                return;
110            }
111        }
112
113        _editListeners.addElement(listener);
114    }
115
116    /** Get the data in the specified dataset. This is returned as
117     *  a two-dimensional array, where the first index specifies
118     *  X or Y data (index 0 or 1 respectively), and the second
119     *  index specifies the point.
120     *  @param dataset The dataset.
121     *  @return The data in the specified dataset.
122     */
123    public double[][] getData(int dataset) {
124        _checkDatasetIndex(dataset);
125
126        ArrayList<PlotPoint> pts = _points.get(dataset);
127        int size = pts.size();
128        double[][] result = new double[2][size];
129
130        for (int i = 0; i < size; i++) {
131            PlotPoint pt = pts.get(i);
132            result[0][i] = pt.x;
133            result[1][i] = pt.y;
134        }
135
136        return result;
137    }
138
139    /** Redo the latest signal editing operation that was undone by
140     *  calling undo(), if there was one.  Otherwise, do nothing.
141     */
142    public void redo() {
143        if (_redoStack.empty()) {
144            return;
145        }
146
147        Object[] save = new Object[2];
148        save[0] = Integer.valueOf(_dataset);
149        save[1] = getData(_dataset);
150        _undoStack.push(save);
151
152        Object[] saved = (Object[]) _redoStack.pop();
153        _setData(((Integer) saved[0]).intValue(), (double[][]) saved[1]);
154
155        // Ensure replot of offscreen buffer.
156        _plotImage = null;
157        repaint();
158        _notifyListeners(_dataset);
159    }
160
161    /** Unregister a edit listener.  If the specified listener has not
162     *  been previously registered, then do nothing.
163     *  @param listener The listener to remove from the list of listeners
164     *   to which edit events are sent.
165     *  @see #addEditListener(EditListener)
166     */
167    public void removeEditListener(EditListener listener) {
168        if (_editListeners == null) {
169            return;
170        }
171
172        _editListeners.removeElement(listener);
173    }
174
175    /** Specify which dataset is editable. By default, if this method is
176     *  not called, dataset number zero is editable.  If you call this
177     *  method with a negative number, then no dataset will be editable.
178     *  @param dataset The editable dataset.
179     */
180    public void setEditable(int dataset) {
181        if (dataset >= 0) {
182            _checkDatasetIndex(dataset);
183        }
184
185        _dataset = dataset;
186    }
187
188    /** Undo the latest signal editing operation, if there was one.
189     *  Otherwise, do nothing.
190     */
191    public void undo() {
192        if (_undoStack.empty()) {
193            return;
194        }
195
196        Object[] save = new Object[2];
197        save[0] = Integer.valueOf(_dataset);
198        save[1] = getData(_dataset);
199        _redoStack.push(save);
200
201        Object[] saved = (Object[]) _undoStack.pop();
202        _setData(((Integer) saved[0]).intValue(), (double[][]) saved[1]);
203
204        // Ensure replot of offscreen buffer.
205        _plotImage = null;
206        repaint();
207        _notifyListeners(_dataset);
208    }
209
210    ///////////////////////////////////////////////////////////////////
211    ////                         private methods                   ////
212    // Clear the editing spec and modify the dataset.
213    private synchronized void _edit(int x, int y) {
214        if (_dataset < 0) {
215            return;
216        }
217
218        // Save for undo.
219        Object[] save = new Object[2];
220        save[0] = Integer.valueOf(_dataset);
221        save[1] = getData(_dataset);
222
223        // FIXME: Need a way to notify menus to enable items...
224        _undoStack.push(save);
225
226        // NOTE: the clear() method was added in jdk 1.2, so we don't
227        // use it here for maximal compatibility...
228        // _redoStack.clear();
229        while (!_redoStack.empty()) {
230            _redoStack.pop();
231        }
232
233        // constrain to be in range
234        if (y > _lry) {
235            y = _lry;
236        }
237
238        if (y < _uly) {
239            y = _uly;
240        }
241
242        if (x > _lrx) {
243            x = _lrx;
244        }
245
246        if (x < _ulx) {
247            x = _ulx;
248        }
249
250        _editPoint(x, y);
251
252        // Edit the points in the signal.
253        ArrayList<PlotPoint> pts = _points.get(_dataset);
254
255        for (int i = 0; i < pts.size(); i++) {
256            PlotPoint pt = pts.get(i);
257
258            // Only bother with points in visual range
259            if (pt.x >= _xMin && pt.x <= _xMax) {
260                int index = (int) ((pt.x - _xMin) * _xscale)
261                        - (_lrx - _ulx - _editSpecX.length);
262
263                if (index >= 0 && index < _editSpecX.length) {
264                    if (_editSpecSet[index]) {
265                        pt.y = _yMax - (_editSpecY[index] - _uly) / _yscale;
266
267                        // For auto-ranging, keep track of min and max.
268                        if (pt.y < _yBottom) {
269                            _yBottom = pt.y;
270                        }
271
272                        if (pt.y > _yTop) {
273                            _yTop = pt.y;
274                        }
275                    }
276                }
277            }
278        }
279
280        // Ensure replot of offscreen buffer.
281        _plotImage = null;
282        repaint();
283
284        // Erase the guide
285        // I don't think we need to do this, since we call repaint().
286        //         graphics.setXORMode(_editColor);
287        //         for (int i = 0; i < _editSpecX.length; i++) {
288        //             if (_editSpecSet[i]) {
289        //                 graphics.drawLine(_editSpecX[i], _editSpecY[i]-1,
290        //                         _editSpecX[i], _editSpecY[i]+1);
291        //             }
292        //         }
293        //         graphics.setPaintMode();
294        _notifyListeners(_dataset);
295    }
296
297    // Make a record of a new edit point.
298    private synchronized void _editPoint(int x, int y) {
299        if (_dataset < 0) {
300            return;
301        }
302
303        Graphics graphics = getGraphics();
304
305        // constrain to be in range
306        if (y > _lry) {
307            y = _lry;
308        }
309
310        if (y < _uly) {
311            y = _uly;
312        }
313
314        if (x > _lrx) {
315            x = _lrx;
316        }
317
318        if (x < _ulx) {
319            x = _ulx;
320        }
321
322        if (x <= _currentEditX || x >= _lrx) {
323            // ignore
324            return;
325        }
326
327        int step = _currentEditX;
328
329        while (step <= x) {
330            int index = step - (_lrx - _editSpecX.length);
331            double proportion = (step - _currentEditX)
332                    / (double) (x - _currentEditX);
333            int newY = (int) (_currentEditY + proportion * (y - _currentEditY));
334
335            if (!_editSpecSet[index]) {
336                _editSpecX[index] = step;
337                _editSpecY[index] = newY;
338                _editSpecSet[index] = true;
339
340                // Draw point, linearly interpolated from previous point
341                graphics.setXORMode(_editColor);
342                graphics.drawLine(step, newY - 1, step, newY + 1);
343                graphics.setPaintMode();
344            }
345
346            step++;
347        }
348
349        _currentEditX = x;
350        _currentEditY = y;
351    }
352
353    // Make a record of the starting x and y position of an edit.
354    private synchronized void _editStart(int x, int y) {
355        if (_dataset < 0) {
356            return;
357        }
358
359        // constrain to be in range
360        if (y > _lry) {
361            y = _lry;
362        }
363
364        if (y < _uly) {
365            y = _uly;
366        }
367
368        if (x > _lrx) {
369            x = _lrx;
370        }
371
372        if (x < _ulx) {
373            x = _ulx;
374        }
375
376        // Allocate a vector to store the points.
377        int size = _lrx - x + 1;
378        _editSpecX = new int[size];
379        _editSpecY = new int[size];
380        _editSpecSet = new boolean[size];
381
382        _editSpecX[0] = x;
383        _editSpecY[0] = y;
384        _editSpecSet[0] = true;
385
386        _currentEditX = x;
387        _currentEditY = y;
388
389        Graphics graphics = getGraphics();
390
391        // Draw point (as a 3 pixel vertical line, for thickness)
392        graphics.setXORMode(_editColor);
393        graphics.drawLine(x, y - 1, x, y + 1);
394        graphics.setPaintMode();
395    }
396
397    // Notify all edit listeners that have registered.
398    private void _notifyListeners(int dataset) {
399        if (_editListeners == null) {
400            return;
401        } else {
402            Enumeration listeners = _editListeners.elements();
403
404            while (listeners.hasMoreElements()) {
405                ((EditListener) listeners.nextElement()).editDataModified(this,
406                        dataset);
407            }
408        }
409    }
410
411    // Set the data in the specified dataset. The argument is of the
412    // form returned by getData.
413    private void _setData(int dataset, double[][] data) {
414        _checkDatasetIndex(dataset);
415
416        ArrayList<PlotPoint> pts = _points.get(dataset);
417        int size = pts.size();
418
419        if (data[0].length < size) {
420            size = data[0].length;
421        }
422
423        for (int i = 0; i < size; i++) {
424            PlotPoint pt = pts.get(i);
425            pt.x = data[0][i];
426            pt.y = data[1][i];
427        }
428    }
429
430    ///////////////////////////////////////////////////////////////////
431    ////                         private variables                 ////
432    private int[] _editSpecX;
433
434    ///////////////////////////////////////////////////////////////////
435    ////                         private variables                 ////
436    private int[] _editSpecY;
437
438    private boolean[] _editSpecSet;
439
440    private int _currentEditX;
441
442    private int _currentEditY;
443
444    private int _dataset = 0;
445
446    // Call setXORMode with a hardwired color because
447    // _background does not work in an application,
448    // and _foreground does not work in an applet
449    private static final Color _editColor = Color.white;
450
451    // Stack for undo.
452    private Stack _undoStack = new Stack();
453
454    private Stack _redoStack = new Stack();
455
456    // Edit listeners.
457    private Vector _editListeners = null;
458
459    ///////////////////////////////////////////////////////////////////
460    ////                         inner classes                     ////
461
462    /** Listen for mouse button events.  See the class comment for
463     * details.
464     */
465    public class EditMouseListener implements MouseListener {
466        /** Ignored by this listener.
467         *  @param event Ignored.
468         */
469        @Override
470        public void mouseClicked(MouseEvent event) {
471        }
472
473        /** Ignored by this listener.
474         *  @param event Ignored.
475         */
476        @Override
477        public void mouseEntered(MouseEvent event) {
478        }
479
480        /** Ignored by this listener.
481         *  @param event Ignored.
482         */
483        @Override
484        public void mouseExited(MouseEvent event) {
485        }
486
487        /** If the 3rd button is pressed, then start the edit.
488         *  @param event The event.
489         */
490        @Override
491        public void mousePressed(MouseEvent event) {
492            if ((event.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
493                EditablePlot.this._editStart(event.getX(), event.getY());
494            }
495        }
496
497        /** If the 3rd button is released, then modify the X and Y
498         *  coordinates of the edit dataset.
499         *  @param event The event.
500         */
501        @Override
502        public void mouseReleased(MouseEvent event) {
503            if ((event.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
504                EditablePlot.this._edit(event.getX(), event.getY());
505            }
506        }
507    }
508
509    /** Listen for mouse motion events.  See the class comment for
510     * details.
511     */
512    public class ModifyListener implements MouseMotionListener {
513        /** If the mouse is dragged and the 3rd button is pressed,
514         *  then make a record of a new edit point.
515         *  @param event The event.
516         */
517        @Override
518        public void mouseDragged(MouseEvent event) {
519            if ((event.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
520                EditablePlot.this._editPoint(event.getX(), event.getY());
521            }
522        }
523
524        /** Ignored by this listener.
525         *  @param event Ignored.
526         */
527        @Override
528        public void mouseMoved(MouseEvent event) {
529        }
530    }
531
532    /** Control-Z is undo and Control-Y is redo.
533     */
534    public class UndoListener implements KeyListener {
535        /** Handle Control, Z or Y being pressed with
536         *  by calling undo() or redo().
537         *  @param e The KeyEvent.
538         */
539        @Override
540        public void keyPressed(KeyEvent e) {
541            int keycode = e.getKeyCode();
542
543            switch (keycode) {
544            case KeyEvent.VK_CONTROL:
545                _control = true;
546                break;
547
548            case KeyEvent.VK_Z:
549
550                if (_control) {
551                    undo();
552                }
553
554                break;
555
556            case KeyEvent.VK_Y:
557
558                if (_control) {
559                    redo();
560                }
561
562                break;
563
564            default:
565                // None
566            }
567        }
568
569        /** Handle Control being released.
570         *  @param e The KeyEvent.
571         */
572        @Override
573        public void keyReleased(KeyEvent e) {
574            int keycode = e.getKeyCode();
575
576            switch (keycode) {
577            case KeyEvent.VK_CONTROL:
578                _control = false;
579                break;
580
581            default:
582                // None
583            }
584        }
585
586        /** Ignored by this class.
587         *  The keyTyped method is broken in jdk 1.1.4.
588         *  It always gets "unknown key code".
589         * @param e Ignored by this method.
590         */
591        @Override
592        public void keyTyped(KeyEvent e) {
593        }
594
595        /** True of the Control key was pressed, but not yet
596         * released.
597         */
598        private boolean _control = false;
599    }
600}