├── BinaryProcessor.java ├── ChartDescriptor.java ├── ChartListener.java ├── ChartsRegion.java ├── Controller.java ├── ControlsRegion.java ├── DataStructureWindow.java ├── Dataset.java ├── FrequencyDomainChart.java ├── GridChangedListener.java ├── HistogramChart.java ├── Main.java ├── Model.java ├── NewChartWindow.java ├── PositionedChart.java ├── SerialPortListener.java ├── StatisticsChart.java ├── Telemetry Viewer.jar ├── Tester.java ├── TimeDomainChart.java └── TimeDomainChartCached.java /BinaryProcessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * An interface for objects that can convert raw serial port data (bytes) into numbers. This is only used in the Binary packet mode. 3 | * The serial port receiver thread uses these to convert the received bytes into numbers that can be inserted into Datasets. 4 | */ 5 | public interface BinaryProcessor { 6 | 7 | /** 8 | * @return Description for this data type. This will be displayed in the DataStructureWindow. 9 | */ 10 | public String toString(); 11 | 12 | /** 13 | * @return Number of bytes used by this data type. 14 | */ 15 | public int getByteCount(); 16 | 17 | /** 18 | * @param rawBytes Unprocessed bytes that were received from the serial port. 19 | * @return The corresponding number, as a double. The number has NOT been scaled by the Dataset conversion factors. 20 | */ 21 | public double extractValue(byte[] rawBytes); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ChartDescriptor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * All charts should have a static getDescriptor() method that returns an object that implements this interface. 3 | * The NewChartWindow uses this information to create new charts and to display appropriate options for the user. 4 | */ 5 | public interface ChartDescriptor { 6 | 7 | /** 8 | * @return The chart's name, such as "Line Chart" etc. 9 | */ 10 | public String toString(); 11 | 12 | /** 13 | * @return A String[] of names for each possible input, or null if 1+ inputs are allowed. 14 | */ 15 | public String[] getInputNames(); 16 | 17 | /** 18 | * @return The minimum number of samples allowed. 19 | */ 20 | public int getMinimumDuration(); 21 | 22 | /** 23 | * @return The maximum number of samples allowed. 24 | */ 25 | public int getMaximumDuration(); 26 | 27 | /** 28 | * @return The default number of samples. 29 | */ 30 | public int getDefaultDuration(); 31 | 32 | /** 33 | * Creates a new chart. 34 | * 35 | * @param x1 The x-coordinate of a bounding-box corner in the ChartsRegion. This is a grid location, not a pixel. 36 | * @param y1 The y-coordinate of a bounding-box corner in the ChartsRegion. This is a grid location, not a pixel. 37 | * @param x2 The x-coordinate of the opposite bounding-box corner in the ChartsRegion. This is a grid location, not a pixel. 38 | * @param y2 The y-coordinate of the opposite bounding-box corner in the ChartsRegion. This is a grid location, not a pixel. 39 | * @param chartDuration How many samples make up the domain. 40 | * @param chartInputs The Datasets to be visualized. 41 | * @return The new chart. 42 | */ 43 | public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /ChartListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Objects that want to be notified when a chart is added or removed must implement this interface. 3 | */ 4 | public interface ChartListener { 5 | 6 | /** 7 | * Called when a new chart has been created. 8 | * 9 | * @param chart The new chart. 10 | */ 11 | public void chartAdded(PositionedChart chart); 12 | 13 | /** 14 | * Called when a chart is being removed. 15 | * 16 | * @param chart The chart being removed. 17 | */ 18 | public void chartRemoved(PositionedChart chart); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ChartsRegion.java: -------------------------------------------------------------------------------- 1 | import java.awt.BasicStroke; 2 | import java.awt.Color; 3 | import java.awt.Dimension; 4 | import java.awt.Graphics; 5 | import java.awt.Graphics2D; 6 | import java.awt.event.ComponentEvent; 7 | import java.awt.event.ComponentListener; 8 | import java.awt.event.MouseEvent; 9 | import java.awt.event.MouseListener; 10 | import java.awt.event.MouseMotionListener; 11 | 12 | import javax.swing.JFrame; 13 | import javax.swing.JPanel; 14 | import javax.swing.SwingUtilities; 15 | 16 | /** 17 | * Manages the grid region and all charts on the screen. 18 | * 19 | * Users can click-and-drag in this region to create new charts. 20 | * When this panel is resized, all contained charts will be repositioned and resized accordingly. 21 | */ 22 | @SuppressWarnings("serial") 23 | public class ChartsRegion extends JPanel { 24 | 25 | // grid size 26 | int columnCount; 27 | int rowCount; 28 | 29 | // grid locations for the opposite corners of where a new chart will be placed 30 | int startX; 31 | int startY; 32 | int endX; 33 | int endY; 34 | 35 | /** 36 | * Create a new ChartsRegion. 37 | */ 38 | public ChartsRegion() { 39 | 40 | super(); 41 | 42 | columnCount = Controller.getGridColumns(); 43 | rowCount = Controller.getGridRows(); 44 | 45 | startX = -1; 46 | startY = -1; 47 | endX = -1; 48 | endY = -1; 49 | 50 | setLayout(null); // absolute layout 51 | setMinimumSize(new Dimension(200, 200)); 52 | 53 | // update the column and row counts when they change 54 | Controller.addGridChangedListener(new GridChangedListener() { 55 | @Override public void gridChanged(int columns, int rows) { 56 | columnCount = columns; 57 | rowCount = rows; 58 | getComponentListeners()[0].componentResized(null); 59 | } 60 | }); 61 | 62 | // add and remove charts as needed 63 | Controller.addChartsListener(new ChartListener() { 64 | @Override public void chartRemoved(PositionedChart chart) { 65 | remove(chart); 66 | revalidate(); 67 | repaint(); 68 | } 69 | 70 | @Override public void chartAdded(PositionedChart chart) { 71 | add(chart); 72 | revalidate(); 73 | repaint(); 74 | } 75 | }); 76 | 77 | // listen for mouse presses and releases (the user clicking and dragging a region to place a new chart) 78 | addMouseListener(new MouseListener() { 79 | 80 | // the mouse was pressed, attempting to start a new chart region 81 | @Override public void mousePressed(MouseEvent me) { 82 | int proposedStartX = me.getX() * columnCount / getWidth(); 83 | int proposedStartY = me.getY() * rowCount / getHeight(); 84 | 85 | if(proposedStartX < columnCount && proposedStartY < rowCount && Controller.gridRegionAvailable(proposedStartX, proposedStartY, proposedStartX, proposedStartY)) { 86 | startX = endX = proposedStartX; 87 | startY = endY = proposedStartY; 88 | repaint(); 89 | } 90 | } 91 | 92 | // the mouse was released, attempting to create a new chart 93 | @Override public void mouseReleased(MouseEvent me) { 94 | 95 | if(endX == -1 || endY == -1) 96 | return; 97 | 98 | int proposedEndX = me.getX() * columnCount / getWidth(); 99 | int proposedEndY = me.getY() * rowCount / getHeight(); 100 | 101 | if(proposedEndX < columnCount && proposedEndY < rowCount && Controller.gridRegionAvailable(startX, startY, proposedEndX, proposedEndY)) { 102 | endX = proposedEndX; 103 | endY = proposedEndY; 104 | } 105 | 106 | JFrame parentWindow = (JFrame) SwingUtilities.windowForComponent(ChartsRegion.this); 107 | new NewChartWindow(parentWindow, startX, startY, endX, endY); 108 | 109 | startX = startY = -1; 110 | endX = endY = -1; 111 | repaint(); 112 | } 113 | 114 | @Override public void mouseExited(MouseEvent me) { } 115 | @Override public void mouseEntered(MouseEvent me) { } 116 | @Override public void mouseClicked(MouseEvent me) { } 117 | 118 | }); 119 | 120 | // listen for mouse drags (the user clicking and dragging a region for a new chart) 121 | addMouseMotionListener(new MouseMotionListener() { 122 | 123 | @Override public void mouseDragged(MouseEvent me) { 124 | 125 | if(endX == -1 || endY == -1) 126 | return; 127 | 128 | int proposedEndX = me.getX() * columnCount / getWidth(); 129 | int proposedEndY = me.getY() * rowCount / getHeight(); 130 | 131 | if(proposedEndX < columnCount && proposedEndY < rowCount && Controller.gridRegionAvailable(startX, startY, proposedEndX, proposedEndY)) { 132 | endX = proposedEndX; 133 | endY = proposedEndY; 134 | repaint(); 135 | } 136 | 137 | } 138 | 139 | @Override public void mouseMoved(MouseEvent me) { } 140 | 141 | }); 142 | 143 | // listen for a change in size 144 | // this is required because paintComponent() will not be automatically called if the window is un-maximized and an existing chart would obscure the rest of the ChartsRegion 145 | addComponentListener(new ComponentListener() { 146 | 147 | @Override public void componentResized(ComponentEvent ce) { 148 | 149 | int width = getWidth(); 150 | int height = getHeight(); 151 | int columnWidth = width / columnCount; 152 | int rowHeight = height / rowCount; 153 | 154 | Controller.repositionCharts(columnWidth, rowHeight); 155 | revalidate(); 156 | repaint(); 157 | 158 | } 159 | 160 | @Override public void componentShown(ComponentEvent ce) { } 161 | @Override public void componentMoved(ComponentEvent ce) { } 162 | @Override public void componentHidden(ComponentEvent ce) { } 163 | 164 | }); 165 | 166 | } 167 | 168 | /** 169 | * Redraws the ChartsRegion based on the current width/height, and the number of columns/rows. 170 | * 171 | * @param g The graphics object. 172 | */ 173 | @Override public void paintComponent(Graphics g) { 174 | 175 | super.paintComponent(g); 176 | Graphics2D g2 = (Graphics2D) g; 177 | 178 | int width = getWidth(); 179 | int height = getHeight(); 180 | int columnWidth = width / columnCount; 181 | int rowHeight = height / rowCount; 182 | 183 | // resize and reposition all charts 184 | Controller.repositionCharts(columnWidth, rowHeight); 185 | revalidate(); 186 | 187 | // draw a neutral background 188 | g2.setColor(getBackground()); 189 | g2.fillRect(0, 0, width, height); 190 | 191 | width = columnWidth * columnCount; 192 | height = rowHeight * rowCount; 193 | 194 | // draw a white background for the grid 195 | g2.setColor(Color.WHITE); 196 | g2.fillRect(0, 0, width, height); 197 | 198 | // set the stroke 199 | float factor = Controller.getDisplayScalingFactor(); 200 | g2.setStroke(new BasicStroke(1.0f * factor, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1.0f, new float[] {5.0f * factor}, 0.0f)); 201 | g2.setColor(Color.BLACK); 202 | 203 | // draw vertical lines 204 | for(int i = 1; i < columnCount; i++) 205 | g2.drawLine(columnWidth * i, 0, columnWidth * i, height); 206 | 207 | // draw horizontal lines 208 | for(int i = 1; i < rowCount; i++) 209 | g2.drawLine(0, rowHeight * i, width, rowHeight * i); 210 | 211 | // draw active bounding box where the user is clicking-and-dragging to place a new chart 212 | g2.setColor(Color.GRAY); 213 | int x = startX < endX ? startX * columnWidth : endX * columnWidth; 214 | int y = startY < endY ? startY * rowHeight : endY * rowHeight; 215 | int boxWidth = (Math.abs(endX - startX) + 1) * columnWidth; 216 | int boxHeight = (Math.abs(endY - startY) + 1) * rowHeight; 217 | g2.fillRect(x, y, boxWidth, boxHeight); 218 | 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /Controller.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import java.awt.Toolkit; 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.PrintWriter; 6 | import java.nio.charset.StandardCharsets; 7 | import java.nio.file.Files; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.Collection; 11 | import java.util.List; 12 | import java.util.Scanner; 13 | import java.util.concurrent.atomic.AtomicBoolean; 14 | 15 | import javax.swing.JOptionPane; 16 | 17 | import com.fazecast.jSerialComm.SerialPort; 18 | 19 | /** 20 | * Handles all non-GUI logic and manages access to the Model (the data). 21 | */ 22 | public class Controller { 23 | 24 | static List gridChangedListeners = new ArrayList(); 25 | static List serialPortListeners = new ArrayList(); 26 | static List chartListeners = new ArrayList(); 27 | static SerialPort port; 28 | static Thread serialPortThread; 29 | static AtomicBoolean dataStructureDefined = new AtomicBoolean(false); 30 | 31 | /** 32 | * @return The percentage of 100dpi that the screen uses. It's currently rounded to an integer, but future plans will make use of floats. 33 | */ 34 | public static float getDisplayScalingFactor() { 35 | 36 | return (int) Math.round((double) Toolkit.getDefaultToolkit().getScreenResolution() / 100.0); 37 | 38 | } 39 | 40 | /** 41 | * Registers a listener that will be notified when the ChartsRegion grid size (column or row count) changes. 42 | * 43 | * @param listener The listener to be notified. 44 | */ 45 | public static void addGridChangedListener(GridChangedListener listener) { 46 | 47 | gridChangedListeners.add(listener); 48 | 49 | } 50 | 51 | /** 52 | * Notifies all registered listeners about a new ChartsRegion grid size. 53 | */ 54 | private static void notifyGridChangedListeners() { 55 | 56 | for(GridChangedListener listener : gridChangedListeners) 57 | listener.gridChanged(Model.gridColumns, Model.gridRows); 58 | 59 | } 60 | 61 | /** 62 | * Changes the ChartsRegion grid column count if it is within the allowed range and would not obscure part of an existing chart. 63 | * 64 | * @param value The new column count. 65 | */ 66 | public static void setGridColumns(int value) { 67 | 68 | boolean chartsObscured = false; 69 | for(PositionedChart chart : Model.charts) 70 | if(chart.regionOccupied(value, 0, Model.gridColumns, Model.gridRows)) 71 | chartsObscured = true; 72 | 73 | if(value >= Model.gridColumnsMinimum && value <= Model.gridColumnsMaximum && !chartsObscured) 74 | Model.gridColumns = value; 75 | 76 | notifyGridChangedListeners(); 77 | 78 | } 79 | 80 | /** 81 | * @return The current ChartsRegion grid column count. 82 | */ 83 | public static int getGridColumns() { 84 | 85 | return Model.gridColumns; 86 | 87 | } 88 | 89 | /** 90 | * Changes the ChartsRegion grid row count if it is within the allowed range and would not obscure part of an existing chart. 91 | * 92 | * @param value The new row count. 93 | */ 94 | public static void setGridRows(int value) { 95 | 96 | boolean chartsObscured = false; 97 | for(PositionedChart chart : Model.charts) 98 | if(chart.regionOccupied(0, value, Model.gridColumns, Model.gridRows)) 99 | chartsObscured = true; 100 | 101 | if(value >= Model.gridRowsMinimum && value <= Model.gridRowsMaximum && !chartsObscured) 102 | Model.gridRows = value; 103 | 104 | notifyGridChangedListeners(); 105 | 106 | } 107 | 108 | /** 109 | * @return The current ChartsRegion grid row count. 110 | */ 111 | public static int getGridRows() { 112 | 113 | return Model.gridRows; 114 | 115 | } 116 | 117 | /** 118 | * @return An array of ChartDescriptor's, one for each possible chart type. 119 | */ 120 | public static ChartDescriptor[] getChartDescriptors() { 121 | 122 | return Model.chartDescriptors; 123 | 124 | } 125 | 126 | /** 127 | * @return The number of CSV columns or Binary elements described in the data structure. 128 | */ 129 | public static int getDatasetsCount() { 130 | 131 | return Model.datasets.size(); 132 | 133 | } 134 | 135 | /** 136 | * @param location CSV column number, or Binary packet byte offset. Locations may be sparse. 137 | * @return The Dataset. 138 | */ 139 | public static Dataset getDatasetByLocation(int location) { 140 | 141 | return Model.datasets.get(location); 142 | 143 | } 144 | 145 | /** 146 | * @param index An index between 0 and getDatasetsCount()-1, inclusive. 147 | * @return The Dataset. 148 | */ 149 | public static Dataset getDatasetByIndex(int index) { 150 | 151 | return (Dataset) Model.datasets.values().toArray()[index]; 152 | 153 | } 154 | 155 | /** 156 | * Creates and stores a new Dataset. If a Dataset already exists for the same location, the new Dataset will replace it. 157 | * 158 | * @param location CSV column number, or Binary packet byte offset. 159 | * @param processor BinaryProcessor for the raw samples in the Binary packet. (Ignored in CSV mode.) 160 | * @param name Descriptive name of what the samples represent. 161 | * @param color Color to use when visualizing the samples. 162 | * @param unit Descriptive name of how the samples are quantified. 163 | * @param conversionFactorA This many unprocessed LSBs... 164 | * @param conversionFactorB ... equals this many units. 165 | */ 166 | public static void insertDataset(int location, BinaryProcessor processor, String name, Color color, String unit, double conversionFactorA, double conversionFactorB) { 167 | 168 | Model.datasets.put(location, new Dataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB)); 169 | 170 | } 171 | 172 | /** 173 | * Removes all charts and Datasets. 174 | */ 175 | public static void removeAllDatasets() { 176 | 177 | Controller.removeAllPositionedCharts(); 178 | 179 | Model.datasets.clear(); 180 | 181 | } 182 | 183 | /** 184 | * @return The Datasets. 185 | */ 186 | public static Collection getAllDatasets() { 187 | 188 | return Model.datasets.values(); 189 | 190 | } 191 | 192 | /** 193 | * Registers a listener that will be notified when the serial port status (connection made or lost) changes. 194 | * 195 | * @param listener The listener to be notified. 196 | */ 197 | public static void addSerialPortListener(SerialPortListener listener) { 198 | 199 | serialPortListeners.add(listener); 200 | 201 | } 202 | 203 | private static final int SERIAL_CONNECTION_OPENED = 0; 204 | private static final int SERIAL_CONNECTION_CLOSED = 1; 205 | /** 206 | * Notifies all registered listeners about a change in the serial port status. 207 | * 208 | * @param status Either SERIAL_CONNECTION_OPENED or SERIAL_CONNECTION_CLOSED. 209 | */ 210 | private static void notifySerialPortListeners(int status) { 211 | 212 | for(SerialPortListener listener : serialPortListeners) 213 | if(status == SERIAL_CONNECTION_OPENED) 214 | listener.connectionOpened(Model.sampleRate, Model.packetType, Model.portName, Model.baudRate); 215 | else if(status == SERIAL_CONNECTION_CLOSED) 216 | listener.connectionClosed(); 217 | 218 | } 219 | 220 | /** 221 | * @return A String[] of names for all serial ports that were detected at the time of this function call, plus the "Test" dummy serial port. 222 | */ 223 | public static String[] getSerialPortNames() { 224 | 225 | SerialPort[] ports = SerialPort.getCommPorts(); 226 | 227 | String[] names = new String[ports.length + 1]; 228 | for(int i = 0; i < ports.length; i++) 229 | names[i] = ports[i].getSystemPortName(); 230 | 231 | names[names.length - 1] = "Test"; 232 | 233 | return names; 234 | 235 | } 236 | 237 | /** 238 | * @return An int[] of supported UART baud rates. 239 | */ 240 | public static int[] getBaudRates() { 241 | 242 | return new int[] {9600, 19200, 38400, 57600, 115200, 230400, 460800, 921600, 1000000, 1500000, 2000000, 3000000}; 243 | 244 | } 245 | 246 | /** 247 | * @return A String[] of descriptions for supported UART packet types. 248 | */ 249 | public static String[] getPacketTypes() { 250 | 251 | return new String[] {"ASCII CSVs", "Binary"}; 252 | 253 | } 254 | 255 | /** 256 | * Connects to a serial port and spawns a new thread to process incoming data. 257 | * 258 | * @param sampleRate Expected samples per second (Hz.) This is used for FFTs. 259 | * @param packetType One of the Strings from Controller.getPacketTypes() 260 | * @param portName One of the Strings from Controller.getSerialPortNames() 261 | * @param baudRate One of the baud rates from Controller.getBaudRates() 262 | */ 263 | @SuppressWarnings("deprecation") 264 | public static void connectToSerialPort(int sampleRate, String packetType, String portName, int baudRate) { 265 | 266 | if(portName.equals("Test")) { 267 | 268 | Tester.populateDataStructure(); 269 | Tester.startTransmission(); 270 | 271 | Model.sampleRate = sampleRate; 272 | Model.packetType = packetType; 273 | Model.portName = portName; 274 | Model.baudRate = 9600; 275 | notifySerialPortListeners(SERIAL_CONNECTION_OPENED); 276 | 277 | return; 278 | 279 | } else if(packetType.equals("ASCII CSVs")) { 280 | 281 | port = SerialPort.getCommPort(portName); 282 | port.setBaudRate(baudRate); 283 | port.setComPortTimeouts(SerialPort.TIMEOUT_SCANNER, 0, 0); 284 | 285 | if(!port.openPort()) { // try 3 times before giving up 286 | if(!port.openPort()) { 287 | if(!port.openPort()) { 288 | notifySerialPortListeners(SERIAL_CONNECTION_CLOSED); 289 | return; 290 | } 291 | } 292 | } 293 | 294 | Model.sampleRate = sampleRate; 295 | Model.packetType = packetType; 296 | Model.portName = portName; 297 | Model.baudRate = baudRate; 298 | 299 | if(serialPortThread != null && serialPortThread.isAlive()) 300 | serialPortThread.stop(); 301 | 302 | serialPortThread = new Thread(new Runnable() { 303 | @Override public void run() { 304 | 305 | // wait for the data structure to be defined 306 | while(!dataStructureDefined.get()) 307 | try { Thread.sleep(10); } catch(Exception e) { } 308 | 309 | Scanner scanner = new Scanner(port.getInputStream()); 310 | 311 | while(scanner.hasNextLine()) { 312 | 313 | // stop receiving data if the thread has been interrupted 314 | if(serialPortThread.isInterrupted()) 315 | break; 316 | 317 | try { 318 | 319 | String line = scanner.nextLine(); 320 | String[] tokens = line.split(","); 321 | double[] samples = new double[tokens.length]; 322 | for(int i = 0; i < tokens.length; i++) 323 | samples[i] = Double.parseDouble(tokens[i]); 324 | Controller.insertSamples(samples); 325 | 326 | } catch(Exception e) { } 327 | 328 | } 329 | scanner.close(); 330 | port.closePort(); 331 | port = null; 332 | notifySerialPortListeners(SERIAL_CONNECTION_CLOSED); 333 | 334 | } 335 | }); 336 | serialPortThread.setPriority(Thread.MAX_PRIORITY); 337 | serialPortThread.setName("Serial Port Receiver"); 338 | serialPortThread.start(); 339 | 340 | notifySerialPortListeners(SERIAL_CONNECTION_OPENED); 341 | return; 342 | 343 | } else if(packetType.equals("Binary")) { 344 | 345 | port = SerialPort.getCommPort(portName); 346 | port.setBaudRate(baudRate); 347 | port.setComPortTimeouts(SerialPort.TIMEOUT_READ_BLOCKING, 0, 0); 348 | 349 | if(!port.openPort()) { // try 3 times before giving up 350 | if(!port.openPort()) { 351 | if(!port.openPort()) { 352 | notifySerialPortListeners(SERIAL_CONNECTION_CLOSED); 353 | return; 354 | } 355 | } 356 | } 357 | 358 | Model.sampleRate = sampleRate; 359 | Model.packetType = packetType; 360 | Model.portName = portName; 361 | Model.baudRate = baudRate; 362 | 363 | if(serialPortThread != null && serialPortThread.isAlive()) 364 | serialPortThread.stop(); 365 | 366 | serialPortThread = new Thread(new Runnable() { 367 | @Override public void run() { 368 | 369 | byte[] rx_buffer = new byte[1024]; 370 | 371 | // wait for data structure to be defined 372 | while(!dataStructureDefined.get()) 373 | try { Thread.sleep(10); } catch(Exception e) { } 374 | 375 | // get packet size (includes 1 byte sync word) 376 | int packetSize = Controller.getBinaryPacketSize(); 377 | double[] samples = new double[Controller.getDatasetsCount()]; 378 | 379 | while(true) { 380 | 381 | // stop receiving data if the thread has been interrupted 382 | if(serialPortThread.isInterrupted()) { 383 | port.closePort(); 384 | port = null; 385 | notifySerialPortListeners(SERIAL_CONNECTION_CLOSED); 386 | return; 387 | } 388 | 389 | // wait for sync byte of 0xAA 390 | while(rx_buffer[0] != (byte) 0xAA) // the byte cast is required! 391 | port.readBytes(rx_buffer, 1); 392 | 393 | // get rest of packet after the sync word 394 | port.readBytes(rx_buffer, packetSize - 1 + 2); // -1 for sync word, +2 for checksum 395 | 396 | // extract all samples from the packet 397 | for(int datasetNumber = 0; datasetNumber < samples.length; datasetNumber++) { 398 | 399 | Dataset dataset = Controller.getDatasetByIndex(datasetNumber); 400 | 401 | byte[] rawData = new byte[dataset.processor.getByteCount()]; 402 | 403 | int rx_buffer_index = dataset.location - 1; // -1 for the sync word 404 | for(int i = 0; i < rawData.length; i++) 405 | rawData[i] = rx_buffer[rx_buffer_index++]; 406 | 407 | samples[datasetNumber] = dataset.processor.extractValue(rawData); 408 | 409 | } 410 | 411 | // calculate the sum 412 | int wordCount = (packetSize - 1) / 2; // -1 for sync word, /2 for 16bit words 413 | 414 | int sum = 0; 415 | int lsb = 0; 416 | int msb = 0; 417 | for(int i = 0; i < wordCount; i++) { 418 | lsb = 0xFF & rx_buffer[i*2]; 419 | msb = 0xFF & rx_buffer[i*2 + 1]; 420 | sum += (msb << 8 | lsb); 421 | } 422 | 423 | // extract the checksum 424 | lsb = 0xFF & rx_buffer[wordCount*2]; 425 | msb = 0xFF & rx_buffer[wordCount*2 + 1]; 426 | int checksum = (msb << 8 | lsb); 427 | 428 | // add samples to the database if the checksum passed 429 | sum %= 65535; 430 | if(sum == checksum) 431 | Controller.insertSamples(samples); 432 | else 433 | System.err.println("checksum failed"); 434 | 435 | } 436 | } 437 | }); 438 | serialPortThread.setPriority(Thread.MAX_PRIORITY); 439 | serialPortThread.setName("Serial Port Receiver"); 440 | serialPortThread.start(); 441 | 442 | notifySerialPortListeners(SERIAL_CONNECTION_OPENED); 443 | return; 444 | 445 | } 446 | 447 | } 448 | 449 | /** 450 | * Disconnects from the active serial port and stops the data processing thread. 451 | */ 452 | public static void disconnectFromSerialPort() { 453 | 454 | dataStructureDefined.set(false); 455 | 456 | if(Model.portName.equals("Test")) { 457 | 458 | Tester.stopTransmission(); 459 | notifySerialPortListeners(SERIAL_CONNECTION_CLOSED); 460 | 461 | } else { 462 | 463 | if(serialPortThread != null) { 464 | serialPortThread.interrupt(); 465 | } 466 | 467 | } 468 | 469 | } 470 | 471 | /** 472 | * Registers a listener that will be notified when a chart is added or removed. 473 | * 474 | * @param listener The listener to be notified. 475 | */ 476 | public static void addChartsListener(ChartListener listener) { 477 | 478 | chartListeners.add(listener); 479 | 480 | } 481 | 482 | /** 483 | * Notifies all registered listeners about a new chart. 484 | */ 485 | private static void notifyChartListenersOfAddition(PositionedChart chart) { 486 | 487 | for(ChartListener listener : chartListeners) 488 | listener.chartAdded(chart); 489 | 490 | } 491 | 492 | /** 493 | * Notifies all registered Listeners about a removed chart. 494 | */ 495 | private static void notifyChartListenersOfRemoval(PositionedChart chart) { 496 | 497 | for(ChartListener listener : chartListeners) 498 | listener.chartRemoved(chart); 499 | 500 | } 501 | 502 | /** 503 | * @param chart New chart to insert and display. 504 | */ 505 | public static void addPositionedChart(PositionedChart chart) { 506 | 507 | Model.charts.add(chart); 508 | notifyChartListenersOfAddition(chart); 509 | 510 | } 511 | 512 | /** 513 | * Removes all charts. 514 | */ 515 | public static void removeAllPositionedCharts() { 516 | 517 | for(PositionedChart chart : Model.charts) 518 | notifyChartListenersOfRemoval(chart); 519 | 520 | Model.charts.clear(); 521 | 522 | } 523 | 524 | /** 525 | * Checks if a region is available in the ChartsRegion. 526 | * 527 | * @param x1 The x-coordinate of a bounding-box corner in the ChartsRegion grid. 528 | * @param y1 The y-coordinate of a bounding-box corner in the ChartsRegion grid. 529 | * @param x2 The x-coordinate of the opposite bounding-box corner in the ChartsRegion grid. 530 | * @param y2 The y-coordinate of the opposite bounding-box corner in the ChartsRegion grid. 531 | * @return True if available, false if not. 532 | */ 533 | public static boolean gridRegionAvailable(int x1, int y1, int x2, int y2) { 534 | 535 | int topLeftX = x1 < x2 ? x1 : x2; 536 | int topLeftY = y1 < y2 ? y1 : y2; 537 | int bottomRightX = x2 > x1 ? x2 : x1; 538 | int bottomRightY = y2 > y1 ? y2 : y1; 539 | 540 | for(PositionedChart chart : Model.charts) 541 | if(chart.regionOccupied(topLeftX, topLeftY, bottomRightX, bottomRightY)) 542 | return false; 543 | 544 | return true; 545 | 546 | } 547 | 548 | /** 549 | * Repositions and resizes all charts. 550 | * 551 | * @param columnWidth The width of a column in the ChartsRegion grid. 552 | * @param rowHeight The height of a row in the ChartsRegion grid. 553 | */ 554 | public static void repositionCharts(int columnWidth, int rowHeight) { 555 | 556 | for(PositionedChart chart : Model.charts) 557 | chart.reposition(columnWidth, rowHeight); 558 | 559 | } 560 | 561 | /** 562 | * @return The frequency of samples, in Hz. 563 | */ 564 | public static int getSampleRate() { 565 | 566 | return Model.sampleRate; 567 | 568 | } 569 | 570 | /** 571 | * @param rate The frequency of samples, in Hz. 572 | */ 573 | public static void setSampleRate(int rate) { 574 | 575 | Model.sampleRate = rate; 576 | 577 | } 578 | 579 | /** 580 | * @return The default color to use when defining the data structure. 581 | */ 582 | public static Color getDefaultLineColor() { 583 | 584 | return Model.lineColorDefault; 585 | 586 | } 587 | 588 | /** 589 | * A helper function that calculates the sample count of datasets. 590 | * Since datasets may contain different numbers of samples (due to live insertion of new samples), the smallest count is returned to ensure validity. 591 | * 592 | * @param dataset The Dataset[]. 593 | * @return Smallest sample count from the datasets. 594 | */ 595 | static int getSamplesCount(Dataset[] dataset) { 596 | 597 | int[] count = new int[dataset.length]; 598 | for(int i = 0; i < dataset.length; i ++) 599 | count[i] = dataset[i].size(); 600 | Arrays.sort(count); 601 | return count[0]; 602 | 603 | } 604 | 605 | /** 606 | * Inserts one new sample into each of the datasets. 607 | * 608 | * @param newSamples A double[] containing one sample for each dataset. 609 | */ 610 | static void insertSamples(double[] newSamples) { 611 | 612 | for(int i = 0; i < newSamples.length; i++) 613 | Controller.getDatasetByIndex(i).add(newSamples[i]); // FIXME should be byLocation, but that breaks Binary mode 614 | 615 | } 616 | 617 | /** 618 | * Allows reception of data from the UART. This should only be called after the data structure has been fully defined. 619 | */ 620 | static void startReceivingData() { 621 | 622 | dataStructureDefined.set(true); 623 | 624 | } 625 | 626 | /** 627 | * @return The number of bytes in a complete Binary data packet (including the 0xAA sync word.) 628 | */ 629 | static int getBinaryPacketSize() { 630 | 631 | Dataset lastDataset = Controller.getDatasetByIndex(Controller.getDatasetsCount() - 1); 632 | return lastDataset.location + lastDataset.processor.getByteCount(); 633 | 634 | } 635 | 636 | /** 637 | * @return An array of BinaryProcessor's that each describe their data type and can convert raw bytes into a number. 638 | */ 639 | static BinaryProcessor[] getBinaryProcessors() { 640 | 641 | BinaryProcessor[] processor = new BinaryProcessor[2]; 642 | 643 | processor[0] = new BinaryProcessor() { 644 | 645 | @Override public String toString() { return "uint16 LSB First"; } 646 | @Override public int getByteCount() { return 2; } 647 | @Override public double extractValue(byte[] rawByte) { return (double) ((0xFF & rawByte[1]) << 8 | (0xFF & rawByte[0])); } 648 | 649 | }; 650 | 651 | processor[1] = new BinaryProcessor() { 652 | 653 | @Override public String toString() { return "uint16 MSB First"; } 654 | @Override public int getByteCount() { return 2; } 655 | @Override public double extractValue(byte[] rawByte) { return (double) ((0xFF & rawByte[0]) << 8 | (0xFF & rawByte[1])); } 656 | 657 | }; 658 | 659 | return processor; 660 | 661 | } 662 | 663 | /** 664 | * Saves the current state to a file. The state consists of: grid row and column counts, serial port settings, data structure definition, and details for each chart. 665 | * 666 | * @param outputFilePath An absolute path to a .txt file. 667 | */ 668 | static void saveLayout(String outputFilePath) { 669 | 670 | try { 671 | 672 | PrintWriter outputFile = new PrintWriter(new File(outputFilePath), "UTF-8"); 673 | outputFile.println("Telemetry Viewer File Format v0.1"); 674 | outputFile.println(""); 675 | 676 | outputFile.println("Grid Settings:"); 677 | outputFile.println(""); 678 | outputFile.println("\tcolumn count = " + Model.gridColumns); 679 | outputFile.println("\trow count = " + Model.gridRows); 680 | outputFile.println(""); 681 | 682 | outputFile.println("Serial Port Settings:"); 683 | outputFile.println(""); 684 | outputFile.println("\tport = " + port.getSystemPortName()); 685 | outputFile.println("\tbaud = " + port.getBaudRate()); 686 | outputFile.println("\tpacket type = " + Model.packetType); 687 | outputFile.println("\tsample rate = " + Model.sampleRate); 688 | outputFile.println(""); 689 | 690 | outputFile.println(Model.datasets.size() + " Data Structure Locations:"); 691 | 692 | for(Dataset dataset : Model.datasets.values()) { 693 | 694 | int processorIndex = 0; 695 | BinaryProcessor[] processors = Controller.getBinaryProcessors(); 696 | for(int i = 0; i < processors.length; i++) 697 | if(dataset.processor == processors[i]) 698 | processorIndex = i; 699 | 700 | outputFile.println(""); 701 | outputFile.println("\tlocation = " + dataset.location); 702 | outputFile.println("\tprocessor index = " + processorIndex); 703 | outputFile.println("\tname = " + dataset.name); 704 | outputFile.println("\tcolor = " + String.format("0x%02X%02X%02X", dataset.color.getRed(), dataset.color.getGreen(), dataset.color.getBlue())); 705 | outputFile.println("\tunit = " + dataset.unit); 706 | outputFile.println("\tconversion factor a = " + dataset.conversionFactorA); 707 | outputFile.println("\tconversion factor b = " + dataset.conversionFactorB); 708 | 709 | } 710 | 711 | outputFile.println(""); 712 | outputFile.println(Model.charts.size() + " Charts:"); 713 | 714 | for(PositionedChart chart : Model.charts) { 715 | 716 | outputFile.println(""); 717 | outputFile.println("\tchart type = " + chart.toString()); 718 | outputFile.println("\tduration = " + chart.duration); 719 | outputFile.println("\ttop left x = " + chart.topLeftX); 720 | outputFile.println("\ttop left y = " + chart.topLeftY); 721 | outputFile.println("\tbottom right x = " + chart.bottomRightX); 722 | outputFile.println("\tbottom right y = " + chart.bottomRightY); 723 | outputFile.println("\tdatasets count = " + chart.datasets.length); 724 | for(int i = 0; i < chart.datasets.length; i++) 725 | outputFile.println("\t\tdataset location = " + chart.datasets[i].location); 726 | 727 | } 728 | 729 | outputFile.close(); 730 | 731 | } catch (IOException e) { 732 | 733 | JOptionPane.showMessageDialog(null, "Unable to save the file.", "Error: Unable to Save the File", JOptionPane.ERROR_MESSAGE); 734 | 735 | } 736 | 737 | } 738 | 739 | /** 740 | * Opens a file and resets the current state to the state defined in that file. 741 | * The state consists of: grid row and column counts, serial port settings, data structure definition, and details for each chart. 742 | * 743 | * @param inputFilePath An absolute path to a .txt file. 744 | */ 745 | static void openLayout(String inputFilePath) { 746 | 747 | Controller.removeAllDatasets(); 748 | Controller.disconnectFromSerialPort(); 749 | 750 | try { 751 | 752 | List lines = Files.readAllLines(new File(inputFilePath).toPath(), StandardCharsets.UTF_8); 753 | int n = 0; 754 | 755 | verify(lines.get(n++).equals("Telemetry Viewer File Format v0.1")); 756 | verify(lines.get(n++).equals("")); 757 | 758 | verify(lines.get(n++).equals("Grid Settings:")); 759 | verify(lines.get(n++).equals("")); 760 | 761 | verify(lines.get(n++).startsWith("\tcolumn count = ")); 762 | int gridColumns = Integer.parseInt(lines.get(n-1).substring(16)); 763 | verify(lines.get(n++).startsWith("\trow count = ")); 764 | int gridRows = Integer.parseInt(lines.get(n-1).substring(13)); 765 | verify(lines.get(n++).equals("")); 766 | 767 | Controller.setGridColumns(gridColumns); 768 | Controller.setGridRows(gridRows); 769 | 770 | verify(lines.get(n++).equals("Serial Port Settings:")); 771 | verify(lines.get(n++).equals("")); 772 | verify(lines.get(n++).startsWith("\tport = ")); 773 | String portName = lines.get(n-1).substring(8); 774 | verify(lines.get(n++).startsWith("\tbaud = ")); 775 | int baudRate = Integer.parseInt(lines.get(n-1).substring(8)); 776 | verify(lines.get(n++).startsWith("\tpacket type = ")); 777 | String packetType = lines.get(n-1).substring(15); 778 | verify(lines.get(n++).startsWith("\tsample rate = ")); 779 | int sampleRate = Integer.parseInt(lines.get(n-1).substring(15)); 780 | verify(lines.get(n++).equals("")); 781 | 782 | Controller.connectToSerialPort(sampleRate, packetType, portName, baudRate); 783 | 784 | verify(lines.get(n++).endsWith(" Data Structure Locations:")); 785 | int locationsCount = Integer.parseInt(lines.get(n-1).split(" ")[0]); 786 | 787 | for(int i = 0; i < locationsCount; i++) { 788 | 789 | verify(lines.get(n++).equals("")); 790 | verify(lines.get(n++).startsWith("\tlocation = ")); 791 | int location = Integer.parseInt(lines.get(n-1).substring(12)); 792 | verify(lines.get(n++).startsWith("\tprocessor index = ")); 793 | int processorIndex = Integer.parseInt(lines.get(n-1).substring(19)); 794 | BinaryProcessor processor = Controller.getBinaryProcessors()[processorIndex]; 795 | verify(lines.get(n++).startsWith("\tname = ")); 796 | String name = lines.get(n-1).substring(8); 797 | verify(lines.get(n++).startsWith("\tcolor = 0x")); 798 | int colorNumber = Integer.parseInt(lines.get(n-1).substring(11), 16); 799 | Color color = new Color(colorNumber); 800 | verify(lines.get(n++).startsWith("\tunit = ")); 801 | String unit = lines.get(n-1).substring(8); 802 | verify(lines.get(n++).startsWith("\tconversion factor a = ")); 803 | double conversionFactorA = Double.parseDouble(lines.get(n-1).substring(23)); 804 | verify(lines.get(n++).startsWith("\tconversion factor b = ")); 805 | double conversionFactorB = Double.parseDouble(lines.get(n-1).substring(23)); 806 | 807 | Controller.insertDataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB); 808 | 809 | } 810 | 811 | Controller.startReceivingData(); 812 | try{ Thread.sleep(3000); } catch(Exception e) { } 813 | 814 | verify(lines.get(n++).equals("")); 815 | verify(lines.get(n++).endsWith(" Charts:")); 816 | int chartsCount = Integer.parseInt(lines.get(n-1).split(" ")[0]); 817 | 818 | for(int i = 0; i < chartsCount; i++) { 819 | 820 | verify(lines.get(n++).equals("")); 821 | verify(lines.get(n++).startsWith("\tchart type = ")); 822 | String chartType = lines.get(n-1).substring(14); 823 | verify(lines.get(n++).startsWith("\tduration = ")); 824 | int duration = Integer.parseInt(lines.get(n-1).substring(12)); 825 | verify(lines.get(n++).startsWith("\ttop left x = ")); 826 | int topLeftX = Integer.parseInt(lines.get(n-1).substring(14)); 827 | verify(lines.get(n++).startsWith("\ttop left y = ")); 828 | int topLeftY = Integer.parseInt(lines.get(n-1).substring(14)); 829 | verify(lines.get(n++).startsWith("\tbottom right x = ")); 830 | int bottomRightX = Integer.parseInt(lines.get(n-1).substring(18)); 831 | verify(lines.get(n++).startsWith("\tbottom right y = ")); 832 | int bottomRightY = Integer.parseInt(lines.get(n-1).substring(18)); 833 | verify(lines.get(n++).startsWith("\tdatasets count = ")); 834 | int datasetsCount = Integer.parseInt(lines.get(n-1).substring(18)); 835 | 836 | Dataset[] datasets = new Dataset[datasetsCount]; 837 | 838 | for(int j = 0; j < datasetsCount; j++) { 839 | 840 | verify(lines.get(n++).startsWith("\t\tdataset location = ")); 841 | int location = Integer.parseInt(lines.get(n-1).substring(21)); 842 | datasets[j] = Controller.getDatasetByLocation(location); 843 | 844 | } 845 | 846 | for(ChartDescriptor descriptor : Controller.getChartDescriptors()) 847 | if(descriptor.toString().equals(chartType)) { 848 | PositionedChart chart = descriptor.createChart(topLeftX, topLeftY, bottomRightX, bottomRightY, duration, datasets); 849 | Controller.addPositionedChart(chart); 850 | break; 851 | } 852 | 853 | } 854 | 855 | } catch (IOException e) { 856 | 857 | JOptionPane.showMessageDialog(null, "Unable to open the file.", "Error: Unable to Open the File", JOptionPane.ERROR_MESSAGE); 858 | 859 | } catch(AssertionError e) { 860 | 861 | JOptionPane.showMessageDialog(null, "Unable to parse the file.", "Error: Unable to Parse the File", JOptionPane.ERROR_MESSAGE); 862 | 863 | } 864 | 865 | } 866 | 867 | /** 868 | * A helper function that is essentially an "assert" and throws an exception if the assert fails. 869 | * 870 | * @param good If true nothing will happen, if false an AssertionError will be thrown. 871 | */ 872 | private static void verify(boolean good) { 873 | 874 | if(!good) 875 | throw new AssertionError(); 876 | 877 | } 878 | 879 | /** 880 | * This specifies the target period for how long to wait between the rendering of frames. 881 | * This is the upper limit, if the CPU/GPU can't keep up, the rendering will of course slow down. 882 | * 15ms will ensure smooth updates on a 60Hz monitor. Making this number bigger will reduce CPU/GPU load but make the charts appear to stutter. 883 | * 884 | * @return The ideal number of milliseconds between frames. 885 | */ 886 | public static int getTargetFramePeriod() { 887 | 888 | return 15; 889 | 890 | } 891 | 892 | } 893 | -------------------------------------------------------------------------------- /ControlsRegion.java: -------------------------------------------------------------------------------- 1 | import java.awt.Dimension; 2 | import java.awt.event.ActionEvent; 3 | import java.awt.event.ActionListener; 4 | import java.awt.event.FocusEvent; 5 | import java.awt.event.FocusListener; 6 | import java.util.concurrent.atomic.AtomicBoolean; 7 | 8 | import javax.swing.Box; 9 | import javax.swing.BoxLayout; 10 | import javax.swing.JButton; 11 | import javax.swing.JComboBox; 12 | import javax.swing.JFileChooser; 13 | import javax.swing.JFrame; 14 | import javax.swing.JLabel; 15 | import javax.swing.JOptionPane; 16 | import javax.swing.JPanel; 17 | import javax.swing.JTextField; 18 | import javax.swing.SwingUtilities; 19 | import javax.swing.border.EmptyBorder; 20 | 21 | /** 22 | * The panel of controls located at the bottom of the main window. 23 | */ 24 | @SuppressWarnings("serial") 25 | public class ControlsRegion extends JPanel { 26 | 27 | JButton openLayoutButton; 28 | JButton saveLayoutButton; 29 | JButton resetButton; 30 | 31 | JTextField columnsTextfield; 32 | JTextField rowsTextfield; 33 | 34 | JTextField sampleRateTextfield; 35 | JComboBox packetTypeCombobox; 36 | JComboBox portNamesCombobox; 37 | JComboBox baudRatesCombobox; 38 | JButton connectButton; 39 | 40 | AtomicBoolean waitingForSerialConnection; 41 | AtomicBoolean waitingForSerialDisconnection; 42 | 43 | /** 44 | * Creates the panel of controls and registers their event handlers. 45 | */ 46 | public ControlsRegion() { 47 | 48 | super(); 49 | setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); 50 | setBorder(new EmptyBorder(5, 5, 5, 5)); 51 | 52 | waitingForSerialConnection = new AtomicBoolean(false); 53 | waitingForSerialDisconnection = new AtomicBoolean(false); 54 | 55 | openLayoutButton = new JButton("Open Layout"); 56 | openLayoutButton.addActionListener(new ActionListener() { 57 | @Override public void actionPerformed(ActionEvent e) { 58 | JFileChooser inputFile = new JFileChooser(); 59 | JFrame parentWindow = (JFrame) SwingUtilities.windowForComponent(ControlsRegion.this); 60 | if(inputFile.showOpenDialog(parentWindow) == JFileChooser.APPROVE_OPTION) { 61 | String filePath = inputFile.getSelectedFile().getAbsolutePath(); 62 | Controller.openLayout(filePath); 63 | } 64 | } 65 | }); 66 | 67 | saveLayoutButton = new JButton("Save Layout"); 68 | saveLayoutButton.setEnabled(false); 69 | saveLayoutButton.addActionListener(new ActionListener() { 70 | @Override public void actionPerformed(ActionEvent e) { 71 | JFileChooser saveFile = new JFileChooser(); 72 | JFrame parentWindow = (JFrame) SwingUtilities.windowForComponent(ControlsRegion.this); 73 | if(saveFile.showSaveDialog(parentWindow) == JFileChooser.APPROVE_OPTION) { 74 | String filePath = saveFile.getSelectedFile().getAbsolutePath(); 75 | if(!filePath.endsWith(".txt")) 76 | filePath += ".txt"; 77 | Controller.saveLayout(filePath); 78 | } 79 | } 80 | }); 81 | 82 | resetButton = new JButton("Reset"); 83 | resetButton.addActionListener(new ActionListener() { 84 | @Override public void actionPerformed(ActionEvent arg0) { 85 | Controller.removeAllPositionedCharts(); 86 | } 87 | }); 88 | 89 | columnsTextfield = new JTextField(Integer.toString(Controller.getGridColumns()), 3); 90 | columnsTextfield.setMinimumSize(columnsTextfield.getPreferredSize()); 91 | columnsTextfield.setMaximumSize(columnsTextfield.getPreferredSize()); 92 | columnsTextfield.addFocusListener(new FocusListener() { 93 | @Override public void focusLost(FocusEvent fe) { 94 | try { 95 | Controller.setGridColumns(Integer.parseInt(columnsTextfield.getText().trim())); 96 | } catch(Exception e) { 97 | columnsTextfield.setText(Integer.toString(Controller.getGridColumns())); 98 | } 99 | } 100 | 101 | @Override public void focusGained(FocusEvent fe) { 102 | columnsTextfield.selectAll(); 103 | } 104 | }); 105 | 106 | rowsTextfield = new JTextField(Integer.toString(Controller.getGridRows()), 3); 107 | rowsTextfield.setMinimumSize(rowsTextfield.getPreferredSize()); 108 | rowsTextfield.setMaximumSize(rowsTextfield.getPreferredSize()); 109 | rowsTextfield.addFocusListener(new FocusListener() { 110 | @Override public void focusLost(FocusEvent fe) { 111 | try { 112 | Controller.setGridRows(Integer.parseInt(rowsTextfield.getText().trim())); 113 | } catch(Exception e) { 114 | rowsTextfield.setText(Integer.toString(Controller.getGridRows())); 115 | } 116 | } 117 | 118 | @Override public void focusGained(FocusEvent fe) { 119 | rowsTextfield.selectAll(); 120 | } 121 | }); 122 | 123 | Controller.addGridChangedListener(new GridChangedListener() { 124 | @Override public void gridChanged(int columns, int rows) { 125 | columnsTextfield.setText(Integer.toString(columns)); 126 | rowsTextfield.setText(Integer.toString(rows)); 127 | } 128 | }); 129 | 130 | sampleRateTextfield = new JTextField(Integer.toString(Controller.getSampleRate()), 4); 131 | sampleRateTextfield.setMinimumSize(sampleRateTextfield.getPreferredSize()); 132 | sampleRateTextfield.setMaximumSize(sampleRateTextfield.getPreferredSize()); 133 | sampleRateTextfield.addFocusListener(new FocusListener() { 134 | @Override public void focusLost(FocusEvent fe) { 135 | try { 136 | Controller.setSampleRate(Integer.parseInt(sampleRateTextfield.getText().trim())); 137 | } catch(Exception e) { 138 | sampleRateTextfield.setText(Integer.toString(Controller.getSampleRate())); 139 | } 140 | } 141 | 142 | @Override public void focusGained(FocusEvent fe) { 143 | sampleRateTextfield.selectAll(); 144 | } 145 | }); 146 | 147 | packetTypeCombobox = new JComboBox(); 148 | for(String packetType : Controller.getPacketTypes()) 149 | packetTypeCombobox.addItem(packetType); 150 | packetTypeCombobox.setMaximumSize(packetTypeCombobox.getPreferredSize()); 151 | 152 | portNamesCombobox = new JComboBox(); 153 | for(String portName : Controller.getSerialPortNames()) 154 | portNamesCombobox.addItem(portName); 155 | portNamesCombobox.setMaximumSize(portNamesCombobox.getPreferredSize()); 156 | 157 | baudRatesCombobox = new JComboBox(); 158 | for(int baudRate : Controller.getBaudRates()) 159 | baudRatesCombobox.addItem(baudRate); 160 | baudRatesCombobox.setMaximumRowCount(baudRatesCombobox.getItemCount()); 161 | baudRatesCombobox.setMaximumSize(baudRatesCombobox.getPreferredSize()); 162 | 163 | Controller.addSerialPortListener(new SerialPortListener() { 164 | @Override public void connectionOpened(int sampleRate, String packetType, String portName, int baudRate) { 165 | 166 | // enable or disable UI elements 167 | openLayoutButton.setEnabled(true); 168 | saveLayoutButton.setEnabled(true); 169 | sampleRateTextfield.setEnabled(false); 170 | packetTypeCombobox.setEnabled(false); 171 | portNamesCombobox.setEnabled(false); 172 | baudRatesCombobox.setEnabled(false); 173 | connectButton.setEnabled(true); 174 | 175 | // update UI state 176 | sampleRateTextfield.setText(Integer.toString(sampleRate)); 177 | for(int i = 0; i < packetTypeCombobox.getItemCount(); i++) 178 | if(packetTypeCombobox.getItemAt(i).equals(packetType)) { 179 | packetTypeCombobox.setSelectedIndex(i); 180 | break; 181 | } 182 | for(int i = 0; i < portNamesCombobox.getItemCount(); i++) 183 | if(portNamesCombobox.getItemAt(i).equals(portName)) { 184 | portNamesCombobox.setSelectedIndex(i); 185 | break; 186 | } 187 | for(int i = 0; i < baudRatesCombobox.getItemCount(); i++) 188 | if(baudRatesCombobox.getItemAt(i).equals(baudRate)) { 189 | baudRatesCombobox.setSelectedIndex(i); 190 | break; 191 | } 192 | connectButton.setText("Disconnect"); 193 | 194 | // show the DataStructureWindow if the user initiated the connection 195 | if(waitingForSerialConnection.compareAndSet(true, false)) { 196 | JFrame parentWindow = (JFrame) SwingUtilities.windowForComponent(ControlsRegion.this); 197 | new DataStructureWindow(parentWindow, packetType, portName.equals("Test")); 198 | } 199 | 200 | } 201 | 202 | @Override public void connectionClosed() { 203 | 204 | // enable or disable UI elements 205 | openLayoutButton.setEnabled(true); 206 | saveLayoutButton.setEnabled(true); 207 | sampleRateTextfield.setEnabled(true); 208 | packetTypeCombobox.setEnabled(true); 209 | portNamesCombobox.setEnabled(true); 210 | baudRatesCombobox.setEnabled(true); 211 | connectButton.setEnabled(true); 212 | 213 | // update UI state 214 | connectButton.setText("Connect"); 215 | 216 | if(waitingForSerialDisconnection.compareAndSet(true, false)){ 217 | // do nothing, this was expected 218 | } else { 219 | // notify the user because they did not initiate the disconnection 220 | JFrame parentWindow = (JFrame) SwingUtilities.windowForComponent(ControlsRegion.this); 221 | JOptionPane.showMessageDialog(parentWindow, "Serial connection lost.", "Serial Connection Lost", JOptionPane.WARNING_MESSAGE); 222 | } 223 | 224 | } 225 | }); 226 | 227 | connectButton = new JButton("Connect"); 228 | connectButton.addActionListener(new ActionListener() { 229 | @Override public void actionPerformed(ActionEvent arg0) { 230 | if(connectButton.getText().equals("Connect")) { 231 | 232 | if(portNamesCombobox.getSelectedItem() == null) { 233 | JOptionPane.showMessageDialog(null, "Error: No port name specified.", "No Port Name Specified", JOptionPane.ERROR_MESSAGE); 234 | return; 235 | } 236 | 237 | int sampleRate = Integer.parseInt(sampleRateTextfield.getText()); 238 | String packetType = packetTypeCombobox.getSelectedItem().toString(); 239 | String portName = portNamesCombobox.getSelectedItem().toString(); 240 | int baudRate = (int) baudRatesCombobox.getSelectedItem(); 241 | 242 | openLayoutButton.setEnabled(false); 243 | saveLayoutButton.setEnabled(false); 244 | sampleRateTextfield.setEnabled(false); 245 | packetTypeCombobox.setEnabled(false); 246 | portNamesCombobox.setEnabled(false); 247 | baudRatesCombobox.setEnabled(false); 248 | connectButton.setEnabled(false); 249 | 250 | waitingForSerialConnection.set(true); 251 | Controller.connectToSerialPort(sampleRate, packetType, portName, baudRate); 252 | 253 | } else if(connectButton.getText().equals("Disconnect")) { 254 | 255 | openLayoutButton.setEnabled(false); 256 | saveLayoutButton.setEnabled(false); 257 | sampleRateTextfield.setEnabled(false); 258 | packetTypeCombobox.setEnabled(false); 259 | portNamesCombobox.setEnabled(false); 260 | baudRatesCombobox.setEnabled(false); 261 | connectButton.setEnabled(false); 262 | 263 | waitingForSerialDisconnection.set(true); 264 | Controller.disconnectFromSerialPort(); 265 | 266 | } 267 | 268 | } 269 | }); 270 | 271 | // show the components 272 | add(openLayoutButton); 273 | add(Box.createHorizontalStrut(5)); 274 | add(saveLayoutButton); 275 | add(Box.createHorizontalStrut(5)); 276 | add(resetButton); 277 | add(Box.createHorizontalStrut(5)); 278 | add(Box.createHorizontalGlue()); 279 | add(new JLabel("Grid size:")); 280 | add(Box.createHorizontalStrut(5)); 281 | add(columnsTextfield); 282 | add(Box.createHorizontalStrut(5)); 283 | add(new JLabel("x")); 284 | add(Box.createHorizontalStrut(5)); 285 | add(rowsTextfield); 286 | add(Box.createHorizontalGlue()); 287 | add(Box.createHorizontalStrut(5)); 288 | add(new JLabel("Sample Rate (Hz)")); 289 | add(Box.createHorizontalStrut(5)); 290 | add(sampleRateTextfield); 291 | add(Box.createHorizontalStrut(5)); 292 | add(packetTypeCombobox); 293 | add(Box.createHorizontalStrut(5)); 294 | add(portNamesCombobox); 295 | add(Box.createHorizontalStrut(5)); 296 | add(baudRatesCombobox); 297 | add(Box.createHorizontalStrut(5)); 298 | add(connectButton); 299 | 300 | // set minimum panel width to 120% of the "preferred" width 301 | Dimension size = getPreferredSize(); 302 | size.width = (int) (size.width * 1.2); 303 | setMinimumSize(size); 304 | setPreferredSize(size); 305 | 306 | } 307 | 308 | } 309 | -------------------------------------------------------------------------------- /DataStructureWindow.java: -------------------------------------------------------------------------------- 1 | import java.awt.BorderLayout; 2 | import java.awt.Color; 3 | import java.awt.Dimension; 4 | import java.awt.GridLayout; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | import java.awt.event.FocusEvent; 8 | import java.awt.event.FocusListener; 9 | import java.awt.event.KeyEvent; 10 | import java.awt.event.KeyListener; 11 | import java.awt.font.FontRenderContext; 12 | import javax.swing.Box; 13 | import javax.swing.JButton; 14 | import javax.swing.JColorChooser; 15 | import javax.swing.JComboBox; 16 | import javax.swing.JDialog; 17 | import javax.swing.JFrame; 18 | import javax.swing.JLabel; 19 | import javax.swing.JOptionPane; 20 | import javax.swing.JPanel; 21 | import javax.swing.JScrollPane; 22 | import javax.swing.JTable; 23 | import javax.swing.JTextField; 24 | import javax.swing.border.EmptyBorder; 25 | import javax.swing.table.AbstractTableModel; 26 | 27 | /** 28 | * The window that the user uses to specify details about the CSV columns or Binary element byte offsets and types. 29 | */ 30 | @SuppressWarnings("serial") 31 | public class DataStructureWindow extends JDialog { 32 | 33 | JPanel dataEntryPanel; 34 | JPanel tablePanel; 35 | 36 | JTextField locationTextfield; 37 | JComboBox datatypeCombobox; 38 | JTextField nameTextfield; 39 | JButton colorButton; 40 | JTextField unitTextfield; 41 | JTextField conversionFactorATextfield; 42 | JTextField conversionFactorBTextfield; 43 | JLabel unitLabel; 44 | 45 | JButton addButton; 46 | JButton resetButton; 47 | JButton doneButton; 48 | 49 | JTable dataStructureTable; 50 | JScrollPane scrollableDataStructureTable; 51 | 52 | /** 53 | * Creates a new window where the user can define the CSV or Binary data structure. 54 | * 55 | * @param parentWindow Which window to center this DataStructureWindow over. 56 | * @param packetType One of the strings from Controller.getPacketTypes() 57 | * @param testMode If true, the user will only be able to view, not edit, the data structure. 58 | */ 59 | public DataStructureWindow(JFrame parentWindow, String packetType, boolean testMode) { 60 | 61 | super(); 62 | 63 | setTitle(testMode ? "Data Structure (Not Editable in Test Mode)" : "Data Structure"); 64 | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 65 | setLayout(new BorderLayout()); 66 | 67 | ActionListener pressEnterToAddRow = new ActionListener() { 68 | @Override public void actionPerformed(ActionEvent e) { 69 | addButton.doClick(); 70 | } 71 | }; 72 | 73 | locationTextfield = new JTextField(packetType.equals("Binary") ? "1" : "0", 3); 74 | locationTextfield.addFocusListener(new FocusListener() { 75 | @Override public void focusLost(FocusEvent fe) { 76 | try { 77 | locationTextfield.setText(locationTextfield.getText().trim()); 78 | int i = Integer.parseInt(locationTextfield.getText()); 79 | if(packetType.equals("Binary") && i == 0) 80 | throw new Exception(); 81 | } catch(Exception e) { 82 | locationTextfield.setText(packetType.equals("Binary") ? "1" : "0"); 83 | } 84 | } 85 | 86 | @Override public void focusGained(FocusEvent fe) { 87 | locationTextfield.selectAll(); 88 | } 89 | }); 90 | if(packetType.equals("ASCII CSVs")) 91 | locationTextfield.setEnabled(false); 92 | 93 | // only used for the Binary packet type 94 | datatypeCombobox = new JComboBox(); 95 | for(BinaryProcessor processor : Controller.getBinaryProcessors()) 96 | datatypeCombobox.addItem(processor); 97 | 98 | nameTextfield = new JTextField("", 15); 99 | nameTextfield.addActionListener(pressEnterToAddRow); 100 | nameTextfield.addFocusListener(new FocusListener() { 101 | @Override public void focusLost(FocusEvent e) { 102 | nameTextfield.setText(nameTextfield.getText().trim()); 103 | } 104 | 105 | @Override public void focusGained(FocusEvent e) { 106 | nameTextfield.selectAll(); 107 | } 108 | }); 109 | 110 | colorButton = new JButton("\u25B2"); 111 | colorButton.setForeground(Controller.getDefaultLineColor()); 112 | colorButton.addActionListener(new ActionListener() { 113 | @Override public void actionPerformed(ActionEvent e) { 114 | Color color = JColorChooser.showDialog(DataStructureWindow.this, "Pick a Color for " + nameTextfield.getText(), Color.BLACK); 115 | if(color != null) 116 | colorButton.setForeground(color); 117 | } 118 | }); 119 | 120 | unitTextfield = new JTextField("", 15); 121 | unitTextfield.addActionListener(pressEnterToAddRow); 122 | unitTextfield.addFocusListener(new FocusListener() { 123 | @Override public void focusLost(FocusEvent arg0) { 124 | unitTextfield.setText(unitTextfield.getText().trim()); 125 | unitLabel.setText(unitTextfield.getText()); 126 | } 127 | 128 | @Override public void focusGained(FocusEvent arg0) { 129 | unitTextfield.selectAll(); 130 | } 131 | }); 132 | unitTextfield.addKeyListener(new KeyListener() { 133 | @Override public void keyReleased(KeyEvent ke) { 134 | unitTextfield.setText(unitTextfield.getText().trim()); 135 | unitLabel.setText(unitTextfield.getText()); 136 | } 137 | @Override public void keyPressed(KeyEvent ke) { } 138 | @Override public void keyTyped(KeyEvent ke) { } 139 | }); 140 | 141 | conversionFactorATextfield = new JTextField("1.0", 4); 142 | conversionFactorATextfield.addActionListener(pressEnterToAddRow); 143 | conversionFactorATextfield.addFocusListener(new FocusListener() { 144 | @Override public void focusLost(FocusEvent arg0) { 145 | try { 146 | conversionFactorATextfield.setText(conversionFactorATextfield.getText().trim()); 147 | double value = Double.parseDouble(conversionFactorATextfield.getText()); 148 | if(value == 0.0 || value == Double.NaN || value == Double.POSITIVE_INFINITY || value == Double.NEGATIVE_INFINITY) throw new Exception(); 149 | } catch(Exception e) { 150 | conversionFactorATextfield.setText("1.0"); 151 | } 152 | } 153 | 154 | @Override public void focusGained(FocusEvent arg0) { 155 | conversionFactorATextfield.selectAll(); 156 | } 157 | }); 158 | 159 | conversionFactorBTextfield = new JTextField("1.0", 4); 160 | conversionFactorBTextfield.addActionListener(pressEnterToAddRow); 161 | conversionFactorBTextfield.addFocusListener(new FocusListener() { 162 | @Override public void focusLost(FocusEvent arg0) { 163 | try { 164 | conversionFactorBTextfield.setText(conversionFactorBTextfield.getText().trim()); 165 | double value = Double.parseDouble(conversionFactorBTextfield.getText()); 166 | if(value == 0.0 || value == Double.NaN || value == Double.POSITIVE_INFINITY || value == Double.NEGATIVE_INFINITY) throw new Exception(); 167 | } catch(Exception e) { 168 | conversionFactorBTextfield.setText("1.0"); 169 | } 170 | } 171 | 172 | @Override public void focusGained(FocusEvent arg0) { 173 | conversionFactorBTextfield.selectAll(); 174 | } 175 | }); 176 | 177 | unitLabel = new JLabel("_______________"); 178 | unitLabel.setMinimumSize(unitLabel.getPreferredSize()); 179 | unitLabel.setPreferredSize(unitLabel.getPreferredSize()); 180 | unitLabel.setHorizontalAlignment(JLabel.LEFT); 181 | unitLabel.setText(""); 182 | 183 | addButton = new JButton("Add"); 184 | addButton.addActionListener(new ActionListener() { 185 | @Override public void actionPerformed(ActionEvent e) { 186 | int location = Integer.parseInt(locationTextfield.getText()); 187 | BinaryProcessor processor = (BinaryProcessor) datatypeCombobox.getSelectedItem(); 188 | String name = nameTextfield.getText().trim(); 189 | Color color = colorButton.getForeground(); 190 | String unit = unitTextfield.getText(); 191 | double conversionFactorA = Double.parseDouble(conversionFactorATextfield.getText()); 192 | double conversionFactorB = Double.parseDouble(conversionFactorBTextfield.getText()); 193 | 194 | if(name.equals("")) { 195 | JOptionPane.showMessageDialog(tablePanel, "A name is required.", "Error: Name Required", JOptionPane.ERROR_MESSAGE); 196 | } else { 197 | Controller.insertDataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB); 198 | dataStructureTable.revalidate(); 199 | dataStructureTable.repaint(); 200 | int newLocation = packetType.equals("ASCII CSVs") ? location + 1 : location + processor.getByteCount(); 201 | locationTextfield.setText(Integer.toString(newLocation)); 202 | nameTextfield.requestFocus(); 203 | nameTextfield.selectAll(); 204 | } 205 | } 206 | }); 207 | 208 | resetButton = new JButton("Reset"); 209 | resetButton.addActionListener(new ActionListener() { 210 | @Override public void actionPerformed(ActionEvent arg0) { 211 | Controller.removeAllDatasets(); 212 | dataStructureTable.revalidate(); 213 | dataStructureTable.repaint(); 214 | locationTextfield.setText(packetType.equals("Binary") ? "1" : "0"); 215 | nameTextfield.requestFocus(); 216 | } 217 | }); 218 | 219 | doneButton = new JButton("Done"); 220 | doneButton.addActionListener(new ActionListener() { 221 | @Override public void actionPerformed(ActionEvent e) { 222 | if(Controller.getDatasetsCount() == 0) { 223 | JOptionPane.showMessageDialog(tablePanel, "At least one entry is required.", "Error: No Entries", JOptionPane.ERROR_MESSAGE); 224 | } else { 225 | Controller.startReceivingData(); 226 | dispose(); 227 | } 228 | } 229 | }); 230 | 231 | dataEntryPanel = new JPanel(); 232 | dataEntryPanel.setBorder(new EmptyBorder(5, 5, 0, 5)); 233 | if(packetType.equals("ASCII CSVs")) 234 | dataEntryPanel.add(new JLabel("Column Number")); 235 | else if(packetType.equals("Binary")) 236 | dataEntryPanel.add(new JLabel("Byte Offset")); 237 | dataEntryPanel.add(locationTextfield); 238 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 239 | if(packetType.equals("Binary")) { 240 | dataEntryPanel.add(datatypeCombobox); 241 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 242 | } 243 | dataEntryPanel.add(new JLabel("Name")); 244 | dataEntryPanel.add(nameTextfield); 245 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 246 | dataEntryPanel.add(new JLabel("Color")); 247 | dataEntryPanel.add(colorButton); 248 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 249 | dataEntryPanel.add(new JLabel("Unit")); 250 | dataEntryPanel.add(unitTextfield); 251 | dataEntryPanel.add(Box.createHorizontalStrut(80)); 252 | dataEntryPanel.add(conversionFactorATextfield); 253 | dataEntryPanel.add(new JLabel(" LSBs = ")); 254 | dataEntryPanel.add(conversionFactorBTextfield); 255 | dataEntryPanel.add(unitLabel); 256 | dataEntryPanel.add(Box.createHorizontalStrut(80)); 257 | dataEntryPanel.add(addButton); 258 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 259 | dataEntryPanel.add(resetButton); 260 | dataEntryPanel.add(Box.createHorizontalStrut(20)); 261 | dataEntryPanel.add(doneButton); 262 | 263 | dataStructureTable = new JTable(new AbstractTableModel() { 264 | @Override public String getColumnName(int column) { 265 | if(column == 0) 266 | return packetType.equals("ASCII CSVs") ? "Column Number" : "Byte Offset, Data Type"; 267 | else if(column == 1) 268 | return "Name"; 269 | else if(column == 2) 270 | return "Color"; 271 | else if(column == 3) 272 | return "Unit"; 273 | else if(column == 4) 274 | return "Conversion Ratio"; 275 | else 276 | return "Error"; 277 | } 278 | 279 | @Override public Object getValueAt(int row, int column) { 280 | 281 | if(row == 0 && packetType.equals("Binary")) { 282 | 283 | if(column == 0) 284 | return "0, Sync Word"; 285 | else if(column == 1) 286 | return "0xAA"; 287 | else if(column == 2) 288 | return ""; 289 | else if(column == 3) 290 | return ""; 291 | else if(column == 4) 292 | return ""; 293 | else 294 | return null; 295 | 296 | } 297 | 298 | if(packetType.equals("Binary")) 299 | row--; 300 | 301 | Dataset dataset = Controller.getDatasetByIndex(row); 302 | 303 | if(column == 0) 304 | return packetType.equals("ASCII CSVs") ? dataset.location : dataset.location + ", " + dataset.processor.toString(); 305 | else if(column == 1) 306 | return dataset.name; 307 | else if(column == 2) 308 | return "\u25B2"; 309 | else if(column == 3) 310 | return dataset.unit; 311 | else if(column == 4) 312 | return String.format("%3.3f LSBs = %3.3f %s", dataset.conversionFactorA, dataset.conversionFactorB, dataset.unit); 313 | else 314 | return null; 315 | } 316 | 317 | @Override public int getRowCount() { 318 | 319 | int count = Controller.getDatasetsCount(); 320 | 321 | if(packetType.equals("Binary")) 322 | return count + 1; 323 | else 324 | return count; 325 | 326 | } 327 | 328 | @Override public int getColumnCount() { 329 | return 5; 330 | } 331 | }); 332 | scrollableDataStructureTable = new JScrollPane(dataStructureTable); 333 | 334 | tablePanel = new JPanel(new GridLayout(1, 1)); 335 | tablePanel.setBorder(new EmptyBorder(5, 5, 5, 5)); 336 | tablePanel.add(scrollableDataStructureTable, BorderLayout.CENTER); 337 | dataStructureTable.setRowHeight((int) tablePanel.getFont().getStringBounds("Abcdefghijklmnopqrstuvwxyz", new FontRenderContext(null, true, true)).getHeight()); // fix display scaling issue 338 | 339 | add(dataEntryPanel, BorderLayout.NORTH); 340 | add(tablePanel, BorderLayout.CENTER); 341 | 342 | pack(); 343 | setMinimumSize(new Dimension(getPreferredSize().width, 500)); 344 | setLocationRelativeTo(parentWindow); 345 | 346 | nameTextfield.requestFocus(); 347 | 348 | if(testMode) { 349 | locationTextfield.setEnabled(false); 350 | datatypeCombobox.setEnabled(false); 351 | nameTextfield.setEnabled(false); 352 | colorButton.setEnabled(false); 353 | unitTextfield.setEnabled(false); 354 | conversionFactorATextfield.setEnabled(false); 355 | conversionFactorBTextfield.setEnabled(false); 356 | addButton.setEnabled(false); 357 | resetButton.setEnabled(false); 358 | } 359 | 360 | setModal(true); 361 | setVisible(true); 362 | 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /Dataset.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import java.util.concurrent.atomic.AtomicInteger; 3 | 4 | /** 5 | * Defines all of the details about a CSV column or Binary packet element, and stores all of its samples. 6 | */ 7 | public class Dataset { 8 | 9 | // constants defined at constructor-time 10 | final int location; 11 | final BinaryProcessor processor; 12 | final String name; 13 | final Color color; 14 | final String unit; 15 | final double conversionFactorA; 16 | final double conversionFactorB; 17 | final double conversionFactor; 18 | 19 | // samples are stored in an array of double[]'s, each containing 1M doubles, and allocated as needed. 20 | // access to the samples is controlled with an atomic integer, providing lockless concurrency if only there is only one writer. 21 | final int slotSize = (int) Math.pow(2, 20); // 1M doubles per slot 22 | final int slotCount = (Integer.MAX_VALUE / slotSize) + 1; 23 | double[][] slot; 24 | AtomicInteger size; 25 | 26 | /** 27 | * Creates a new object that describes one dataset and stores all of its samples. 28 | * 29 | * @param location CSV column number, or Binary byte offset. 30 | * @param processor Data processor for the raw samples in the Binary data packet. (Ignored in CSV mode.) 31 | * @param name Descriptive name of what the samples represent. 32 | * @param color Color to use when visualizing the samples. 33 | * @param unit Descriptive name of how the samples are quantified. 34 | * @param conversionFactorA This many unprocessed LSBs... 35 | * @param conversionFactorB ... equals this many units. 36 | */ 37 | public Dataset(int location, BinaryProcessor processor, String name, Color color, String unit, double conversionFactorA, double conversionFactorB) { 38 | 39 | this.location = location; 40 | this.processor = processor; 41 | this.name = name; 42 | this.color = color; 43 | this.unit = unit; 44 | this.conversionFactorA = conversionFactorA; 45 | this.conversionFactorB = conversionFactorB; 46 | this.conversionFactor = conversionFactorB / conversionFactorA; 47 | 48 | slot = new double[slotCount][]; 49 | size = new AtomicInteger(0); 50 | 51 | } 52 | 53 | /** 54 | * @return The name of this dataset. 55 | */ 56 | @Override public String toString() { 57 | 58 | return name; 59 | 60 | } 61 | 62 | /** 63 | * @return Count of the stored samples. 64 | */ 65 | public int size() { 66 | 67 | return size.get(); 68 | 69 | } 70 | 71 | /** 72 | * @param index Which sample to obtain. 73 | * @return The sample. 74 | */ 75 | public double get(int index) { 76 | 77 | int slotNumber = index / slotSize; 78 | int slotIndex = index % slotSize; 79 | return slot[slotNumber][slotIndex]; 80 | 81 | } 82 | 83 | /** 84 | * @param value New raw sample to be converted and then appended to the dataset. 85 | */ 86 | public void add(double value) { 87 | 88 | int currentSize = size.get(); 89 | int slotNumber = currentSize / slotSize; 90 | int slotIndex = currentSize % slotSize; 91 | if(slotIndex == 0) 92 | slot[slotNumber] = new double[slotSize]; 93 | slot[slotNumber][slotIndex] = value * conversionFactor; 94 | size.incrementAndGet(); 95 | 96 | } 97 | 98 | /** 99 | * Empties the dataset, but does not free any memory. 100 | */ 101 | public void clear() { 102 | 103 | size.set(0); 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /FrequencyDomainChart.java: -------------------------------------------------------------------------------- 1 | import java.awt.Graphics; 2 | import java.awt.Image; 3 | import java.awt.image.BufferedImage; 4 | 5 | import javax.swing.SwingUtilities; 6 | 7 | import org.jfree.chart.ChartFactory; 8 | import org.jfree.chart.JFreeChart; 9 | import org.jfree.chart.axis.LogAxis; 10 | import org.jfree.chart.axis.NumberAxis; 11 | import org.jfree.data.xy.XYSeries; 12 | import org.jfree.data.xy.XYSeriesCollection; 13 | 14 | /** 15 | * A line chart of a Fourier transform. 16 | */ 17 | @SuppressWarnings("serial") 18 | public class FrequencyDomainChart extends PositionedChart { 19 | 20 | Image chartImage; 21 | 22 | public static ChartDescriptor getDescriptor() { 23 | 24 | return new ChartDescriptor() { 25 | 26 | @Override public String toString() { return "Frequency Domain Chart"; } 27 | @Override public int getMinimumDuration() { return 10; } 28 | @Override public int getDefaultDuration() { return 1000; } 29 | @Override public int getMaximumDuration() { return Integer.MAX_VALUE; } 30 | @Override public String[] getInputNames() { return new String[] {"Data"}; } 31 | 32 | @Override public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 33 | return new FrequencyDomainChart(x1, y1, x2, y2, chartDuration, chartInputs); 34 | } 35 | 36 | }; 37 | 38 | } 39 | 40 | @Override public String toString() { 41 | 42 | return "Frequency Domain Chart"; 43 | 44 | } 45 | 46 | public FrequencyDomainChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 47 | 48 | super(x1, y1, x2, y2, chartDuration, chartInputs); 49 | 50 | // spawn a thread that draws the chart 51 | Thread thread = new Thread(new Runnable() { 52 | @Override public void run() { 53 | 54 | while(true) { 55 | 56 | long startTime = System.currentTimeMillis(); 57 | 58 | // draw the chart 59 | int chartWidth = getWidth(); 60 | int chartHeight = getHeight(); 61 | if(chartWidth < 1) chartWidth = 1; 62 | if(chartHeight < 1) chartHeight = 1; 63 | 64 | String chartTitle = ""; 65 | String xAxisTitle = "Frequency (Hz)"; 66 | String yAxisTitle = "Power (W)"; 67 | 68 | BufferedImage image = getFrequencyDomainChart(chartTitle, xAxisTitle, yAxisTitle, chartWidth, chartHeight); 69 | 70 | SwingUtilities.invokeLater(new Runnable() { 71 | @Override public void run() { 72 | 73 | // free resources of old image 74 | if(chartImage != null) 75 | chartImage.flush(); 76 | 77 | // paint new image 78 | chartImage = image; 79 | repaint(); 80 | 81 | } 82 | }); 83 | 84 | // end this thread if we are no longer visible (the user clicked the "Reset" button) 85 | if(!isShowing()) 86 | break; 87 | 88 | // wait if needed before drawing a new frame 89 | long timeToNextFrame = startTime + Controller.getTargetFramePeriod() - System.currentTimeMillis(); 90 | if(timeToNextFrame <= 0) 91 | continue; 92 | else 93 | try{ Thread.sleep(timeToNextFrame); } catch(Exception e) {} 94 | 95 | } 96 | 97 | } 98 | 99 | }); 100 | String inputNames = ""; 101 | for(int i = 0; i < datasets.length; i++) 102 | inputNames += datasets[i].name + ", "; 103 | thread.setName(String.format("FrequencyDomainChart of: %s", inputNames)); 104 | thread.start(); 105 | 106 | } 107 | 108 | private BufferedImage getFrequencyDomainChart(String chartTitle, String xAxisTitle, String yAxisTitle, int chartWidth, int chartHeight) { 109 | 110 | int maxX = Controller.getSamplesCount(datasets) - 1; 111 | int minX = maxX - duration; 112 | if(minX < 0) minX = 0; 113 | 114 | // bin size (in Hertz) is the reciprocal of the window size (in seconds) 115 | // example: 500ms window -> 1/0.5 = 2 Hz bin size 116 | double samplesPerSecond = Controller.getSampleRate(); 117 | double sampleCount = maxX - minX + 1; 118 | double binSizeHz = 1.0 / (sampleCount / samplesPerSecond); 119 | 120 | // maximum frequency range (in Hertz) is from 0 to the sample rate (in Hertz), divided by 2 121 | // example: sampling at 1kHz -> 0 Hz to 1000/2 = 500 Hz 122 | double maxFrequencyHz = samplesPerSecond / 2.0; 123 | 124 | // calc the DFT, assuming the samples are in Volts, and assuming the load is a unit load (1 ohm) 125 | XYSeries powerLevels = new XYSeries("DFT of " + datasets[0].name); 126 | double realV; 127 | double imaginaryV; 128 | double powerW; 129 | for(double frequencyHz = 0; frequencyHz <= maxFrequencyHz; frequencyHz += binSizeHz) { 130 | realV = 0.0; 131 | imaginaryV = 0.0; 132 | for(int x = minX; x <= maxX; x++) { 133 | double sample = datasets[0].get(x); 134 | double timeSec = (double) x / samplesPerSecond; 135 | realV += sample * Math.cos(2.0 * Math.PI * frequencyHz * timeSec); 136 | imaginaryV += sample * Math.sin(2.0 * Math.PI * frequencyHz * timeSec); 137 | } 138 | realV /= sampleCount; 139 | imaginaryV /= sampleCount; 140 | powerW = (realV * realV) + (imaginaryV * imaginaryV); 141 | powerW *= 2; // because DFT is from -Fs to +Fs 142 | powerLevels.add(frequencyHz, powerW); 143 | } 144 | 145 | XYSeriesCollection seriesCollection = new XYSeriesCollection(powerLevels); 146 | JFreeChart frequencyDomainChart = ChartFactory.createXYLineChart(chartTitle, xAxisTitle, yAxisTitle, seriesCollection); 147 | // frequencyDomainChart.removeLegend(); 148 | frequencyDomainChart.getXYPlot().getRenderer().setSeriesPaint(0, datasets[0].color); 149 | LogAxis verticalAxis = new LogAxis("Power (W)"); 150 | verticalAxis.setLabelFont(frequencyDomainChart.getXYPlot().getDomainAxis().getLabelFont()); 151 | verticalAxis.setTickLabelFont(frequencyDomainChart.getXYPlot().getDomainAxis().getTickLabelFont()); 152 | verticalAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 153 | // verticalAxis.setRange(Math.pow(10.0, -8.0), Math.pow(10.0, 2.0)); 154 | frequencyDomainChart.getXYPlot().setRangeAxis(verticalAxis); 155 | frequencyDomainChart.getXYPlot().getDomainAxis().setRange(0.0, maxFrequencyHz); 156 | frequencyDomainChart.getTitle().setFont(Model.chartTitleFont); 157 | 158 | return frequencyDomainChart.createBufferedImage(chartWidth, chartHeight); 159 | 160 | } 161 | 162 | @Override protected void paintComponent(Graphics g) { 163 | 164 | super.paintComponent(g); 165 | 166 | if(chartImage != null) 167 | g.drawImage(chartImage, 0, 0, null); 168 | 169 | } 170 | 171 | } -------------------------------------------------------------------------------- /GridChangedListener.java: -------------------------------------------------------------------------------- 1 | public interface GridChangedListener { 2 | 3 | public void gridChanged(int columns, int rows); 4 | 5 | } 6 | -------------------------------------------------------------------------------- /HistogramChart.java: -------------------------------------------------------------------------------- 1 | import java.awt.Graphics; 2 | import java.awt.Image; 3 | import java.awt.image.BufferedImage; 4 | 5 | import javax.swing.SwingUtilities; 6 | 7 | import org.jfree.chart.ChartFactory; 8 | import org.jfree.chart.JFreeChart; 9 | import org.jfree.chart.plot.PlotOrientation; 10 | import org.jfree.data.statistics.HistogramDataset; 11 | import org.jfree.data.statistics.HistogramType; 12 | 13 | @SuppressWarnings("serial") 14 | public class HistogramChart extends PositionedChart { 15 | 16 | Image chartImage; 17 | 18 | public static ChartDescriptor getDescriptor() { 19 | 20 | return new ChartDescriptor() { 21 | 22 | @Override public String toString() { return "Histogram"; } 23 | @Override public int getMinimumDuration() { return 10; } 24 | @Override public int getDefaultDuration() { return 1000; } 25 | @Override public int getMaximumDuration() { return Integer.MAX_VALUE; } 26 | @Override public String[] getInputNames() { return new String[] {"Data"}; } 27 | 28 | @Override public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 29 | return new HistogramChart(x1, y1, x2, y2, chartDuration, chartInputs); 30 | } 31 | 32 | }; 33 | 34 | } 35 | 36 | @Override public String toString() { 37 | 38 | return "Histogram"; 39 | 40 | } 41 | 42 | public HistogramChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 43 | 44 | super(x1, y1, x2, y2, chartDuration, chartInputs); 45 | 46 | // spawn a thread that draws the chart 47 | Thread thread = new Thread(new Runnable() { 48 | @Override public void run() { 49 | 50 | while(true) { 51 | 52 | long startTime = System.currentTimeMillis(); 53 | 54 | // draw the chart 55 | int chartWidth = getWidth(); 56 | int chartHeight = getHeight(); 57 | if(chartWidth < 1) chartWidth = 1; 58 | if(chartHeight < 1) chartHeight = 1; 59 | 60 | String chartTitle = ""; 61 | String xAxisTitle = datasets[0].unit + " (" + chartDuration + " Samples of " + datasets[0].name + ")"; 62 | String yAxisTitle = "Relative Frequency"; 63 | 64 | BufferedImage image = getHistogram(chartTitle, xAxisTitle, yAxisTitle, chartWidth, chartHeight); 65 | 66 | SwingUtilities.invokeLater(new Runnable() { 67 | @Override public void run() { 68 | 69 | // free resources of old image 70 | if(chartImage != null) 71 | chartImage.flush(); 72 | 73 | // paint new image 74 | chartImage = image; 75 | repaint(); 76 | 77 | } 78 | }); 79 | 80 | // end this thread if we are no longer visible (the user clicked the "Reset" button) 81 | if(!isShowing()) 82 | break; 83 | 84 | // wait if needed before drawing a new frame 85 | long timeToNextFrame = startTime + Controller.getTargetFramePeriod() - System.currentTimeMillis(); 86 | if(timeToNextFrame <= 0) 87 | continue; 88 | else 89 | try{ Thread.sleep(timeToNextFrame); } catch(Exception e) {} 90 | 91 | } 92 | 93 | } 94 | 95 | }); 96 | String inputNames = ""; 97 | for(int i = 0; i < datasets.length; i++) 98 | inputNames += datasets[i].name + ", "; 99 | thread.setName(String.format("HistogramChart of: %s", inputNames)); 100 | thread.start(); 101 | 102 | } 103 | 104 | private BufferedImage getHistogram(String chartTitle, String xAxisTitle, String yAxisTitle, int chartWidth, int chartHeight) { 105 | 106 | // // create the histogram bins 107 | // double idealVoltage = 50.0; 108 | // double voltageRange = 100.0; 109 | // int binCount = 50; 110 | // double binWidth = voltageRange / (double) binCount; 111 | // 112 | // SimpleHistogramDataset histogramDataset = new SimpleHistogramDataset("Histogram"); 113 | // for(int i = binCount/2; i > 0; i--) { 114 | // double left = idealVoltage - (i * binWidth); 115 | // double right =idealVoltage - ((i-1) * binWidth); 116 | // histogramDataset.addBin(new SimpleHistogramBin(left, right, true, false)); 117 | // } 118 | // for(int i = 0; i < binCount/2; i++) { 119 | // double left = idealVoltage + (i * binWidth); 120 | // double right = idealVoltage + ((i+1) * binWidth); 121 | // histogramDataset.addBin(new SimpleHistogramBin(left, right, true, false)); 122 | // } 123 | // 124 | // // populate the histogram bins 125 | // for(int i = 0; i < dataset[0].getItemCount(); i++) 126 | // try { 127 | // histogramDataset.addObservation((double) dataset[0].getY(i)); 128 | // } catch(Exception e) { 129 | // // occurs if the value doesn't fit in one of the bins 130 | // } 131 | 132 | int sampleCount = Controller.getSamplesCount(datasets); 133 | int maxX = sampleCount - 1; 134 | int minX = maxX - duration; 135 | if(minX < 0) minX = 0; 136 | 137 | double[] samples = new double[maxX - minX + 1]; 138 | for(int i = 0; i < samples.length; i++) 139 | samples[i] = datasets[0].get(i + minX); 140 | 141 | if(samples.length == 0) 142 | return null; 143 | 144 | HistogramDataset histogramDataset = new HistogramDataset(); 145 | histogramDataset.addSeries(datasets[0].name, samples, 50); 146 | histogramDataset.setType(HistogramType.RELATIVE_FREQUENCY); 147 | 148 | // setup the histogram 149 | final JFreeChart histogram = ChartFactory.createHistogram(chartTitle, xAxisTitle, yAxisTitle, histogramDataset, PlotOrientation.VERTICAL, false, false, false); 150 | histogram.getXYPlot().getRenderer().setSeriesPaint(0, datasets[0].color); 151 | // histogram.getXYPlot().getRangeAxis().setStandardTickUnits(NumberAxis.createIntegerTickUnits()); 152 | histogram.getTitle().setFont(Model.chartTitleFont); 153 | 154 | return histogram.createBufferedImage(chartWidth, chartHeight); 155 | 156 | } 157 | 158 | @Override protected void paintComponent(Graphics g) { 159 | 160 | super.paintComponent(g); 161 | 162 | if(chartImage != null) 163 | g.drawImage(chartImage, 0, 0, null); 164 | 165 | } 166 | 167 | } -------------------------------------------------------------------------------- /Main.java: -------------------------------------------------------------------------------- 1 | import java.awt.BorderLayout; 2 | import java.awt.GraphicsEnvironment; 3 | 4 | import javax.swing.JFrame; 5 | import javax.swing.UIManager; 6 | 7 | public class Main { 8 | 9 | JFrame window; 10 | ChartsRegion chartsRegion; 11 | ControlsRegion controlsRegion; 12 | 13 | public Main() { 14 | 15 | window = new JFrame("Telemetry Viewer"); 16 | chartsRegion = new ChartsRegion(); 17 | controlsRegion = new ControlsRegion(); 18 | 19 | window.setLayout(new BorderLayout()); 20 | window.add(chartsRegion, BorderLayout.CENTER); 21 | window.add(controlsRegion, BorderLayout.SOUTH); 22 | 23 | window.setExtendedState(JFrame.MAXIMIZED_BOTH); 24 | window.setSize( (int) (GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().width * 0.6), (int) (GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds().height * 0.6) ); 25 | window.setLocationRelativeTo(null); 26 | 27 | window.setMinimumSize(window.getPreferredSize()); 28 | window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 29 | window.setVisible(true); 30 | 31 | } 32 | 33 | public static void main(String[] args) { 34 | 35 | try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } catch(Exception e){} 36 | 37 | new Main(); 38 | 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Model.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import java.awt.Font; 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.TreeMap; 8 | 9 | public class Model { 10 | 11 | // the grid is the area in the ChartsRegion where users can position and size the charts 12 | final static int gridColumnsDefault = 8; 13 | final static int gridColumnsMinimum = 1; 14 | final static int gridColumnsMaximum = 12; 15 | static int gridColumns = gridColumnsDefault; 16 | 17 | final static int gridRowsDefault = 8; 18 | final static int gridRowsMinimum = 1; 19 | final static int gridRowsMaximum = 12; 20 | static int gridRows = gridRowsDefault; 21 | 22 | // serial port state 23 | static int sampleRate = 1000; 24 | static String packetType = ""; 25 | static String portName = ""; 26 | static int baudRate = 0; 27 | 28 | final static Color lineColorDefault = Color.RED; 29 | 30 | final static Font chartTitleFont = new Font("Arial", Font.PLAIN, 18); 31 | 32 | // attributes of the possible charts 33 | static ChartDescriptor[] chartDescriptors = new ChartDescriptor[] { 34 | TimeDomainChart.getDescriptor(), 35 | TimeDomainChartCached.getDescriptor(), 36 | FrequencyDomainChart.getDescriptor(), 37 | HistogramChart.getDescriptor(), 38 | StatisticsChart.getDescriptor() 39 | }; 40 | 41 | static Map datasets = Collections.synchronizedMap(new TreeMap()); 42 | 43 | static List charts = Collections.synchronizedList(new ArrayList()); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /NewChartWindow.java: -------------------------------------------------------------------------------- 1 | import java.awt.GridLayout; 2 | import java.awt.event.ActionEvent; 3 | import java.awt.event.ActionListener; 4 | import java.awt.event.FocusEvent; 5 | import java.awt.event.FocusListener; 6 | import java.util.ArrayList; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Map.Entry; 11 | 12 | import javax.swing.JButton; 13 | import javax.swing.JCheckBox; 14 | import javax.swing.JComboBox; 15 | import javax.swing.JFrame; 16 | import javax.swing.JLabel; 17 | import javax.swing.JOptionPane; 18 | import javax.swing.JPanel; 19 | import javax.swing.JTextField; 20 | import javax.swing.border.EmptyBorder; 21 | 22 | @SuppressWarnings("serial") 23 | public class NewChartWindow extends JFrame { 24 | 25 | public NewChartWindow(JFrame parentWindow, int x1, int y1, int x2, int y2) { 26 | 27 | if(Controller.getDatasetsCount() == 0) { 28 | JOptionPane.showMessageDialog(this, "Error: The data structure table must be setup before adding charts. Do this by clicking the Connect button located at the bottom-right corner of the main window.", "Error: Empty Data Structure Table", JOptionPane.ERROR_MESSAGE); 29 | dispose(); 30 | return; 31 | } 32 | 33 | setTitle("Add New Chart"); 34 | setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); 35 | 36 | JPanel panel = new JPanel(); 37 | panel.setBorder(new EmptyBorder(10, 10, 10, 10)); 38 | panel.setLayout(new GridLayout(0, 2, 10, 10)); 39 | 40 | add(panel); 41 | 42 | setVisible(true); 43 | 44 | JComboBox chartTypeCombobox = new JComboBox(); 45 | for(ChartDescriptor descriptor : Controller.getChartDescriptors()) 46 | chartTypeCombobox.addItem(descriptor); 47 | 48 | chartTypeCombobox.addActionListener(new ActionListener() { 49 | @Override public void actionPerformed(ActionEvent arg0) { 50 | 51 | panel.removeAll(); 52 | 53 | ChartDescriptor chart = (ChartDescriptor) chartTypeCombobox.getSelectedItem(); 54 | 55 | List> inputComboboxesList = new ArrayList>(); 56 | 57 | panel.add(new JLabel("Chart Type")); 58 | panel.add(chartTypeCombobox); 59 | 60 | JTextField durationTextfield = new JTextField(Integer.toString(chart.getDefaultDuration())); 61 | durationTextfield.addFocusListener(new FocusListener() { 62 | @Override public void focusLost(FocusEvent arg0) { 63 | try { 64 | int value = Integer.parseInt(durationTextfield.getText().trim()); 65 | if(value < chart.getMinimumDuration()) 66 | durationTextfield.setText(Integer.toString(chart.getMinimumDuration())); 67 | else if(value > chart.getMaximumDuration()) 68 | durationTextfield.setText(Integer.toString(chart.getMaximumDuration())); 69 | } catch(Exception e) { 70 | durationTextfield.setText(Integer.toString(chart.getDefaultDuration())); 71 | } 72 | } 73 | 74 | @Override public void focusGained(FocusEvent arg0) { 75 | durationTextfield.selectAll(); 76 | } 77 | }); 78 | 79 | panel.add(new JLabel("Duration (Sample Count)")); 80 | panel.add(durationTextfield); 81 | 82 | if(chart.getInputNames() != null) { 83 | 84 | // if the chart type has an inputNames array, it accepts a specific number of inputs, so show drop-down boxes for each of the possible inputs 85 | for(String inputName : chart.getInputNames()) { 86 | panel.add(new JLabel(inputName)); 87 | JComboBox inputValue = new JComboBox(); 88 | for(Dataset dataset : Controller.getAllDatasets()) 89 | inputValue.addItem(dataset); 90 | inputComboboxesList.add(inputValue); 91 | panel.add(inputValue); 92 | } 93 | 94 | panel.add(new JLabel(" ")); 95 | panel.add(new JLabel(" ")); 96 | 97 | JButton addChartButton = new JButton("Add Chart"); 98 | addChartButton.addActionListener(new ActionListener() { 99 | @Override public void actionPerformed(ActionEvent arg0) { 100 | Dataset[] chartInputs = new Dataset[inputComboboxesList.size()]; 101 | for(int i = 0; i < inputComboboxesList.size(); i++) 102 | chartInputs[i] = (Dataset) inputComboboxesList.get(i).getSelectedItem(); 103 | 104 | int chartDuration = Integer.parseInt(durationTextfield.getText().trim()); 105 | Controller.addPositionedChart(chart.createChart(x1, y1, x2, y2, chartDuration, chartInputs)); 106 | dispose(); 107 | } 108 | }); 109 | 110 | panel.add(new JLabel(" ")); 111 | panel.add(addChartButton); 112 | 113 | } else { 114 | 115 | // if the chart type has a null inputNames array, it accepts 1+ inputs (no limit), so show checkboxes for every possible input 116 | panel.add(new JLabel("Specify inputs: ")); 117 | Map inputs = new LinkedHashMap(); 118 | for(Dataset dataset : Controller.getAllDatasets()) { 119 | JCheckBox checkbox = new JCheckBox(dataset.name); 120 | inputs.put(checkbox, dataset); 121 | 122 | panel.add(checkbox); 123 | panel.add(new JLabel("")); 124 | } 125 | 126 | panel.add(new JLabel(" ")); 127 | 128 | JButton addChartButton = new JButton("Add Chart"); 129 | addChartButton.addActionListener(new ActionListener() { 130 | @Override public void actionPerformed(ActionEvent arg0) { 131 | 132 | // calc how many inputs were chosen 133 | int inputsCount = 0; 134 | for(JCheckBox inputPossibility : inputs.keySet()) 135 | if(inputPossibility.isSelected()) 136 | inputsCount++; 137 | 138 | if(inputsCount == 0) { 139 | JOptionPane.showMessageDialog(null, "At least one input is required.", "Error: No Inputs", JOptionPane.ERROR_MESSAGE); 140 | return; 141 | } 142 | 143 | // get the chosen inputs 144 | Dataset[] chartInputs = new Dataset[inputsCount]; 145 | int input = 0; 146 | for(Entry inputPossibility : inputs.entrySet()) 147 | if(inputPossibility.getKey().isSelected()) 148 | chartInputs[input++] = inputPossibility.getValue(); 149 | 150 | int chartDuration = Integer.parseInt(durationTextfield.getText().trim()); 151 | Controller.addPositionedChart(chart.createChart(x1, y1, x2, y2, chartDuration, chartInputs)); 152 | dispose(); 153 | } 154 | }); 155 | 156 | panel.add(new JLabel(" ")); 157 | panel.add(addChartButton); 158 | 159 | } 160 | 161 | panel.revalidate(); 162 | panel.repaint(); 163 | setSize(getPreferredSize()); 164 | } 165 | }); 166 | 167 | chartTypeCombobox.getActionListeners()[0].actionPerformed(null); 168 | 169 | setLocationRelativeTo(parentWindow); 170 | 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /PositionedChart.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | import javax.swing.BorderFactory; 3 | import javax.swing.JOptionPane; 4 | import javax.swing.JPanel; 5 | 6 | @SuppressWarnings("serial") 7 | public class PositionedChart extends JPanel { 8 | 9 | // grid coordinates, not pixels 10 | int topLeftX; 11 | int topLeftY; 12 | int bottomRightX; 13 | int bottomRightY; 14 | 15 | int duration; 16 | Dataset[] datasets; 17 | 18 | public PositionedChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 19 | 20 | super(); 21 | 22 | topLeftX = x1 < x2 ? x1 : x2; 23 | topLeftY = y1 < y2 ? y1 : y2; 24 | bottomRightX = x2 > x1 ? x2 : x1; 25 | bottomRightY = y2 > y1 ? y2 : y1; 26 | 27 | duration = chartDuration; 28 | datasets = chartInputs; 29 | 30 | setBorder(BorderFactory.createLineBorder(Color.BLACK)); 31 | 32 | } 33 | 34 | public void reposition(int columnWidth, int rowHeight) { 35 | 36 | int x = topLeftX * columnWidth; 37 | int y = topLeftY * rowHeight; 38 | int width = (bottomRightX - topLeftX + 1) * columnWidth; 39 | int height = (bottomRightY - topLeftY + 1) * rowHeight; 40 | setBounds(x, y, width, height); 41 | 42 | } 43 | 44 | public boolean regionOccupied(int startX, int startY, int endX, int endY) { 45 | 46 | for(int x = startX; x <= endX; x++) 47 | for(int y = startY; y <= endY; y++) 48 | if(x >= topLeftX && x <= bottomRightX && y >= topLeftY && y <= bottomRightY) 49 | return true; 50 | 51 | return false; 52 | 53 | } 54 | 55 | public void reconnectDatasets() { 56 | 57 | try { 58 | for(int i = 0; i < datasets.length; i++) 59 | datasets[i] = Controller.getDatasetByLocation(datasets[i].location); 60 | } catch(Exception e) { 61 | JOptionPane.showMessageDialog(this, "The data structure has significantly changed so the charts will be removed. New charts can be added as usual.", "Notice: Data Structure Changed Significantly", JOptionPane.WARNING_MESSAGE); 62 | Controller.removeAllPositionedCharts(); 63 | } 64 | 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /SerialPortListener.java: -------------------------------------------------------------------------------- 1 | 2 | public interface SerialPortListener { 3 | 4 | public void connectionOpened(int sampleRate, String packetType, String portName, int baudRate); 5 | 6 | public void connectionClosed(); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /StatisticsChart.java: -------------------------------------------------------------------------------- 1 | import java.awt.BorderLayout; 2 | import java.awt.Font; 3 | import java.awt.font.FontRenderContext; 4 | 5 | import javax.swing.JLabel; 6 | import javax.swing.JScrollPane; 7 | import javax.swing.JTable; 8 | import javax.swing.SwingConstants; 9 | import javax.swing.SwingUtilities; 10 | import javax.swing.border.EmptyBorder; 11 | import javax.swing.table.DefaultTableCellRenderer; 12 | 13 | import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; 14 | 15 | @SuppressWarnings("serial") 16 | public class StatisticsChart extends PositionedChart { 17 | 18 | JTable statisticsTable; 19 | JLabel statisticsLabel; 20 | 21 | public static ChartDescriptor getDescriptor() { 22 | 23 | return new ChartDescriptor() { 24 | 25 | @Override public String toString() { return "Statistics"; } 26 | @Override public int getMinimumDuration() { return 10; } 27 | @Override public int getDefaultDuration() { return 1000; } 28 | @Override public int getMaximumDuration() { return Integer.MAX_VALUE; } 29 | @Override public String[] getInputNames() { return null; } 30 | 31 | @Override public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 32 | return new StatisticsChart(x1, y1, x2, y2, chartDuration, chartInputs); 33 | } 34 | 35 | }; 36 | 37 | } 38 | 39 | @Override public String toString() { 40 | 41 | return "Statistics"; 42 | 43 | } 44 | 45 | public StatisticsChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 46 | 47 | super(x1, y1, x2, y2, chartDuration, chartInputs); 48 | 49 | // prepare the panel 50 | setLayout(new BorderLayout()); 51 | 52 | String[] columnNames = new String[datasets.length]; 53 | for(int i = 0; i < columnNames.length; i++) 54 | columnNames[i] = datasets[i].name; 55 | 56 | statisticsTable = new JTable(new String[13][datasets.length], columnNames); 57 | statisticsTable.setFont(new Font("Consolas", Font.PLAIN, statisticsTable.getFont().getSize())); 58 | statisticsTable.setRowHeight((int) statisticsTable.getFont().getStringBounds("Abcdefghijklmnopqrstuvwxyz", new FontRenderContext(null, true, true)).getHeight()); // fix display scaling issue 59 | statisticsTable.getTableHeader().setReorderingAllowed(false); // prevent users from dragging columns, which breaks the logic below 60 | 61 | DefaultTableCellRenderer rightAligned = new DefaultTableCellRenderer(); 62 | rightAligned.setHorizontalAlignment(SwingConstants.RIGHT); 63 | for(int i = 0; i < statisticsTable.getColumnModel().getColumnCount(); i++) 64 | statisticsTable.getColumnModel().getColumn(i).setCellRenderer(rightAligned); 65 | 66 | statisticsLabel = new JLabel("Statistics (Last " + duration + " Samples)"); 67 | statisticsLabel.setFont(new Font("Geneva", Font.BOLD, 20 * (int) Controller.getDisplayScalingFactor())); 68 | statisticsLabel.setHorizontalAlignment(SwingConstants.CENTER); 69 | statisticsLabel.setBorder(new EmptyBorder(5, 5, 5, 5)); 70 | 71 | JScrollPane scrollableStatisticsTable = new JScrollPane(statisticsTable); 72 | scrollableStatisticsTable.setBorder(new EmptyBorder(5, 5, 5, 5)); 73 | 74 | add(statisticsLabel, BorderLayout.NORTH); 75 | add(scrollableStatisticsTable, BorderLayout.CENTER); 76 | 77 | // spawn a thread that draws the chart 78 | Thread thread = new Thread(new Runnable() { 79 | @Override public void run() { 80 | 81 | while(true) { 82 | 83 | long startTime = System.currentTimeMillis(); 84 | 85 | // redraw the table 86 | int sampleCount = Controller.getSamplesCount(datasets); 87 | int maxX = sampleCount - 1; 88 | int minX = maxX - duration; 89 | if(minX < 0) minX = 0; 90 | 91 | for(int column = 0; column < datasets.length; column++) { 92 | 93 | double[] samples = new double[maxX - minX + 1]; 94 | for(int i = 0; i < samples.length; i++) 95 | samples[i] = datasets[column].get(i + minX); 96 | 97 | DescriptiveStatistics stats = new DescriptiveStatistics(samples); 98 | String[] rowText = new String[13]; 99 | rowText[0] = String.format(" Mean: % 9.3f %s", stats.getMean(), datasets[column].unit); 100 | rowText[1] = String.format(" Minimum: % 9.3f %s", stats.getMin(), datasets[column].unit); 101 | rowText[2] = String.format(" Maximum: % 9.3f %s", stats.getMax(), datasets[column].unit); 102 | rowText[3] = String.format(" Standard Deviation: % 9.3f %s", stats.getStandardDeviation(), datasets[column].unit); 103 | rowText[4] = String.format(" 90th Percentile: % 9.3f %s", stats.getPercentile(90.0), datasets[column].unit); 104 | rowText[5] = String.format(" 80th Percentile: % 9.3f %s", stats.getPercentile(80.0), datasets[column].unit); 105 | rowText[6] = String.format(" 70th Percentile: % 9.3f %s", stats.getPercentile(70.0), datasets[column].unit); 106 | rowText[7] = String.format(" 60th Percentile: % 9.3f %s", stats.getPercentile(60.0), datasets[column].unit); 107 | rowText[8] = String.format("(Median) 50th Percentile: % 9.3f %s", stats.getPercentile(50.0), datasets[column].unit); 108 | rowText[9] = String.format(" 40th Percentile: % 9.3f %s", stats.getPercentile(40.0), datasets[column].unit); 109 | rowText[10] = String.format(" 30th Percentile: % 9.3f %s", stats.getPercentile(30.0), datasets[column].unit); 110 | rowText[11] = String.format(" 20th Percentile: % 9.3f %s", stats.getPercentile(20.0), datasets[column].unit); 111 | rowText[12] = String.format(" 10th Percentile: % 9.3f %s", stats.getPercentile(10.0), datasets[column].unit); 112 | int col = column; 113 | 114 | SwingUtilities.invokeLater(new Runnable() { 115 | @Override public void run() { 116 | 117 | for(int row = 0; row < rowText.length; row++) 118 | statisticsTable.setValueAt(rowText[row], row, col); 119 | 120 | } 121 | }); 122 | 123 | } 124 | 125 | // end this thread if we are no longer visible (the user clicked the "Reset" button) 126 | if(!isShowing()) 127 | break; 128 | 129 | // wait if needed before drawing a new frame 130 | long timeToNextFrame = startTime + Controller.getTargetFramePeriod() - System.currentTimeMillis(); 131 | if(timeToNextFrame <= 0) 132 | continue; 133 | else 134 | try{ Thread.sleep(timeToNextFrame); } catch(Exception e) {} 135 | 136 | } 137 | 138 | } 139 | 140 | }); 141 | String inputNames = ""; 142 | for(int i = 0; i < datasets.length; i++) 143 | inputNames += datasets[i].name + ", "; 144 | thread.setName(String.format("StatisticsChart of: %s", inputNames)); 145 | thread.start(); 146 | 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /Telemetry Viewer.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/farrellf/Telemetry-Viewer/82f1ec0195acdd516487c9b072e67fc50d42199d/Telemetry Viewer.jar -------------------------------------------------------------------------------- /Tester.java: -------------------------------------------------------------------------------- 1 | import java.awt.Color; 2 | 3 | public class Tester { 4 | 5 | private static Thread transmitter; 6 | 7 | /** 8 | * Simulates the transmission of 3 numbers every 1ms. 9 | * The numbers are pseudo random, and scaled to form a sort of sawtooth waveform. 10 | * This is used to check for proper autoscaling of charts, etc. 11 | */ 12 | @SuppressWarnings("deprecation") 13 | public static void startTransmission() { 14 | 15 | if(transmitter != null) 16 | transmitter.stop(); 17 | 18 | transmitter = new Thread(new Runnable() { 19 | @Override public void run() { 20 | 21 | while(true) { 22 | double scalar = ((System.currentTimeMillis() % 30000) - 15000) / 100.0; 23 | double[] newSamples = new double[] { 24 | (System.nanoTime() % 100) * scalar * 1.0, 25 | (System.nanoTime() % 100) * scalar * 0.8, 26 | (System.nanoTime() % 100) * scalar * 0.6 27 | }; 28 | Controller.insertSamples(newSamples); 29 | Controller.insertSamples(newSamples); 30 | Controller.insertSamples(newSamples); 31 | Controller.insertSamples(newSamples); 32 | Controller.insertSamples(newSamples); 33 | Controller.insertSamples(newSamples); 34 | Controller.insertSamples(newSamples); 35 | Controller.insertSamples(newSamples); 36 | Controller.insertSamples(newSamples); 37 | Controller.insertSamples(newSamples); 38 | try { Thread.sleep(1); } catch(Exception e) {} // wait 1ms 39 | } 40 | 41 | } 42 | }); 43 | transmitter.setName("Test Transmitter"); 44 | transmitter.start(); 45 | 46 | } 47 | 48 | @SuppressWarnings("deprecation") 49 | public static void stopTransmission() { 50 | 51 | if(transmitter != null) 52 | transmitter.stop(); 53 | 54 | } 55 | 56 | public static void populateDataStructure() { 57 | 58 | Controller.removeAllDatasets(); 59 | 60 | int location = 0; 61 | BinaryProcessor processor = Controller.getBinaryProcessors()[0]; 62 | String name = ""; 63 | Color color = null; 64 | String unit = "Volts"; 65 | double conversionFactorA = 1; 66 | double conversionFactorB = 1; 67 | 68 | location = 0; 69 | name = "Waveform A"; 70 | color = Color.RED; 71 | Controller.insertDataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB); 72 | 73 | location = 1; 74 | name = "Waveform B"; 75 | color = Color.GREEN; 76 | Controller.insertDataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB); 77 | 78 | location = 2; 79 | name = "Waveform C"; 80 | color = Color.BLUE; 81 | Controller.insertDataset(location, processor, name, color, unit, conversionFactorA, conversionFactorB); 82 | 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /TimeDomainChart.java: -------------------------------------------------------------------------------- 1 | import java.awt.Graphics; 2 | import java.awt.Image; 3 | import java.awt.image.BufferedImage; 4 | 5 | import javax.swing.SwingUtilities; 6 | 7 | import org.jfree.chart.ChartFactory; 8 | import org.jfree.chart.JFreeChart; 9 | import org.jfree.data.xy.XYSeries; 10 | import org.jfree.data.xy.XYSeriesCollection; 11 | 12 | @SuppressWarnings("serial") 13 | public class TimeDomainChart extends PositionedChart { 14 | 15 | Image chartImage; 16 | 17 | public static ChartDescriptor getDescriptor() { 18 | 19 | return new ChartDescriptor() { 20 | 21 | @Override public String toString() { return "Time Domain Chart"; } 22 | @Override public int getMinimumDuration() { return 10; } 23 | @Override public int getDefaultDuration() { return 1000; } 24 | @Override public int getMaximumDuration() { return Integer.MAX_VALUE; } 25 | @Override public String[] getInputNames() { return null; } 26 | 27 | @Override public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 28 | return new TimeDomainChart(x1, y1, x2, y2, chartDuration, chartInputs); 29 | } 30 | 31 | }; 32 | 33 | } 34 | 35 | @Override public String toString() { 36 | 37 | return "Time Domain Chart"; 38 | 39 | } 40 | 41 | public TimeDomainChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 42 | 43 | super(x1, y1, x2, y2, chartDuration, chartInputs); 44 | 45 | // spawn a thread that draws the chart 46 | Thread thread = new Thread(new Runnable() { 47 | @Override public void run() { 48 | 49 | while(true) { 50 | 51 | long startTime = System.currentTimeMillis(); 52 | 53 | // draw the chart 54 | int chartWidth = getWidth(); 55 | int chartHeight = getHeight(); 56 | if(chartWidth < 1) chartWidth = 1; 57 | if(chartHeight < 1) chartHeight = 1; 58 | 59 | String chartTitle = ""; 60 | String xAxisTitle = "Sample Number"; 61 | String yAxisTitle = datasets[0].unit; 62 | 63 | BufferedImage image = getTimeDomainChart(chartTitle, xAxisTitle, yAxisTitle, chartWidth, chartHeight); 64 | 65 | SwingUtilities.invokeLater(new Runnable() { 66 | @Override public void run() { 67 | 68 | // free resources of old image 69 | if(chartImage != null) 70 | chartImage.flush(); 71 | 72 | // paint new image 73 | chartImage = image; 74 | repaint(); 75 | 76 | } 77 | }); 78 | 79 | // end this thread if we are no longer visible (the user clicked the "Reset" button) 80 | if(!isShowing()) 81 | break; 82 | 83 | // wait if needed before drawing a new frame 84 | long timeToNextFrame = startTime + Controller.getTargetFramePeriod() - System.currentTimeMillis(); 85 | if(timeToNextFrame <= 0) 86 | continue; 87 | else 88 | try{ Thread.sleep(timeToNextFrame); } catch(Exception e) {} 89 | 90 | } 91 | 92 | } 93 | 94 | }); 95 | String inputNames = ""; 96 | for(int i = 0; i < datasets.length; i++) 97 | inputNames += datasets[i].name + ", "; 98 | thread.setName(String.format("TimeDomainChart of: %s", inputNames)); 99 | thread.start(); 100 | 101 | } 102 | 103 | private BufferedImage getTimeDomainChart(String chartTitle, String xAxisTitle, String yAxisTitle, int chartWidth, int chartHeight) { 104 | 105 | XYSeriesCollection seriesCollection = new XYSeriesCollection(); 106 | JFreeChart lineGraph = ChartFactory.createXYLineChart(chartTitle, xAxisTitle, yAxisTitle, seriesCollection); 107 | 108 | // calculate domain 109 | int sampleCount = Controller.getSamplesCount(datasets); 110 | int maxX = sampleCount - 1; 111 | int minX = maxX - duration; 112 | if(minX < 0) minX = 0; 113 | 114 | for(int i = 0; i < datasets.length; i++) { 115 | 116 | XYSeries series = new XYSeries(datasets[i].name); 117 | for(int x = minX; x <= maxX; x++) { 118 | double y = datasets[i].get(x); 119 | series.add(x, y); 120 | } 121 | 122 | seriesCollection.addSeries(series); 123 | lineGraph.getXYPlot().getRenderer().setSeriesPaint(i, datasets[i].color); 124 | 125 | } 126 | 127 | lineGraph.getXYPlot().getDomainAxis().setRange(minX, maxX + 2); // +2 to prevent a range of (0,0) 128 | lineGraph.getTitle().setFont(Model.chartTitleFont); 129 | 130 | return lineGraph.createBufferedImage(chartWidth, chartHeight); 131 | 132 | } 133 | 134 | @Override protected void paintComponent(Graphics g) { 135 | 136 | super.paintComponent(g); 137 | 138 | if(chartImage != null) 139 | g.drawImage(chartImage, 0, 0, null); 140 | 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /TimeDomainChartCached.java: -------------------------------------------------------------------------------- 1 | import java.awt.BasicStroke; 2 | import java.awt.Color; 3 | import java.awt.Font; 4 | import java.awt.Graphics; 5 | import java.awt.Graphics2D; 6 | import java.awt.RenderingHints; 7 | import java.awt.font.FontRenderContext; 8 | import java.awt.geom.AffineTransform; 9 | import java.awt.geom.Line2D; 10 | import java.awt.geom.Path2D; 11 | import java.awt.image.VolatileImage; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.concurrent.atomic.AtomicBoolean; 15 | 16 | import javax.swing.SwingUtilities; 17 | 18 | /** 19 | * This class generates a time-domain line chart and caches the old data (as pixels) to speed up rendering. 20 | * Some GPU-acceleration is also used by means of the VolatileImage class. 21 | * 22 | * Since a cache is used, this class will not be very fast if the cache frequently needs to be flushed and regenerated. 23 | * This can happen if: 24 | * - The chart scrolls by extremely fast 25 | * - The range (y-axis) changes frequently 26 | * - The chart image size changes frequently 27 | * - The domain (x-axis) division size changes 28 | * 29 | * Implementation Notes: 30 | * - The cache is in the form of a VolatileImage[] which is used as a ring buffer. 31 | * - Each image is called a slice. The index of each slice is calculated assuming an infinite cache, 32 | * and that index modulo the array length obtains the actual index for the VolatileImage[] ring buffer. 33 | */ 34 | @SuppressWarnings("serial") 35 | public class TimeDomainChartCached extends PositionedChart { 36 | 37 | VolatileImage image1; 38 | VolatileImage image2; 39 | AtomicBoolean paintImage1; 40 | final boolean drawPartialLastSlice = true; 41 | 42 | public static ChartDescriptor getDescriptor() { 43 | 44 | return new ChartDescriptor() { 45 | 46 | @Override public String toString() { return "Time Domain Chart (Cached)"; } 47 | @Override public int getMinimumDuration() { return 10; } 48 | @Override public int getDefaultDuration() { return 1000; } 49 | @Override public int getMaximumDuration() { return Integer.MAX_VALUE; } 50 | @Override public String[] getInputNames() { return null; } 51 | 52 | @Override public PositionedChart createChart(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartInputs) { 53 | return new TimeDomainChartCached(x1, y1, x2, y2, chartDuration, chartInputs); 54 | } 55 | 56 | }; 57 | 58 | } 59 | 60 | @Override public String toString() { 61 | 62 | return "Time Domain Chart (Cached)"; 63 | 64 | } 65 | 66 | /** 67 | * Constructor, creates a new CachedTimeDomainChart. 68 | * 69 | * @param chartDuration How many samples make up the domain (x-axis.) 70 | * @param displayScalingFactor Determines how big the fonts and line strokes are. 1 = 100%, 2 = 200%, etc. 71 | */ 72 | public TimeDomainChartCached(int x1, int y1, int x2, int y2, int chartDuration, Dataset[] chartDatasets) { 73 | 74 | super(x1, y1, x2, y2, chartDuration, chartDatasets); 75 | 76 | paintImage1 = new AtomicBoolean(true); 77 | 78 | // spawn a thread that draws the chart 79 | Thread thread = new Thread(new Runnable() { 80 | 81 | VolatileImage[] slices; 82 | 83 | // internal state tracking 84 | int previousImageWidth; 85 | int previousImageHeight; 86 | double previousMinY; 87 | double previousMaxY; 88 | double previousXdivisionSpacing; 89 | int lastRenderedSlice; 90 | 91 | // settings 92 | int displayScalingFactor; 93 | Font tickFont; 94 | Font xAxisFont; 95 | Font yAxisFont; 96 | Font legendFont; 97 | int tickFontHeight; 98 | int xAxisFontHeight; 99 | int yAxisFontHeight; 100 | int legendFontHeight; 101 | FontRenderContext frc; 102 | 103 | @Override public void run() { 104 | 105 | slices = new VolatileImage[0]; 106 | 107 | displayScalingFactor = (int) Controller.getDisplayScalingFactor(); 108 | 109 | tickFont = new Font("Geneva", Font.PLAIN, 12 * displayScalingFactor); 110 | xAxisFont = new Font("Geneva", Font.BOLD, 20 * displayScalingFactor); 111 | yAxisFont = new Font("Geneva", Font.BOLD, 20 * displayScalingFactor); 112 | legendFont = new Font("Geneva", Font.BOLD, 14 * displayScalingFactor); 113 | 114 | frc = new FontRenderContext(null, true, true); 115 | tickFontHeight = tickFont.createGlyphVector(frc, "Test").getPixelBounds(frc, 0, 0).height; 116 | xAxisFontHeight = xAxisFont.createGlyphVector(frc, "Test").getPixelBounds(frc, 0, 0).height; 117 | yAxisFontHeight = yAxisFont.createGlyphVector(frc, "Test").getPixelBounds(frc, 0, 0).height; 118 | legendFontHeight = legendFont.createGlyphVector(frc, "Test").getPixelBounds(frc, 0, 0).height; 119 | 120 | previousImageWidth = 0; 121 | previousImageHeight = 0; 122 | previousMinY = 0; 123 | previousMaxY = 0; 124 | previousXdivisionSpacing = 0; 125 | lastRenderedSlice = -1; 126 | 127 | while(true) { 128 | 129 | long startTime = System.currentTimeMillis(); 130 | 131 | // draw the chart 132 | int chartWidth = getWidth(); 133 | int chartHeight = getHeight(); 134 | if(chartWidth < 1) chartWidth = 1; 135 | if(chartHeight < 1) chartHeight = 1; 136 | 137 | generateChart(chartWidth, chartHeight); 138 | 139 | // end this thread if we are no longer visible (the user clicked the "Reset" button) 140 | if(!isShowing()) 141 | break; 142 | 143 | // wait if needed before drawing a new frame 144 | long timeToNextFrame = startTime + Controller.getTargetFramePeriod() - System.currentTimeMillis(); 145 | if(timeToNextFrame <= 0) 146 | continue; 147 | else 148 | try{ Thread.sleep(timeToNextFrame); } catch(Exception e) {} 149 | 150 | } 151 | 152 | } 153 | 154 | /** 155 | * Determines the best y values to use for vertical divisions. The 1/2/5 pattern is used (1,2,5,10,20,50,100,200,500...) 156 | * 157 | * @param chartRegionHeight Number of pixels for the y-axis 158 | * @param minY Y value at the bottom of the chart 159 | * @param maxY Y value at the top of the chart 160 | * @param font Division label font (only used to calculate spacing) 161 | * @return A Map of the y values for each division, keys are Doubles and values are formatted Strings 162 | */ 163 | private Map getVerticalDivisions(int chartRegionHeight, double minY, double maxY) { 164 | 165 | // calculate the best vertical division size 166 | int minSpacingBetweenText = 2 * tickFontHeight; 167 | int maxDivisionsCount = chartRegionHeight / (tickFontHeight + minSpacingBetweenText) + 1; 168 | double divisionSize = (maxY - minY) / maxDivisionsCount; 169 | double closestDivSize1 = Math.pow(10.0, Math.ceil(Math.log10(divisionSize/1.0))) * 1.0; // closest (10^n)*1 that is >= divisionSize, such as 1,10,100,1000 170 | double closestDivSize2 = Math.pow(10.0, Math.ceil(Math.log10(divisionSize/2.0))) * 2.0; // closest (10^n)*2 that is >= divisionSize, such as 2,20,200,2000 171 | double closestDivSize5 = Math.pow(10.0, Math.ceil(Math.log10(divisionSize/5.0))) * 5.0; // closest (10^n)*5 that is >= divisionSize, such as 5,50,500,5000 172 | double error1 = closestDivSize1 - divisionSize; 173 | double error2 = closestDivSize2 - divisionSize; 174 | double error5 = closestDivSize5 - divisionSize; 175 | if(error1 < error2 && error1 < error5) 176 | divisionSize = closestDivSize1; 177 | else if(error2 < error1 && error2 < error5) 178 | divisionSize = closestDivSize2; 179 | else 180 | divisionSize= closestDivSize5; 181 | 182 | // decide if the numbers should be displayed as integers, or as floats to one significant decimal place 183 | int precision = 0; 184 | String format = ""; 185 | if(divisionSize < 0.99) { 186 | precision = 1; 187 | double size = divisionSize; 188 | while(size * Math.pow(10, precision) < 1.0) 189 | precision++; 190 | format = "%." + precision + "f"; 191 | } 192 | 193 | // calculate the values for each vertical division 194 | double firstDivision = maxY - (maxY % divisionSize); 195 | double lastDivision = minY - (minY % divisionSize); 196 | if(firstDivision > maxY) 197 | firstDivision -= divisionSize; 198 | if(lastDivision < minY) 199 | lastDivision += divisionSize; 200 | int divisionCount = (int) Math.round((firstDivision - lastDivision) / divisionSize) + 1; 201 | 202 | Map yValues = new HashMap(); 203 | for(int i = 0; i < divisionCount; i++) { 204 | double number = firstDivision - (i * divisionSize); 205 | String text; 206 | if(precision == 0) { 207 | text = Integer.toString((int) number); 208 | } else { 209 | text = String.format(format, number); 210 | } 211 | yValues.put(number, text); 212 | } 213 | 214 | return yValues; 215 | 216 | } 217 | 218 | /** 219 | * Determines the best x values to use for horizontal divisions. The 1/2/5 pattern is used (1,2,5,10,20,50,100,200,500...) 220 | * 221 | * @param chartRegionWidth Number of pixels for the x-axis 222 | * @param minX X value at the left of the chart 223 | * @param maxX X value at the right of the chart 224 | * @param font Division label font (only used to calculate spacing) 225 | * @return A Map of the x values for each division, keys are Integers and values are formatted Strings 226 | */ 227 | private Map getHorizontalDivisions(int chartRegionWidth, int minX, int maxX) { 228 | 229 | // calculate the best horizontal division size 230 | int textWidth = (int) Double.max(tickFont.getStringBounds(Integer.toString(maxX), frc).getWidth(), tickFont.getStringBounds(Integer.toString(minX), frc).getWidth()); 231 | int minSpacingBetweenText = textWidth; 232 | int maxDivisionsCount = chartRegionWidth / (textWidth + minSpacingBetweenText) + 1; 233 | int divisionSize = (maxX - minX) / maxDivisionsCount; 234 | int closestDivSize1 = (int) Math.pow(10.0, Math.ceil(Math.log10(divisionSize/1.0))) * 1; // closest (10^n)*1 that is >= divisionSize, such as 1,10,100,1000 235 | int closestDivSize2 = (int) Math.pow(10.0, Math.ceil(Math.log10(divisionSize/2.0))) * 2; // closest (10^n)*2 that is >= divisionSize, such as 2,20,200,2000 236 | int closestDivSize5 = (int) Math.pow(10.0, Math.ceil(Math.log10(divisionSize/5.0))) * 5; // closest (10^n)*5 that is >= divisionSize, such as 5,50,500,5000 237 | int error1 = closestDivSize1 - divisionSize; 238 | int error2 = closestDivSize2 - divisionSize; 239 | int error5 = closestDivSize5 - divisionSize; 240 | if(error1 < error2 && error1 < error5) 241 | divisionSize = closestDivSize1; 242 | else if(error2 < error1 && error2 < error5) 243 | divisionSize = closestDivSize2; 244 | else 245 | divisionSize= closestDivSize5; 246 | 247 | // calculate the values for each horizontal division 248 | int firstDivision = maxX - (maxX % divisionSize); 249 | int lastDivision = minX - (minX % divisionSize); 250 | if(firstDivision > maxX) 251 | firstDivision -= divisionSize; 252 | if(lastDivision < minX) 253 | lastDivision += divisionSize; 254 | int divisionCount = ((firstDivision - lastDivision) / divisionSize + 1); 255 | 256 | Map xValues = new HashMap(); 257 | for(int i = 0; i < divisionCount; i++) { 258 | int number = lastDivision + (i * divisionSize); 259 | String text = Integer.toString(number); 260 | xValues.put(number, text); 261 | } 262 | 263 | return xValues; 264 | 265 | } 266 | 267 | /** 268 | * Generates a new chart image. 269 | * 270 | * @param imageWidth Width, in pixels. 271 | * @param imageHeight Height, in pixels. 272 | */ 273 | public void generateChart(int imageWidth, int imageHeight) { 274 | 275 | // long startTime = System.currentTimeMillis(); 276 | 277 | // settings 278 | final int sliceWidth = 32; // pixels 279 | final int tickLength = 6 * displayScalingFactor; // pixels 280 | final int tickTextPadding = 3 * displayScalingFactor; // pixels 281 | final double rangeHysteresis = 0.10; // 10% 282 | final int legendTextPadding = 5 * displayScalingFactor; 283 | final int strokeWidth = displayScalingFactor; 284 | final int imagePadding = 5 * displayScalingFactor; 285 | 286 | // calculate domain 287 | int sampleCount = Controller.getSamplesCount(datasets); 288 | int maxX = sampleCount - 1; 289 | int minX = maxX - duration; 290 | 291 | // calculate min/max y values 292 | double maxY = 0.0; 293 | double minY = 0.0; 294 | if(sampleCount > 0) { 295 | int firstX = minX >= 0 ? minX : 0; 296 | maxY = minY = datasets[0].get(firstX); 297 | for(Dataset dataset : datasets) 298 | for(int i = firstX; i <= maxX; i++) { 299 | double value = dataset.get(i); 300 | if(value > maxY) maxY = value; 301 | if(value < minY) minY = value; 302 | } 303 | } 304 | 305 | // ensure the range is >0 306 | if(minY == maxY) { 307 | double value = minY; 308 | minY = value - 0.001; 309 | maxY = value + 0.001; 310 | } 311 | 312 | // apply hysteresis to min/max y values 313 | boolean newMaxYisOutsideOldRange = maxY < Math.min(previousMaxY, previousMinY) || maxY > Math.max(previousMaxY, previousMinY); 314 | boolean newMinYisOutsideOldRange = minY < Math.min(previousMaxY, previousMinY) || minY > Math.max(previousMaxY, previousMinY); 315 | 316 | boolean newMaxYisFarAway = Math.abs(Math.max(previousMaxY, previousMinY) - maxY) > 1.5 * rangeHysteresis * Math.abs(previousMaxY - previousMinY); 317 | boolean newMinYisFarAway = Math.abs(Math.min(previousMaxY, previousMinY) - minY) > 1.5 * rangeHysteresis * Math.abs(previousMaxY - previousMinY); 318 | 319 | boolean newMaxYisTooClose = false;//Math.abs(Math.max(previousMaxY, previousMinY) - maxY) < 0.5 * rangeHysteresis * Math.abs(previousMaxY - previousMinY); 320 | boolean newMinYisTooClose = false;//Math.abs(Math.min(previousMaxY, previousMinY) - minY) < 0.5 * rangeHysteresis * Math.abs(previousMaxY - previousMinY); 321 | 322 | if(newMaxYisOutsideOldRange || newMinYisOutsideOldRange || newMaxYisFarAway || newMinYisFarAway || newMaxYisTooClose || newMinYisTooClose) { 323 | // reset range, centered on the current true range + hysteresis 324 | maxY += Math.abs(maxY - minY) * rangeHysteresis; 325 | minY -= Math.abs(maxY - minY) * rangeHysteresis; 326 | } else { 327 | // old range is fine, use the old range 328 | maxY = previousMaxY; 329 | minY = previousMinY; 330 | } 331 | 332 | double range = maxY - minY; 333 | 334 | // calculate x and y positions of everything 335 | int xStartOfLegendBorder = imagePadding; 336 | int yEndOfLegendBorder = imageHeight - imagePadding; 337 | int yStartOfLegendBorder = yEndOfLegendBorder - strokeWidth - legendTextPadding - legendFontHeight - legendTextPadding - strokeWidth; 338 | int yStartOfLegend = yStartOfLegendBorder + strokeWidth + legendTextPadding; 339 | int yBaselineOfLegend = yStartOfLegend + legendFontHeight; 340 | 341 | int yEndOfTopPadding = imagePadding; 342 | int yStartOfChartBorder = yEndOfTopPadding; 343 | int yStartOfChart = yStartOfChartBorder + strokeWidth; 344 | 345 | int yEndOfImage = imageHeight - 1; 346 | int yBaselineOfXaxisTitle = yEndOfImage - imagePadding; 347 | int yBaselineOfXaxisDivisionLabels = yBaselineOfXaxisTitle - xAxisFontHeight - xAxisFontHeight; // use font height also as padding amount 348 | if(yBaselineOfXaxisDivisionLabels > yStartOfLegendBorder - legendTextPadding - legendTextPadding) 349 | yBaselineOfXaxisDivisionLabels = yStartOfLegendBorder - legendTextPadding - legendTextPadding; 350 | 351 | int yEndOfXaxisTickMarks = yBaselineOfXaxisDivisionLabels - tickFontHeight - tickTextPadding; 352 | int yStartOfXaxisTickMarks = yEndOfXaxisTickMarks - tickLength; 353 | int yEndofChartBorder = yStartOfXaxisTickMarks; 354 | int yEndOfChart = yEndofChartBorder - strokeWidth; 355 | 356 | int graphHeight = yEndOfChart - yStartOfChart + 1; 357 | if(graphHeight < 1) 358 | return; 359 | 360 | Map yDivisions = getVerticalDivisions(graphHeight, minY, maxY); 361 | 362 | int maxYtickLabelWidth = 0; 363 | for(String text : yDivisions.values()) { 364 | int width = (int) tickFont.getStringBounds(text, frc).getWidth(); 365 | if(width > maxYtickLabelWidth) 366 | maxYtickLabelWidth = width; 367 | } 368 | 369 | int xEndOfLeftPadding = imagePadding; 370 | int xBaselineOfYaxisTitle = xEndOfLeftPadding + yAxisFontHeight; 371 | int xLeftSideOfYaxisDivisionLabels = xBaselineOfYaxisTitle + yAxisFontHeight; // use font height also as padding amount 372 | int xRightSideOfYaxisDivisionLabels = xLeftSideOfYaxisDivisionLabels + maxYtickLabelWidth; 373 | int xStartOfYaxisTickMarks = xRightSideOfYaxisDivisionLabels + tickTextPadding; 374 | int xEndOfYaxisTickMarks = xStartOfYaxisTickMarks + tickLength; 375 | int xStartOfChartBorder = xEndOfYaxisTickMarks; 376 | int xStartOfChart = xStartOfChartBorder + strokeWidth; 377 | 378 | int xEndOfImage = imageWidth - 1; 379 | int xEndOfChartBorder = xEndOfImage - imagePadding; 380 | int xEndOfChart = xEndOfChartBorder - strokeWidth; 381 | 382 | int graphWidth = xEndOfChart - xStartOfChart + 1; 383 | if(graphWidth < 1) 384 | return; 385 | 386 | Map xDivisions = getHorizontalDivisions(graphWidth, minX, maxX); 387 | 388 | int yStartOfYaxisTitle = yEndOfChart - (graphHeight / 2) + ((int) yAxisFont.getStringBounds(datasets[0].unit, frc).getWidth() / 2); 389 | int xStartOfXaxisTitle = xStartOfChart + (graphWidth / 2) - ((int) xAxisFont.getStringBounds("Sample Number", frc).getWidth() / 2); 390 | 391 | // delete both images if the image size has changed 392 | if(imageWidth != previousImageWidth || imageHeight != previousImageHeight) { 393 | if(image1 != null) 394 | image1.flush(); 395 | image1 = null; 396 | 397 | if(image2 != null) 398 | image2.flush(); 399 | image2 = null; 400 | } 401 | 402 | // draw on the image that is not scheduled to be painted 403 | VolatileImage chartImage; 404 | if(paintImage1.get()) { 405 | if(image2 == null) 406 | image2 = createVolatileImage(imageWidth, imageHeight); 407 | chartImage = image2; 408 | } else { 409 | if(image1 == null) 410 | image1 = createVolatileImage(imageWidth, imageHeight); 411 | chartImage = image1; 412 | } 413 | 414 | Graphics2D chart = chartImage.createGraphics(); 415 | chart.setStroke(new BasicStroke(strokeWidth)); 416 | chart.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 417 | chart.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 418 | 419 | // determine the last slice that can be rendered 420 | int lastSliceIndex = (int) Math.floor((double) sampleCount / (double) duration * (double) graphWidth / (double) sliceWidth) - 1; 421 | if(drawPartialLastSlice) 422 | lastSliceIndex++; 423 | 424 | // determine if the x division size changed 425 | boolean xDivisionSpacingChanged = false; 426 | if(xDivisions.size() > 1) { 427 | Integer[] divs = xDivisions.keySet().toArray(new Integer[1]); 428 | int currentXdivisionSpacing = Math.abs(divs[1] - divs[0]); 429 | if(currentXdivisionSpacing != previousXdivisionSpacing) { 430 | xDivisionSpacingChanged = true; 431 | previousXdivisionSpacing = currentXdivisionSpacing; 432 | } 433 | } 434 | 435 | // we need to erase all slices and redraw them if the image size changed, or if the range (y-axis) changed or if the x-axis division spacing changed 436 | if(previousImageWidth != imageWidth || previousImageHeight != imageHeight || previousMinY != minY || previousMaxY != maxY || xDivisionSpacingChanged) { 437 | 438 | previousImageWidth = imageWidth; 439 | previousImageHeight = imageHeight; 440 | 441 | previousMinY = minY; 442 | previousMaxY = maxY; 443 | 444 | int slicesCount = (int) Math.ceil(graphWidth / sliceWidth) + 1; 445 | if(drawPartialLastSlice) 446 | slicesCount++; 447 | slices = new VolatileImage[slicesCount]; 448 | 449 | lastRenderedSlice = lastSliceIndex - slicesCount; 450 | if(lastRenderedSlice < -1) 451 | lastRenderedSlice = -1; 452 | 453 | // System.err.println("slices flushed"); 454 | 455 | } 456 | 457 | // generate any new slices 458 | for(int sliceNumber = lastRenderedSlice + 1; sliceNumber <= lastSliceIndex; sliceNumber++) { 459 | 460 | VolatileImage sliceImage; 461 | 462 | // reuse existing slice image object, or create a new one if it doesn't exist 463 | int rbIndex = sliceNumber % slices.length; 464 | if(slices[rbIndex] == null) { 465 | slices[rbIndex] = createVolatileImage(sliceWidth, graphHeight); 466 | if(slices[rbIndex] == null) return; 467 | sliceImage = slices[rbIndex]; 468 | } else { 469 | sliceImage = slices[rbIndex]; 470 | } 471 | 472 | Graphics2D slice = (Graphics2D) sliceImage.createGraphics(); 473 | slice.setStroke(new BasicStroke(strokeWidth)); // for display scaling 474 | slice.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 475 | slice.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); 476 | 477 | // draw gray background 478 | slice.setColor(new Color(230, 230, 230)); 479 | slice.fillRect(0, 0, sliceWidth, graphHeight); 480 | 481 | // determine which samples need to be drawn in this slice 482 | int firstIndex = (int) Math.floor((double) (sliceNumber * sliceWidth) * ((double) duration / (double) graphWidth)); 483 | int lastIndex = (int) Math.ceil((double) ((sliceNumber+1) * sliceWidth) * ((double) duration / (double) graphWidth)); 484 | if(lastIndex == firstIndex) 485 | lastIndex++; 486 | 487 | if(lastIndex > sampleCount - 1){ 488 | lastIndex = sampleCount - 1; 489 | } 490 | 491 | // draw horizontal division lines 492 | slice.setColor(Color.LIGHT_GRAY); 493 | for(Double entry : yDivisions.keySet()) { 494 | 495 | int y = graphHeight - (int) ((entry - minY) / range * (double) graphHeight); 496 | slice.drawLine(0, y, sliceWidth, y); 497 | 498 | } 499 | 500 | // draw vertical division lines 501 | slice.setColor(Color.LIGHT_GRAY); 502 | for(Integer entry : xDivisions.keySet()) { 503 | 504 | double x = (double) entry * (double) graphWidth / (double) duration - (sliceNumber * sliceWidth); 505 | Line2D.Double line = new Line2D.Double(x, 0.0, x, graphHeight); 506 | slice.draw(line); 507 | 508 | } 509 | 510 | // draw a path for each dataset 511 | for(Dataset dataset : datasets) { 512 | 513 | Path2D.Double path = new Path2D.Double(); 514 | 515 | // start the path at the first point for this slice 516 | double x = (double) firstIndex * (double) graphWidth / (double) duration - (sliceNumber * sliceWidth); 517 | double y = (double) graphHeight - (dataset.get(firstIndex) - minY) / range * (double) graphHeight; 518 | path.moveTo(x, y); 519 | 520 | for(int i = firstIndex + 1; i < lastIndex; i++) { 521 | x = (double) i * (double) graphWidth / (double) duration - (sliceNumber * sliceWidth); 522 | y = (double) graphHeight - (dataset.get(i) - minY) / range * (double) graphHeight; 523 | path.lineTo(x, y); 524 | } 525 | 526 | slice.setColor(dataset.color); 527 | slice.draw(path); 528 | 529 | } 530 | 531 | // free resources 532 | slice.dispose(); 533 | 534 | lastRenderedSlice = sliceNumber; 535 | 536 | } 537 | 538 | if(drawPartialLastSlice) 539 | lastRenderedSlice--; 540 | 541 | // draw chart background 542 | chart.setColor(new Color(230, 230, 230)); 543 | chart.fillRect(xStartOfChartBorder, yStartOfChartBorder, (xEndOfChartBorder - xStartOfChartBorder), (yEndofChartBorder - yStartOfChartBorder)); 544 | 545 | // draw slices on screen 546 | int pixelCount = (int) Math.floor((double) sampleCount / (double) duration * (double) graphWidth); // what sliceCount would be if slices were 1px wide 547 | int xOffset = pixelCount - ((lastSliceIndex + 1) * sliceWidth); 548 | 549 | int lastSlice = lastSliceIndex; 550 | int firstSlice = lastSlice - (graphWidth / sliceWidth); 551 | if(drawPartialLastSlice) 552 | firstSlice--; 553 | if(firstSlice < 0) 554 | firstSlice = 0; 555 | 556 | for(int currentSlice = firstSlice; currentSlice <= lastSlice; currentSlice++) { 557 | int x = xStartOfChart - xOffset + graphWidth - ((lastSlice - currentSlice + 1) * sliceWidth); 558 | int y = yStartOfChart; 559 | chart.drawImage(slices[currentSlice % slices.length], x, y, null); 560 | } 561 | 562 | // draw black outline around chart 563 | chart.setColor(Color.BLACK); 564 | chart.drawRect(xStartOfChartBorder, yStartOfChartBorder, (xEndOfChartBorder - xStartOfChartBorder), (yEndofChartBorder - yStartOfChartBorder)); 565 | 566 | // draw white background 567 | chart.setColor(Color.WHITE); 568 | chart.fillRect(0, 0, imageWidth, yStartOfChartBorder - 1); // top 569 | chart.fillRect(0, 0, xStartOfChartBorder - 1, imageHeight); // left 570 | chart.fillRect(0, yEndofChartBorder + 1, imageWidth, (yEndOfImage - yEndofChartBorder)); // bottom 571 | chart.fillRect(xEndOfChartBorder + 1, 0, (xEndOfImage - xEndOfChartBorder), imageHeight); // right 572 | 573 | // draw legend 574 | int x = xStartOfLegendBorder + strokeWidth + legendTextPadding; 575 | chart.setFont(legendFont); 576 | for(Dataset dataset : datasets) { 577 | chart.setColor(dataset.color); 578 | chart.fillRect(x, yStartOfLegend, legendFontHeight, legendFontHeight); 579 | x += legendFontHeight + legendTextPadding; 580 | chart.setColor(Color.BLACK); 581 | chart.drawString(dataset.name, x, yBaselineOfLegend); 582 | x += (int) legendFont.getStringBounds(dataset.name, frc).getWidth() + 50; // leave 50px after each name 583 | } 584 | x -= 49; 585 | chart.drawRect(xStartOfLegendBorder, yStartOfLegendBorder, x, (yEndOfLegendBorder - yStartOfLegendBorder)); 586 | 587 | // draw y-axis label if there is room for it 588 | chart.setColor(Color.BLACK); 589 | chart.setFont(yAxisFont); 590 | if(yStartOfYaxisTitle < yEndOfChart) { 591 | String yAxisText = datasets[0].unit; 592 | AffineTransform original = chart.getTransform(); 593 | chart.rotate(-Math.PI / 2.0, xBaselineOfYaxisTitle, yStartOfYaxisTitle); 594 | chart.drawString(yAxisText, xBaselineOfYaxisTitle, yStartOfYaxisTitle); 595 | chart.setTransform(original); 596 | } 597 | 598 | // draw x-axis label, shifting it to the right if the legend gets in the way 599 | chart.setColor(Color.BLACK); 600 | chart.setFont(xAxisFont); 601 | if(xStartOfXaxisTitle < x + legendTextPadding + legendTextPadding) 602 | xStartOfXaxisTitle = x + legendTextPadding + legendTextPadding; 603 | chart.drawString("Sample Number", xStartOfXaxisTitle, yBaselineOfXaxisTitle); 604 | 605 | // draw range tick marks and text 606 | chart.setColor(Color.BLACK); 607 | chart.setFont(tickFont); 608 | for(Map.Entry entry : yDivisions.entrySet()) { 609 | 610 | int tickY = yEndOfChart + 1 - (int) ((entry.getKey() - minY) / range * (double) graphHeight); 611 | chart.drawLine(xStartOfYaxisTickMarks, tickY, xEndOfYaxisTickMarks, tickY); 612 | 613 | String text = entry.getValue(); 614 | int textWidth = (int) tickFont.getStringBounds(text, frc).getWidth(); 615 | int textX = xStartOfYaxisTickMarks - tickTextPadding - textWidth; 616 | int textY = tickY + (tickFontHeight / 2); 617 | chart.drawString(text, textX, textY); 618 | 619 | } 620 | 621 | // draw domain tick marks and text 622 | chart.setColor(Color.BLACK); 623 | chart.setFont(tickFont); 624 | for(Map.Entry entry : xDivisions.entrySet()) { 625 | 626 | int tickX = xStartOfChart + 1 + (int) ((entry.getKey() - minX) / (double) (maxX - minX) * (double) graphWidth); 627 | chart.drawLine(tickX, yStartOfXaxisTickMarks, tickX, yEndOfXaxisTickMarks); 628 | 629 | String text = entry.getValue(); 630 | int textWidth = (int) tickFont.getStringBounds(text, frc).getWidth(); 631 | int textX = tickX - (textWidth / 2); 632 | int textY = yEndOfXaxisTickMarks + tickTextPadding + tickFontHeight; 633 | chart.drawString(text, textX, textY); 634 | 635 | } 636 | 637 | // free resources 638 | chart.dispose(); 639 | 640 | // schedule a repaint of this panel 641 | SwingUtilities.invokeLater(new Runnable() { 642 | @Override public void run() { 643 | repaint(); 644 | } 645 | }); 646 | 647 | if(!paintImage1.compareAndSet(true, false)) 648 | paintImage1.set(true); 649 | 650 | // System.out.println((System.currentTimeMillis() - startTime) + "ms"); 651 | 652 | } 653 | 654 | }); 655 | String inputNames = ""; 656 | for(Dataset dataset : datasets) 657 | inputNames += dataset.name + ", "; 658 | thread.setName(String.format("TimeDomainChartCached of: %s", inputNames)); 659 | thread.start(); 660 | 661 | } 662 | 663 | @Override protected void paintComponent(Graphics g) { 664 | 665 | super.paintComponent(g); 666 | 667 | if(paintImage1.get() == true && image1 != null) { 668 | g.drawImage(image1, 0, 0, null); 669 | } else if(paintImage1.get() == false && image2 != null){ 670 | g.drawImage(image2, 0, 0, null); 671 | } 672 | 673 | } 674 | 675 | } 676 | --------------------------------------------------------------------------------