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}