View Javadoc

1   /* OpenLogViewer
2    *
3    * Copyright 2011
4    *
5    * This file is part of the OpenLogViewer project.
6    *
7    * OpenLogViewer software is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation, either version 3 of the License, or
10   * (at your option) any later version.
11   *
12   * OpenLogViewer software is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with any OpenLogViewer software.  If not, see http://www.gnu.org/licenses/
19   *
20   * I ask that if you make any changes to this file you fork the code on github.com!
21   *
22   */
23  package org.diyefi.openlogviewer.graphing;
24  
25  import java.awt.Dimension;
26  import java.awt.event.ActionEvent;
27  import java.awt.event.ActionListener;
28  import java.awt.event.InputEvent;
29  import java.awt.event.KeyEvent;
30  import java.awt.event.KeyListener;
31  import java.awt.event.MouseEvent;
32  import java.awt.event.MouseListener;
33  import java.awt.event.MouseMotionListener;
34  import java.awt.event.MouseWheelEvent;
35  import java.awt.event.MouseWheelListener;
36  
37  import javax.swing.JPanel;
38  import javax.swing.Timer;
39  
40  import org.diyefi.openlogviewer.OpenLogViewer;
41  import org.diyefi.openlogviewer.genericlog.GenericLog;
42  
43  public class EntireGraphingPanel extends JPanel implements ActionListener, MouseMotionListener, MouseListener, MouseWheelListener, KeyListener {
44  	private static final long serialVersionUID = 1L;
45  
46  	private MultiGraphLayeredPane multiGraph;
47  	private GraphPositionPanel graphPositionPanel;
48  	private double graphPosition;
49  	private int graphSize;
50  	private boolean playing;
51  	private boolean wasPlaying;
52  	private Timer playTimer;
53  	private Timer flingTimer;
54  	private boolean dragging;
55  	private boolean flinging;
56  	private long thePast;
57  	private int prevDragXCoord;
58  	private int flingInertia;
59  	private int zoom;
60  	private boolean zoomedOutBeyondOneToOne;
61  
62  	public EntireGraphingPanel() {
63  		super();
64  		init();
65  	}
66  
67  	private void init() {
68  		this.setName("Graphing Panel");
69  		this.setLayout(new java.awt.BorderLayout());
70  		multiGraph = new MultiGraphLayeredPane();
71  		multiGraph.setPreferredSize(new Dimension(600, 400));
72  		this.add(multiGraph, java.awt.BorderLayout.CENTER);
73  		graphPositionPanel = new GraphPositionPanel();
74  		graphPositionPanel.setPreferredSize(new Dimension(600, 20));
75  		this.add(graphPositionPanel, java.awt.BorderLayout.SOUTH);
76  		zoom = 1;
77  		zoomedOutBeyondOneToOne = false;
78  		resetGraphPosition();
79  		setGraphSize(0);
80  		playing = false;
81  		wasPlaying = false;
82  		playTimer = new Timer(10, this);
83  		playTimer.setInitialDelay(0);
84  		flingTimer = new Timer(10, this);
85  		flingTimer.setInitialDelay(0);
86  		addMouseListener(this);
87  		addMouseMotionListener(this);
88  		addMouseWheelListener(this);
89  		addMouseListener(multiGraph.getInfoPanel());
90  		addMouseMotionListener(multiGraph.getInfoPanel());
91  		stopDragging();
92  		stopFlinging();
93  		thePast = System.currentTimeMillis();
94  	}
95  
96  	public final void actionPerformed(final ActionEvent e) {
97  		if (playing && graphPosition < getGraphPositionMax()) {
98  			if(zoomedOutBeyondOneToOne){
99  				moveGraphPosition(zoom);
100 			} else {
101 				moveGraphPosition(1);
102 			}
103 		} else if ((flinging && graphPosition < getGraphPositionMax()) && (graphPosition > getGraphPositionMin())) {
104 			if (flingInertia == 0) {
105 				stopFlinging();
106 			} else {
107 				moveEntireGraphingPanel(flingInertia);
108 				if (flingInertia > 0) {
109 					flingInertia--;
110 				} else {
111 					flingInertia++;
112 				}
113 			}
114 		}
115 	}
116 
117 	public final MultiGraphLayeredPane getMultiGraphLayeredPane() {
118 		return multiGraph;
119 	}
120 
121 	public final GraphPositionPanel getGraphPositionPanel() {
122 		return graphPositionPanel;
123 	}
124 
125 	public final void setLog(final GenericLog genLog) {
126 		playing = false;
127 		resetGraphPosition();
128 		multiGraph.setLog(genLog);
129 		graphPositionPanel.setLog(genLog);
130 	}
131 
132 
133 	/**
134 	 * The tightest the user should be allowed to zoom in.
135 	 */
136 	private final int getTightestZoom(){
137 		return this.getWidth() - 1;
138 	}
139 
140 	/**
141 	 * The widest the user should be allowed to zoom out.
142 	 */
143 	private final int getWidestZoom(){
144 		return Integer.MAX_VALUE;
145 	}
146 
147 	/**
148 	 * Zoom in by one. This control zooms finer than the coarse zoom control.
149 	 * This assumes you are zooming in on the data centered in the screen.
150 	 * If you need to zoom in on a different location then you must move
151 	 * the graph accordingly.
152 	 */
153 	public final void zoomIn() {
154 		final double graphWidth = this.getWidth();
155 		double move = 0;
156 
157 		if(zoomedOutBeyondOneToOne) {
158 			if (zoom == 2) {
159 				zoomedOutBeyondOneToOne = false;
160 			}
161 			zoom--;
162 			move = graphWidth / (double)(zoom * 2);
163 		} else if (zoom < getTightestZoom()) {
164 			move = graphWidth / (double)(zoom * 2);
165 			zoom++;
166 		}
167 
168 		moveEntireGraphingPanel(move);
169 	}
170 
171 	/**
172 	 * Zoom in using steps larger the further away from 1:1 you are.
173 	 * This assumes you are zooming in on the data centered in the screen.
174 	 * If you need to zoom in on a different location then you must use
175 	 * zoomIn() repeatedly coupled with a move each time.
176 	 */
177 	public final void zoomInCoarse(){
178 		final int zoomAmount = (int)Math.sqrt(zoom);
179 		for (int i = 0; i < zoomAmount; i++){
180 			zoomIn();
181 		}
182 	}
183 
184 	/**
185 	 * Zoom out by one. This control zooms finer than the coarse zoom control.
186 	 * This assumes you are zooming out from the data centered in the screen.
187 	 * If you need to zoom out from a different location then you must move
188 	 * the graph accordingly.
189 	 */
190 	public final void zoomOut() {
191 		final double graphWidth = this.getWidth();
192 		double move = 0;
193 
194 		if (!zoomedOutBeyondOneToOne) {
195 			if (zoom == 1) {
196 				zoomedOutBeyondOneToOne = true;
197 				zoom = 2;
198 				move = graphWidth / (double)(zoom * 2);
199 			} else {
200 				move = graphWidth / (double)(zoom * 2);
201 				zoom--;
202 			}
203 		} else if (zoom < getWidestZoom()){
204 			zoom++;
205 			move = graphWidth / (double)(zoom * 2);
206 		}
207 
208 		moveEntireGraphingPanel(-move);
209 	}
210 
211 	/**
212 	 * Zoom out using steps larger the further away from 1:1 you are.
213 	 * This assumes you are zooming out with the data centered in the screen.
214 	 * If you need to zoom out on a different location then you must use
215 	 * zoomOut() repeatedly coupled with a move each time.
216 	 */
217 	public final void zoomOutCoarse(){
218 		final int zoomAmount = (int)Math.sqrt(zoom);
219 		for (int i = 0; i < zoomAmount; i++){
220 			zoomOut();
221 		}
222 	}
223 
224 	/**
225 	 * Zoom the graph so that if it is centered, then the
226 	 * entire graph will fit within the display. Usually
227 	 * this will result in ultimately zooming out, but if the
228 	 * graph is small enough and/or the display is large enough
229 	 * then zooming in will be more appropriate.
230 	 *
231 	 * If the graph will fit perfectly inside the display
232 	 * then it will be sized down one more time so that
233 	 * there is always at least 4 pixels of blank space to
234 	 * the left and right of the graph so the user will
235 	 * know they are seeing the entire graph trace.
236 	 */
237 	private final void zoomGraphToFit(final int dataPointsToFit){
238 		final int graphWindowWidth = this.getWidth() - 8; //Remove 4 pixels per side.
239 		int dataPointsThatFitInDisplay = 0;
240 		if (zoomedOutBeyondOneToOne){
241 			dataPointsThatFitInDisplay = graphWindowWidth * zoom;
242 		} else {
243 			dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
244 		}
245 
246 		// Zoom in until the data no longer fits in the display.
247 		while (dataPointsToFit < dataPointsThatFitInDisplay && zoom != getTightestZoom()){
248 			zoomIn();
249 			if (zoomedOutBeyondOneToOne){
250 				dataPointsThatFitInDisplay = graphWindowWidth * zoom;
251 			} else {
252 				dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
253 			}
254 		}
255 
256 		// Zoom out one or more times until the data just fits in the display.
257 		while (dataPointsToFit > dataPointsThatFitInDisplay && zoom != getWidestZoom()){
258 			zoomOut();
259 			if (zoomedOutBeyondOneToOne){
260 				dataPointsThatFitInDisplay = graphWindowWidth * zoom;
261 			} else {
262 				dataPointsThatFitInDisplay =  graphWindowWidth / zoom;
263 			}
264 		}
265 	}
266 
267 	public boolean isZoomedOutBeyondOneToOne(){
268 		return zoomedOutBeyondOneToOne;
269 	}
270 
271 	public final void play() {
272 		if (playing) {
273 			pause();
274 		} else {
275 			playing = true;
276 			stopDragging();
277 			stopFlinging();
278 			playTimer.start();
279 		}
280 	}
281 
282 	public final void pause() {
283 		playing = false;
284 		playTimer.stop();
285 		stopDragging();
286 		stopFlinging();
287 	}
288 
289 	/**
290 	 * Increases the speed of the graph by 1 ms until 0, at which speed cannot be advanced any further and will essentially update as fast as possible.
291 	 */
292 	public final void fastForward() {
293 		final int currentDelay = playTimer.getDelay();
294 		if (currentDelay > 0) {
295 			playTimer.setDelay(currentDelay - 1);
296 		}
297 	}
298 
299 	public final void eject() {
300 		resetGraphPosition();
301 	}
302 
303 	public final void stop() {
304 		playing = false;
305 		wasPlaying = false;
306 		playTimer.stop();
307 		resetGraphPosition();
308 	}
309 
310 	/**
311 	 * Slows the speed of playback by 1 ms
312 	 */
313 	public final void slowDown() {
314 		final int currentDelay = playTimer.getDelay();
315 		playTimer.setDelay(currentDelay + 1);
316 	}
317 
318 	public final void fling() {
319 		flinging = true;
320 		flingTimer.start();
321 	}
322 
323 	private final double getGraphPositionMin(){
324 		double min = 0.0;
325 		if(zoomedOutBeyondOneToOne){
326 			min = -((this.getWidth() - 1) * zoom);
327 		} else {
328 			min = -(((double)this.getWidth() - 1.0) / (double)zoom);
329 		}
330 		return min;
331 	}
332 
333 	public final double getGraphPosition() {
334 		return graphPosition;
335 	}
336 
337 	private final int getGraphPositionMax() {
338 		return graphSize;
339 	}
340 
341 	public final void setGraphPosition(final double newPos) {
342 		graphPosition = newPos;
343 		repaint();
344 	}
345 
346 	/**
347 	 * How many available data records we are dealing with.
348 	 */
349 	public final void setGraphSize(final int newGraphSize) {
350 		graphSize = newGraphSize;
351 		if(graphSize > 0){
352 			centerGraphPosition(0, graphSize);
353 			zoomGraphToFit(graphSize);
354 		}
355 	}
356 
357 	/**
358 	 * Move the graph to the right so that only one valid
359 	 * data point shows on the right-most part of the display.
360 	 */
361 	private final void resetGraphPosition() {
362 		setGraphPosition(getGraphPositionMin());
363 	}
364 
365 	/**
366 	 * Move the graph to the center of the two provided graph positions
367 	 * so that there are equal data points to the left and to the right.
368 	 *
369 	 * Right now the method is expecting to get integer data points as
370 	 * it should be impossible to select fractions of a data point.
371 	 */
372 	private final void centerGraphPosition(final int beginPosition, final int endPosition) {
373 		final int halfScreen = this.getWidth() / 2;
374 		double pointsThatFitInHalfScreen = 0;
375 		if (zoomedOutBeyondOneToOne){
376 			pointsThatFitInHalfScreen = halfScreen * zoom;
377 		} else {
378 			pointsThatFitInHalfScreen = halfScreen / zoom;
379 		}
380 		final int distanceBetweenPositions = endPosition - beginPosition;
381 		final double halfwayBetweenTwoPositions = distanceBetweenPositions / 2;
382 		final double centerPosition = (beginPosition + halfwayBetweenTwoPositions) - pointsThatFitInHalfScreen;
383 		setGraphPosition(centerPosition);
384 	}
385 
386 	/**
387 	 * Move the graph to the left so that only one valid
388 	 * data point shows on the left-most part of the display.
389 	 */
390 	private void goToLastGraphPosition() {
391 		setGraphPosition(getGraphPositionMax());
392 	}
393 
394 	public final boolean isPlaying() {
395 		return playing;
396 	}
397 
398 	public final int getZoom() {
399 		return zoom;
400 	}
401 
402 	/**
403 	 * Take the current graph position and move amount positions forward.
404 	 */
405 	private void moveGraphPosition(final double amount) {
406 		final double newPos = graphPosition + amount;
407 		if (newPos > getGraphPositionMax()){
408 			goToLastGraphPosition();
409 		} else if (newPos < getGraphPositionMin()){
410 			resetGraphPosition();
411 		} else {
412 			setGraphPosition(newPos);
413 		}
414 	}
415 
416 	/**
417 	 * Move the graph position to newPosition where newPosition is dictated by
418 	 * an x screen coordinate.
419 	 */
420 	private void moveEntireGraphingPanel(final double newPosition) {
421 		double move = -1.0;
422 		if(zoomedOutBeyondOneToOne){
423 			move = newPosition * zoom;
424 		} else {
425 			move = newPosition / zoom;
426 		}
427 		if (graphPosition + move < getGraphPositionMax()) {
428 			if (graphPosition + move < getGraphPositionMin()) {
429 				resetGraphPosition();
430 			} else {
431 				moveGraphPosition(move);
432 			}
433 		} else {
434 			goToLastGraphPosition();
435 		}
436 	}
437 
438 	private void stopDragging() {
439 		dragging = false;
440 		prevDragXCoord = -1;
441 	}
442 
443 	private void stopFlinging() {
444 		flinging = false;
445 		flingInertia = 0;
446 	}
447 
448 	// Mouse listener functionality
449 	@Override
450 	public final void mouseClicked(final MouseEvent e) {
451 		if (!dragging) {
452 			final int half = this.getWidth() / 2;
453 			moveEntireGraphingPanel(e.getX() - half);
454 		} else {
455 			stopDragging();
456 			stopFlinging();
457 		}
458 	}
459 
460 	@Override
461 	public final void mouseDragged(final MouseEvent e) {
462 		dragging = true;
463 		final int xMouseCoord = e.getX();
464 		if ((prevDragXCoord > 0) && (prevDragXCoord != xMouseCoord)) {
465 			moveEntireGraphingPanel(prevDragXCoord - xMouseCoord);
466 			flingInertia = ((prevDragXCoord - xMouseCoord) * 2);
467 			thePast = System.currentTimeMillis();
468 		}
469 		prevDragXCoord = xMouseCoord;
470 	}
471 
472 	@Override
473 	public final void mouseMoved(final MouseEvent e) {
474 		// What should be here?
475 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
476 	}
477 
478 	@Override
479 	public final void mouseEntered(final MouseEvent e) {
480 		// What should be here?
481 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
482 	}
483 
484 	@Override
485 	public final void mouseExited(final MouseEvent e) {
486 		// What should be here?
487 		// Ben says eventually there might be stuff here, and it is required implementation for the MouseMovementListener interface.
488 	}
489 
490 	@Override
491 	public final void mousePressed(final MouseEvent e) {
492 		wasPlaying = playing;
493 		if (playing) {
494 			pause();
495 		}
496 
497 		stopDragging();
498 		stopFlinging();
499 	}
500 
501 	@Override
502 	public final void mouseReleased(final MouseEvent e) {
503 		stopDragging();
504 
505 		final long now = System.currentTimeMillis();
506 		if ((now - thePast) > 50) {
507 			stopFlinging(); // If over 50 milliseconds since dragging then don't fling
508 		}
509 
510 		if (flingInertia != 0) {
511 			fling();
512 		}
513 
514 		if (wasPlaying) {
515 			play();
516 		}
517 	}
518 
519 	@Override
520 	public final void mouseWheelMoved(final MouseWheelEvent e) {
521 		final int xMouseCoord = e.getX();
522 		final double center = this.getWidth() / 2.0;
523 		final int notches = e.getWheelRotation();
524 		double move = 0;
525 		final int zoomAmount = (int)Math.sqrt(zoom);
526 		if (notches < 0) {
527 			for (int i = 0; i < zoomAmount; i++){
528 				if(zoomedOutBeyondOneToOne){
529 					move = (xMouseCoord - center) / (zoom - 1.0);
530 				} else {
531 					move = (xMouseCoord - center) / zoom;
532 				}
533 				if (!(!zoomedOutBeyondOneToOne && zoom == getTightestZoom())){
534 					zoomIn();
535 					moveEntireGraphingPanel(move);
536 				}
537 			}
538 		} else {
539 			for (int i = 0; i < zoomAmount; i++){
540 				if(zoomedOutBeyondOneToOne || zoom == 1){
541 					move = -(xMouseCoord - center) / (zoom + 1.0);
542 				} else {
543 					move = -(xMouseCoord - center) / zoom;
544 				}
545 				if (!(zoomedOutBeyondOneToOne && zoom == getWidestZoom())){
546 					zoomOut();
547 					moveEntireGraphingPanel(move);
548 				}
549 			}
550 		}
551 	}
552 
553 	// Key listener functionality
554 	@Override
555 	public final void keyPressed(final KeyEvent e) {
556 		switch (e.getKeyCode()) {
557 			// Play key binding
558 			case KeyEvent.VK_SPACE: {
559 				play();
560 				break;
561 			}
562 
563 			// Enter full screen key binding
564 			case KeyEvent.VK_ENTER: {
565 				if (e.getModifiers() == InputEvent.ALT_MASK
566 						&& e.getKeyLocation() == KeyEvent.KEY_LOCATION_STANDARD) {
567 					OpenLogViewer.getInstance().enterFullScreen();
568 				}
569 				break;
570 			}
571 
572 			// Exit full screen key binding
573 			case KeyEvent.VK_ESCAPE: {
574 				OpenLogViewer.getInstance().exitFullScreen();
575 				break;
576 			}
577 
578 			// Toggle full screen key binding
579 			case KeyEvent.VK_F11: {
580 				OpenLogViewer.getInstance().toggleFullScreen();
581 				break;
582 			}
583 
584 			// Home key binding
585 			case KeyEvent.VK_HOME: {
586 				resetGraphPosition();
587 				break;
588 			}
589 
590 			// End key binding
591 			case KeyEvent.VK_END: {
592 				goToLastGraphPosition();
593 				break;
594 			}
595 
596 			// Scroll left key bindings
597 			case KeyEvent.VK_PAGE_UP: {
598 				//Big scroll
599 				moveEntireGraphingPanel(-(this.getWidth() * 0.75));
600 				break;
601 			}
602 
603 			case KeyEvent.VK_LEFT: {
604 				int localZoom = zoom;
605 				if(zoomedOutBeyondOneToOne){
606 					localZoom = 1;
607 				}
608 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
609 					//Big scroll
610 					moveEntireGraphingPanel(-(this.getWidth() * 0.75));
611 				} else {
612 					moveEntireGraphingPanel(-localZoom);
613 				}
614 				break;
615 			}
616 
617 			case KeyEvent.VK_KP_LEFT: {
618 				int localZoom = zoom;
619 				if(zoomedOutBeyondOneToOne){
620 					localZoom = 1;
621 				}
622 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
623 					//Big scroll
624 					moveEntireGraphingPanel(-(this.getWidth() * 0.75));
625 				} else {
626 					moveEntireGraphingPanel(-localZoom);
627 				}
628 				break;
629 			}
630 
631 			// Scroll right key bindings
632 			case KeyEvent.VK_PAGE_DOWN: {
633 				//Big scroll
634 				moveEntireGraphingPanel(this.getWidth() * 0.75);
635 				break;
636 			}
637 
638 			case KeyEvent.VK_RIGHT: {
639 				int localZoom = zoom;
640 				if(zoomedOutBeyondOneToOne){
641 					localZoom = 1;
642 				}
643 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
644 					//Big scroll
645 					moveEntireGraphingPanel(this.getWidth() * 0.75);
646 				} else {
647 					moveEntireGraphingPanel(localZoom);
648 				}
649 				break;
650 			}
651 
652 			case KeyEvent.VK_KP_RIGHT: {
653 				int localZoom = zoom;
654 				if(zoomedOutBeyondOneToOne){
655 					localZoom = 1;
656 				}
657 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
658 					//Big scroll
659 					moveEntireGraphingPanel(this.getWidth() * 0.75);
660 				} else {
661 					moveEntireGraphingPanel(localZoom);
662 				}
663 				break;
664 			}
665 
666 			// Zoom in key bindings
667 			case KeyEvent.VK_UP:
668 			case KeyEvent.VK_KP_UP: {
669 				zoomInCoarse();
670 				break;
671 			}
672 
673 			case KeyEvent.VK_ADD: {
674 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
675 					zoomInCoarse();
676 				}
677 				break;
678 			}
679 
680 			// Zoom out key bindings
681 			case KeyEvent.VK_DOWN:
682 			case KeyEvent.VK_KP_DOWN: {
683 				zoomOutCoarse();
684 				break;
685 			}
686 
687 			case KeyEvent.VK_SUBTRACT: {
688 				if (e.getModifiers() == InputEvent.CTRL_MASK) {
689 					zoomOutCoarse();
690 				}
691 				break;
692 			}
693 		}
694 	}
695 
696 	@Override
697 	public final void keyReleased(final KeyEvent e) {
698 		// What should be here?
699 		// Ben says eventually there might be stuff here, and it is required implementation for the KeyListener interface.
700 	}
701 
702 	@Override
703 	public final void keyTyped(final KeyEvent e) {
704 		// What should be here?
705 		// Ben says eventually there might be stuff here, and it is required implementation for the KeyListener interface.
706 	}
707 }