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.FontMetrics;
27  import java.awt.Graphics;
28  import java.awt.Graphics2D;
29  import java.math.BigDecimal;
30  import java.math.MathContext;
31  
32  import javax.swing.JPanel;
33  
34  import org.diyefi.openlogviewer.OpenLogViewer;
35  import org.diyefi.openlogviewer.genericlog.GenericLog;
36  
37  public class GraphPositionPanel extends JPanel {
38  	private static final long serialVersionUID = 1L;
39  
40  	private GenericLog genLog;
41  	private Color majorGraduationColor;
42  	private Color positionDataColor;
43  	private Color backgroundColor;
44  	private boolean[] validSnappingPositions;
45  	private double[] graduationSpacingMultiplier;
46  	private double majorGraduationSpacing;
47  
48  	public GraphPositionPanel() {
49  		super();
50  		init();
51  	}
52  
53  	private void init() {
54  		this.setOpaque(true);
55  		this.setLayout(null);
56  		majorGraduationColor = Color.GRAY;
57  		positionDataColor = majorGraduationColor;
58  		backgroundColor = Color.BLACK;
59  		validSnappingPositions = new boolean[this.getWidth()];
60  		graduationSpacingMultiplier = new double[] {2.0, 2.5, 2.0};
61  		setGraduationSpacing();
62  	}
63  
64  	@Override
65  	public final void paint(final Graphics g) { // override paint because there will be no components in this pane
66  		if (!this.getSize().equals(this.getParent().getSize())) {
67  			this.setSize(this.getParent().getSize());
68  		}
69  		setGraduationSpacing();
70  		final Graphics2D g2d = (Graphics2D) g;
71  		g2d.setColor(backgroundColor);
72  		g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
73  		if (genLog ==  null) {
74  			paintPositionBar(g2d, false);
75  		} else {
76  			if (genLog.getLogStatus() == GenericLog.LogState.LOG_LOADING) {
77  				paintPositionBar(g2d, false);
78  			} else if (genLog.getLogStatus() == GenericLog.LogState.LOG_LOADED) {
79  				final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
80  				final boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
81  				if (!zoomedOut || zoom == 1) {
82  					setupMouseCursorLineSnappingPositions();
83  				}
84  				paintPositionBar(g2d, zoomedOut);
85  				paintPositionData(g2d, zoomedOut);
86  			}
87  		}
88  	}
89  
90  	private void paintPositionBar(final Graphics2D g2d, final boolean zoomedOut) {
91  		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
92  		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
93  		double offset = 0d;
94  		double margin = 0d;
95  		if (zoomedOut) {
96  			offset = majorGraduationSpacing / zoom;
97  			offset = Math.ceil(offset);
98  			margin = (1d / zoom) / 2d;
99  		} else {
100 			offset = majorGraduationSpacing * zoom;
101 			offset = Math.round(offset);
102 			margin = (1d / zoom) / 2d;
103 		}
104 
105 		g2d.setColor(majorGraduationColor);
106 
107 		//Find first position marker placement
108 		double nextPositionMarker = getFirstPositionMarkerPlacement();
109 
110 		//Paint left to right
111 		double position = graphPosition - majorGraduationSpacing;
112 		for (int i = -(int) offset; i < this.getWidth() + (int) offset; i++) { // TODO It's ugly having the - on the left side of the cast, but moving it *could* change behaviour, so leaving it alone and adding this instead!
113 			if (position >= nextPositionMarker - margin) {
114 				int xCoord = i;
115 				if (xCoord >= 0 && xCoord < validSnappingPositions.length) {
116 					if (validSnappingPositions[xCoord]) {
117 						// TODO Ugly, restructure! Check this first to see if there is no need to modify xCoord.
118 					} else if (xCoord + 1 < validSnappingPositions.length && validSnappingPositions[xCoord + 1]) {
119 						xCoord++;
120 					} else if (xCoord > 0 && validSnappingPositions[xCoord - 1]) {
121 						xCoord--;
122 					}
123 				}
124 				g2d.drawLine(xCoord, 0, xCoord, 6);
125 				nextPositionMarker += majorGraduationSpacing;
126 			}
127 			if (zoomedOut) {
128 				position += zoom;
129 			} else {
130 				position += (1d / zoom);
131 			}
132 		}
133 		g2d.drawLine(0, 0, this.getWidth(), 0);
134 	}
135 
136 	private void paintPositionData(final Graphics2D g2d, final boolean zoomedOut) {
137 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
138 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
139 		double offset = 0d;
140 		double margin = 0d;
141 		if (zoomedOut) {
142 			offset = majorGraduationSpacing / zoom;
143 			offset = Math.ceil(offset);
144 			margin = (1d / zoom) / 2d;
145 		} else {
146 			offset = majorGraduationSpacing * zoom;
147 			offset = Math.round(offset);
148 			margin = (1d / zoom) / 2d;
149 		}
150 		g2d.setColor(positionDataColor);
151 		final FontMetrics fm = this.getFontMetrics(this.getFont()); //For getting string width
152 
153 		//Find first position marker placement
154 		double nextPositionMarker = getFirstPositionMarkerPlacement();
155 
156 		//Paint left to right
157 		double position = graphPosition - majorGraduationSpacing;
158 		for (int i = -(int) offset; i < this.getWidth() + (int) offset; i++) { // TODO Ditto!
159 			if (position >= nextPositionMarker - margin) {
160 				int xCoord = i;
161 				if (xCoord >= 0 && xCoord < validSnappingPositions.length) {
162 					if (validSnappingPositions[xCoord]) {
163 						//Check this first to see if there is no need to modify xCoord.
164 					} else if (xCoord + 1 < validSnappingPositions.length && validSnappingPositions[xCoord + 1]) {
165 						xCoord++;
166 					} else if (xCoord > 0 && validSnappingPositions[xCoord - 1]) {
167 						xCoord--;
168 					}
169 				}
170 				String positionDataString = "";
171 				BigDecimal positionData = new BigDecimal(nextPositionMarker);
172 				if (majorGraduationSpacing > 0.5) {
173 					positionDataString = positionData.toPlainString();
174 				} else {
175 					positionDataString = roundDecimalsOnlyToTwoSignificantFigures(positionData);
176 				}
177 				final int stringWidth = fm.stringWidth(positionDataString);
178 				g2d.drawString(positionDataString, xCoord - (stringWidth / 2), 18);
179 
180 				nextPositionMarker += majorGraduationSpacing;
181 			}
182 			if (zoomedOut) {
183 				position += zoom;
184 			} else {
185 				position += (1.0 / zoom);
186 			}
187 		}
188 	}
189 
190 	private double getFirstPositionMarkerPlacement() {
191 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
192 
193 		double nextPositionMarker = 0d;
194 		if (graphPosition < 0d) {
195 			while (nextPositionMarker - graphPosition >= majorGraduationSpacing) {
196 				nextPositionMarker -= majorGraduationSpacing;
197 			}
198 		} else {
199 			while (nextPositionMarker - graphPosition < 0.0) {
200 				nextPositionMarker += majorGraduationSpacing;
201 			}
202 		}
203 
204 		nextPositionMarker -= majorGraduationSpacing; // Start with one graduation off-screen to the left
205 		return nextPositionMarker;
206 	}
207 
208 	private void setupMouseCursorLineSnappingPositions() {
209 		validSnappingPositions = new boolean[this.getWidth()];
210 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
211 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
212 		MultiGraphLayeredPane multiGraph = OpenLogViewer.getInstance().getEntireGraphingPanel().getMultiGraphLayeredPane();
213 		final int availableData = (multiGraph.graphSize() - 1) * zoom;
214 		long count = Math.round(graphPosition * zoom);
215 
216 		// Fill array with valid snapping points from left to right
217 		for (int i = 0; i < this.getWidth(); i++) {
218 			if (count < -1 || count > availableData + 1 || count % zoom == 0) {
219 				validSnappingPositions[i] = true;
220 			}
221 			count++;
222 		}
223 	}
224 
225 	public final void setLog(final GenericLog log) {
226 		genLog = log;
227 		repaint();
228 	}
229 
230 	private void setGraduationSpacing() {
231 		int zoom = 1;
232 		boolean zoomedOut = false;
233 		if (OpenLogViewer.getInstance() != null) {
234 			zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
235 			zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
236 		}
237 
238 		majorGraduationSpacing = 100.0;
239 		int count = (int) (Math.log((double) zoom) / Math.log(2.0));  // Base-2 logarithm of zoom
240 
241 		if (zoomedOut){
242 			for (int i = 0; i < count; i++) {
243 				majorGraduationSpacing *= graduationSpacingMultiplier[i % 3];
244 
245 			}
246 		} else {
247 			for (int i = 0; i < count; i++) {
248 				majorGraduationSpacing /= graduationSpacingMultiplier[i % 3];
249 			}
250 		}
251 	}
252 
253 	public final int getBestSnappingPosition(final int xMouseCoord) {
254 		int bestPosition = xMouseCoord;
255 		if (!validSnappingPositions[xMouseCoord]) {
256 			boolean found = false;
257 			final int startPosition = xMouseCoord;
258 			for (int distance = 1; !found; distance++) {
259 				final int next = startPosition + distance;
260 				final int prev = startPosition - distance;
261 				if (next > validSnappingPositions.length - 1 || prev < 0) {
262 					bestPosition = xMouseCoord;
263 					found = true;
264 				} else if (validSnappingPositions[next]) {
265 					bestPosition = next;
266 					found = true;
267 				} else if (validSnappingPositions[prev]) {
268 					bestPosition = prev;
269 					found = true;
270 				}
271 			}
272 		}
273 		return bestPosition;
274 	}
275 
276 	/**
277 	* Use this if you want to avoid BigDecimal in the future:
278 	* http://stackoverflow.com/questions/202302/rounding-to-an-arbitrary-number-of-significant-digits
279 	*
280 	* Update: That algorithm has now been implemented in MathUtils.roundToSignificantFigures()
281 	*
282 	* @param BigDecimal input - The BigDecimal you're interested in rounding.
283 	* @return String - A string representation of input rounded to two significant figures to the right of the decimal.
284 	*/
285 
286 	private final String roundDecimalsOnlyToTwoSignificantFigures(final BigDecimal input) {
287 		String result = "";
288 		int indexOfMostSignificantDigit = 0;
289 
290 		// Find out if negative or not.
291 		result = input.toPlainString();
292 		if (result.substring(0, 1).equalsIgnoreCase("-")) {
293 			indexOfMostSignificantDigit++;
294 		}
295 
296 		// Fred says: LOL @ this! :-)
297 		// If there are decimals, then count the number of whole integers and leading zeros
298 		// to find out what rounding precision is needed to end up with two significant
299 		// figures for the decimal portion only.
300 		int amountOfIntegerDigits = 0;
301 		int amountOfLeadingZerosInDecimalPortion = 0;
302 		final int indexOfDecimalPoint = result.indexOf('.');
303 		if (indexOfDecimalPoint != -1) {
304 			amountOfIntegerDigits = result.substring(indexOfMostSignificantDigit, indexOfDecimalPoint).length();
305 			boolean done = false;
306 			for (int i = indexOfDecimalPoint + 1; i < result.length() && !done; i++) {
307 				if (result.substring(i, i).equalsIgnoreCase("0")) {
308 					amountOfLeadingZerosInDecimalPortion++;
309 				} else {
310 					done = true;
311 				}
312 			}
313 
314 		} else {
315 			amountOfIntegerDigits = result.substring(indexOfMostSignificantDigit).length();
316 
317 		}
318 		final int amountOfDesiredDecimalDigits = amountOfLeadingZerosInDecimalPortion + 2;
319 		final int sigFigs = amountOfIntegerDigits + amountOfDesiredDecimalDigits;
320 		BigDecimal roundedInput = input.round(new MathContext(sigFigs));
321 		roundedInput = roundedInput.stripTrailingZeros();
322 		result = roundedInput.toPlainString();
323 
324 		// Add on decimal point and trailing zero if doesn't exist.
325 		if (result.indexOf('.') == -1) {
326 			result = result + ".0";
327 		}
328 
329 		// Check for result extremely close to zero.
330 		if (result.length() > 16) {
331 			if (result.substring(0, 16).equalsIgnoreCase("-0.0000000000000")
332 					|| result.substring(0, 15).equalsIgnoreCase("0.0000000000000")){
333 				result = "0.0";
334 			}
335 		}
336 
337 		return result;
338 	}
339 }