diff --git a/build.gradle b/build.gradle index 531cf13..3ce8dc7 100644 --- a/build.gradle +++ b/build.gradle @@ -24,8 +24,9 @@ dependencies { implementation 'org.json:json:20210307' implementation 'org.knowm.xchart:xchart:3.6.1' implementation 'org.java-websocket:Java-WebSocket:1.5.1' + implementation 'org.jetbrains:annotations:19.0.0' - testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' } test { diff --git a/data-example/config-hotfireTest.json b/data-example/config-hotfireTest.json index e279ef1..e202ad0 100644 --- a/data-example/config-hotfireTest.json +++ b/data-example/config-hotfireTest.json @@ -25,9 +25,11 @@ "Abort Burn", "Max States" ], - "timestampIndex": 0, - "stateIndex": 1, - "seperator": "," + "indexes": { + "timestamp": 0, + "state": 1 + }, + "separator": "," } ], "stateEvents": [ diff --git a/data-example/config-octoberSky.json b/data-example/config-octoberSky.json index 8bf3153..a047ee6 100644 --- a/data-example/config-octoberSky.json +++ b/data-example/config-octoberSky.json @@ -37,14 +37,14 @@ "Ground", "Max States" ], - "coordinateIndexes": { + "indexes": { + "timestamp": 1, + "state": 19, "altitude": 10, "latitude": 8, "longitude": 9 }, - "timestampIndex": 1, - "stateIndex": 19, - "seperator": "," + "separator": "," } ] } \ No newline at end of file diff --git a/src/main/java/uorocketry/basestation/Main.java b/src/main/java/uorocketry/basestation/Main.java index ce2c42d..95ee431 100644 --- a/src/main/java/uorocketry/basestation/Main.java +++ b/src/main/java/uorocketry/basestation/Main.java @@ -8,22 +8,15 @@ import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; -import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.Writer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; -import java.util.Set; import javax.swing.BorderFactory; import javax.swing.JFileChooser; @@ -36,7 +29,6 @@ import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableModel; import org.json.JSONArray; import org.json.JSONException; @@ -44,7 +36,6 @@ import org.knowm.xchart.XChartPanel; import org.knowm.xchart.XYChart; import org.knowm.xchart.XYChartBuilder; -import org.knowm.xchart.XYSeries; import org.knowm.xchart.XYSeries.XYSeriesRenderStyle; import org.knowm.xchart.style.Styler.LegendPosition; import org.knowm.xchart.style.Styler.YAxisPosition; @@ -52,28 +43,21 @@ import com.fazecast.jSerialComm.SerialPort; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.FileConfig; import uorocketry.basestation.connections.DeviceConnection; import uorocketry.basestation.connections.DeviceConnectionHolder; import uorocketry.basestation.connections.DataReceiver; import uorocketry.basestation.control.StateButton; -import uorocketry.basestation.data.DataHandler; -import uorocketry.basestation.data.DataTableCellRenderer; -import uorocketry.basestation.data.DataType; +import uorocketry.basestation.data.*; import uorocketry.basestation.external.GoogleEarthUpdater; import uorocketry.basestation.external.WebViewUpdater; -import uorocketry.basestation.panel.DataChart; -import uorocketry.basestation.panel.SnapPanel; -import uorocketry.basestation.panel.SnapPanelListener; +import uorocketry.basestation.panel.*; public class Main implements ComponentListener, ChangeListener, ActionListener, MouseListener, ListSelectionListener, DataReceiver, SnapPanelListener { /** Constants */ - /** The location of the comma separated labels without the extension. */ - public static final String CONFIG_LOCATION = "data/config.json"; - /** How many data points are there. By default, it is the number of labels */ - public static List dataLength = new ArrayList<>(2); - /** Separator for the data */ - public static final String SEPARATOR = ","; + /** Data file location for the simulation (new line separated for each event). This does not include the extension/ */ public static final String SIM_DATA_LOCATION = "data/data"; public static final String SIM_DATA_EXTENSION = ".txt"; @@ -84,7 +68,9 @@ public class Main implements ComponentListener, ChangeListener, ActionListener, public static boolean googleEarth = false; /** Where the updating Google Earth kml file is stored */ public static final String GOOGLE_EARTH_DATA_LOCATION = "data/positions.kml"; - + + private Config config; + /** Used for the map view */ GoogleEarthUpdater googleEarthUpdater; @@ -95,26 +81,13 @@ public class Main implements ComponentListener, ChangeListener, ActionListener, /** Used for the web view */ WebViewUpdater webViewUpdater; - /** Where to save the log file */ - public static final String LOG_FILE_SAVE_LOCATION = "data/"; - public static final String DEFAULT_LOG_FILE_NAME = "log"; - public static final String LOG_FILE_EXTENSION = ".txt"; - /** Will have a number appended to the end to not overwrite old logs */ - ArrayList currentLogFileName = new ArrayList(2); - /** Used to limit displayed data points to speed up rendering */ public static int maxDataPointsDisplayed = 800; - /** How many data sources to record data from. It is set when the config is loaded. */ - public static int dataSourceCount = 1; - /** Is this running in simulation mode. Must be set at the beginning as it changes the setup. */ public static boolean simulation = false; - public List> allData = new ArrayList<>(2); - - List labels = new ArrayList<>(); - JSONObject config = null; + public DataProcessor dataProcessor; /** Index of the current data point being looked at */ ArrayList currentDataIndexes = new ArrayList<>(2); @@ -135,7 +108,7 @@ public class Main implements ComponentListener, ChangeListener, ActionListener, public Window window; /** The chart last clicked */ - DataChart selectedChart; + Chart selectedChart; Border selectionBorder = BorderFactory.createLineBorder(Color.blue); /** The width and height of the chart container to resize elements inside on resize. */ @@ -151,9 +124,6 @@ public class Main implements ComponentListener, ChangeListener, ActionListener, /** If true, clicking on data in a chart will hide it */ public boolean dataDeletionMode = false; - /** What will be written to the log file */ - StringBuilder logFileStringBuilder = new StringBuilder(); - public static void main(String[] args) { // Find different possible commands for (int i = 0; i + 1 < args.length; i++) { @@ -170,10 +140,10 @@ public static void main(String[] args) { public Main() { // Load labels - loadConfig(); + config = new FileConfig(); // Create window - window = new Window(this); + window = new Window(this, config); window.addComponentListener(this); @@ -181,7 +151,7 @@ public Main() { // Different setups depending on if simulation or not setupData(); - + // Setup Google Earth map support if (googleEarth) { setupGoogleEarth(); @@ -196,42 +166,41 @@ public Main() { updateUI(); } - public void setupData() { - allData = new ArrayList<>(dataSourceCount); - currentDataIndexes = new ArrayList<>(dataSourceCount); - minDataIndexes = new ArrayList<>(dataSourceCount); + private void setupData() { + dataProcessor = new DataProcessor(config, window.dataTables); - for (int i = 0; i < dataSourceCount; i++) { - allData.add(new ArrayList<>()); + currentDataIndexes = new ArrayList<>(config.getDataSourceCount()); + minDataIndexes = new ArrayList<>(config.getDataSourceCount()); + for (int i = 0; i < config.getDataSourceCount(); i++) { // Add data indexes currentDataIndexes.add(0); minDataIndexes.add(0); - + // Reset sliders window.maxSliders.get(i).setValue(0); window.minSliders.get(i).setValue(0); } - + // Load simulation data if necessary if (simulation) { loadSimulationData(); - + window.savingToLabel.setText(""); } - + setupSerialComList(); setupLogFileName(); updateUI(); } - + public void setupSerialComList() { deviceConnectionHolder.setAllSerialPorts(SerialPort.getCommPorts()); - + // Make array for the selector String[] comSelectorData = new String[deviceConnectionHolder.getAllSerialPorts().length]; - + for (int i = 0; i < deviceConnectionHolder.getAllSerialPorts().length; i++) { comSelectorData[i] = deviceConnectionHolder.getAllSerialPorts()[i].getDescriptivePortName(); } @@ -240,70 +209,22 @@ public void setupSerialComList() { deviceConnection.getSelectorList().setListData(comSelectorData); } } - + public void setupLogFileName() { - // Figure out file name for logging - File folder = new File(LOG_FILE_SAVE_LOCATION); - File[] listOfLogFiles = folder.listFiles(); - Set usedFileNames = new HashSet(); - - for (File file: listOfLogFiles) { - if (file.isFile() && file.getName().contains(DEFAULT_LOG_FILE_NAME)) { - usedFileNames.add(file.getName()); - } - } - - int logIndex = 0; - - JSONArray dataSets = config.getJSONArray("datasets"); - - // Find a suitable filename - for (int i = 0; i <= listOfLogFiles.length; i++) { - boolean containsFile = false; - for (int j = 0 ; j < dataSourceCount; j++) { - if (usedFileNames.contains(DEFAULT_LOG_FILE_NAME + "_" + dataSets.getJSONObject(j).getString("name").toLowerCase() + "_" + logIndex + LOG_FILE_EXTENSION)) { - containsFile = true; - break; - } - } - - if (containsFile) { - logIndex++; - } else { - break; - } - } - - // Set the names - for (int i = 0 ; i < dataSourceCount; i++) { - currentLogFileName.add(DEFAULT_LOG_FILE_NAME + "_" + dataSets.getJSONObject(i).getString("name").toLowerCase() + "_" + logIndex + LOG_FILE_EXTENSION); - } - - window.savingToLabel.setText("Saving to " + formattedSavingToLocations()); - } - - public String formattedSavingToLocations() { - StringBuilder savingToText = new StringBuilder(); - - // Add text for each file - for (int i = 0 ; i < dataSourceCount; i++) { - if (i != 0) savingToText.append(", "); - - savingToText.append(LOG_FILE_SAVE_LOCATION + currentLogFileName.get(i)); - } - - return savingToText.toString(); + dataProcessor.setupLogFileName(); + + window.savingToLabel.setText("Saving to " + dataProcessor.formattedSavingToLocations()); } - + public void setupUI() { addChart(); - + // Add slider listeners - for (int i = 0; i < dataSourceCount; i++) { + for (int i = 0; i < config.getDataSourceCount(); i++) { window.maxSliders.get(i).addChangeListener(this); window.minSliders.get(i).addChangeListener(this); } - + // Buttons window.clearDataButton.addActionListener(this); window.refreshComSelectorButton.addActionListener(this); @@ -311,96 +232,89 @@ public void setupUI() { window.hideBarsButton.addActionListener(this); window.pauseButton.addActionListener(this); window.latestButton.addActionListener(this); - + window.addChartButton.addActionListener(this); - + window.setMaxDataPointsButton.addActionListener(this); - + window.restoreDeletedData.addActionListener(this); - + window.saveLayout.addActionListener(this); window.loadLayout.addActionListener(this); - + // Checkboxes window.googleEarthCheckBox.addActionListener(this); window.webViewCheckBox.addActionListener(this); window.simulationCheckBox.addActionListener(this); window.onlyShowLatestDataCheckBox.addActionListener(this); window.dataDeletionModeCheckBox.addActionListener(this); - + // Set simulation checkbox to be default window.simulationCheckBox.setSelected(simulation); - + // Setup listeners for table - for (JTable dataTable : window.dataTables) { - dataTable.getSelectionModel().addListSelectionListener(this); - dataTable.addMouseListener(this); + for (TableHolder tableHolder : window.dataTables) { + tableHolder.getReceivedDataTable().getSelectionModel().addListSelectionListener(this); + tableHolder.getReceivedDataTable().addMouseListener(this); } - - + // Setup Snap Panel system synchronized (window.charts) { selectedChart = window.charts.get(0); - selectedChart.snapPanel.setSnapPanelListener(this); - - snapPanelSelected(selectedChart.snapPanel); + selectedChart.getSpanPanel().setSnapPanelListener(this); + + snapPanelSelected(selectedChart.getSpanPanel()); } } - + public void setupGoogleEarth() { googleEarthUpdater = new GoogleEarthUpdater(); - + // Setup updater file // googleEarthUpdater.createKMLUpdaterFile(); } - - public void setupWebView() { + + public void setupWebView() { if (webViewUpdater != null) webViewUpdater.close(); webViewUpdater = new WebViewUpdater(); } - + public void updateUI() { // If not ready yet - if (allData.size() == 0 || updatingUI) return; - + if (dataProcessor== null || dataProcessor.getDataPointHolder().size() == 0 || updatingUI) return; + updatingUI = true; - + // Update UI on another thread new Thread(this::updateUIInternal).start(); } - + private void updateUIInternal() { try { // Update every table's data - for (int i = 0; i < allData.size(); i++) { + for (int i = 0; i < dataProcessor.getDataPointHolder().size(); i++) { // If not ready yet - if (allData.get(i).size() == 0) continue; - + if (dataProcessor.getDataPointHolder().get(i).size() == 0) continue; + // Don't change slider if paused if (!paused) { // Set max value of the sliders - window.maxSliders.get(i).setMaximum(allData.get(i).size() - 1); - window.minSliders.get(i).setMaximum(allData.get(i).size() - 1); - + window.maxSliders.get(i).setMaximum(dataProcessor.getDataPointHolder().get(i).size() - 1); + window.minSliders.get(i).setMaximum(dataProcessor.getDataPointHolder().get(i).size() - 1); + // Move position to end if (latest) { - window.maxSliders.get(i).setValue(allData.get(i).size() - 1); + window.maxSliders.get(i).setValue(dataProcessor.getDataPointHolder().get(i).size() - 1); } } - - DataHandler currentDataHandler = allData.get(i).get(currentDataIndexes.get(i)); - - if (currentDataHandler != null) { - currentDataHandler.updateTableUIWithData(window.dataTables.get(i), labels.get(i)); - } else { - setTableToError(i, window.dataTables.get(i)); - } - - if (window.stateButtons.size() > i && currentDataHandler != null) { + + DataPoint dataPoint = dataProcessor.setTableTo(i, currentDataIndexes.get(i), latest && !paused); + + if (window.stateButtons.size() > i && dataPoint != null && dataPoint.getReceivedData() != null) { try { - int stateIndex = config.getJSONArray("datasets").getJSONObject(i).getInt("stateIndex"); + int stateIndex = config.getDataSet(i).getIndex("state"); for (StateButton stateButton: window.stateButtons.get(i)) { - Float value = currentDataHandler.data[stateIndex].getDecimalValue(); + Float value = dataPoint.getReceivedData().data[stateIndex].getDecimalValue(); if (value != null) { stateButton.stateChanged(value.intValue()); } @@ -408,18 +322,18 @@ private void updateUIInternal() { } catch (JSONException ignored) {} } } - + if (googleEarth) { - googleEarthUpdater.updateKMLFile(allData, minDataIndexes, currentDataIndexes, config.getJSONArray("datasets"), false); + googleEarthUpdater.updateKMLFile(dataProcessor.getDataPointHolder(), minDataIndexes, currentDataIndexes, config, false); } - + if (webView) { - webViewUpdater.sendUpdate(allData, minDataIndexes, currentDataIndexes, config.getJSONArray("datasets")); + webViewUpdater.sendUpdate(dataProcessor.getDataPointHolder(), minDataIndexes, currentDataIndexes, config); } - + // Update every chart synchronized (window.charts) { - for (DataChart chart : window.charts) { + for (Chart chart : window.charts) { updateChart(chart); } } @@ -427,150 +341,30 @@ private void updateUIInternal() { // Don't let an exception while updating break the program e.printStackTrace(); } - + updatingUI = false; } - - public void setTableToError(int index, JTable table) { - TableModel tableModel = table.getModel(); - - // Set first item to "Error" - tableModel.setValueAt("Parsing Error", 0, 0); - tableModel.setValueAt(currentDataIndexes.get(index), 0, 1); - - for (int i = 1; i < dataLength.get(index); i++) { - // Set label - tableModel.setValueAt("", i, 0); - - // Set data - tableModel.setValueAt("", i, 1); - } - } /** * Update the chart with data up to currentDataIndex, and then call window.repaint() - * + * * @param chart The chart to update */ - public void updateChart(DataChart chart) { - // Update altitude chart - ArrayList altitudeDataX = new ArrayList<>(); - ArrayList> altitudeDataY = new ArrayList>(); - - // Add all array lists - for (int i = 0; i < chart.xTypes.length; i++) { - altitudeDataY.add(new ArrayList()); - } - - // Add y axis - { - int maxDataIndex = currentDataIndexes.get(chart.yType.tableIndex); - int minDataIndex = minDataIndexes.get(chart.yType.tableIndex); - if (onlyShowLatestData) minDataIndex = Math.max(maxDataIndex - maxDataPointsDisplayed, minDataIndex); - - for (int i = minDataIndex; i <= maxDataIndex; i++) { - if (allData.get(chart.yType.tableIndex).size() == 0) continue; - - DataHandler data = allData.get(chart.yType.tableIndex).get(i); - - DataHandler other = allData.get(chart.xTypes[0].tableIndex).get(i); - - if (data != null && (other == null || !other.hiddenDataTypes.contains(other.types[chart.xTypes[0].index]))) { - altitudeDataX.add(data.data[chart.yType.index].getDecimalValue()); - } - } - } - - - // Add x axis - for (int i = 0; i < chart.xTypes.length; i++) { - // Used to limit the max number of data points displayed - float targetRatio = (float) maxDataPointsDisplayed / (currentDataIndexes.get(chart.xTypes[i].tableIndex) - minDataIndexes.get(chart.xTypes[i].tableIndex)); - int dataPointsAdded = 0; - - int maxDataIndex = currentDataIndexes.get(chart.xTypes[i].tableIndex); - int minDataIndex = minDataIndexes.get(chart.xTypes[i].tableIndex); - if (onlyShowLatestData) minDataIndex = Math.max(maxDataIndex - maxDataPointsDisplayed, minDataIndex); + public void updateChart(Chart chart) { + int maxDataIndex = currentDataIndexes.get(chart.getYType().tableIndex); + int minDataIndex = minDataIndexes.get(chart.getYType().tableIndex); - for (int j = minDataIndex; j <= maxDataIndex; j++) { - if (allData.get(chart.yType.tableIndex).size() == 0) continue; + dataProcessor.updateChart(chart, minDataIndex, maxDataIndex, onlyShowLatestData, maxDataPointsDisplayed); - DataHandler data = allData.get(chart.xTypes[i].tableIndex).get(j); - - if (data != null) { - // Ensures that not too many data points are displayed - // Always show data if only showing latest data (that is handled by changing the minSlider) - boolean shouldShowDataPoint = onlyShowLatestData || ((float) dataPointsAdded / j <= targetRatio); - - if (!data.hiddenDataTypes.contains(data.types[chart.xTypes[i].index]) && shouldShowDataPoint ) { - altitudeDataY.get(i).add(data.data[chart.xTypes[i].index].getDecimalValue()); - - dataPointsAdded++; - } else if (!shouldShowDataPoint) { - // Hidden data - altitudeDataY.get(i).add(null); - } - } - } - } - - if (altitudeDataX.size() == 0) { - // Add default data - altitudeDataX.add(0f); - - for (int j = 0; j < chart.xTypes.length; j++) { - altitudeDataY.get(j).add(0f); - }; - } - - String[] newActiveSeries = new String[chart.xTypes.length]; - StringBuilder title = new StringBuilder(); - - // Set Labels - for (int i = 0; i < chart.xTypes.length; i++) { - String xTypeTitle = labels.get(chart.xTypes[i].tableIndex)[chart.xTypes[i].index]; - - if (title.length() != 0) title.append(", "); - title.append(xTypeTitle); - - chart.xyChart.setYAxisGroupTitle(i, xTypeTitle); - - XYSeries series = null; - - if (chart.activeSeries.length > i) { - series = chart.xyChart.updateXYSeries("series" + i, altitudeDataX, altitudeDataY.get(i), null); - } else { - series = chart.xyChart.addSeries("series" + i, altitudeDataX, altitudeDataY.get(i), null); - } - - series.setLabel(xTypeTitle); - series.setYAxisGroup(i); - - newActiveSeries[i] = "series" + i; - } - - String yTypeTitle = labels.get(chart.yType.tableIndex)[chart.yType.index]; - - chart.xyChart.setTitle(title + " vs " + yTypeTitle); - - chart.xyChart.setXAxisTitle(yTypeTitle); - - // Remove extra series - for (int i = chart.xTypes.length; i < chart.activeSeries.length; i++) { - chart.xyChart.removeSeries("series" + i); - } - - chart.activeSeries = newActiveSeries; - window.repaint(); } - - /** + + /** * Run once at the beginning of simulation mode */ public void loadSimulationData() { // Load simulation data - for (int i = 0; i < dataSourceCount; i++) { + for (int i = 0; i < config.getDataSourceCount(); i++) { loadSimulationData(i, SIM_DATA_LOCATION + i + SIM_DATA_EXTENSION); } } @@ -581,18 +375,16 @@ public void loadSimulationData(int index, String fileName) { br = new BufferedReader(new FileReader(fileName)); } catch (FileNotFoundException e) { e.printStackTrace(); + return; } - List dataHandlers = new ArrayList(); - allData.set(index, dataHandlers); - try { try { String line = null; while ((line = br.readLine()) != null) { // Parse this line and add it as a data point - dataHandlers.add(parseData(line, index)); + dataProcessor.receivedData(index, line.getBytes(StandardCharsets.UTF_8)); } } finally { br.close(); @@ -602,110 +394,6 @@ public void loadSimulationData(int index, String fileName) { } } - public DataHandler parseData(String data, int tableIndex) { - DataHandler dataHandler = new DataHandler(tableIndex, config.getJSONArray("datasets").getJSONObject(tableIndex)); - - // Clear out the b' ' stuff added that is only meant for the radio to see - data = data.replaceAll("b'|\\\\r\\\\n'", ""); - if (data.endsWith(",")) data = data.substring(0, data.length() - 1); - - // Semi-colon separated - String[] splitData = data.split(SEPARATOR); - if (splitData.length != dataHandler.data.length) { - //invalid data - System.err.println("Line with invalid data (Not the correct amount of data). It was " + splitData.length); - - return null; - } - - // Ensure that the timestamp has not gone back in time - try { - DataHandler lastDataPointDataHandler = findLastValidDataPoint(allData.get(tableIndex)); - - int timestampIndex = config.getJSONArray("datasets").getJSONObject(tableIndex).getInt("timestampIndex"); - if (lastDataPointDataHandler != null) { - Float value = lastDataPointDataHandler.data[timestampIndex].getDecimalValue(); - if (value != null && Float.parseFloat(splitData[timestampIndex]) < value) { - System.err.println("Timestamp just went backwards"); - - // Treat as invalid data - return null; - } - } - } catch (NumberFormatException | JSONException e) {} - - for (int i = 0; i < splitData.length; i++) { - if (!dataHandler.set(i, splitData[i])) { - System.err.println("Failed to set data handler"); - - // Parsing failed - return null; - } - } - - return dataHandler; - } - - /** - * Find last non null data point - * - * @param currentTableData - * @return DataHandler if found, null otherwise - */ - private DataHandler findLastValidDataPoint(List currentTableData) { - for (int i = currentTableData.size() - 1; i >= 0; i--) { - if (currentTableData.get(i) != null) { - return currentTableData.get(i); - } - } - - return null; - } - - /** - * Run once at the beginning of simulation mode - */ - public void loadConfig() { - loadConfig(CONFIG_LOCATION); - } - - public void loadConfig(String fileName) { - String configString = null; - try { - configString = new String(Files.readAllBytes(Paths.get(fileName)), StandardCharsets.UTF_8); - } catch (IOException e) { - e.printStackTrace(); - - JOptionPane.showMessageDialog(window, "The config file was not found in " + fileName + - "\r\n\r\nIf you plan downloaded a release build, you might want to download the version with labels and sample data included."); - - return; - } - - config = new JSONObject(configString); - - JSONArray datasetsJSONArray = config.getJSONArray("datasets"); - dataSourceCount = datasetsJSONArray.length(); - - // Add all data - for (int i = 0; i < datasetsJSONArray.length(); i++) { - JSONObject currentDataset = datasetsJSONArray.getJSONObject(i); - - JSONArray labelsJsonArray = currentDataset.getJSONArray("labels"); - - // Load labels - String[] labelsArray = new String[labelsJsonArray.length()]; - - for (int j = 0; j < labelsArray.length; j++) { - labelsArray[j] = labelsJsonArray.getString(j); - } - - labels.add(labelsArray); - - dataLength.add(labelsArray.length); - } - } - /** * Triggered every time the slider changes */ @@ -743,32 +431,9 @@ public void stateChanged(ChangeEvent e) { @Override public void receivedData(DeviceConnection deviceConnection, byte[] data) { - String delimitedMessage = new String(data, StandardCharsets.UTF_8); - - allData.get(deviceConnection.getTableIndex()).add(parseData(delimitedMessage, deviceConnection.getTableIndex())); + dataProcessor.receivedData(deviceConnection, data); updateUI(); - - // Add this message to the log file - logFileStringBuilder.append(delimitedMessage); - - // Get string - String logFileString = logFileStringBuilder.toString(); - - if (deviceConnection.isWriting()) { - deviceConnection.setWriting(true); - - // Write to file - try (Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream( - LOG_FILE_SAVE_LOCATION + currentLogFileName.get(deviceConnection.getTableIndex())) - , StandardCharsets.UTF_8))) { - writer.write(logFileString); - } catch (IOException err) { - err.printStackTrace(); - } - - deviceConnection.setWriting(false); - } } @Override @@ -776,8 +441,8 @@ public void actionPerformed(ActionEvent e) { if (e.getSource() == window.clearDataButton) { if (JOptionPane.showConfirmDialog(window, "Are you sure you would like to clear all the data?") == 0) { - for (int i = 0; i < allData.size(); i++) { - allData.get(i).clear(); + for (int i = 0; i < dataProcessor.getDataPointHolder().size(); i++) { + dataProcessor.getDataPointHolder().get(i).clear(); } updateUI(); @@ -816,7 +481,7 @@ public void actionPerformed(ActionEvent e) { window.latestButton.setText("Detach From Latest"); for (int i = 0; i < window.maxSliders.size(); i++) { - window.maxSliders.get(i).setValue(allData.get(0).size() - 1); + window.maxSliders.get(i).setValue(dataProcessor.getDataPointHolder().get(0).size() - 1); } } else { window.latestButton.setText("Latest"); @@ -835,7 +500,7 @@ public void actionPerformed(ActionEvent e) { String warningMessage = ""; if (window.simulationCheckBox.isSelected()) { warningMessage = "Are you sure you would like to enable simulation mode?\n\n" - + "The current data will be deleted from the UI. You can find it in " + formattedSavingToLocations(); + + "The current data will be deleted from the UI. You can find it in " + dataProcessor.formattedSavingToLocations(); } else { warningMessage = "Are you sure you would like to disable simulation mode?"; } @@ -856,11 +521,10 @@ public void actionPerformed(ActionEvent e) { } else if (e.getSource() == window.dataDeletionModeCheckBox) { dataDeletionMode = window.dataDeletionModeCheckBox.isSelected(); } else if (e.getSource() == window.restoreDeletedData) { - for (List dataHandlers : allData) { - for (DataHandler dataHandler : dataHandlers) { - // See if the hidden list needs to be cleared - if (dataHandler != null && !dataHandler.hiddenDataTypes.isEmpty()) { - dataHandler.hiddenDataTypes.clear(); + for (List dataPoints : dataProcessor.getDataPointHolder()) { + for (DataPoint dataPoint : dataPoints) { + if (dataPoint != null) { + dataPoint.clearHiddenTypes(); } } } @@ -891,17 +555,17 @@ public void actionPerformed(ActionEvent e) { JSONArray chartsArray = new JSONArray(); saveObject.put("charts", chartsArray); - for (DataChart chart: window.charts) { + for (Chart chart: window.charts) { JSONObject chartData = new JSONObject(); - chartData.put("x", chart.snapPanel.relX); - chartData.put("y", chart.snapPanel.relY); - chartData.put("width", chart.snapPanel.relWidth); - chartData.put("height", chart.snapPanel.relHeight); + chartData.put("x", chart.getSpanPanel().relX); + chartData.put("y", chart.getSpanPanel().relY); + chartData.put("width", chart.getSpanPanel().relWidth); + chartData.put("height", chart.getSpanPanel().relHeight); // Add xTypes JSONArray xTypeArray = new JSONArray(); - for (DataType dataType: chart.xTypes) { + for (DataType dataType: chart.getXTypes()) { JSONObject xTypeData = new JSONObject(); xTypeData.put("index", dataType.index); @@ -913,8 +577,8 @@ public void actionPerformed(ActionEvent e) { // Add yType JSONObject yTypeData = new JSONObject(); - yTypeData.put("index", chart.yType.index); - yTypeData.put("tableIndex", chart.yType.tableIndex); + yTypeData.put("index", chart.getYType().index); + yTypeData.put("tableIndex", chart.getYType().tableIndex); chartData.put("yType", yTypeData); chartsArray.put(chartData); @@ -953,9 +617,9 @@ public void actionPerformed(ActionEvent e) { JSONArray chartsArray = loadedLayout.getJSONArray("charts"); // Clear current charts - for (DataChart dataChart: window.charts) { + for (Chart dataChart: window.charts) { // Remove from the UI - window.centerChartPanel.remove(dataChart.chartPanel); + window.centerChartPanel.remove(dataChart.getPanel()); } // Finally, remove it from the list @@ -966,35 +630,35 @@ public void actionPerformed(ActionEvent e) { addChart(true); - DataChart chart = window.charts.get(i); + Chart chart = window.charts.get(i); // Get location - chart.snapPanel.relX = chartData.getDouble("x"); - chart.snapPanel.relY = chartData.getDouble("y"); - chart.snapPanel.relWidth = chartData.getDouble("width"); - chart.snapPanel.relHeight = chartData.getDouble("height"); + chart.getSpanPanel().relX = chartData.getDouble("x"); + chart.getSpanPanel().relY = chartData.getDouble("y"); + chart.getSpanPanel().relWidth = chartData.getDouble("width"); + chart.getSpanPanel().relHeight = chartData.getDouble("height"); - chart.snapPanel.updateBounds(window.centerChartPanel.getWidth(), window.centerChartPanel.getHeight()); + chart.getSpanPanel().updateBounds(window.centerChartPanel.getWidth(), window.centerChartPanel.getHeight()); // Get xTypes JSONArray xTypeArray = chartData.getJSONArray("xTypes"); - chart.xTypes = new DataType[xTypeArray.length()]; - for (int j = 0; j < chart.xTypes.length; j++) { + chart.setXTypes(new DataType[xTypeArray.length()]); + for (int j = 0; j < chart.getXTypes().length; j++) { JSONObject xTypeData = xTypeArray.getJSONObject(j); - chart.xTypes[j] = new DataType(xTypeData.getInt("index"), xTypeData.getInt("tableIndex")); + chart.getXTypes()[j] = new DataType(xTypeData.getInt("index"), xTypeData.getInt("tableIndex")); } // Add yType JSONObject yTypeData = chartData.getJSONObject("yType"); - chart.yType = new DataType(yTypeData.getInt("index"), yTypeData.getInt("tableIndex")); + chart.setYType(new DataType(yTypeData.getInt("index"), yTypeData.getInt("tableIndex"))); } updateUI(); } } } - + public void addChart() { addChart(false); } @@ -1022,10 +686,10 @@ public void addChart(boolean silent) { XChartPanel chartPanel = new XChartPanel<>(xyChart); window.centerChartPanel.add(chartPanel); - DataChart dataChart = new DataChart(this, xyChart, chartPanel); + DataChart dataChart = new DataChart(this, config, xyChart, chartPanel); // Set default size - dataChart.snapPanel.setRelSize(600, 450); + dataChart.getSpanPanel().setRelSize(600, 450); // Add these default charts to the list synchronized (window.charts) { @@ -1034,14 +698,14 @@ public void addChart(boolean silent) { // Set to be selected window.centerChartPanel.setComponentZOrder(chartPanel, 0); - dataChart.snapPanel.setSnapPanelListener(this); + dataChart.getSpanPanel().setSnapPanelListener(this); - if (selectedChart != null) selectedChart.chartPanel.setBorder(null); + if (selectedChart != null) selectedChart.getPanel().setBorder(null); if (!silent) { selectedChart = dataChart; - snapPanelSelected(selectedChart.snapPanel); + snapPanelSelected(selectedChart.getSpanPanel()); updateUI(); } @@ -1052,7 +716,8 @@ public void addChart(boolean silent) { public void valueChanged(ListSelectionEvent e) { if(e.getSource() instanceof ListSelectionModel && !ignoreSelections) { for (int i = 0; i < window.dataTables.size(); i++) { - JTable dataTable = window.dataTables.get(i); + TableHolder tableHolder = window.dataTables.get(i); + JTable dataTable = tableHolder.getReceivedDataTable(); // Just this table for now if (e.getSource() == dataTable.getSelectionModel()) { int[] selections = dataTable.getSelectedRows(); @@ -1061,14 +726,14 @@ public void valueChanged(ListSelectionEvent e) { moveSelectionsToNewTable(i, true); for (int j = 0; j < formattedSelections.length; j++) { - formattedSelections[j] = new DataType(selections[j], window.dataTables.indexOf(dataTable)); + formattedSelections[j] = new DataType(selections[j], window.dataTables.indexOf(tableHolder)); } synchronized (window.charts) { // Set chart to be based on this row - selectedChart.xTypes = formattedSelections; + selectedChart.setXTypes(formattedSelections); } - + dataTable.setColumnSelectionInterval(0, 0); updateUI(); @@ -1082,7 +747,7 @@ public void valueChanged(ListSelectionEvent e) { @Override public void mousePressed(MouseEvent e) { for (int i = 0; i < window.dataTables.size(); i++) { - JTable dataTable = window.dataTables.get(i); + JTable dataTable = window.dataTables.get(i).getReceivedDataTable(); if (e.getSource() == dataTable && e.getButton() == MouseEvent.BUTTON3) { // Left clicking the dataTable @@ -1092,7 +757,7 @@ public void mousePressed(MouseEvent e) { moveSelectionsToNewTable(i, false); - selectedChart.yType = new DataType(row, i); + selectedChart.setYType(new DataType(row, i)); ((DataTableCellRenderer) dataTable.getDefaultRenderer(Object.class)).coloredRow = row; dataTable.repaint(); @@ -1108,40 +773,40 @@ public void moveSelectionsToNewTable(int newTableIndex, boolean changingX) { boolean movingXType = false; // Clear previous selections - for (int j = 0; j < selectedChart.xTypes.length; j++) { - if (selectedChart.xTypes[j].tableIndex != newTableIndex) { - int currentTableIndex = selectedChart.xTypes[j].tableIndex; + for (int j = 0; j < selectedChart.getXTypes().length; j++) { + if (selectedChart.getXTypes()[j].tableIndex != newTableIndex) { + int currentTableIndex = selectedChart.getXTypes()[j].tableIndex; // Clear that table's selection - window.dataTables.get(currentTableIndex).clearSelection(); - window.dataTables.get(currentTableIndex).repaint(); + window.dataTables.get(currentTableIndex).getReceivedDataTable().clearSelection(); + window.dataTables.get(currentTableIndex).getReceivedDataTable().repaint(); movingXType = true; } } if (movingXType && !changingX) { - selectedChart.xTypes = new DataType[1]; - selectedChart.xTypes[0] = new DataType(1, newTableIndex); + selectedChart.setXTypes(new DataType[1]); + selectedChart.getXTypes()[0] = new DataType(1, newTableIndex); - window.dataTables.get(newTableIndex).setRowSelectionInterval(1, 1); - window.dataTables.get(newTableIndex).setColumnSelectionInterval(0, 0); - window.dataTables.get(newTableIndex).repaint(); + window.dataTables.get(newTableIndex).getReceivedDataTable().setRowSelectionInterval(1, 1); + window.dataTables.get(newTableIndex).getReceivedDataTable().setColumnSelectionInterval(0, 0); + window.dataTables.get(newTableIndex).getReceivedDataTable().repaint(); } // Move yType selection if needed - if (selectedChart.yType.tableIndex != newTableIndex) { + if (selectedChart.getYType().tableIndex != newTableIndex) { // Deselect the old one - JTable oldDataTable = window.dataTables.get(selectedChart.yType.tableIndex); + JTable oldDataTable = window.dataTables.get(selectedChart.getYType().tableIndex).getReceivedDataTable(); ((DataTableCellRenderer) oldDataTable.getDefaultRenderer(Object.class)).coloredRow = -1; oldDataTable.repaint(); // Select this default selection - JTable dataTable = window.dataTables.get(newTableIndex); + JTable dataTable = window.dataTables.get(newTableIndex).getReceivedDataTable(); ((DataTableCellRenderer) dataTable.getDefaultRenderer(Object.class)).coloredRow = 0; dataTable.repaint(); - selectedChart.yType = new DataType(0, newTableIndex); + selectedChart.setYType(new DataType(0, newTableIndex)); } } @@ -1152,29 +817,29 @@ public void moveSelectionsToNewTable(int newTableIndex, boolean changingX) { public void snapPanelSelected(SnapPanel snapPanel) { if (snapPanel.chart != null) { // Remove border on old object - selectedChart.chartPanel.setBorder(null); + selectedChart.getPanel().setBorder(null); selectedChart = snapPanel.chart; // Add border - selectedChart.chartPanel.setBorder(selectionBorder); + selectedChart.getPanel().setBorder(selectionBorder); // Add selections ignoreSelections = true; for (int i = 0; i < window.dataTables.size(); i++) { - JTable dataTable = window.dataTables.get(i); + JTable dataTable = window.dataTables.get(i).getReceivedDataTable(); dataTable.clearSelection(); - for (int j = 0; j < selectedChart.xTypes.length; j++) { - if (selectedChart.xTypes[j].tableIndex == i) { - dataTable.addRowSelectionInterval(selectedChart.xTypes[j].index, selectedChart.xTypes[j].index); + for (int j = 0; j < selectedChart.getXTypes().length; j++) { + if (selectedChart.getXTypes()[j].tableIndex == i) { + dataTable.addRowSelectionInterval(selectedChart.getXTypes()[j].index, selectedChart.getXTypes()[j].index); } } // Update yType - if (selectedChart.yType.tableIndex == i) { - ((DataTableCellRenderer) dataTable.getDefaultRenderer(Object.class)).coloredRow = selectedChart.yType.index; + if (selectedChart.getYType().tableIndex == i) { + ((DataTableCellRenderer) dataTable.getDefaultRenderer(Object.class)).coloredRow = selectedChart.getYType().index; } window.repaint(); @@ -1203,8 +868,8 @@ public void componentResized(ComponentEvent e) { int currentChartContainerHeight = window.centerChartPanel.getHeight(); synchronized (window.charts) { - for (DataChart chart : window.charts) { - chart.snapPanel.containerResized(currentChartContainerWidth, currentChartContainerHeight); + for (Chart chart : window.charts) { + chart.getSpanPanel().containerResized(currentChartContainerWidth, currentChartContainerHeight); } } diff --git a/src/main/java/uorocketry/basestation/Window.java b/src/main/java/uorocketry/basestation/Window.java index d292ade..1de954a 100644 --- a/src/main/java/uorocketry/basestation/Window.java +++ b/src/main/java/uorocketry/basestation/Window.java @@ -32,21 +32,27 @@ import org.json.JSONException; import org.json.JSONObject; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.DataSet; import uorocketry.basestation.connections.DeviceConnection; import uorocketry.basestation.connections.DeviceConnectionHolder; import uorocketry.basestation.connections.DataReceiver; import uorocketry.basestation.control.StateButton; import uorocketry.basestation.data.DataTableCellRenderer; +import uorocketry.basestation.data.RssiProcessor; +import uorocketry.basestation.panel.Chart; import uorocketry.basestation.panel.DataChart; +import uorocketry.basestation.panel.TableHolder; public class Window extends JFrame { private static final long serialVersionUID = -5397816377154627951L; private Main main; - + private Config config; + private JPanel dataTablePanel; - ArrayList dataTables = new ArrayList<>(); - + ArrayList dataTables = new ArrayList<>(); + private JPanel leftPanel; private JScrollPane scrollPane; JCheckBox googleEarthCheckBox; @@ -87,7 +93,7 @@ public class Window extends JFrame { public JPanel centerChartPanel; - public final ArrayList charts = new ArrayList<>(); + public final ArrayList charts = new ArrayList<>(); JButton addChartButton; private JPanel savingToPanel; @@ -97,8 +103,9 @@ public class Window extends JFrame { JButton loadLayout; private JSplitPane splitPane; - public Window(Main main) { + public Window(Main main, Config config) { this.main = main; + this.config = config; // Set look and feel try { @@ -128,8 +135,8 @@ public Window(Main main) { dataTablePanel.setLayout(new BoxLayout(dataTablePanel, BoxLayout.X_AXIS)); leftPanel.add(dataTablePanel); - for (int i = 0; i < Main.dataSourceCount; i++) { - addJTable(i, main.config.getJSONArray("datasets").getJSONObject(i)); + for (int i = 0; i < config.getDataSourceCount(); i++) { + generateTelemetryPanel(i, config.getDataSet(i)); } scrollPane = new JScrollPane(leftPanel); @@ -207,8 +214,8 @@ public Window(Main main) { sliderTabs = new JTabbedPane(JTabbedPane.TOP); - for (int i = 0; i < Main.dataSourceCount; i++) { - addSlider(main.config.getJSONArray("datasets").getJSONObject(i)); + for (int i = 0; i < config.getDataSourceCount(); i++) { + addSlider(config.getObject().getJSONArray("datasets").getJSONObject(i)); } sliderSection.add(sliderTabs, BorderLayout.SOUTH); @@ -252,7 +259,7 @@ public Window(Main main) { sidePanel.add(comPanelParent, BorderLayout.SOUTH); try { - JSONArray array = main.config.getJSONArray("stateEvents"); + JSONArray array = config.getObject().getJSONArray("stateEvents"); if (array.length() > 0) { stateSendingPanel = new JPanel(); @@ -281,8 +288,8 @@ public Window(Main main) { } } - for (int i = 0; i < Main.dataSourceCount; i++) { - addComSelectorPanel(main.config.getJSONArray("datasets").getJSONObject(i), main); + for (int i = 0; i < config.getDataSourceCount(); i++) { + addComSelectorPanel(config.getObject().getJSONArray("datasets").getJSONObject(i), main); } comPanelParent.setLayout(new GridLayout(comPanelParent.getComponentCount(), 1, 0, 0)); @@ -295,38 +302,47 @@ public Window(Main main) { setVisible(true); } - - public void addJTable(int tableIndex, JSONObject dataSet) { - JTable dataTable = new JTable(Main.dataLength.get(tableIndex), 2); - dataTable.setBorder(new LineBorder(new Color(0, 0, 0))); - dataTable.setDefaultRenderer(Object.class, new DataTableCellRenderer()); - dataTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - dataTable.setAlignmentY(Component.TOP_ALIGNMENT); - dataTable.setAlignmentX(Component.LEFT_ALIGNMENT); - dataTable.setCellSelectionEnabled(true); + + + public void generateTelemetryPanel(int tableIndex, DataSet dataSet) { + JPanel borderPanel = new JPanel(); + borderPanel.setBorder(BorderFactory.createTitledBorder(dataSet.getName())); + borderPanel.setLayout(new BoxLayout(borderPanel, BoxLayout.Y_AXIS)); + + JTable receivedDataTable = createTable(config.getDataLength(tableIndex), 2, 30, 130); + JTable connectionInfoTable = createTable(RssiProcessor.labels.length, 2, 30, 130); + borderPanel.add(receivedDataTable); + borderPanel.add(connectionInfoTable); + + dataTables.add(new TableHolder(receivedDataTable, connectionInfoTable)); + + dataTablePanel.add(borderPanel); + } + + public JTable createTable(int rows, int columns, int height, int width) { + JTable table = new JTable(rows, columns); + table.setBorder(new LineBorder(new Color(0, 0, 0))); + table.setDefaultRenderer(Object.class, new DataTableCellRenderer()); + table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + table.setAlignmentY(Component.TOP_ALIGNMENT); + table.setAlignmentX(Component.LEFT_ALIGNMENT); + table.setCellSelectionEnabled(true); // Make non editable - dataTable.setDefaultEditor(Object.class, null); + table.setDefaultEditor(Object.class, null); // Increase row height - dataTable.setRowHeight(30); + table.setRowHeight(height); // Adjust width - dataTable.getColumnModel().getColumn(0).setPreferredWidth(130); - dataTable.getColumnModel().getColumn(1).setPreferredWidth(130); - - dataTable.setFont(new Font("Arial", Font.PLAIN, 15)); - - // Make outer title - JPanel borderPanel = new JPanel(); - borderPanel.setBorder(BorderFactory.createTitledBorder(dataSet.getString("name"))); + table.getColumnModel().getColumn(0).setPreferredWidth(width); + table.getColumnModel().getColumn(1).setPreferredWidth(width); - borderPanel.add(dataTable); - - dataTablePanel.add(borderPanel); - dataTables.add(dataTable); + table.setFont(new Font("Arial", Font.PLAIN, 15)); + + return table; } - + public void addSlider(JSONObject dataSet) { // Add sliders to tabbedPane JPanel sliders = new JPanel(); diff --git a/src/main/java/uorocketry/basestation/config/Config.java b/src/main/java/uorocketry/basestation/config/Config.java new file mode 100644 index 0000000..2ea029e --- /dev/null +++ b/src/main/java/uorocketry/basestation/config/Config.java @@ -0,0 +1,33 @@ +package uorocketry.basestation.config; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Config { + + protected List dataSet = new ArrayList<>(2); + + protected JSONObject configObject = null; + + public Integer getDataLength(int index) { + return getDataSet(index).getLabels().length; + } + + public String[] getLabels(int index) { + return getDataSet(index).getLabels(); + } + + public int getDataSourceCount() { + return dataSet.size(); + } + + public DataSet getDataSet(int index) { + return dataSet.get(index); + } + + public JSONObject getObject() { + return configObject; + } +} diff --git a/src/main/java/uorocketry/basestation/config/DataSet.java b/src/main/java/uorocketry/basestation/config/DataSet.java new file mode 100644 index 0000000..3f62027 --- /dev/null +++ b/src/main/java/uorocketry/basestation/config/DataSet.java @@ -0,0 +1,107 @@ +package uorocketry.basestation.config; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class DataSet { + private String name; + private String color; + + private String[] labels; + private String[] states; + private Map indexes; + + private String separator; + + public DataSet(JSONObject dataSet) { + if (dataSet.has("name")) name = dataSet.getString("name"); + if (dataSet.has("color")) color = dataSet.getString("color"); + if (dataSet.has("labels")) { + labels = jsonStringArrayToArray(dataSet.getJSONArray("labels")); + } + if (dataSet.has("states")) { + states = jsonStringArrayToArray(dataSet.getJSONArray("states")); + } + + if (dataSet.has("name")) { + JSONObject indexesJson = dataSet.getJSONObject("indexes"); + indexes = new HashMap<>(); + for (Iterator it = indexesJson.keys(); it.hasNext(); ) { + String key = it.next(); + try { + indexes.put(key, indexesJson.getInt(key)); + } catch (JSONException e) {} + } + } + + if (dataSet.has("separator")) separator = dataSet.getString("separator"); + } + + public DataSet(String name, String color, String[] labels, String[] states, + Map indexes, String separator) { + this.name = name; + this.color = color; + this.labels = labels; + this.states = states; + this.indexes = indexes; + this.separator = separator; + } + + private String[] jsonStringArrayToArray(JSONArray jsonArray) { + String[] result = new String[jsonArray.length()]; + for (int i = 0; i < result.length; i++) { + result[i] = jsonArray.getString(i); + } + + return result; + } + + public String getName() { + return name; + } + + public String getColor() { + return color; + } + + public String[] getLabels() { + return labels; + } + + public String[] getStates() { + return states; + } + + public String getState(int index) { + return states != null ? states[index] : String.valueOf(index); + } + + public Map getIndexes() { + return indexes; + } + + public boolean hasIndex(String key) { + return indexes != null && indexes.containsKey(key); + } + + public boolean indexEquals(String key, int index) { + if (indexes == null) return false; + + Integer result = indexes.get(key); + return result != null && result == index; + } + + public Integer getIndex(String key) { + return indexes != null ? indexes.get(key) : null; + } + + public String getSeparator() { + return separator; + } +} diff --git a/src/main/java/uorocketry/basestation/config/FileConfig.java b/src/main/java/uorocketry/basestation/config/FileConfig.java new file mode 100644 index 0000000..c9fd365 --- /dev/null +++ b/src/main/java/uorocketry/basestation/config/FileConfig.java @@ -0,0 +1,47 @@ +package uorocketry.basestation.config; + +import org.json.JSONArray; +import org.json.JSONObject; + +import javax.swing.*; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class FileConfig extends Config { + + /** The location of the comma separated labels without the extension. */ + public static final String CONFIG_LOCATION = "data/config.json"; + + /** + * Run once at the beginning of simulation mode + */ + public FileConfig() { + this(CONFIG_LOCATION); + } + + public FileConfig(String fileName) { + String configString = null; + try { + configString = new String(Files.readAllBytes(Paths.get(fileName)), StandardCharsets.UTF_8); + } catch (IOException e) { + e.printStackTrace(); + + JOptionPane.showMessageDialog(null, "The config file was not found in " + fileName + + "\r\n\r\nIf you plan downloaded a release build, you might want to download the version with labels and sample data included."); + + return; + } + + configObject = new JSONObject(configString); + + JSONArray datasetsJSONArray = configObject.getJSONArray("datasets"); + dataSet = new ArrayList<>(datasetsJSONArray.length()); + for (int i = 0; i < datasetsJSONArray.length(); i++) { + dataSet.add(new DataSet(datasetsJSONArray.getJSONObject(i))); + } + } +} diff --git a/src/main/java/uorocketry/basestation/connections/DataReceiver.java b/src/main/java/uorocketry/basestation/connections/DataReceiver.java index f401177..600ebc4 100644 --- a/src/main/java/uorocketry/basestation/connections/DataReceiver.java +++ b/src/main/java/uorocketry/basestation/connections/DataReceiver.java @@ -1,5 +1,7 @@ package uorocketry.basestation.connections; +import org.jetbrains.annotations.NotNull; + public interface DataReceiver { - void receivedData(DeviceConnection deviceConnection, byte[] data); + void receivedData(@NotNull DeviceConnection deviceConnection, byte[] data); } diff --git a/src/main/java/uorocketry/basestation/connections/method/SerialConnectionMethod.java b/src/main/java/uorocketry/basestation/connections/method/SerialConnectionMethod.java index 36e701b..c3d4d1c 100644 --- a/src/main/java/uorocketry/basestation/connections/method/SerialConnectionMethod.java +++ b/src/main/java/uorocketry/basestation/connections/method/SerialConnectionMethod.java @@ -83,8 +83,13 @@ public boolean delimiterIndicatesEndOfMessage() { @Override public void serialEvent(SerialPortEvent event) { - if (listener != null) { - listener.receivedData(event.getReceivedData()); + try { + if (listener != null) { + listener.receivedData(event.getReceivedData()); + } + } catch (Exception e) { + // Serial event hijacks exceptions, this will cause them to still be printed + e.printStackTrace(); } } } diff --git a/src/main/java/uorocketry/basestation/data/Data.java b/src/main/java/uorocketry/basestation/data/Data.java index d8407be..f8890ab 100644 --- a/src/main/java/uorocketry/basestation/data/Data.java +++ b/src/main/java/uorocketry/basestation/data/Data.java @@ -4,16 +4,16 @@ public class Data { //the actual data point - public Float data; - - Long dataLong; + private Float data; + + private Long dataLong; //used for special data types - float minutes; - - Types type; - - DecimalFormat format = new DecimalFormat("#.######"); + private float minutes; + + private Types type; + + private DecimalFormat format = new DecimalFormat("#.######"); enum Types { NORMAL, diff --git a/src/main/java/uorocketry/basestation/data/DataHandler.java b/src/main/java/uorocketry/basestation/data/DataHolder.java similarity index 57% rename from src/main/java/uorocketry/basestation/data/DataHandler.java rename to src/main/java/uorocketry/basestation/data/DataHolder.java index 47249da..d87be8d 100644 --- a/src/main/java/uorocketry/basestation/data/DataHandler.java +++ b/src/main/java/uorocketry/basestation/data/DataHolder.java @@ -6,31 +6,17 @@ import javax.swing.JTable; import javax.swing.table.TableModel; -import org.json.JSONException; -import org.json.JSONObject; +import org.jetbrains.annotations.Nullable; -import uorocketry.basestation.Main; +import uorocketry.basestation.config.DataSet; -public class DataHandler { - - public static final DataType TIMESTAMP = new DataType(0, 0); - public static final DataType ALTITUDE = new DataType(1, 0); - public static final DataType LATITUDE = new DataType(2, 0); - public static final DataType LONGITUDE = new DataType(3, 0); - public static final DataType PITCH = new DataType(4, 0); - public static final DataType ROLL = new DataType(5, 0); - public static final DataType YAW = new DataType(6, 0); - public static final DataType ACCELX = new DataType(7, 0); - public static final DataType ACCELY = new DataType(8, 0); - public static final DataType ACCELZ = new DataType(9, 0); - public static final DataType VELOCITY = new DataType(10, 0); - public static final DataType BRAKE_PERCENTAGE = new DataType(10, 0); - public static final DataType ACTUAL_BRAKE_VALUE = new DataType(12, 0); - public static final DataType GPS_FIX = new DataType(13, 0); - public static final DataType GPS_FIX_QUALITY = new DataType(14, 0); +public class DataHolder { public Data[] data; public DataType[] types; + + public static final DataType TIMESTAMP = new DataType(0, 0); + public static final DataType ALTITUDE = new DataType(1, 0); /** Which of this data should be hidden for any reason */ public List hiddenDataTypes = new LinkedList(); @@ -39,21 +25,22 @@ public class DataHandler { * This chooses which table this data is displayed in */ public int tableIndex = 0; + + private DataSet dataSet; + - private JSONObject datasetConfig; - - public DataHandler(int tableIndex, JSONObject datasetConfig) { + public DataHolder(int tableIndex, DataSet dataSet) { this.tableIndex = tableIndex; - this.datasetConfig = datasetConfig; + this.dataSet = dataSet; - this.data = new Data[Main.dataLength.get(tableIndex)]; + this.data = new Data[dataSet.getLabels().length]; - types = new DataType[Main.dataLength.get(tableIndex)]; + types = new DataType[dataSet.getLabels().length]; for (int i = 0; i < types.length; i++) { types[i] = new DataType(i, tableIndex); } } - + public void updateTableUIWithData(JTable table, String[] labels) { TableModel tableModel = table.getModel(); @@ -63,9 +50,9 @@ public void updateTableUIWithData(JTable table, String[] labels) { String dataText = data[i].getFormattedString(); if (hiddenDataTypes.contains(types[i])) dataText = "Hidden Data"; - - if (datasetConfig.getInt("stateIndex") == i && data[i].getDecimalValue() != null) { - dataText = datasetConfig.getJSONArray("states").getString(data[i].getDecimalValue().intValue()); + + if (dataSet.indexEquals("state", i) && data[i].getDecimalValue() != null) { + dataText = dataSet.getState(data[i].getDecimalValue().intValue()); } // Set data @@ -75,17 +62,10 @@ public void updateTableUIWithData(JTable table, String[] labels) { public boolean set(int index, String currentData) { // Check for special cases first - boolean isFormattedCoordinate = false; - boolean isTimestamp = false; - try { - isTimestamp = datasetConfig.getInt("timestampIndex") == index; - - JSONObject coordinateIndexes = datasetConfig.getJSONObject("coordinateIndexes"); - isFormattedCoordinate = coordinateIndexes.has("formattedCoordinates") - && coordinateIndexes.getBoolean("formattedCoordinates") - && (coordinateIndexes.getInt("latitude") == index || coordinateIndexes.getInt("longitude") == index); - } catch (JSONException e) {} - + boolean isFormattedCoordinate = (dataSet.indexEquals("latitude", index) && dataSet.indexEquals("latitudeFormatted", index)) + || (dataSet.indexEquals("longitude", index) && dataSet.indexEquals("longitudeFormatted", index)); + boolean isTimestamp = dataSet.indexEquals("timestamp", index); + if (isFormattedCoordinate) { // These need to be converted to decimal coordinates to be used diff --git a/src/main/java/uorocketry/basestation/data/DataPoint.java b/src/main/java/uorocketry/basestation/data/DataPoint.java new file mode 100644 index 0000000..3f37b14 --- /dev/null +++ b/src/main/java/uorocketry/basestation/data/DataPoint.java @@ -0,0 +1,54 @@ +package uorocketry.basestation.data; + +import java.util.List; + +public class DataPoint { + private int receivedDataIndex; + private DataHolder receivedData; + + private int connectionInfoIndex; + private DataHolder connectionInfoData; + + public DataPoint(int receivedDataIndex, DataHolder receivedData, int connectionInfoIndex, DataHolder connectionInfoData) { + this.receivedDataIndex = receivedDataIndex; + this.receivedData = receivedData; + + this.connectionInfoIndex = connectionInfoIndex; + this.connectionInfoData = connectionInfoData; + } + + public DataPoint(List receivedDataHolders, List connectionInfoDataHolders, + int receivedDataIndex, int connectionInfoIndex) { + this(receivedDataIndex, + receivedDataIndex > 0 && receivedDataIndex < receivedDataHolders.size() ? receivedDataHolders.get(receivedDataIndex) : null, + connectionInfoIndex, + connectionInfoIndex > 0 && connectionInfoIndex < connectionInfoDataHolders.size() ? connectionInfoDataHolders.get(connectionInfoIndex): null); + } + + public void clearHiddenTypes() { + clearHiddenTypes(receivedData); + clearHiddenTypes(connectionInfoData); + } + + private void clearHiddenTypes(DataHolder dataHolder) { + if (dataHolder != null && !dataHolder.hiddenDataTypes.isEmpty()) { + dataHolder.hiddenDataTypes.clear(); + } + } + + public int getReceivedDataIndex() { + return receivedDataIndex; + } + + public DataHolder getReceivedData() { + return receivedData; + } + + public int getConnectionInfoIndex() { + return connectionInfoIndex; + } + + public DataHolder getConnectionInfoData() { + return connectionInfoData; + } +} diff --git a/src/main/java/uorocketry/basestation/data/DataPointHolder.java b/src/main/java/uorocketry/basestation/data/DataPointHolder.java new file mode 100644 index 0000000..2c45675 --- /dev/null +++ b/src/main/java/uorocketry/basestation/data/DataPointHolder.java @@ -0,0 +1,79 @@ +package uorocketry.basestation.data; + +import uorocketry.basestation.connections.DeviceConnection; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class DataPointHolder implements Iterable> { + private final List> allReceivedData; + private final List> allConnectionInfoData; + private final List> dataPoints; + + public DataPointHolder(int dataSourceCount) { + allReceivedData = new ArrayList<>(dataSourceCount); + allConnectionInfoData = new ArrayList<>(dataSourceCount); + dataPoints = new ArrayList<>(dataSourceCount); + + for (int i = 0; i < dataSourceCount; i++) { + allReceivedData.add(new ArrayList<>()); + allConnectionInfoData.add(new ArrayList<>()); + dataPoints.add(new ArrayList<>()); + } + } + + public List get(int i) { + return dataPoints.get(i); + } + + public int size() { + return dataPoints.size(); + } + + public List> getAllReceivedData() { + return allReceivedData; + } + + public List> getAllConnectionInfoData() { + return allConnectionInfoData; + } + + @Override + public Iterator> iterator() { + return dataPoints.iterator(); + } + + /** + * Convert an index for the general dataPoints array into an index for the + * specific array allConnectionInfoData + */ + public int toReceivedDataIndex(int tableIndex, int dataPointIndex) { + return toReceivedDataIndex(dataPoints.get(tableIndex), dataPointIndex); + } + + /** + * Convert an index for the general dataPoints array into an index for the + * specific array allReceivedData + */ + public int toReceivedDataIndex(List dataPoints, int dataPointIndex) { + return dataPoints.get(dataPointIndex).getReceivedDataIndex(); + } + + /** + * Convert an index for the general dataPoints array into an index for the + * specific array allConnectionInfoData + * @return + */ + public int toConnectionInfoDataIndex(int tableIndex, int dataPointIndex) { + return toConnectionInfoDataIndex(dataPoints.get(tableIndex), dataPointIndex); + } + + /** + * Convert an index for the general dataPoints array into an index for the + * specific array allConnectionInfoData + */ + public int toConnectionInfoDataIndex(List dataPoints, int dataPointIndex) { + return dataPoints.get(dataPointIndex).getConnectionInfoIndex(); + } +} diff --git a/src/main/java/uorocketry/basestation/data/DataProcessor.java b/src/main/java/uorocketry/basestation/data/DataProcessor.java new file mode 100644 index 0000000..5aca138 --- /dev/null +++ b/src/main/java/uorocketry/basestation/data/DataProcessor.java @@ -0,0 +1,317 @@ +package uorocketry.basestation.data; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; + +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.DataSet; +import uorocketry.basestation.connections.DeviceConnection; +import uorocketry.basestation.panel.Chart; +import uorocketry.basestation.panel.TableHolder; + +import javax.swing.*; +import javax.swing.table.TableModel; + +public class DataProcessor { + + /** Separator for the data */ + public static final String SEPARATOR = ","; + + private DataPointHolder dataPointHolder; + private final Config mainConfig; + private final List rssiDataSets; + + /** Where to save the log file */ + private static final String LOG_FILE_SAVE_LOCATION = "data/"; + private static final String DEFAULT_LOG_FILE_NAME = "log"; + private static final String LOG_FILE_EXTENSION = ".txt"; + /** Will have a number appended to the end to not overwrite old logs */ + private final ArrayList currentLogFileName; + private final ArrayList> logQueues; + + private final ArrayList dataTables; + + public DataProcessor(Config mainConfig, ArrayList dataTables) { + this.mainConfig = mainConfig; + this.dataTables = dataTables; + + dataPointHolder = new DataPointHolder(mainConfig.getDataSourceCount()); + currentLogFileName = new ArrayList<>(mainConfig.getDataSourceCount()); + logQueues = new ArrayList<>(mainConfig.getDataSourceCount()); + rssiDataSets = new ArrayList<>(mainConfig.getDataSourceCount()); + + for (int i = 0; i < mainConfig.getDataSourceCount(); i++) { + logQueues.add(new ArrayDeque<>()); + + rssiDataSets.add(new DataSet(mainConfig.getDataSet(i).getName() + " RSSI", + mainConfig.getDataSet(i).getColor(), RssiProcessor.labels, null, null, SEPARATOR)); + } + } + + public void receivedData(@NotNull DeviceConnection deviceConnection, byte[] data) { + receivedData(deviceConnection.getTableIndex(), data); + + logData(deviceConnection, data); + } + + public void receivedData(int tableIndex, byte[] data) { + String delimitedMessage = new String(data, StandardCharsets.UTF_8); + + if (RssiProcessor.isValid(delimitedMessage)) { + DataHolder dataHolder = parseRSSI(tableIndex, delimitedMessage); + List connectionInfoData = dataPointHolder.getAllConnectionInfoData().get(tableIndex); + connectionInfoData.add(dataHolder); + + List receivedData = dataPointHolder.getAllReceivedData().get(tableIndex); + dataPointHolder.get(tableIndex).add(new DataPoint(receivedData, connectionInfoData, + receivedData.size() - 1, + connectionInfoData.size() - 1)); + } else { + DataHolder dataHolder = parseData(tableIndex, delimitedMessage); + List receivedData = dataPointHolder.getAllReceivedData().get(tableIndex); + receivedData.add(dataHolder); + + List connectionInfoData = dataPointHolder.getAllConnectionInfoData().get(tableIndex); + dataPointHolder.get(tableIndex).add(new DataPoint(receivedData, connectionInfoData, + receivedData.size() - 1, + connectionInfoData.size() - 1)); + } + } + + protected DataHolder parseData(int tableIndex, String data) { + DataHolder dataHolder = new DataHolder(tableIndex, mainConfig.getDataSet(tableIndex)); + + // Clear out the b' ' stuff added that is only meant for the radio to see + data = data.replaceAll("b'|(?:\\\\r\\\\n|\\r\\n)'?", ""); + if (data.endsWith(",")) data = data.substring(0, data.length() - 1); + + // Semi-colon separated + String[] splitData = data.split(SEPARATOR); + if (splitData.length != dataHolder.data.length) { + //invalid data + System.err.println("Line with invalid data (Not the correct amount of data). It was " + + splitData.length + " vs " + dataHolder.data.length + ". " + data); + + return null; + } + + // Ensure that the timestamp has not gone back in time + Integer timestampIndex = mainConfig.getDataSet(tableIndex).getIndex("timestamp"); + if (timestampIndex != null) { + DataHolder lastDataPointDataHolder = findLastValidDataPoint(dataPointHolder.getAllReceivedData().get(tableIndex)); + if (lastDataPointDataHolder != null) { + Float value = lastDataPointDataHolder.data[timestampIndex].getDecimalValue(); + try { + if (value != null && Float.parseFloat(splitData[timestampIndex]) < value) { + System.err.println("Timestamp just went backwards. Original was " + value); + + // Treat as invalid data + return null; + } + } catch (NumberFormatException e) {} + + } + } + + for (int i = 0; i < splitData.length; i++) { + if (!dataHolder.set(i, splitData[i])) { + System.err.println("Failed to set data handler"); + + // Parsing failed + return null; + } + } + + return dataHolder; + } + + protected DataHolder parseRSSI(int tableIndex, String data) { + DataHolder dataHolder = new DataHolder(tableIndex, rssiDataSets.get(tableIndex)); + + if (RssiProcessor.setDataHolder(dataHolder, data)) { + return dataHolder; + } else { + System.err.println("RSSI Line with invalid data. " + data); + return null; + } + + } + + /** + * Sets tables up for the given index + * + * @param showMaxIndex If true, it will make sure to always show the latest index, + * for use when connection info and received data drift apart + * @return The received DataHolder + */ + public DataPoint setTableTo(int tableIndex, int index, boolean showMaxIndex) { + DataPoint dataPoint = dataPointHolder.get(tableIndex).get(index); + if (dataPoint == null) return null; + + DataHolder currentDataHolder = dataPoint.getReceivedData(); + JTable receivedDataTable = dataTables.get(tableIndex).getReceivedDataTable(); + updateTable(index, currentDataHolder, receivedDataTable, mainConfig.getDataSet(tableIndex)); + + DataHolder connectionInfoHolder = dataPoint.getConnectionInfoData(); + JTable connectionInfoTable = dataTables.get(tableIndex).getConnectionInfoTable(); + updateTable(index, connectionInfoHolder, connectionInfoTable, rssiDataSets.get(tableIndex)); + + return dataPoint; + } + + private void updateTable(int index, DataHolder currentDataHolder, JTable dataTable, DataSet dataSet) { + if (currentDataHolder != null) { + currentDataHolder.updateTableUIWithData(dataTable, dataSet.getLabels()); + } else { + setTableToError(index, dataTable); + } + } + + private void setTableToError(int index, JTable table) { + TableModel tableModel = table.getModel(); + + // Set first item to "Error" + tableModel.setValueAt("Parsing Error", 0, 0); + tableModel.setValueAt(index, 0, 1); + + for (int i = 1; i < tableModel.getRowCount(); i++) { + // Set label + tableModel.setValueAt("", i, 0); + + // Set data + tableModel.setValueAt("", i, 1); + } + } + + public void updateChart(Chart chart, int minDataIndex, int maxDataIndex, boolean onlyShowLatestData, int maxDataPointsDisplayed) { + chart.update(dataPointHolder, minDataIndex, maxDataIndex, onlyShowLatestData, maxDataPointsDisplayed); + } + + /** + * Log the raw bytes received + * + * @param deviceConnection + * @param data + */ + private void logData(DeviceConnection deviceConnection, byte[] data) { + if (!deviceConnection.isWriting()) { + deviceConnection.setWriting(true); + + // Write to file + try (OutputStream outputStream = new FileOutputStream( + LOG_FILE_SAVE_LOCATION + currentLogFileName.get(deviceConnection.getTableIndex()), true)) { + outputStream.write(data); + + Queue logQueue = logQueues.get(deviceConnection.getTableIndex()); + if (!logQueue.isEmpty()) { + byte[][] queueItems = new byte[100][]; + synchronized (logQueues) { + for (int i = 0; !logQueue.isEmpty() && i < queueItems.length; i++) { + queueItems[i] = logQueue.remove(); + } + } + + for (int i = 0; i < queueItems.length && queueItems[i] != null; i++) { + outputStream.write(queueItems.length); + } + } + + + } catch (IOException err) { + err.printStackTrace(); + } + + deviceConnection.setWriting(false); + } else { + synchronized (logQueues) { + logQueues.get(deviceConnection.getTableIndex()).add(data); + } + } + } + + public void setupLogFileName() { + // Figure out file name for logging + File folder = new File(LOG_FILE_SAVE_LOCATION); + File[] listOfLogFiles = folder.listFiles(); + Set usedFileNames = new HashSet(); + + for (File file: listOfLogFiles) { + if (file.isFile() && file.getName().contains(DEFAULT_LOG_FILE_NAME)) { + usedFileNames.add(file.getName()); + } + } + + int logIndex = 0; + + JSONArray dataSets = mainConfig.getObject().getJSONArray("datasets"); + + // Find a suitable filename + for (int i = 0; i <= listOfLogFiles.length; i++) { + boolean containsFile = false; + for (int j = 0 ; j < getDataSourceCount(); j++) { + if (usedFileNames.contains(DEFAULT_LOG_FILE_NAME + "_" + dataSets.getJSONObject(j).getString("name").toLowerCase() + "_" + logIndex + LOG_FILE_EXTENSION)) { + containsFile = true; + break; + } + } + + if (containsFile) { + logIndex++; + } else { + break; + } + } + + // Set the names + for (int i = 0 ; i < getDataSourceCount(); i++) { + currentLogFileName.add(DEFAULT_LOG_FILE_NAME + "_" + dataSets.getJSONObject(i).getString("name").toLowerCase() + "_" + logIndex + LOG_FILE_EXTENSION); + } + } + + public String formattedSavingToLocations() { + StringBuilder savingToText = new StringBuilder(); + + // Add text for each file + for (int i = 0 ; i < getDataSourceCount(); i++) { + if (i != 0) savingToText.append(", "); + + savingToText.append(LOG_FILE_SAVE_LOCATION + currentLogFileName.get(i)); + } + + return savingToText.toString(); + } + + private int getDataSourceCount() { + return dataPointHolder.size(); + } + + /** + * Find last non null data point + * + * @param currentTableData + * @return DataHolder if found, null otherwise + */ + private DataHolder findLastValidDataPoint(List currentTableData) { + for (int i = currentTableData.size() - 1; i >= 0; i--) { + if (currentTableData.get(i) != null) { + return currentTableData.get(i); + } + } + + return null; + } + + @Deprecated + public List> getAllReceivedData() { + return dataPointHolder.getAllReceivedData(); + } + + public DataPointHolder getDataPointHolder() { + return dataPointHolder; + } + +} diff --git a/src/main/java/uorocketry/basestation/data/RssiProcessor.java b/src/main/java/uorocketry/basestation/data/RssiProcessor.java new file mode 100644 index 0000000..6676dff --- /dev/null +++ b/src/main/java/uorocketry/basestation/data/RssiProcessor.java @@ -0,0 +1,82 @@ +package uorocketry.basestation.data; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RssiProcessor { + + private static final Pattern validMessage = Pattern.compile("L\\/R RSSI: .+dco=[^\\s]$"); + + private static final Pattern localRSSI = Pattern.compile("L\\/R RSSI: ([^\\s]+)\\/"); + private static final Pattern remoteRSSI = Pattern.compile("L\\/R RSSI: [^\\s]+\\/([^\\s]+)"); + private static final Pattern localNoise = Pattern.compile("L\\/R noise: ([^\\s]+)\\/"); + private static final Pattern remoteNoise = Pattern.compile("L\\/R noise: [^\\s]+\\/([^\\s]+)"); + private static final Pattern packets = Pattern.compile("pkts: ([^\\s]+)"); + private static final Pattern txe = Pattern.compile("txe=([^\\s]+)"); + private static final Pattern rxe = Pattern.compile("rxe=([^\\s]+)"); + private static final Pattern stx = Pattern.compile("stx=([^\\s]+)"); + private static final Pattern srx = Pattern.compile("srx=([^\\s]+)"); + private static final Pattern ecc1 = Pattern.compile("ecc=([^\\s]+)\\/"); + private static final Pattern ecc2 = Pattern.compile("ecc=[^\\s]+\\/([^\\s]+)"); + private static final Pattern temperature = Pattern.compile("temp=([^\\s]+)"); + private static final Pattern dco = Pattern.compile("dco=([^\\s]+)"); + + private static final Pattern[] patterns = { + localRSSI, + remoteRSSI, + localNoise, + remoteNoise, + packets, + temperature, + txe, + rxe, + stx, + srx, + ecc1, + ecc2, + dco + }; + + public static String[] labels = { + "Local RSSI", + "Remote RSSI", + "Local Noise", + "Remote Noise", + "Packets", + "Temperature", + "txe", + "rxe", + "stx", + "srx", + "ecc1", + "ecc2", + "dco" + }; + + public static boolean isValid(String data) { + return validMessage.matcher(data).find(); + } + + public static boolean setDataHolder(DataHolder dataHolder, String data) { + for (int i = 0; i < patterns.length; i++) { + String value = getCapturedValue(patterns[i], data); + + // If setting failed + if (value == null || !dataHolder.set(i, value)) { + return false; + } + } + + return true; + } + + private static String getCapturedValue(Pattern pattern, String data) { + Matcher matcher = pattern.matcher(data); + + if (matcher.find()) { + return matcher.group(1); + } else { + return null; + } + } +} diff --git a/src/main/java/uorocketry/basestation/external/GoogleEarthUpdater.java b/src/main/java/uorocketry/basestation/external/GoogleEarthUpdater.java index 24583d0..d933867 100644 --- a/src/main/java/uorocketry/basestation/external/GoogleEarthUpdater.java +++ b/src/main/java/uorocketry/basestation/external/GoogleEarthUpdater.java @@ -15,8 +15,12 @@ import org.json.JSONObject; import uorocketry.basestation.Main; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.DataSet; import uorocketry.basestation.data.Data; -import uorocketry.basestation.data.DataHandler; +import uorocketry.basestation.data.DataHolder; +import uorocketry.basestation.data.DataPoint; +import uorocketry.basestation.data.DataPointHolder; public class GoogleEarthUpdater { @@ -32,10 +36,8 @@ public GoogleEarthUpdater() { /** * Generates a kml file from the data currentDataIndex - * - * @param main */ - public String generateKMLFile(List> allData, List minDataIndex, List currentDataIndex, JSONArray dataSets) { + public String generateKMLFile(DataPointHolder dataPointHolder, List minDataIndex, List currentDataIndex, Config config) { StringBuilder content = new StringBuilder(); content.append("\r\n" + @@ -49,24 +51,27 @@ public String generateKMLFile(List> allData, List min " " + " "); - for (int i = 0; i < allData.size(); i++) { + for (int i = 0; i < dataPointHolder.size(); i++) { + int minIndex = dataPointHolder.toReceivedDataIndex(i, minDataIndex.get(i)); + int maxIndex = dataPointHolder.toReceivedDataIndex(i, currentDataIndex.get(i)); + // Add style content.append(""); content.append("\r\n"); - content.append("Path of " + dataSets.getJSONObject(i).getString("name")); + content.append("Path of " + config.getDataSet(i).getName()); content.append("\r\n"); content.append("#blackLine"); content.append("absolute\r\n"); - for (int j = minDataIndex.get(i); j <= currentDataIndex.get(i); j++) { - String currentString = getCoordinateString(allData.get(i).get(j), dataSets.getJSONObject(i).getJSONObject("coordinateIndexes")); + for (int j = minIndex; j <= maxIndex; j++) { + String currentString = getCoordinateString(dataPointHolder.getAllReceivedData().get(i).get(j), config.getDataSet(i)); if (currentString != null) { content.append(currentString + "\r\n"); @@ -78,10 +83,10 @@ public String generateKMLFile(List> allData, List min content.append("\r\n"); //Add the latest coordinate as a placemark - String latestDataString = getCoordinateString(allData.get(i).get(currentDataIndex.get(i)), dataSets.getJSONObject(i).getJSONObject("coordinateIndexes")); + String latestDataString = getCoordinateString(dataPointHolder.getAllReceivedData().get(i).get(maxIndex), config.getDataSet(i)); if (latestDataString != null) { content.append("\r\n"); - content.append("Latest Position of " + dataSets.getJSONObject(i).getString("name")); + content.append("Latest Position of " + config.getDataSet(i).getName()); content.append("\r\n"); content.append("\r\nabsolute\r\n"); @@ -99,14 +104,14 @@ public String generateKMLFile(List> allData, List min /** * Updates the KML file with the data up to currentDataIndex. * - * @param tableIndex - * @param allData + * @param dataPointHolder + * @param minDataIndex * @param currentDataIndex - * @param dataSets + * @param config * @param secondRun Is this a second run? This is true if it is being run from a task called by this function. * The task is run to force Google Earth to update the display. */ - public void updateKMLFile(List> allData, List minDataIndex, List currentDataIndex, JSONArray dataSets, boolean secondRun) { + public void updateKMLFile(DataPointHolder dataPointHolder, List minDataIndex, List currentDataIndex, Config config, boolean secondRun) { if (!secondRun) { if (mapRefreshTaskTimer != null) { // No need to update again that recently @@ -117,7 +122,7 @@ public void updateKMLFile(List> allData, List minData mapRefreshTaskTimer = new TimerTask() { @Override public void run() { - updateKMLFile(allData, minDataIndex, currentDataIndex, dataSets, true); + updateKMLFile(dataPointHolder, minDataIndex, currentDataIndex, config, true); mapRefreshTaskTimer = null; } @@ -129,7 +134,7 @@ public void run() { } } - String fileContent = generateKMLFile(allData, minDataIndex, currentDataIndex, dataSets); + String fileContent = generateKMLFile(dataPointHolder, minDataIndex, currentDataIndex, config); try (Writer writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream(Main.GOOGLE_EARTH_DATA_LOCATION), StandardCharsets.UTF_8))) { @@ -139,23 +144,16 @@ public void run() { } } - public String getCoordinateString(DataHandler dataPoint, JSONObject coordinateIndexes) { + public String getCoordinateString(DataHolder dataPoint, DataSet dataSet) { if (dataPoint == null) return null; - Data altitudeData = dataPoint.data[coordinateIndexes.getInt("altitude")]; - Data longitudeData = dataPoint.data[coordinateIndexes.getInt("longitude")]; - Data latitudeData = dataPoint.data[coordinateIndexes.getInt("latitude")]; - - String prefixString = ""; - try { - if (coordinateIndexes.getBoolean("formattedCoordinates")) { - // Assume west - prefixString = "-"; - } - } catch (JSONException e) {} + Data altitudeData = dataPoint.data[dataSet.getIndex("altitude")]; + Data longitudeData = dataPoint.data[dataSet.getIndex("longitude")]; + Data latitudeData = dataPoint.data[dataSet.getIndex("latitude")]; - if (longitudeData.data != 0 && latitudeData.data != 0 && longitudeData.getDecimalValue() != null && latitudeData.getDecimalValue() != null) { - return prefixString + longitudeData.getDecimalValue() + "," + latitudeData.getDecimalValue() + "," + altitudeData.data; + if (longitudeData.getDecimalValue() != null && latitudeData.getDecimalValue() != null && altitudeData.getDecimalValue() != null + && longitudeData.getDecimalValue() != 0 && latitudeData.getDecimalValue() != 0) { + return longitudeData.getDecimalValue() + "," + latitudeData.getDecimalValue() + "," + altitudeData.getDecimalValue(); } return null; diff --git a/src/main/java/uorocketry/basestation/external/WebViewUpdater.java b/src/main/java/uorocketry/basestation/external/WebViewUpdater.java index 17c7e95..e7db229 100644 --- a/src/main/java/uorocketry/basestation/external/WebViewUpdater.java +++ b/src/main/java/uorocketry/basestation/external/WebViewUpdater.java @@ -12,8 +12,11 @@ import org.json.JSONObject; import uorocketry.basestation.Main; +import uorocketry.basestation.config.Config; import uorocketry.basestation.data.Data; -import uorocketry.basestation.data.DataHandler; +import uorocketry.basestation.data.DataHolder; +import uorocketry.basestation.data.DataPoint; +import uorocketry.basestation.data.DataPointHolder; /** * DOES NOT support multiple data sources @@ -22,9 +25,11 @@ * */ public class WebViewUpdater extends WebSocketServer { + + private final int TABLE_INDEX = 0; List connections = new ArrayList<>(); - + public WebViewUpdater() { super(new InetSocketAddress(Main.WEBVIEW_PORT)); @@ -33,44 +38,38 @@ public WebViewUpdater() { /** * Generates a json from the data currentDataIndex - * - * @param main */ - public JSONObject generateJSON(List> allData, List minDataIndex, List currentDataIndex, JSONArray dataSets) { - JSONObject coordinateIndexes = dataSets.getJSONObject(0).getJSONObject("coordinateIndexes"); - + public JSONObject generateJSON(DataPointHolder dataPointHolder, List minDataIndex, List currentDataIndex, Config config) { JSONObject jsonObject = new JSONObject(); - int index = currentDataIndex.get(0); - List currentDataList = allData.get(0); + int index = currentDataIndex.get(TABLE_INDEX); + List currentDataList = dataPointHolder.get(TABLE_INDEX); if (currentDataList == null) return null; - DataHandler dataPoint = currentDataList.get(index); - if (dataPoint == null) return null; + DataHolder dataHolder = currentDataList.get(index).getReceivedData(); + if (dataHolder == null) return null; - Data altitudeData = dataPoint.data[coordinateIndexes.getInt("altitude")]; - Data longitudeData = dataPoint.data[coordinateIndexes.getInt("longitude")]; - Data latitudeData = dataPoint.data[coordinateIndexes.getInt("latitude")]; + Data altitudeData = dataHolder.data[config.getDataSet(TABLE_INDEX).getIndex("altitude")]; + Data longitudeData = dataHolder.data[config.getDataSet(TABLE_INDEX).getIndex("longitude")]; + Data latitudeData = dataHolder.data[config.getDataSet(TABLE_INDEX).getIndex("latitude")]; if (altitudeData == null || longitudeData == null || latitudeData == null) return null; - - + jsonObject.put("latitude", latitudeData.getDecimalValue()); jsonObject.put("longitude", longitudeData.getDecimalValue()); jsonObject.put("altitude", altitudeData.getDecimalValue()); - + return jsonObject; } /** * Sends updated data over the websocket channel * - * @param tableIndex - * @param allData + * @param dataPointHolder * @param currentDataIndex - * @param dataSets + * @param config */ - public void sendUpdate(List> allData, List minDataIndex, List currentDataIndex, JSONArray dataSets) { - JSONObject jsonObject = generateJSON(allData, minDataIndex, currentDataIndex, dataSets); + public void sendUpdate(DataPointHolder dataPointHolder, List minDataIndex, List currentDataIndex, Config config) { + JSONObject jsonObject = generateJSON(dataPointHolder, minDataIndex, currentDataIndex, config); if (jsonObject == null) return; String fileContent = jsonObject.toString(); diff --git a/src/main/java/uorocketry/basestation/panel/Chart.java b/src/main/java/uorocketry/basestation/panel/Chart.java new file mode 100644 index 0000000..18acb72 --- /dev/null +++ b/src/main/java/uorocketry/basestation/panel/Chart.java @@ -0,0 +1,31 @@ +package uorocketry.basestation.panel; + +import uorocketry.basestation.data.DataHolder; +import uorocketry.basestation.data.DataPoint; +import uorocketry.basestation.data.DataPointHolder; +import uorocketry.basestation.data.DataType; + +import javax.swing.*; +import java.awt.event.MouseEvent; +import java.util.List; + +public interface Chart { + void update(DataPointHolder dataPointHolder, int minDataIndex, int maxDataIndex, boolean onlyShowLatestData, int maxDataPointsDisplayed); + + JPanel getPanel(); + SnapPanel getSpanPanel(); + + DataType[] getXTypes(); + void setXTypes(DataType[] xTypes); + DataType getYType(); + void setYType(DataType yTypes); + + /** + * + * @param e + * @return if the drag event has been handled. If true, the snap panel will not process window resizes. + */ + default boolean mouseDragged(MouseEvent e) { + return false; + } +} diff --git a/src/main/java/uorocketry/basestation/panel/DataChart.java b/src/main/java/uorocketry/basestation/panel/DataChart.java index 436746c..b207010 100644 --- a/src/main/java/uorocketry/basestation/panel/DataChart.java +++ b/src/main/java/uorocketry/basestation/panel/DataChart.java @@ -2,33 +2,43 @@ import org.knowm.xchart.XChartPanel; import org.knowm.xchart.XYChart; - +import org.knowm.xchart.XYSeries; import uorocketry.basestation.Main; -import uorocketry.basestation.data.DataHandler; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.data.DataHolder; +import uorocketry.basestation.data.DataPoint; +import uorocketry.basestation.data.DataPointHolder; import uorocketry.basestation.data.DataType; -public class DataChart { - public XYChart xyChart; - - public XChartPanel chartPanel; +import javax.swing.*; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +public class DataChart implements Chart { + private XYChart xyChart; + + private XChartPanel chartPanel; /** The snap panel for this chart */ - public SnapPanel snapPanel; - - public Main main; + private SnapPanel snapPanel; + + private Main main; + private Config config; // The active chart series on this chart - public String[] activeSeries = new String[0]; - - public DataType[] xTypes = {DataHandler.ALTITUDE}; - public DataType yType = DataHandler.TIMESTAMP; + private String[] activeSeries = new String[0]; + + private DataType[] xTypes = {DataHolder.ALTITUDE}; + private DataType yType = DataHolder.TIMESTAMP; - public DataChart(Main main, XYChart xyChart, XChartPanel chartPanel) { + public DataChart(Main main, Config config, XYChart xyChart, XChartPanel chartPanel) { this.main = main; + this.config = config; this.xyChart = xyChart; this.chartPanel = chartPanel; - this.snapPanel = new SnapPanel(this); + this.snapPanel = new SnapPanel(main, this); activeSeries = new String[xTypes.length]; for (int i = 0; i < xTypes.length; i++) { @@ -36,9 +46,210 @@ public DataChart(Main main, XYChart xyChart, XChartPanel chartPanel) { } } - public DataChart(Main main, XYChart xyChart, XChartPanel chartPanel, DataType[] xTypes) { - this(main, xyChart, chartPanel); + public DataChart(Main main, Config config, XYChart xyChart, XChartPanel chartPanel, DataType[] xTypes) { + this(main, config, xyChart, chartPanel); + + this.xTypes = xTypes; + } + + @Override + public void update(DataPointHolder dataPointHolder, int minDataPointIndex, int maxDataPointIndex, boolean onlyShowLatestData, int maxDataPointsDisplayed) { + if ((minDataPointIndex <= 0 && maxDataPointIndex <= 0) || toDataHolder(dataPointHolder).size() <= 0) return; + + // Update altitude chart + ArrayList altitudeDataX = new ArrayList<>(); + ArrayList> altitudeDataY = new ArrayList>(); + + // Add all array lists + for (int i = 0; i < xTypes.length; i++) { + altitudeDataY.add(new ArrayList<>()); + } + + if (onlyShowLatestData) minDataPointIndex = Math.max(minDataPointIndex - maxDataPointsDisplayed, minDataPointIndex); + + // Add y axis + { + int minDataIndex = toDataHolderIndex(dataPointHolder, yType.tableIndex, minDataPointIndex); + int maxDataIndex = toDataHolderIndex(dataPointHolder, yType.tableIndex, maxDataPointIndex); + if (minDataIndex <= 0 && maxDataIndex <= 0) return; // Not Ready + + for (int i = minDataIndex; i <= maxDataIndex; i++) { + if (dataPointHolder.get(yType.tableIndex).size() == 0) continue; + + DataHolder data = toDataHolder(dataPointHolder).get(yType.tableIndex).get(i); + DataHolder other = toDataHolder(dataPointHolder).get(xTypes[0].tableIndex).get(i); + + if (data != null && (other == null || !other.hiddenDataTypes.contains(other.types[xTypes[0].index]))) { + altitudeDataX.add(data.data[yType.index].getDecimalValue()); + } + } + } + + + // Add x axis + for (int i = 0; i < xTypes.length; i++) { + if (xTypes[i].tableIndex != yType.tableIndex) continue; + + int minDataIndex = toDataHolderIndex(dataPointHolder, xTypes[i].tableIndex, minDataPointIndex); + int maxDataIndex = toDataHolderIndex(dataPointHolder, xTypes[i].tableIndex, maxDataPointIndex); + if (minDataIndex <= 0 && maxDataIndex <= 0) return; // Not Ready + + // Used to limit the max number of data points displayed + float targetRatio = (float) maxDataPointsDisplayed / (maxDataIndex - minDataIndex); + int dataPointsAdded = 0; + + for (int j = minDataIndex; j <= maxDataIndex; j++) { + if (dataPointHolder.get(yType.tableIndex).size() == 0) continue; + + DataHolder data = toDataHolder(dataPointHolder).get(xTypes[i].tableIndex).get(j); + + if (data != null) { + // Ensures that not too many data points are displayed + // Always show data if only showing latest data (that is handled by changing the minSlider) + boolean shouldShowDataPoint = onlyShowLatestData || ((float) dataPointsAdded / j <= targetRatio); + + if (!data.hiddenDataTypes.contains(data.types[xTypes[i].index]) && shouldShowDataPoint ) { + altitudeDataY.get(i).add(data.data[xTypes[i].index].getDecimalValue()); + + dataPointsAdded++; + } else if (!shouldShowDataPoint) { + // Hidden data + altitudeDataY.get(i).add(null); + } + } + } + } + + if (altitudeDataX.size() == 0) { + // Add default data + altitudeDataX.add(0f); + + for (int j = 0; j < xTypes.length; j++) { + altitudeDataY.get(j).add(0f); + }; + } + + String[] newActiveSeries = new String[xTypes.length]; + StringBuilder title = new StringBuilder(); + + // Set Labels + for (int i = 0; i < xTypes.length; i++) { + String xTypeTitle = config.getLabels(xTypes[i].tableIndex)[xTypes[i].index]; + if (title.length() != 0) title.append(", "); + title.append(xTypeTitle); + + xyChart.setYAxisGroupTitle(i, xTypeTitle); + + XYSeries series = null; + + if (activeSeries.length > i) { + series = xyChart.updateXYSeries("series" + i, altitudeDataX, altitudeDataY.get(i), null); + } else { + series = xyChart.addSeries("series" + i, altitudeDataX, altitudeDataY.get(i), null); + } + + series.setLabel(xTypeTitle); + series.setYAxisGroup(i); + + newActiveSeries[i] = "series" + i; + } + + String yTypeTitle = config.getLabels(yType.tableIndex)[yType.index]; + + xyChart.setTitle(title + " vs " + yTypeTitle); + + xyChart.setXAxisTitle(yTypeTitle); + + // Remove extra series + for (int i = xTypes.length; i < activeSeries.length; i++) { + xyChart.removeSeries("series" + i); + } + + activeSeries = newActiveSeries; + } + + /** + * Convert an index for the general dataPoints array into an index for the + * specific array dataHolder array + */ + public int toDataHolderIndex(DataPointHolder dataPointHolder, int tableIndex, int index) { + //TODO: Choose between receivedDataIndex and connectionInfoDataIndex depending on chart type + return dataPointHolder.toReceivedDataIndex(tableIndex, index); + } + + public List> toDataHolder(DataPointHolder dataPointHolder) { + //TODO: Choose between receivedData and connectionInfoData depending on chart type + return dataPointHolder.getAllReceivedData(); + } + + @Override + public boolean mouseDragged(MouseEvent e) { + if (main.dataDeletionMode) { + double xMousePos = chartPanel.getChart().getChartXFromCoordinate(e.getX()); + + // Start value - Last value is the total chart size in chart coordinates + double chartSizeX = Math.abs(chartPanel.getChart().getChartXFromCoordinate(0) - + chartPanel.getChart().getChartXFromCoordinate(chartPanel.getChart().getWidth())); + + // Find all data points near the click + for (int xTypeIndex = 0; xTypeIndex < xTypes.length; xTypeIndex++) { + DataType currentType = xTypes[xTypeIndex]; + List dataHolders = toDataHolder(main.dataProcessor.getDataPointHolder()).get(currentType.tableIndex); + + // Y axis depends on the which data is being checked + double yMousePos = chartPanel.getChart().getChartYFromCoordinate(e.getY(), xTypeIndex); + double chartSizeY = Math.abs(chartPanel.getChart().getChartYFromCoordinate(0, xTypeIndex) - + chartPanel.getChart().getChartYFromCoordinate(chartPanel.getChart().getHeight(), xTypeIndex)); + + for (DataHolder dataHolder: dataHolders) { + // See if click is anywhere near this point + if (dataHolder != null && Math.abs(dataHolder.data[yType.index].getDecimalValue() - xMousePos) <= chartSizeX / main.maxDataPointsDisplayed + && Math.abs(dataHolder.data[currentType.index].getDecimalValue() - yMousePos) <= chartSizeY / 100 + && !dataHolder.hiddenDataTypes.contains(currentType)) { + + // Hide this point + dataHolder.hiddenDataTypes.add(new DataType(currentType.index, currentType.tableIndex)); + } + } + } + + main.updateUI(); + + // Don't allow window moving + return true; + } + + return false; + } + + @Override + public JPanel getPanel() { + return chartPanel; + } + + @Override + public SnapPanel getSpanPanel() { + return snapPanel; + } + + @Override + public DataType[] getXTypes() { + return xTypes; + } + + @Override + public void setXTypes(DataType[] xTypes) { this.xTypes = xTypes; } + + @Override + public DataType getYType() { + return yType; + } + + @Override + public void setYType(DataType yTypes) { + this.yType = yTypes; + } } diff --git a/src/main/java/uorocketry/basestation/panel/SnapPanel.java b/src/main/java/uorocketry/basestation/panel/SnapPanel.java index 239d302..fad7fd3 100644 --- a/src/main/java/uorocketry/basestation/panel/SnapPanel.java +++ b/src/main/java/uorocketry/basestation/panel/SnapPanel.java @@ -10,13 +10,16 @@ import javax.swing.JPanel; -import uorocketry.basestation.data.DataHandler; +import uorocketry.basestation.Main; +import uorocketry.basestation.data.DataHolder; import uorocketry.basestation.data.DataType; /** * Makes JPanel have the ability to snap and move in an absolute layout */ public class SnapPanel implements MouseListener, MouseMotionListener { + + private Main main; int lastMouseX = -1; int lastMouseY = -1; @@ -25,7 +28,7 @@ public class SnapPanel implements MouseListener, MouseMotionListener { // Used to check for double clicks long lastClickTime = 0; - public DataChart chart; + public Chart chart; public JPanel panel; SnapPanelListener snapPanelListener; @@ -43,10 +46,11 @@ public class SnapPanel implements MouseListener, MouseMotionListener { public double relWidth; public double relHeight; - public SnapPanel(DataChart chart) { + public SnapPanel(Main main, Chart chart) { this.chart = chart; - this.panel = chart.chartPanel; - + this.panel = chart.getPanel(); + this.main = main; + panel.addMouseListener(this); panel.addMouseMotionListener(this); } @@ -69,10 +73,10 @@ public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON2) { // Close this - synchronized (chart.main.window.charts) { - chart.main.window.charts.remove(chart); + synchronized (main.window.charts) { + main.window.charts.remove(chart); } - chart.main.window.centerChartPanel.remove(panel); + main.window.centerChartPanel.remove(panel); } @@ -102,23 +106,23 @@ public void snapToMaxSize(int mouseX, int mouseY) { // Snap Dimension panelSize = panel.getParent().getSize(); - DataChart closestLeftChart = findClosestChart(mouseX, mouseY, 0, 0); - DataChart closestRightChart = findClosestChart(mouseX, mouseY, 1, 0); + Chart closestLeftChart = findClosestChart(mouseX, mouseY, 0, 0); + Chart closestRightChart = findClosestChart(mouseX, mouseY, 1, 0); - DataChart closestTopChart = findClosestChart(mouseX, mouseY, 0, 1); - DataChart closestBottomChart = findClosestChart(mouseX, mouseY, 1, 1); + Chart closestTopChart = findClosestChart(mouseX, mouseY, 0, 1); + Chart closestBottomChart = findClosestChart(mouseX, mouseY, 1, 1); int x = 0; - if (closestLeftChart != null) x = (int) (closestLeftChart.chartPanel.getBounds().getX() + closestLeftChart.chartPanel.getBounds().getWidth()); + if (closestLeftChart != null) x = (int) (closestLeftChart.getPanel().getBounds().getX() + closestLeftChart.getPanel().getBounds().getWidth()); int width = (int) panelSize.getWidth() - x; - if (closestRightChart != null) width = (int) (panelSize.getWidth() - x - (panelSize.getWidth() - closestRightChart.chartPanel.getBounds().getX())); + if (closestRightChart != null) width = (int) (panelSize.getWidth() - x - (panelSize.getWidth() - closestRightChart.getPanel().getBounds().getX())); int y = 0; - if (closestTopChart != null) y = (int) (closestTopChart.chartPanel.getBounds().getY() + closestTopChart.chartPanel.getBounds().getHeight()); + if (closestTopChart != null) y = (int) (closestTopChart.getPanel().getBounds().getY() + closestTopChart.getPanel().getBounds().getHeight()); int height = (int) panelSize.getHeight() - y; - if (closestBottomChart != null) height = (int) (panelSize.getHeight() - y - (panelSize.getHeight() - closestBottomChart.chartPanel.getBounds().getY())); + if (closestBottomChart != null) height = (int) (panelSize.getHeight() - y - (panelSize.getHeight() - closestBottomChart.getPanel().getBounds().getY())); setRelBounds(x, y, width, height); } @@ -130,8 +134,8 @@ public void snapToMaxSize(int mouseX, int mouseY) { * @param coordinate 0 for x, 1 for y * @return */ - public DataChart findClosestChart(int x, int y, int direction, int coordinate) { - DataChart closestChart = null; + public Chart findClosestChart(int x, int y, int direction, int coordinate) { + Chart closestChart = null; // This is the variable that will be compared against int pos = x; @@ -142,32 +146,32 @@ public DataChart findClosestChart(int x, int y, int direction, int coordinate) { otherPos = x; } - synchronized (chart.main.window.charts) { - for (DataChart chart : chart.main.window.charts) { + synchronized (main.window.charts) { + for (Chart chart : main.window.charts) { if (chart == this.chart) continue; - int currentChartPos = (int) chart.chartPanel.getBounds().getX(); - int currentChartOtherPos = (int) chart.chartPanel.getBounds().getY(); - int currentChartSize = (int) chart.chartPanel.getBounds().getWidth(); - int currentChartOtherSize = (int) chart.chartPanel.getBounds().getHeight(); + int currentChartPos = (int) chart.getPanel().getBounds().getX(); + int currentChartOtherPos = (int) chart.getPanel().getBounds().getY(); + int currentChartSize = (int) chart.getPanel().getBounds().getWidth(); + int currentChartOtherSize = (int) chart.getPanel().getBounds().getHeight(); int closestChartPos = 0; int closestChartSize = 0; // To prevent null pointer exceptions if (closestChart != null) { - closestChartPos = (int) closestChart.chartPanel.getBounds().getX(); - closestChartSize = (int) closestChart.chartPanel.getBounds().getWidth(); + closestChartPos = (int) closestChart.getPanel().getBounds().getX(); + closestChartSize = (int) closestChart.getPanel().getBounds().getWidth(); } // For Y coordinate if (coordinate == 1) { - currentChartPos = (int) chart.chartPanel.getBounds().getY(); - currentChartOtherPos = (int) chart.chartPanel.getBounds().getX(); - currentChartSize = (int) chart.chartPanel.getBounds().getHeight(); - currentChartOtherSize = (int) chart.chartPanel.getBounds().getWidth(); + currentChartPos = (int) chart.getPanel().getBounds().getY(); + currentChartOtherPos = (int) chart.getPanel().getBounds().getX(); + currentChartSize = (int) chart.getPanel().getBounds().getHeight(); + currentChartOtherSize = (int) chart.getPanel().getBounds().getWidth(); if (closestChart != null) { - closestChartPos = (int) closestChart.chartPanel.getBounds().getY(); - closestChartSize = (int) closestChart.chartPanel.getBounds().getHeight(); + closestChartPos = (int) closestChart.getPanel().getBounds().getY(); + closestChartSize = (int) closestChart.getPanel().getBounds().getHeight(); } } @@ -193,41 +197,9 @@ public DataChart findClosestChart(int x, int y, int direction, int coordinate) { @Override public void mouseDragged(MouseEvent e) { - if (chart.main.dataDeletionMode) { - double xMousePos = chart.chartPanel.getChart().getChartXFromCoordinate(e.getX()); - - // Start value - Last value is the total chart size in chart coordinates - double chartSizeX = Math.abs(chart.chartPanel.getChart().getChartXFromCoordinate(0) - - chart.chartPanel.getChart().getChartXFromCoordinate(chart.chartPanel.getChart().getWidth())); - - // Find all data points near the click - for (int xTypeIndex = 0; xTypeIndex < chart.xTypes.length; xTypeIndex++) { - DataType currentType = chart.xTypes[xTypeIndex]; - List dataHandlers = chart.main.allData.get(currentType.tableIndex); - - // Y axis depends on the which data is being checked - double yMousePos = chart.chartPanel.getChart().getChartYFromCoordinate(e.getY(), xTypeIndex); - double chartSizeY = Math.abs(chart.chartPanel.getChart().getChartYFromCoordinate(0, xTypeIndex) - - chart.chartPanel.getChart().getChartYFromCoordinate(chart.chartPanel.getChart().getHeight(), xTypeIndex)); - - for (DataHandler dataHandler: dataHandlers) { - // See if click is anywhere near this point - if (dataHandler != null && Math.abs(dataHandler.data[chart.yType.index].data - xMousePos) < chartSizeX / 100 - && Math.abs(dataHandler.data[currentType.index].data - yMousePos) < chartSizeY / 100 - && !dataHandler.hiddenDataTypes.contains(currentType)) { - - // Hide this point - dataHandler.hiddenDataTypes.add(new DataType(currentType.index, currentType.tableIndex)); - } - } - } - - chart.main.updateUI(); - - // Don't allow window moving - return; - } - + // If the chart returned true, then windw movement should not happen + if (chart.mouseDragged(e)) return; + int currentX = e.getXOnScreen(); int currentY = e.getYOnScreen(); @@ -440,9 +412,6 @@ public void updateBounds(int width, int height) { /** * Called whenever the parent is resized to change the layout to the new size. - * - * @param xFactor The factor the x is stretched by (new/old) - * @param yFactor The factor the y is stretched by (new/old) */ public void containerResized(int newWidth, int newHeight) { updateBounds(newWidth, newHeight); diff --git a/src/main/java/uorocketry/basestation/panel/TableHolder.java b/src/main/java/uorocketry/basestation/panel/TableHolder.java new file mode 100644 index 0000000..a7c9095 --- /dev/null +++ b/src/main/java/uorocketry/basestation/panel/TableHolder.java @@ -0,0 +1,28 @@ +package uorocketry.basestation.panel; + +import javax.swing.*; + +public class TableHolder { + private JTable receivedDataTable, connectionInfoTable; + + public TableHolder(JTable receivedDataTable, JTable connectionInfoTable) { + this.receivedDataTable = receivedDataTable; + this.connectionInfoTable = connectionInfoTable; + } + + public JTable getReceivedDataTable() { + return receivedDataTable; + } + + public void setReceivedDataTable(JTable receivedDataTable) { + this.receivedDataTable = receivedDataTable; + } + + public JTable getConnectionInfoTable() { + return connectionInfoTable; + } + + public void setConnectionInfoTable(JTable connectionInfoTable) { + this.connectionInfoTable = connectionInfoTable; + } +} diff --git a/src/test/java/uorocketry/basestation/config/FakeConfig.java b/src/test/java/uorocketry/basestation/config/FakeConfig.java new file mode 100644 index 0000000..db115a3 --- /dev/null +++ b/src/test/java/uorocketry/basestation/config/FakeConfig.java @@ -0,0 +1,13 @@ +package uorocketry.basestation.config; + +import org.json.JSONObject; + +import java.util.List; + +public class FakeConfig extends Config { + + public FakeConfig(List dataSet, JSONObject configObject) { + this.dataSet = dataSet; + this.configObject = configObject; + } +} diff --git a/src/test/java/uorocketry/basestation/data/DataHandlerUnitTest.java b/src/test/java/uorocketry/basestation/data/DataHolderUnitTest.java similarity index 54% rename from src/test/java/uorocketry/basestation/data/DataHandlerUnitTest.java rename to src/test/java/uorocketry/basestation/data/DataHolderUnitTest.java index abf840a..aedb8f9 100644 --- a/src/test/java/uorocketry/basestation/data/DataHandlerUnitTest.java +++ b/src/test/java/uorocketry/basestation/data/DataHolderUnitTest.java @@ -4,42 +4,42 @@ import org.json.JSONObject; import org.junit.jupiter.api.Test; import uorocketry.basestation.Main; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.DataSet; +import uorocketry.basestation.config.FakeConfig; import javax.swing.*; import javax.swing.table.TableModel; -import java.util.Arrays; -import java.util.Collections; +import java.lang.reflect.Field; +import java.util.*; import static org.junit.jupiter.api.Assertions.assertEquals; -public class DataHandlerUnitTest { +public class DataHolderUnitTest { @Test public void updateTableUI() { - JSONObject datasetConfig = new JSONObject(); - datasetConfig.put("timestampIndex", 0); - datasetConfig.put("stateIndex", 1); - JSONArray jsonArray = new JSONArray(); - jsonArray.put("First State"); - jsonArray.put("Second State"); - datasetConfig.put("states", jsonArray); - String[] labels = new String[] {"Timestamp (ns)", "State Value", "Hidden Value", "Overflow", "NaN", "Decmial", "Bigger Decimal"}; - Main.dataLength = Collections.singletonList(labels.length); + String[] states = new String[] {"First State", "Second State"}; + Map indexes = new HashMap<>(); + indexes.put("timestamp", 0); + indexes.put("state", 1); + + DataSet dataSet = new DataSet("Testing Set", "#AB1C2A", labels, states, indexes, ","); - DataHandler dataHandler = new DataHandler(0, datasetConfig); - dataHandler.hiddenDataTypes.add(new DataType(2, 0)); + DataHolder dataHolder = new DataHolder(0, dataSet); + dataHolder.hiddenDataTypes.add(new DataType(2, 0)); - dataHandler.set(0, "102020399293"); // has to be long (timestamp) - dataHandler.set(1, "1"); - dataHandler.set(2, "2"); - dataHandler.set(3, "ovf"); - dataHandler.set(4, "nan"); - dataHandler.set(5, "5.2"); - dataHandler.set(6, "2321.34"); + dataHolder.set(0, "102020399293"); // has to be long (timestamp) + dataHolder.set(1, "1"); + dataHolder.set(2, "2"); + dataHolder.set(3, "ovf"); + dataHolder.set(4, "nan"); + dataHolder.set(5, "5.2"); + dataHolder.set(6, "2321.34"); JTable table = new JTable(labels.length, 2); - dataHandler.updateTableUIWithData(table, labels); + dataHolder.updateTableUIWithData(table, labels); TableModel tableModel = table.getModel(); diff --git a/src/test/java/uorocketry/basestation/data/DataProcessorUnitTest.java b/src/test/java/uorocketry/basestation/data/DataProcessorUnitTest.java new file mode 100644 index 0000000..786a201 --- /dev/null +++ b/src/test/java/uorocketry/basestation/data/DataProcessorUnitTest.java @@ -0,0 +1,113 @@ +package uorocketry.basestation.data; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import uorocketry.basestation.Main; +import uorocketry.basestation.config.Config; +import uorocketry.basestation.config.DataSet; +import uorocketry.basestation.config.FakeConfig; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DataProcessorUnitTest { + + Config config; + + @BeforeEach + public void setup() { + String[] labels = new String[] {"Timestamp (ns)", "Value 1", "Value 2", "Value 3", "Value 4", "Value 5", "Value 6", "Value 7", "Value 8", "Value 9"}; + String[] states = new String[] {"First State", "Second State"}; + Map indexes = new HashMap<>(); + indexes.put("timestamp", 0); + + DataSet dataSet = new DataSet("Processor Testing Set", "#AC1C3A", labels, states, indexes, ","); + config = new FakeConfig(Collections.singletonList(dataSet), null); + } + + @Test + public void parseData_plain() { + String data = "102020399293,2,182.12,192,12.41,2,1,331,12,1"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_newline() { + String data = "102020399293,2,182.12,192,12.41,2,1,331,12,1\r\n"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_plainNewline() { + String data = "102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_plainNewlineAndNewline() { + String data = "102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n\r\n"; + assertAndParseData(setupParseDataConfig(), data); + } +/* + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + } + + @Test + public void parseData_binarySyntax() { + String data = "b'102020399293,2,182.12,192,12.41,2,1,331,12,1\\r\\n'"; + assertAndParseData(setupParseDataConfig(), data); + }*/ + + private DataProcessor setupParseDataConfig() { + return new DataProcessor(config, null); + } + + private void assertAndParseData(DataProcessor testObject, String data) { + DataHolder result = testObject.parseData(0, data); + + assertEquals(102020399293L, result.data[0].getLongValue()); + assertEquals("102,020,399,293", result.data[0].getFormattedString()); + assertEquals(2, result.data[1].getDecimalValue()); + assertEquals(182.12f, result.data[2].getDecimalValue()); + assertEquals(192, result.data[3].getDecimalValue()); + assertEquals(12.41f, result.data[4].getDecimalValue()); + assertEquals(2, result.data[5].getDecimalValue()); + assertEquals(1, result.data[6].getDecimalValue()); + assertEquals("1", result.data[6].getFormattedString()); + assertEquals(331, result.data[7].getDecimalValue()); + assertEquals(12, result.data[8].getDecimalValue()); + assertEquals(1, result.data[9].getDecimalValue()); + } +}