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.LOG_LOADING) {
77  				paintPositionBar(g2d, false);
78  			} else if (genLog.getLogStatus() == GenericLog.LOG_LOADED) {
79  				final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
80  				final boolean zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
81  				paintPositionBar(g2d, zoomedOut);
82  				paintPositionData(g2d, zoomedOut);
83  				if(!zoomedOut && zoom > 1){
84  					setupMouseCursorLineSnappingPositions();
85  				}
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 = 0;
94  		if(zoomedOut){
95  			offset = majorGraduationSpacing / zoom;
96  		} else {
97  			offset = majorGraduationSpacing * zoom;
98  		}
99  		offset = Math.round(offset);
100 		g2d.setColor(majorGraduationColor);
101 
102 		//Find first position marker placement
103 		double nextPositionMarker = getFirstPositionMarkerPlacement();
104 
105 		//Paint left to right
106 		double position = graphPosition - majorGraduationSpacing;
107 		for (int i = -(int)offset; i < this.getWidth() + (int)offset; i++) {
108 			if (position >= nextPositionMarker){
109 				g2d.drawLine(i, 0, i, 6);
110 				nextPositionMarker += majorGraduationSpacing;
111 			}
112 			if(zoomedOut){
113 				position += zoom;
114 			} else {
115 				position += (1.0 / zoom);
116 			}
117 		}
118 		g2d.drawLine(0, 0, this.getWidth(), 0);
119 	}
120 
121 	private void paintPositionData(final Graphics2D g2d, final boolean zoomedOut) {
122 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
123 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
124 		double offset = 0;
125 		if(zoomedOut){
126 			offset = majorGraduationSpacing / zoom;
127 		} else {
128 			offset = majorGraduationSpacing * zoom;
129 		}
130 		g2d.setColor(positionDataColor);
131 		FontMetrics fm = this.getFontMetrics(this.getFont());  //For getting string width
132 
133 		//Find first position marker placement
134 		double nextPositionMarker = getFirstPositionMarkerPlacement();
135 
136 		//Paint left to right
137 		double position = graphPosition - majorGraduationSpacing;
138 		for (int i = -(int)offset; i < this.getWidth() + (int)offset; i++) {
139 			if (position >= nextPositionMarker){
140 				String positionDataString = "";
141 				BigDecimal positionData = new BigDecimal(nextPositionMarker);
142 				if(majorGraduationSpacing > 0.5){
143 					positionDataString = positionData.toPlainString();
144 				} else {
145 					positionDataString = roundDecimalsOnlyToTwoSignificantFigures(positionData);
146 				}
147 				int stringWidth = fm.stringWidth(positionDataString);
148 				g2d.drawString(positionDataString, i - (stringWidth / 2), 18);
149 
150 				nextPositionMarker += majorGraduationSpacing;
151 			}
152 			if(zoomedOut){
153 				position += zoom;
154 			} else {
155 				position += (1.0 / zoom);
156 			}
157 		}
158 	}
159 
160 	private final double getFirstPositionMarkerPlacement(){
161 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
162 
163 		double nextPositionMarker = 0.0;
164 		if(graphPosition < 0.0){
165 			while (nextPositionMarker - graphPosition >= majorGraduationSpacing){
166 				nextPositionMarker -= majorGraduationSpacing;
167 			}
168 		} else {
169 			while (nextPositionMarker - graphPosition < 0.0){
170 				nextPositionMarker += majorGraduationSpacing;
171 			}
172 		}
173 
174 		nextPositionMarker -= majorGraduationSpacing; // Start with one graduation off-screen to the left
175 		return nextPositionMarker;
176 	}
177 
178 	private void setupMouseCursorLineSnappingPositions() {
179 		validSnappingPositions = new boolean[this.getWidth()];
180 		final double graphPosition = OpenLogViewer.getInstance().getEntireGraphingPanel().getGraphPosition();
181 		final int zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
182 		long count = Math.round(graphPosition * zoom);
183 
184 		// Fill array with valid snapping points from left to right
185 		for (int i = 0; i < this.getWidth(); i++) {
186 			if (count % zoom == 0) {
187 				validSnappingPositions[i] = true;
188 			}
189 			count++;
190 		}
191 	}
192 
193 	public final void setLog(final GenericLog log) {
194 		genLog = log;
195 		repaint();
196 	}
197 
198 	private void setGraduationSpacing() {
199 		int zoom = 1;
200 		boolean zoomedOut = false;
201 		if (OpenLogViewer.getInstance() != null) {
202 			zoom = OpenLogViewer.getInstance().getEntireGraphingPanel().getZoom();
203 			zoomedOut = OpenLogViewer.getInstance().getEntireGraphingPanel().isZoomedOutBeyondOneToOne();
204 		}
205 
206 		majorGraduationSpacing = 100.0;
207 		int count = (int)(Math.log((double)zoom) / Math.log(2.0));  // Base-2 logarithm of zoom
208 
209 		if (zoomedOut){
210 			for (int i = 0; i < count; i++){
211 				majorGraduationSpacing *= graduationSpacingMultiplier[i % 3];
212 
213 			}
214 		} else {
215 			for (int i = 0; i < count; i++){
216 				majorGraduationSpacing /= graduationSpacingMultiplier[i % 3];
217 			}
218 		}
219 	}
220 
221 	public final int getBestSnappingPosition(final int xMouseCoord) {
222 		int bestPosition = 0;
223 		if (validSnappingPositions[xMouseCoord]) {
224 			bestPosition = xMouseCoord;
225 		} else {
226 			boolean found = false;
227 			final int startPosition = xMouseCoord;
228 			for (int distance = 1; !found; distance++) {
229 				final int next = startPosition + distance;
230 				final int prev = startPosition - distance;
231 				if (next > validSnappingPositions.length - 1 || prev < 0) {
232 					bestPosition = xMouseCoord;
233 					found = true;
234 				} else if (validSnappingPositions[next]) {
235 					bestPosition = next;
236 					found = true;
237 				} else if (validSnappingPositions[prev]) {
238 					bestPosition = prev;
239 					found = true;
240 				}
241 			}
242 		}
243 		return bestPosition;
244 	}
245 
246 	/**
247 	* Use this if you want to avoid BigDecimal in the future:
248 	* http://stackoverflow.com/questions/202302/rounding-to-an-arbitrary-number-of-significant-digits
249 	*
250 	* @param BigDecimal input - The BigDecimal you're interested in rounding.
251 	* @return String - A string representation of input rounded to two significant figures to the right of the decimal.
252 	*/
253 
254 	private final String roundDecimalsOnlyToTwoSignificantFigures(final BigDecimal input) {
255 		String result = "";
256 		int indexOfMostSignificantDigit = 0;
257 
258 		// Find out if negative or not.
259 		result = input.toPlainString();
260 		if(result.substring(0, 1).equalsIgnoreCase("-")){
261 			indexOfMostSignificantDigit++;
262 		}
263 
264 		// If there are decimals, then count the number of whole integers and leading zeros
265 		// to find out what rounding precision is needed to end up with two significant
266 		// figures for the decimal portion only.
267 		int amountOfIntegerDigits = 0;
268 		int amountOfLeadingZerosInDecimalPortion = 0;
269 		final int indexOfDecimalPoint = result.indexOf('.');
270 		if (indexOfDecimalPoint != -1){
271 			amountOfIntegerDigits = result.substring(indexOfMostSignificantDigit, indexOfDecimalPoint).length();
272 			boolean done = false;
273 			for (int i = indexOfDecimalPoint + 1; i < result.length() && !done; i++){
274 				if (result.substring(i, i).equalsIgnoreCase("0")){
275 					amountOfLeadingZerosInDecimalPortion++;
276 				} else {
277 					done = true;
278 				}
279 			}
280 
281 		} else {
282 			amountOfIntegerDigits = result.substring(indexOfMostSignificantDigit).length();
283 
284 		}
285 		final int amountOfDesiredDecimalDigits = amountOfLeadingZerosInDecimalPortion + 2;
286 		final int sigFigs = amountOfIntegerDigits + amountOfDesiredDecimalDigits;
287 		BigDecimal roundedInput = input.round(new MathContext(sigFigs));
288 		roundedInput = roundedInput.stripTrailingZeros();
289 		result = roundedInput.toPlainString();
290 
291 		// Add on decimal point and trailing zero if doesn't exist.
292 		if (result.indexOf('.') == -1){
293 			result = result + ".0";
294 		}
295 
296 		// Check for result extremely close to zero.
297 		if(result.length() > 16){
298 			if (result.substring(0, 16).equalsIgnoreCase("-0.0000000000000")
299 					|| result.substring(0, 15).equalsIgnoreCase("0.0000000000000")){
300 				result = "0.0";
301 			}
302 		}
303 
304 		return result;
305 	}
306 }