package org.openaltimeter.desktopapp; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Paint; import java.awt.event.AdjustmentEvent; import java.awt.event.AdjustmentListener; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.List; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.SwingUtilities; import org.jfree.chart.ChartFactory; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.ValueAxis; import org.jfree.chart.event.AxisChangeEvent; import org.jfree.chart.event.AxisChangeListener; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.data.xy.XYSeries; import org.jfree.data.xy.XYSeriesCollection; import org.openaltimeter.data.AltitudeConverter; import org.openaltimeter.data.HeightUnits; import org.openaltimeter.desktopapp.annotations.AltimeterAnnotationManager; public class AltimeterChart { public HeightUnits heightUnits = HeightUnits.METERS; private static final Color TEMPERATURE_COLOR = Color.GRAY; private static final Color SERVO_COLOR = Color.BLUE; private static final Color VOLTAGE_COLOR = new Color(82, 255, 99); static final Color PRESSURE_COLOR = new Color(56, 136, 255); static final Color BG_COLOR = new Color(204, 224, 255); private static final float LINE_WIDTH = 1.1f; private JFreeChart chart; private JScrollBar domainScrollBar; private ChartPanel chartPanel; private AltimeterAnnotationManager annotationManager; XYSeries altitudeSeries = new XYSeries("Altitude"); XYSeries batterySeries = new XYSeries("Battery voltage"); XYSeries temperatureSeries = new XYSeries("Temperature"); XYSeries servoSeries = new XYSeries("Servo"); public AltimeterChart() { chart = createChart(); chartPanel = createChartPanel(); annotationManager = new AltimeterAnnotationManager(chartPanel); annotationManager.addMouseListener(); } public ChartPanel getChartPanel() { return chartPanel; } private JFreeChart createChart() { XYSeriesCollection seriesColl = new XYSeriesCollection(); seriesColl.addSeries(altitudeSeries); XYSeriesCollection batteryColl = new XYSeriesCollection(); batteryColl.addSeries(batterySeries); XYSeriesCollection tempColl = new XYSeriesCollection(); tempColl.addSeries(temperatureSeries); XYSeriesCollection servoColl = new XYSeriesCollection(); servoColl.addSeries(servoSeries); chart = ChartFactory.createXYLineChart( null, "Time (s)", "Altitude (m)", seriesColl, PlotOrientation.VERTICAL, false, // legend? true, // tooltips? false // URLs? ); final XYPlot plot = chart.getXYPlot(); plot.setDomainGridlinesVisible(false); plot.setRangeGridlinesVisible(false); plot.setBackgroundPaint(BG_COLOR); NumberAxis axis = new NumberAxis("Time (s)"); plot.setDomainAxis(axis); addAxis(plot, "Altitude (m)", 0, 2); addAxis(plot, "Battery (V)", 1, 2); addAxis(plot, "Servo (us)", 2, 2); addAxis(plot, "Temperature (C)", 3, 2); plot.setDataset(0, seriesColl); plot.setDataset(1, batteryColl); plot.setDataset(2, servoColl); plot.setDataset(3, tempColl); plot.mapDatasetToRangeAxis(0, 0); plot.mapDatasetToRangeAxis(1, 1); plot.mapDatasetToRangeAxis(2, 2); plot.mapDatasetToRangeAxis(3, 3); addRenderer(plot, PRESSURE_COLOR, 0); addRenderer(plot, VOLTAGE_COLOR, 1); addRenderer(plot, SERVO_COLOR, 2); addRenderer(plot, TEMPERATURE_COLOR, 3); plot.setDomainPannable(false); plot.setRangePannable(false); plot.setDomainCrosshairLockedOnData(true); plot.setRangeCrosshairLockedOnData(true); plot.getDomainAxis().addChangeListener(new AxisChangeListener() { @Override public void axisChanged(AxisChangeEvent arg0) { int l = (int) plot.getDomainAxis().getRange().getLowerBound(); int r = (int) plot.getDomainAxis().getRange().getUpperBound(); domainScrollBar.setValues(l, r - l, 0, domainScrollBar.getMaximum()); } }); return chart; } private void addRenderer(XYPlot plot, Paint color, int series) { StandardXYItemRenderer renderer = new StandardXYItemRenderer(); renderer.setSeriesPaint(0, color); renderer.setSeriesStroke(0, new BasicStroke(LINE_WIDTH)); plot.setRenderer(series, renderer); } private void addAxis(XYPlot plot, String name, int series, int digits) { NumberAxis axis = new NumberAxis(name); axis.setAutoRangeIncludesZero(false); NumberFormat df = DecimalFormat.getInstance(); df.setMaximumFractionDigits(digits); axis.setNumberFormatOverride(df); plot.setRangeAxis(series, axis); } private ChartPanel createChartPanel() { final ChartPanel cp = new ChartPanel(chart); domainScrollBar = getScrollBar(((XYPlot)chart.getPlot()).getDomainAxis()); JPanel pnl = new JPanel(); pnl.setLayout(new BorderLayout()); pnl.add(cp, BorderLayout.CENTER); pnl.add(domainScrollBar, BorderLayout.SOUTH); return cp; } private JScrollBar getScrollBar(final ValueAxis domainAxis){ final JScrollBar scrollBar = new JScrollBar(JScrollBar.HORIZONTAL, 0, 0, 0, 0); scrollBar.addAdjustmentListener( new AdjustmentListener() { public void adjustmentValueChanged(AdjustmentEvent e) { int x = e.getValue(); domainAxis.setRange(x, x + scrollBar.getVisibleAmount()); } }); return scrollBar; } // copy of the altitude data in meters private double[] altitudeDataInM; // the altitude data in the selected unit system private double[] altitudeDataInternal; private double altitudeTimeStep; public void setAltitudeData(final double[] data, final double timeStep) { // we make a copy of the altitude data (in m) so that we can deal with unit changes altitudeDataInM = data.clone(); altitudeTimeStep = timeStep; updateAltitudeDataInUserUnits(); setDataSeries(altitudeDataInternal, timeStep, altitudeSeries); } public void setBatteryData(final double[] data, final double timeStep) { setDataSeries(data, timeStep, batterySeries); } public void setTemperatureData(final double[] data, final double timeStep) { setDataSeries(data, timeStep, temperatureSeries); } public void setServoData(final double[] data, final double timeStep) { setDataSeries(data, timeStep, servoSeries); } public void setDataSeries(final double[] data, final double timeStep, final XYSeries series) { SwingUtilities.invokeLater(new Runnable() { public void run() { series.clear(); for (int i = 0; i < data.length; i++) series.add(timeStep * i, data[i], false); series.fireSeriesChanged(); domainScrollBar.setValues(0, (int) (data.length / timeStep) + 1, 0, (int) (data.length * timeStep) + 1); }}); } // These annotations are added when the data is loaded, and are never erased. They // are not managed (or indeed touched) by the annotation manager. A point that should // be borne in mind is that plot.clearAnnotations() should never be called as a result, // as this would blitz these file end markers. // TODO: maybe better to fold this in to the annotation manager? public void addEOFAnnotations(final List<Integer> eofIndices, final double timeStep) { annotationManager.addEOFAnnotations(eofIndices, timeStep); } public void setAltitudePlotVisible(boolean selected) { showPlot(selected, 0); } public void setVoltagePlotVisible(boolean selected) { showPlot(selected, 1); } public void setServoPlotVisible(boolean selected) { showPlot(selected, 2); } public void setTemperaturePlotVisible(boolean selected) { showPlot(selected, 3); } private void showPlot(boolean selected, int index) { chart.getXYPlot().getRenderer(index).setSeriesVisible(0, selected); chart.getXYPlot().getRangeAxis(index).setVisible(selected); } public void setHeightUnits(HeightUnits hu) { heightUnits = hu; // when the units are changed we: change the axis label; update the data; update the annotations // -- change the axis label String label; switch (heightUnits) { case METERS: label = "Altitude (m)"; break; case FT: default: label = "Altitude (ft)"; break; } chart.getXYPlot().getRangeAxis(0).setLabel(label); // -- update the data updateAltitudeDataInUserUnits(); // -- clear the annotations annotationManager.clearAllAnnotations(); } private void updateAltitudeDataInUserUnits() { if (altitudeDataInM != null) { altitudeDataInternal = new double[altitudeDataInM.length]; for (int i = 0; i < altitudeDataInM.length; i++) altitudeDataInternal[i] = convertToUserHeightUnits(altitudeDataInM[i]); setDataSeries(altitudeDataInternal, altitudeTimeStep, altitudeSeries); } } // these methods are for adding annotations to the plot programmatically. // they all take heights in meters, the conversion is taken care of here. public void addDLGHeightAnnotation(double time, double heightInM) { annotationManager.addDLGHeightAnnotation(time, convertToUserHeightUnits(heightInM)); } public void addDLGMaxHeightAnnotation(double time, double heightInM) { annotationManager.addDLGMaxHeightAnnotation(time, convertToUserHeightUnits(heightInM)); } public void addDLGStartAnnotation(double time, double heightInM) { annotationManager.addDLGStartAnnotation(time, convertToUserHeightUnits(heightInM)); } public void clearHeightAndVarioAnnotations() { annotationManager.clearHeightAndVarioAnnotations(); } public void clearDLGAnalysis() { annotationManager.clearDLGAnnotations(); } public void clearAllAnnotations() { annotationManager.clearAllAnnotations(); } public void resetAnnotations() { annotationManager.resetAnnotations(); } public double getVisibleDomainLowerBound() { return chart.getXYPlot().getDomainAxis().getLowerBound(); } public double getVisibleDomainUpperBound() { return chart.getXYPlot().getDomainAxis().getUpperBound(); } private double convertToUserHeightUnits(double heightInM) { switch (heightUnits) { case METERS: default: return heightInM; case FT: return AltitudeConverter.feetFromM(heightInM); } } }