/** * Copyright (C) 2009, 2010 SC 4ViewSoft SRL * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sf.openrocket.android.simulation; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import net.sf.openrocket.android.util.AndroidLogWrapper; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.FlightEvent; import net.sf.openrocket.unit.Unit; import org.achartengine.chart.LineChart; import org.achartengine.chart.PointStyle; import org.achartengine.chart.XYChart; import org.achartengine.model.XYMultipleSeriesDataset; import org.achartengine.model.XYSeries; import org.achartengine.renderer.XYMultipleSeriesRenderer; import org.achartengine.renderer.XYSeriesRenderer; import android.graphics.Color; import android.graphics.Paint.Align; /** * This is really a flyweight object so we can serialize the * values behind a simulation chart. Since OpenRocketDocument, FlightDataBranch, * FlightDataType, Unit and all the other underlying types are not serializable, * we have to resort to persisting just the bare minimum of information. * * This also means without further changes to FlightDataType, we cannot actually * restore the displayed series. * * TODO make FlightDataBranch serializable or at least reconstructable from * from some the name. * */ public class SimulationChart implements Serializable { private final int simulationIndex; private transient FlightDataType series1; private transient FlightDataType series2; private transient List<FlightEvent> events; // Define 4 different colors and point styles to use for the series. // For now only 2 series are supported though. private final static int[] colors = new int[] { Color.BLUE, Color.YELLOW, Color.GREEN, Color.RED }; private final static PointStyle[] styles = new PointStyle[] { PointStyle.CIRCLE, PointStyle.DIAMOND, PointStyle.TRIANGLE, PointStyle.SQUARE }; public SimulationChart(int simulationIndex) { super(); this.simulationIndex = simulationIndex; } private static String formatFlightDataTypeAxisLabel( FlightDataType fdt ) { return fdt.getName() + " (" + fdt.getUnitGroup().getDefaultUnit().toString() + ")"; } public void setSeries1(FlightDataType series1) { this.series1 = series1; } public FlightDataType getSeries1() { return series1; } public void setSeries2(FlightDataType series2) { this.series2 = series2; } public FlightDataType getSeries2() { return series2; } public void setEvents( List<FlightEvent> events ) { this.events = events; } public List<FlightEvent> getEvents() { return events; } public FlightDataBranch getFlightDataBranch( OpenRocketDocument rocketDocument ) { Simulation sim = rocketDocument.getSimulation(simulationIndex); FlightDataBranch flightDataBranch = sim.getSimulatedData().getBranch(0); return flightDataBranch; } /** * Executes the chart demo. * * @param context the context * @return the built intent */ public XYChart buildChart(OpenRocketDocument rocketDocument) { Simulation sim = rocketDocument.getSimulation(simulationIndex); FlightDataBranch flightDataBranch = sim.getSimulatedData().getBranch(0); FlightDataType time = FlightDataType.TYPE_TIME; if (series1== null) { series1 = flightDataBranch.getTypes()[1]; } if (series2== null) { series2 = flightDataBranch.getTypes()[2]; } if ( events == null ) { events = new ArrayList<FlightEvent>(); for ( FlightEvent event : flightDataBranch.getEvents() ) { switch( event.getType()) { case LAUNCHROD: case APOGEE: case BURNOUT: case EJECTION_CHARGE: events.add(event); default: break; } } } /* * TODO - * Figure out why you can pan all over the place even where there are no visible points. */ int seriesCount = 2; // if the same series is selected twice, only plot it once. if ( series1 == series2 ) { seriesCount = 1; } XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(seriesCount); renderer.setAxisTitleTextSize(16); renderer.setChartTitleTextSize(20); renderer.setLabelsTextSize(15); renderer.setLegendTextSize(15); renderer.setPointSize(5f); renderer.setXLabels(10); renderer.setYLabels(10); renderer.setShowGrid(true); renderer.setZoomButtonsVisible(true); renderer.setChartTitle(sim.getName()); renderer.setShowCustomTextGrid(true); renderer.setXLabelsAlign(Align.RIGHT); renderer.setXLabelsAngle(90); // rotate right for( FlightEvent event : events ) { renderer.addXTextLabel(event.getTime(), event.getType().toString()); } renderer.setMargins(new int[] { 50, 30, 0, 20 }); { for (int i = 0; i < seriesCount; i++) { XYSeriesRenderer r = new XYSeriesRenderer(); r.setColor(colors[i]); r.setPointStyle(styles[i]); r.setFillPoints(true); renderer.addSeriesRenderer(r); // setting the YAximMin to 0 locks the origins. renderer.setYAxisMin(0.0, i); } } renderer.setXTitle(formatFlightDataTypeAxisLabel(time)); renderer.setXLabelsAlign(Align.RIGHT); renderer.setYTitle(formatFlightDataTypeAxisLabel(series1),0); renderer.setYLabelsAlign(Align.RIGHT,0); if ( seriesCount > 1 ) { renderer.setYTitle(formatFlightDataTypeAxisLabel(series2), 1); renderer.setYAxisAlign(Align.RIGHT, 1); renderer.setYLabelsAlign(Align.LEFT, 1); } renderer.setAxesColor(Color.LTGRAY); renderer.setLabelsColor(Color.LTGRAY); XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset(); List<Double> timevalues = flightDataBranch.get(time); List<Double> series1values = new ArrayList<Double>( flightDataBranch.get(series1).size() ); { Unit u = series1.getUnitGroup().getDefaultUnit(); for( Double d: flightDataBranch.get(series1) ) { series1values.add( u.toUnit(d)); } } // compute the axis limits using timevalues and series1values. double xmin = 0; double ymin = 0; renderer.setXAxisMin(xmin); renderer.setYAxisMin(ymin); double ymax = computeMaxValueWithPadding( series1values ); double xmax = Math.ceil( timevalues.get( timevalues.size()-1)); AndroidLogWrapper.d(SimulationChart.class,"ymax = " + ymax); renderer.setXAxisMax(xmax); renderer.setYAxisMax(ymax); // These configurations don't really work well just now. //renderer.setPanLimits(new double[] { xmin, xmax, ymin, ymax }); //renderer.setZoomLimits(new double[] { xmin, xmax, ymin, ymax }); // Add first series addXYSeries(dataset, series1.getName(), timevalues, series1values, 0); if ( seriesCount > 1 ) { // Add second series List<Double> series2values = new ArrayList<Double>( flightDataBranch.get(series2).size() ); { Unit u = series2.getUnitGroup().getDefaultUnit(); for( Double d: flightDataBranch.get(series2) ) { series2values.add( u.toUnit(d)); } } addXYSeries(dataset, series2.getName(), timevalues, series2values, 1); } XYChart chart = new LineChart(dataset, renderer); return chart; } private static void addXYSeries(XYMultipleSeriesDataset dataset, String titles, List<Double> xValues, List<Double> yValues, int scale) { XYSeries series = new XYSeries(titles, scale); int datasize = xValues.size(); for( int i = 0; i<datasize; i++ ) { series.add(xValues.get(i), yValues.get(i)); } dataset.addSeries(series); } private static double computeMaxValueWithPadding( List<Double> list ) { double max = list.get(0); for( double v : list ) { if ( v > max ) { max = v; } } if ( max <= 0 ) return 1.0; // Do something stupid. // return: // 10 if max <= 10 // next 10 if 10 < max < 1000 // next 100 if 1000 < max < 10,000 // next 1000 if max >= 10,000 double numdigits = Math.floor(Math.log10(max)); if ( numdigits <= 1.0 ) { return 10.0; } else if ( numdigits <= 3.0 ) { return 10.0 * ( Math.ceil( max/10.0)); } else if ( numdigits <= 4.0 ) { return 100.0 * ( Math.ceil( max/ 100.0) ); } else { return 1000.0 * ( Math.ceil( max / 1000.0 )); } } }