From 7a1b639eae4b89f06035e6e51c51436c879e806f Mon Sep 17 00:00:00 2001 From: Phil <20448080+phil-the-frog@users.noreply.github.com> Date: Sat, 26 Oct 2024 12:39:33 -0500 Subject: [PATCH] Added New G-Force Meter Acceleration Chart Type Summary Some of the automotive enthusiast user base have always wanted an ability to chart the x and y acceleration in a g-force meter Work was done to add a new chart type that is very similar to the BMW and Audi g-force acceleration meters that they display on their instrument clusters in their production cars Also added various functionality with the ability to customize the meter and any specified scale and adjust the size of the reticle Changes + added new OpenGLAccelerationChart.java file with class and helper functions to display the acceleration meter + modified the ChartsController to include the new chart type + added second dataset for y axis to the acceleration chart + added axis minimum and maximum to adjust the guidelines of the acceleration chart + added ability to scale the acceleration reticle --- Telemetry Viewer/src/ChartsController.java | 4 +- .../src/OpenGLAccelerationChart.java | 352 ++++++++++++++++++ 2 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 Telemetry Viewer/src/OpenGLAccelerationChart.java diff --git a/Telemetry Viewer/src/ChartsController.java b/Telemetry Viewer/src/ChartsController.java index ec08c3e..553f8ac 100644 --- a/Telemetry Viewer/src/ChartsController.java +++ b/Telemetry Viewer/src/ChartsController.java @@ -79,7 +79,8 @@ public static String[] getChartTypes() { "Dial", "Quaternion", "Camera", - "Timeline" + "Timeline", + "Acceleration" }; } @@ -106,6 +107,7 @@ public static PositionedChart createAndAddChart(String chartType, int x1, int y1 else if(chartType.equals("Quaternion")) chart = new OpenGLQuaternionChart(x1, y1, x2, y2); else if(chartType.equals("Camera")) chart = new OpenGLCameraChart(x1, y1, x2, y2); else if(chartType.equals("Timeline")) chart = new OpenGLTimelineChart(x1, y1, x2, y2); + else if(chartType.equals("Acceleration")) chart = new OpenGLAccelerationChart(x1, y1, x2, y2); if(chart != null) ChartsController.addChart(chart); diff --git a/Telemetry Viewer/src/OpenGLAccelerationChart.java b/Telemetry Viewer/src/OpenGLAccelerationChart.java new file mode 100644 index 0000000..6209bf4 --- /dev/null +++ b/Telemetry Viewer/src/OpenGLAccelerationChart.java @@ -0,0 +1,352 @@ +import java.nio.FloatBuffer; + +import com.jogamp.opengl.GL2ES3; +import com.jogamp.opengl.GL3; + +/** + * Renders an acceleration chart showing the value of the most recent sample. + * + * User settings: + * DatasetX to visualize. + * DatasetY to visualize. + * Chart minimum value can be fixed or autoscaled. + * Chart maximum value can be fixed or autoscaled. + * Sample count (this is used for autoscaling and for statistics.) + * Current reading label can be displayed. + * Dataset label can be displayed. + * Chart minimum and maximum labels can be displayed. + */ +public class OpenGLAccelerationChart extends PositionedChart { + + final int dialResolution = 400; // how many quads to draw + //final float dialThickness = 0.4f; // percentage of the radius + // + final float accGuideAngle = 7 * (float)Math.PI / 4; // put the acceleration labels at 235 degrees + final float[] white = new float[] {1, 1, 1, 1}; + final float[] black = new float[] {0, 0, 0, 1}; + + // the size of the little circle that moves in the acceleration chart + final int CircleSizeDefault = 3; + final int CircleSizeMin = 1; + final int CircleSizeMax = 10; + + float chartMin; // what the value of the dataset y sample is if the acceleration circle is at the left edge of the acceleration guide + float chartMax; // what the value of the dataset x sample is if the acceleration circle is at the right edge of the acceleration guide + int circleSize = CircleSizeDefault; // user configurable acceleration circle size + + // plot region + float xPlotLeft; + float xPlotRight; + float plotWidth; + float yPlotTop; + float yPlotBottom; + float plotHeight; + + // min max labels + boolean showMinMaxLabels; + + // reading label + boolean showReadingLabel; + String readingLabel; + float readingLabelWidth; + float xReadingLabelLeft; + float yReadingLabelBaseline; + float yReadingLabelTop; + float readingLabelRadius; + + // dataset label + boolean showDatasetLabel; + String datasetLabel; + float datasetLabelWidth; + float yDatasetLabelBaseline; + float yDatasetLabelTop; + float xDatasetLabelLeft; + float datasetLabelRadius; + + // control widgets + WidgetDatasets datasetWidgetX; + WidgetDatasets datasetWidgetY; + WidgetTextfieldsOptionalMinMax minMaxWidgetX; + WidgetTextfieldsOptionalMinMax minMaxWidget; + WidgetCheckbox showReadingLabelWidget; + WidgetCheckbox showDatasetLabelWidget; + WidgetCheckbox showMinMaxLabelsWidget; + WidgetTextfieldInteger circleSizeWidget; + + // second dataset for the y axis + DatasetsInterface datasets2 = new DatasetsInterface(); + + @Override public String toString() { + + return "Acceleration"; + + } + + public OpenGLAccelerationChart(int x1, int y1, int x2, int y2) { + + super(x1, y1, x2, y2); + + datasetWidgetX = new WidgetDatasets(newDatasets -> datasets.setNormals(newDatasets), + null, + null, + null, + false, + new String[] {"DatasetX"}); + + datasetWidgetY = new WidgetDatasets(newDatasets -> datasets2.setNormals(newDatasets), + null, + null, + null, + false, + new String[] {"DatasetY"}); + + minMaxWidget = new WidgetTextfieldsOptionalMinMax("Axis", + false, + -4, + 4, + -Float.MAX_VALUE, + Float.MAX_VALUE, + (newAutoscaleMin, newManualMin) -> chartMin = newManualMin, + (newAutoscaleMax, newManualMax) -> chartMax = newManualMax); + + circleSizeWidget = new WidgetTextfieldInteger("Acceleration Circle Size", + CircleSizeDefault, + CircleSizeMin, + CircleSizeMax, + newSize -> { + circleSize = newSize; + }); + + showReadingLabelWidget = new WidgetCheckbox("Show Reading Label", + true, + newShowReadingLabel -> showReadingLabel = newShowReadingLabel); + + showDatasetLabelWidget = new WidgetCheckbox("Show Dataset Label", + true, + newShowDatasetLabel -> showDatasetLabel = newShowDatasetLabel); + + showMinMaxLabelsWidget = new WidgetCheckbox("Show Min/Max Labels", + true, + newShowMinMaxLabels -> showMinMaxLabels = newShowMinMaxLabels); + + widgets = new Widget[9]; + widgets[0] = datasetWidgetX; + widgets[1] = datasetWidgetY; + widgets[2] = null; + widgets[3] = minMaxWidget; + widgets[4] = null; + widgets[5] = showDatasetLabelWidget; + widgets[6] = showReadingLabelWidget; + widgets[7] = showMinMaxLabelsWidget; + widgets[8] = circleSizeWidget; + + } + + /** + * helper function to draw the acceleration guideline labels + * they will be displayed next to each guide subdivision + * + * @param gl The OpenGL context. + * @param xCenter The acceleration guide circles's center x value + * @param yCenter The acceleration guide circles's center x value + * @param value The value to display in the label + * @param radius The radius at which you want to display the label, should match the acc guide subdivision's radius + */ + private void drawAccGuideLabel(GL2ES3 gl, float xCenter, float yCenter, double value, float radius) { + String label = ChartUtils.formattedNumber(value, 2); + float x = radius * (float) Math.cos(accGuideAngle) + xCenter; + float y = radius * (float) Math.sin(accGuideAngle) + yCenter; + float labelRadius = OpenGL.smallTextWidth(gl, label) / 3; + float xLabelLeft = x + labelRadius * (float) Math.cos(accGuideAngle); + float yLabelTop = y + labelRadius * (float) Math.sin(accGuideAngle); + OpenGL.drawSmallText(gl, label, (int) xLabelLeft, (int) yLabelTop, 0); + } + + /** + * helper function to draw the acceleration guideline circle lines + * + * @param gl The OpenGL context. + * @param xCenter The acceleration guide circles's center x value + * @param yCenter The acceleration guide circles's center x value + * @param radius The radius of the acceleration guide circle + * @param buffer The buffer to use when storing the vertices + */ + private void drawAccelerationGuide(GL2ES3 gl, float xCenter, float yCenter, float radius, FloatBuffer buffer ) { + buffer.rewind(); + int vertexCount = 0; + + for (float angle = 0; angle < 2 * Math.PI; angle += Math.PI / dialResolution) { + float x = radius * (float) Math.cos(angle) + xCenter; + float y = radius * (float) Math.sin(angle) + yCenter; + buffer.put(x).put(y); + vertexCount++; + } + + buffer.rewind(); + OpenGL.drawLinesXy(gl, GL3.GL_LINE_LOOP, black, buffer, vertexCount); + } + + /** + * helper function to draw a filled circle + * + * @param gl The OpenGL context. + * @param buffer The buffer to use when storing the vertices + * @param xCenter The acceleration guide circles's center x value + * @param yCenter The acceleration guide circles's center x value + * @param radius The radius of the acceleration guide circle + * @param resolution How many straight lines should the circle be made up of, higher means a smoother circle + * @param color The color, as a float[] {r,g,b,a}. + */ + public static void drawFilledCircle(GL2ES3 gl, FloatBuffer buffer, float xCenter, float yCenter, float radius, int resolution, float[] color) { + buffer.rewind(); + int vertexCount = 0; + + // Put the center + buffer.put(xCenter).put(yCenter); + vertexCount++; + + for (float angle = 0; angle < 2 * Math.PI; angle += Math.PI / resolution) { + float x1 = radius * (float) Math.cos(angle) + xCenter; + float y1 = radius * (float) Math.sin(angle) + yCenter; + float x2 = radius * (float) Math.cos(angle + Math.PI / resolution) + xCenter; + float y2 = radius * (float) Math.sin(angle + Math.PI / resolution) + yCenter; + + buffer.put(x1).put(y1); + vertexCount++; + buffer.put(x2).put(y2); + vertexCount++; + } + + buffer.rewind(); + OpenGL.drawTrianglesXY(gl, GL3.GL_TRIANGLE_FAN, color, buffer, vertexCount); + } + + @Override public EventHandler drawChart(GL2ES3 gl, float[] chartMatrix, int width, int height, long endTimestamp, int endSampleNumber, double zoomLevel, int mouseX, int mouseY) { + + EventHandler handler = null; + + // sanity check + if(datasets.normalsCount() != 1) + return handler; + + // get the sample + int lastSampleNumber = endSampleNumber; + int trueLastSampleNumber = datasets.connection.getSampleCount() - 1; + if(lastSampleNumber > trueLastSampleNumber) + lastSampleNumber = trueLastSampleNumber; + Dataset dataset = datasets.getNormal(0); + Dataset datasetY = datasets2.getNormal(0); + float sampleX = lastSampleNumber > 0 ? datasets.getSample(dataset, lastSampleNumber) : 0; + float sampleY = lastSampleNumber > 0 ? datasets2.getSample(datasetY, lastSampleNumber) : 0; + + // calculate x and y positions of everything + xPlotLeft = Theme.tilePadding; + xPlotRight = width - Theme.tilePadding; + plotWidth = xPlotRight - xPlotLeft; + yPlotTop = height - Theme.tilePadding; + yPlotBottom = Theme.tilePadding; + plotHeight = yPlotTop - yPlotBottom; + + + // used to scale the graphics in the center of the plot to ensure white spaces is around them + float minPlotLength = Float.min(plotHeight, plotWidth); + + float accelerationGuideRadius = minPlotLength/3; // the biggest outer guide + float accelerationGuideRadius2 = 2*(accelerationGuideRadius/3); // the medium guide + float accelerationGuideRadius3 = accelerationGuideRadius/3; // the smallest guide + + float xCenter = plotWidth / 2 + Theme.tilePadding; + float yCenter = plotHeight / 2 + Theme.tilePadding; + float xNormalized = (sampleX - chartMin) / (chartMax - chartMin) * 2 - 1; + float yNormalized = (sampleY - chartMin) / (chartMax - chartMin) * 2 - 1; + float xCircleCenter = xCenter + xNormalized * accelerationGuideRadius + Theme.tilePadding; + float yCircleCenter = yCenter + yNormalized * accelerationGuideRadius + Theme.tilePadding; + + float circleOuterRadius = minPlotLength * ((float)circleSize/100); + + // stop if the dial is too small + if(circleOuterRadius < 0) + return handler; + + + // draw white inner circle + drawFilledCircle(gl, OpenGL.buffer, xCenter, yCenter, accelerationGuideRadius, dialResolution, white); + + // show the readings label if checked + if(showReadingLabel && lastSampleNumber >= 0) { + double magnitude = Math.sqrt(((sampleX*sampleX) + (sampleY*sampleY))); + readingLabel = ChartUtils.formattedNumber(magnitude, 6) + " " + dataset.unit; + readingLabelWidth = OpenGL.largeTextWidth(gl, readingLabel); + xReadingLabelLeft = xCenter - (readingLabelWidth / 2); + yReadingLabelBaseline = yPlotBottom; + yReadingLabelTop = yReadingLabelBaseline + OpenGL.largeTextHeight; + readingLabelRadius = Math.abs(yReadingLabelTop - yCenter); + + // make sure the text tops are below the acceleration guide's bottom edge + if(readingLabelRadius + Theme.tickTextPadding > accelerationGuideRadius) + OpenGL.drawLargeText(gl, readingLabel, (int) xReadingLabelLeft, (int) yReadingLabelBaseline, 0); + } + + // show the max number of acceleration on the accelerationGuide circle + if(showMinMaxLabels) { + drawAccGuideLabel(gl, xCenter, yCenter, chartMax, accelerationGuideRadius); + drawAccGuideLabel(gl, xCenter, yCenter, 2*(chartMax/3), accelerationGuideRadius2); + drawAccGuideLabel(gl, xCenter, yCenter, chartMax/3, accelerationGuideRadius3); + } + + // show the dataset label if checked + if(showDatasetLabel && lastSampleNumber >= 0) { + datasetLabel = dataset.name + " and " + datasetY.name; + datasetLabelWidth = OpenGL.largeTextWidth(gl, datasetLabel); + yDatasetLabelBaseline = showReadingLabel ? yReadingLabelTop + Theme.tickTextPadding + Theme.legendTextPadding : yPlotBottom; + yDatasetLabelTop = yDatasetLabelBaseline + OpenGL.largeTextHeight; + xDatasetLabelLeft = xCenter - (datasetLabelWidth / 2); + datasetLabelRadius = Math.abs(yDatasetLabelTop - yCenter); + + // make sure the text tops are below the acceleration guide's bottom edge + if(datasetLabelRadius + Theme.tickTextPadding > accelerationGuideRadius) { + float xMouseoverLeft = xDatasetLabelLeft - Theme.legendTextPadding; + float xMouseoverRight = xDatasetLabelLeft + datasetLabelWidth + Theme.legendTextPadding; + float yMouseoverBottom = yDatasetLabelBaseline - Theme.legendTextPadding; + float yMouseoverTop = yDatasetLabelTop + Theme.legendTextPadding; + if(mouseX >= xMouseoverLeft && mouseX <= xMouseoverRight && mouseY >= yMouseoverBottom && mouseY <= yMouseoverTop) { + // not really sure what to do with this because technically we are using 2 datasets, so I guess we'll just show the first dataset + OpenGL.drawQuad2D(gl, Theme.legendBackgroundColor, xMouseoverLeft, yMouseoverBottom, xMouseoverRight, yMouseoverTop); + OpenGL.drawQuadOutline2D(gl, Theme.tickLinesColor, xMouseoverLeft, yMouseoverBottom, xMouseoverRight, yMouseoverTop); + handler = EventHandler.onPress(event -> ConfigureView.instance.forDataset(dataset)); + } + OpenGL.drawLargeText(gl, datasetLabel, (int) xDatasetLabelLeft, (int) yDatasetLabelBaseline, 0); + } + } + + // draw the outside acceleration guide + drawAccelerationGuide(gl, xCenter, yCenter, accelerationGuideRadius ,OpenGL.buffer); + drawAccelerationGuide(gl, xCenter, yCenter, accelerationGuideRadius2 ,OpenGL.buffer); + drawAccelerationGuide(gl, xCenter, yCenter, accelerationGuideRadius3 ,OpenGL.buffer); + + // draw the acceleration cross + // first the horizontal + OpenGL.buffer.rewind(); + float cx1 = xCenter - accelerationGuideRadius; + float cy1 = yCenter; + float cx2 = xCenter + accelerationGuideRadius; + OpenGL.buffer.put(cx1); OpenGL.buffer.put(cy1); + OpenGL.buffer.put(cx2); OpenGL.buffer.put(cy1); + // now the vertical + cx1 = xCenter; + cy1 = yCenter - accelerationGuideRadius; + float cy2 = yCenter + accelerationGuideRadius; + OpenGL.buffer.put(cx1); OpenGL.buffer.put(cy1); + OpenGL.buffer.put(cx1); OpenGL.buffer.put(cy2); + OpenGL.buffer.rewind(); + OpenGL.drawLinesXy(gl, GL3.GL_LINES, black, OpenGL.buffer, 4); + + + // draw the acceleration circle + drawFilledCircle(gl, OpenGL.buffer, xCircleCenter, yCircleCenter, circleOuterRadius, dialResolution, dataset.glColor); + + return handler; + + } + +}