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.Color;
26  import java.awt.Graphics;
27  import java.awt.Graphics2D;
28  import java.awt.event.HierarchyBoundsListener;
29  import java.awt.event.HierarchyEvent;
30  import java.beans.PropertyChangeEvent;
31  import java.beans.PropertyChangeListener;
32  
33  import javax.swing.JPanel;
34  import org.diyefi.openlogviewer.OpenLogViewer;
35  import org.diyefi.openlogviewer.genericlog.GenericDataElement;
36  
37  /**
38   * SingleGraphPanel is a JPanel that uses a transparent background.
39   * The graph trace is drawn to this panel and used in conjunction with a JLayeredPane
40   * to give the appearance of all the graph traces drawn together.
41   *
42   * This layer listens for window resizes and property changes.
43   * @author Bryan Harris and Ben Fenner
44   */
45  public class SingleGraphPanel extends JPanel implements HierarchyBoundsListener, PropertyChangeListener {
46  	private static final long serialVersionUID = 1L;
47  
48  	private static final double GRAPH_TRACE_SIZE_AS_PERCENTAGE_OF_TOTAL_GRAPH_SIZE = 0.95;
49  	private GenericDataElement GDE;
50  	private double[] dataPointsToDisplay;
51  	private int availableDataRecords;
52  
53  	public SingleGraphPanel() {
54  		this.setOpaque(false);
55  		this.setLayout(null);
56  		this.GDE = null;
57  		dataPointsToDisplay = null;
58  	}
59  
60  	@Override
61  	public void ancestorMoved(final HierarchyEvent e) {
62  	}
63  
64  	@Override
65  	public final void ancestorResized(final HierarchyEvent e) {
66  		if (e.getID() == HierarchyEvent.ANCESTOR_RESIZED) {
67  			sizeGraph();
68  		}
69  	}
70  
71  	@Override
72  	public final void propertyChange(final PropertyChangeEvent evt) {
73  		if (evt.getPropertyName().equalsIgnoreCase("Split")) {
74  			sizeGraph();
75  		}
76  	}
77  
78  	@Override
79  	public final void paint(final Graphics g) { // overridden paint because there will be no other painting other than this
80  		boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
81  		if(zoomedOut){
82  			initGraphZoomedOut();
83  		} else{
84  			initGraphZoomed();
85  		}
86  		if (hasDataPointToDisplay()) {
87  			paintDataPointsAndTraces(g);
88  		}
89  	}
90  
91  	private void paintDataPointsAndTraces(final Graphics g) {
92  
93  		// Setup graphics stuff
94  		final Graphics2D g2d = (Graphics2D) g;
95  		g2d.setColor(GDE.getDisplayColor());
96  
97  		// Initialize current, previous and next graph trace data points
98  		double leftOfTraceData = -Double.MAX_VALUE;
99  		double traceData = -Double.MAX_VALUE;
100 		double rightOfTraceData = dataPointsToDisplay[0];
101 
102 		// Initialize graph status markers
103 		boolean beforeGraphBeginning = true;
104 		boolean atGraphBeginning = false;
105 		boolean atGraphEnd = false;
106 
107 		// Initialize and setup data point screen location stuff
108 		final boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
109 		int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
110 		if(zoomedOut){
111 			zoom = 1;
112 		}
113 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
114 		final double offset = (graphPosition % 1) * zoom;
115 		int screenPositionXCoord = -(int)Math.round(offset);  // Start with one point off-screen to the left
116 		int screenPositionYCoord = Integer.MIN_VALUE;
117 		int nextScreenPositionYCoord = getScreenPositionYCoord(rightOfTraceData, GDE.getDisplayMinValue(), GDE.getDisplayMaxValue());
118 
119 		// Draw data points and trace lines from left to right including one off screen to the right
120 		for (int i = 0; i < dataPointsToDisplay.length; i++) {
121 
122 			// Setup current, previous and next graph trace data points
123 			try{
124 				leftOfTraceData = dataPointsToDisplay[i - 1];
125 			} catch (ArrayIndexOutOfBoundsException e){
126 				leftOfTraceData = -Double.MAX_VALUE;
127 			}
128 			traceData = dataPointsToDisplay[i];
129 			try{
130 				rightOfTraceData = dataPointsToDisplay[i + 1];
131 			} catch (ArrayIndexOutOfBoundsException e){
132 				rightOfTraceData = -Double.MAX_VALUE;
133 			}
134 
135 			// Setup graph beginning and end markers
136 			atGraphBeginning = false;
137 			if(leftOfTraceData == -Double.MAX_VALUE && traceData != -Double.MAX_VALUE){
138 				atGraphBeginning = true;
139 				beforeGraphBeginning = false;
140 			}
141 			if(traceData != -Double.MAX_VALUE && rightOfTraceData == -Double.MAX_VALUE){
142 				atGraphEnd = true;
143 			}
144 
145 			// Setup data point screen location stuff
146 			screenPositionYCoord = nextScreenPositionYCoord;
147 			nextScreenPositionYCoord = getScreenPositionYCoord(rightOfTraceData, GDE.getDisplayMinValue(), GDE.getDisplayMaxValue());
148 
149 			// Draw data point
150 			if(!zoomedOut && zoom > 5){
151 				// Draw fat data point
152 				// fillRect() is 95% faster than fillOval() for a 3x3 square on Ben's dev machine
153 				if (traceData != leftOfTraceData || traceData != rightOfTraceData) {
154 					g2d.fillRect(screenPositionXCoord - 1, screenPositionYCoord - 1, 3, 3);
155 				} else if (atGraphBeginning){
156 					g2d.fillRect(screenPositionXCoord - 1, screenPositionYCoord - 1, 3, 3);
157 				}
158 			} else {
159 				// Draw small data point
160 				// drawLine() is 33% faster than fillRect() for a single pixel on Ben's dev machine
161 				g2d.drawLine(screenPositionXCoord, screenPositionYCoord, screenPositionXCoord, screenPositionYCoord);
162 			}
163 
164 			// Draw graph trace line
165 			if (!beforeGraphBeginning && !atGraphEnd){
166 				g2d.drawLine(screenPositionXCoord, screenPositionYCoord, screenPositionXCoord + zoom, nextScreenPositionYCoord);
167 			}
168 
169 			// Move to the right in preparation of drawing more
170 			screenPositionXCoord += zoom;
171 		}
172 	}
173 
174 	private int getScreenPositionYCoord(final Double traceData, final double minValue, final double maxValue) {
175 		int point = 0;
176 		final int height = (int) (this.getHeight() * GRAPH_TRACE_SIZE_AS_PERCENTAGE_OF_TOTAL_GRAPH_SIZE);
177 		if (maxValue != minValue) {
178 			point = (int) (height - (height * ((traceData - minValue) / (maxValue - minValue))));
179 		}
180 		return point;
181 	}
182 
183 	private boolean hasDataPointToDisplay() {
184 		boolean result = false;
185 		if ((dataPointsToDisplay != null) && (dataPointsToDisplay.length > 0)) {
186 			result = true;
187 		}
188 		return result;
189 	}
190 
191 	/**
192 	 * this is where the GDE is referenced and the graph gets initialized for the first time
193 	 * @param GDE
194 	 */
195 	public final void setData(final GenericDataElement GDE) {
196 		this.GDE = GDE;
197 		this.availableDataRecords = GDE.size() + 1; // Size is currently position, this will need cleaning up later, leave it to me.
198 		// The main thing is to take away 10 calls to the GDE per view on something that is fairly static and cache it internally
199 		sizeGraph();
200 	}
201 
202 	public final GenericDataElement getData() {
203 		return GDE;
204 	}
205 
206 	/**
207 	 * Used for InfoLayer to get the data from the single graphs for data under the mouse
208 	 *
209 	 * @param pointerDistanceFromCenter
210 	 * @return Double representation of info at the mouse cursor line which snaps to data points or null if no data under cursor
211 	 */
212 	public final Double getMouseInfo(final int cursorPosition) {
213 		boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
214 		Double info = null;
215 		if(zoomedOut){
216 			info = getMouseInfoZoomedOut(cursorPosition);
217 		} else {
218 			info = getMouseInfoZoomed(cursorPosition);
219 		}
220 		return info;
221 	}
222 
223 	/**
224 	 * Used for InfoLayer to get the data from the single graphs for data under the mouse when not zoomed out
225 	 *
226 	 * @param pointerDistanceFromCenter
227 	 * @return Double representation of info at the mouse cursor line which snaps to data points or null if no data under cursor
228 	 */
229 	private final Double getMouseInfoZoomed(final int cursorPosition){
230 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
231 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
232 		final double offset = (graphPosition % 1) * zoom;
233 		final int cursorPositionPlusOffset = cursorPosition + (int) offset;
234 		double numSnapsFromCenter = ((double) cursorPositionPlusOffset / (double) zoom);
235 		numSnapsFromCenter = Math.round(numSnapsFromCenter);
236 		final int dataLocation = (int) graphPosition + (int) numSnapsFromCenter;
237 		if ((dataLocation >= 0) && (dataLocation < availableDataRecords)) {
238 			return GDE.get(dataLocation);
239 		} else {
240 			return null;
241 		}
242 	}
243 
244 	/**
245 	 * Used for InfoLayer to get the data from the single graphs for data under the mouse when zoomed out
246 	 *
247 	 * @param pointerDistanceFromCenter
248 	 * @return Double representation of info at the mouse cursor line which snaps to data points or null if no data under cursor
249 	 */
250 	private final Double getMouseInfoZoomedOut(int cursorPosition){
251 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
252 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
253 		final int dataLocation = (int) graphPosition + (cursorPosition * zoom);
254 		if ((dataLocation >= 0) && (dataLocation < availableDataRecords)) {
255 			return GDE.get(dataLocation);
256 		} else {
257 			return null;
258 		}
259 	}
260 
261 	public final Color getColor() {
262 		return GDE.getDisplayColor();
263 	}
264 
265 	public final void setColor(final Color c) {
266 		GDE.setDisplayColor(c);
267 	}
268 
269 	/**
270 	 * initialize the graph any time you need to paint
271 	 */
272 	public final void initGraphZoomed() {
273 		if (GDE != null) {
274 			final int graphPosition = (int)OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
275 			int graphWindowWidth = OpenLogViewer.getInstance().getEntireGraphingPanel().getWidth();
276 			final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
277 			int numberOfPointsThatFitInDisplay = graphWindowWidth / zoom;
278 			numberOfPointsThatFitInDisplay += 3; // Add three for off-screen points to the right
279 			dataPointsToDisplay = new double[numberOfPointsThatFitInDisplay];
280 			int position = graphPosition;
281 
282 			// Setup data points.
283 			for (int i = 0; i < numberOfPointsThatFitInDisplay; i++) {
284 				if (position >= 0 && position < availableDataRecords) {
285 					dataPointsToDisplay[i] = GDE.get(position);
286 				} else {
287 					dataPointsToDisplay[i] = -Double.MAX_VALUE;
288 				}
289 				position++;
290 			}
291 		}
292 	}
293 
294 	/**
295 	 * initialize the graph any time you need to paint
296 	 */
297 	public final void initGraphZoomedOut() {
298 		if (GDE != null) {
299 			final int graphPosition = (int)OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
300 			int graphWindowWidth = OpenLogViewer.getInstance().getEntireGraphingPanel().getWidth();
301 			final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
302 			dataPointsToDisplay = new double[graphWindowWidth + 1]; // Add one data point for off-screen to the right
303 			final int numberOfRealPointsThatFitInDisplay = (graphWindowWidth * zoom) + zoom; // Add one data point for off-screen to the right
304 			final int rightGraphPosition = graphPosition + numberOfRealPointsThatFitInDisplay;
305 
306 			/*
307 			* Setup data points.
308 			*
309 			* The data point to display is calculated by taking the average of
310 			* the data point spread and comparing it to the previous calculated
311 			* data point. If the average is higher, then the highest value of
312 			* the data spread is used. If the average is lower, then the lowest
313 			* value of the data point spread is used.
314 			*
315 			* In other words, if the graph is trending upward, the peak is used.
316 			* If the graph is trending downward, the valley is used.
317 			* This keeps the peaks and valleys intact and the middle stuff is
318 			* lost. This maintains the general shape of the graph, and assumes
319 			* that local peaks and valleys are the most interesting parts of the
320 			* graph to display.
321 			*/
322 			int nextAarrayIndex = 0;
323 			double leftOfNewData = GDE.get(0);
324 			for (int i = graphPosition; i < rightGraphPosition; i+=zoom) {
325 
326 				if (i >= 0 && i < availableDataRecords) {
327 					double minData = Double.MAX_VALUE;
328 					double maxData = -Double.MAX_VALUE;
329 					double newData = 0.0;
330 					double acummulateData = 0.0;
331 					int divisor = 0;
332 
333 					for (int j = 0; j < zoom; j++){
334 						if (i + j >= 0 && i + j < availableDataRecords) {
335 							newData = GDE.get(i + j);
336 							acummulateData += newData;
337 							divisor++;
338 							if (newData < minData){
339 								minData = newData;
340 							}
341 							if (newData > maxData){
342 								maxData = newData;
343 							}
344 						}
345 					}
346 					double averageData = acummulateData / divisor;
347 					if (averageData > leftOfNewData){
348 						dataPointsToDisplay[nextAarrayIndex] = maxData;
349 						leftOfNewData = maxData;
350 					} else if (averageData < leftOfNewData){
351 						dataPointsToDisplay[nextAarrayIndex] = minData;
352 						leftOfNewData = minData;
353 					} else {
354 						dataPointsToDisplay[nextAarrayIndex] = averageData;
355 						leftOfNewData = averageData;
356 					}
357 					nextAarrayIndex++;
358 				} else {
359 					dataPointsToDisplay[nextAarrayIndex] = -Double.MAX_VALUE;
360 					nextAarrayIndex++;
361 				}
362 			}
363 		}
364 	}
365 
366 	/**
367 	 * maintains the size of the graph when applying divisions
368 	 */
369 	public final void sizeGraph() {
370 		final MultiGraphLayeredPane lg = OpenLogViewer.getInstance().getMultiGraphLayeredPane();
371 		int wherePixel = 0;
372 		if (lg.getTotalSplits() > 1) {
373 			if (GDE.getSplitNumber() <= lg.getTotalSplits()) {
374 				wherePixel += lg.getHeight() / lg.getTotalSplits() * GDE.getSplitNumber() - (lg.getHeight() / lg.getTotalSplits());
375 			} else {
376 				wherePixel += lg.getHeight() / lg.getTotalSplits() * lg.getTotalSplits() - (lg.getHeight() / lg.getTotalSplits());
377 			}
378 		}
379 
380 		this.setBounds(0, wherePixel, lg.getWidth(), lg.getHeight() / (lg.getTotalSplits()));
381 		final boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
382 		if(zoomedOut){
383 			initGraphZoomedOut();
384 		} else {
385 			initGraphZoomed();
386 		}
387 	}
388 
389 	/**
390 	 * Graph total size
391 	 * @return GDE.size()
392 	 */
393 	public final int graphSize() {
394 		return availableDataRecords;
395 	}
396 }