From db9daaa8ac9e178c3ed22c02860cc2da3044d1a5 Mon Sep 17 00:00:00 2001 From: Sergey Solovyev Date: Mon, 5 Dec 2011 23:25:42 +0400 Subject: [PATCH] plotting --- res/values-ru/strings.xml | 12 +- res/values/strings.xml | 9 +- .../CalculatorActivityLauncher.java | 15 +- .../android/calculator/CalculatorModel.java | 123 +++++---- .../calculator/CalculatorPlotActivity.java | 220 ++------------- .../android/calculator/plot/MyXYSeries.java | 259 ++++++++++++++++++ .../android/calculator/plot/PlotUtils.java | 157 +++++++++++ .../calculator/plot/PlotUtilsTest.java | 47 ++++ 8 files changed, 572 insertions(+), 270 deletions(-) create mode 100644 src/main/java/org/solovyev/android/calculator/plot/MyXYSeries.java create mode 100644 src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java create mode 100644 src/test/java/org/solovyev/android/calculator/plot/PlotUtilsTest.java diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index 5599619c..2266b115 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -32,7 +32,8 @@ на итальянский - Gabriele Ravanetti\n\n Это приложение использует следующие открытые библиотеки:\n Simple (XML serialization)\n - JSCL + JSCL\n + AChartEngine назад @@ -40,6 +41,10 @@ вставить переменные + Копировать + Построить график + График + Подсветка выражений Округление результата Включает/выключает округление результата @@ -235,10 +240,9 @@ deg(4.67748) = 268\n (2i + 1) ^ = -3 + 4i\n e ^ i = 0.5403 + 0.84147i\n \n -Умеет ли К++ рисовать графики функций?\n -\n -Нет.\n +Умеет ли К++ строить графики функций?\n \n +Да, введите выражение с 1 неизвестной переменной (например, cos(t)) и нажмите на результат. В контекстном меню выберите \'Построить график\'\n Поддерживает ли К++ матричные вычисления?\n \n Нет.\n diff --git a/res/values/strings.xml b/res/values/strings.xml index 1f3a2be3..85cce047 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -33,7 +33,8 @@ Italian - Gabriele Ravanetti\n\n This application uses next open source libraries:\n Simple (XML serialization)\n - JSCL + JSCL\n + AChartEngine undo @@ -43,6 +44,10 @@ paste vars + Copy + Plot graph + Graph + Highlight expressions Round result Toggles rounding of the result @@ -240,7 +245,7 @@ e ^ i = 0.5403 + 0.84147i\n \n Can C++ plot graph of the function?\n \n -No, currently C++ cannot plot functions\' graphs.\n +Yes, type expression which contains 1 undefined variable (e.g. cos(t) and t has no value) and click on the result. In the context menu choose \'Plot graph\'.\n \n Does C++ support matrix calculations?\n \n diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java b/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java index 9954faa5..b948e75c 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java @@ -3,22 +3,10 @@ package org.solovyev.android.calculator; import android.content.Context; import android.content.Intent; import android.widget.Toast; -import jscl.math.Expression; import jscl.math.Generic; -import jscl.math.JsclInteger; -import jscl.math.NumericWrapper; import jscl.math.function.Constant; -import jscl.math.numeric.Complex; -import jscl.math.numeric.Numeric; -import jscl.math.numeric.Real; import org.achartengine.ChartFactory; -import org.achartengine.model.XYMultipleSeriesDataset; -import org.achartengine.model.XYSeries; -import org.achartengine.renderer.XYMultipleSeriesRenderer; -import org.achartengine.renderer.XYSeriesRenderer; -import org.achartengine.util.MathHelper; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.solovyev.android.calculator.help.HelpActivity; import org.solovyev.common.utils.StringUtils; @@ -57,8 +45,9 @@ public class CalculatorActivityLauncher { context.startActivity(new Intent(context, CalculatorVarsActivity.class)); } - public static void plotGraph(@NotNull final Context context, @NotNull Generic generic, @NotNull Constant constant) throws ArithmeticException { + public static void plotGraph(@NotNull final Context context, @NotNull Generic generic, @NotNull Constant constant){ final Intent intent = new Intent(); + intent.putExtra(ChartFactory.TITLE, context.getString(R.string.c_graph)); intent.putExtra(CalculatorPlotActivity.INPUT, new CalculatorPlotActivity.Input(generic.toString(), constant.getName())); intent.setClass(context, CalculatorPlotActivity.class); context.startActivity(intent); diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorModel.java b/src/main/java/org/solovyev/android/calculator/CalculatorModel.java index 823a5af1..e592c2ba 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorModel.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorModel.java @@ -8,6 +8,7 @@ package org.solovyev.android.calculator; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; +import android.content.DialogInterface; import android.content.SharedPreferences; import android.os.Handler; import android.text.ClipboardManager; @@ -69,58 +70,7 @@ public enum CalculatorModel implements CursorControl, HistoryControl notSystemConstants = new HashSet(); - for (Constant constant : genericResult.getConstants()) { - Var var = CalculatorEngine.instance.getVarsRegister().get(constant.getName()); - if (var != null && !var.isSystem()) { - notSystemConstants.add(constant); - } - } - - if ( notSystemConstants.size() > 0 ) { - if (notSystemConstants.size() > 1) { - copyResult(activity, cd); - } else { - final Constant constant = CollectionsUtils.getFirstCollectionElement(notSystemConstants); - assert constant != null; - try { - CalculatorActivityLauncher.plotGraph(activity, genericResult, constant); - } catch (ArithmeticException e) { - copyResult(activity, cd); - } - } - } else { - copyResult(activity, cd); - } - } else { - copyResult(activity, cd); - } - break; - case elementary: - case numeric: - copyResult(activity, cd); - break; - } - } else { - final String errorMessage = cd.getErrorMessage(); - if ( errorMessage != null ) { - showEvaluationError(activity, errorMessage); - } - } - } - } - }); - + this.display.setOnClickListener(new CalculatorDisplayOnClickListener(activity)); final CalculatorHistoryState lastState = CalculatorHistory.instance.getLastHistoryState(); if (lastState == null) { @@ -439,4 +389,73 @@ public enum CalculatorModel implements CursorControl, HistoryControl notSystemConstants = new HashSet(); + for (Constant constant : genericResult.getConstants()) { + Var var = CalculatorEngine.instance.getVarsRegister().get(constant.getName()); + if (var != null && !var.isSystem()) { + notSystemConstants.add(constant); + } + } + + if ( notSystemConstants.size() > 0 ) { + if (notSystemConstants.size() > 1) { + copyResult(activity, cd); + } else { + final CharSequence[] items = {activity.getText(R.string.c_plot), activity.getText(R.string.c_copy)}; + + AlertDialog.Builder builder = new AlertDialog.Builder(activity); + builder.setItems(items, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int item) { + if (item == 0) { + final Constant constant = CollectionsUtils.getFirstCollectionElement(notSystemConstants); + assert constant != null; + CalculatorActivityLauncher.plotGraph(activity, genericResult, constant); + } else if ( item == 1 ) { + copyResult(activity, cd); + } + } + }); + builder.create().show(); + + } + } else { + copyResult(activity, cd); + } + } else { + copyResult(activity, cd); + } + break; + case elementary: + case numeric: + copyResult(activity, cd); + break; + } + } else { + final String errorMessage = cd.getErrorMessage(); + if ( errorMessage != null ) { + showEvaluationError(activity, errorMessage); + } + } + } + } + } } diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java b/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java index 9959027f..7aeb3048 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java @@ -16,13 +16,7 @@ import android.view.Window; import android.widget.Toast; import jscl.math.Expression; import jscl.math.Generic; -import jscl.math.JsclInteger; -import jscl.math.NumericWrapper; import jscl.math.function.Constant; -import jscl.math.numeric.Complex; -import jscl.math.numeric.Numeric; -import jscl.math.numeric.Real; -import jscl.text.MutableInt; import jscl.text.ParseException; import org.achartengine.ChartFactory; import org.achartengine.GraphicalView; @@ -37,16 +31,13 @@ import org.achartengine.renderer.XYSeriesRenderer; import org.achartengine.tools.PanListener; import org.achartengine.tools.ZoomEvent; import org.achartengine.tools.ZoomListener; -import org.achartengine.util.MathHelper; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.solovyev.android.calculator.plot.MyXYSeries; +import org.solovyev.android.calculator.plot.PlotUtils; import org.solovyev.common.utils.MutableObject; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; /** * User: serso @@ -63,7 +54,7 @@ public class CalculatorPlotActivity extends Activity { public static final String INPUT = "org.solovyev.android.calculator.CalculatorPlotActivity_input"; - public static final long EVAL_DELAY_MILLIS = 400; + public static final long EVAL_DELAY_MILLIS = 200; private XYChart chart; @@ -78,8 +69,6 @@ public class CalculatorPlotActivity extends Activity { @NotNull private Constant variable; - private static final double MAX_Y_DIFF = Math.pow(10, 6); - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -132,21 +121,10 @@ public class CalculatorPlotActivity extends Activity { double maxY = Double.MIN_VALUE; for (XYSeries series : chart.getDataset().getSeries()) { - if (checkMinMaxValue(series.getMinX())) { - minX = Math.min(minX, series.getMinX()); - } - - if (checkMinMaxValue(series.getMinY())) { - minY = Math.min(minY, series.getMinY()); - } - - if (checkMinMaxValue(series.getMaxX())) { - maxX = Math.max(maxX, series.getMaxX()); - } - - if (checkMinMaxValue(series.getMaxY())) { - maxY = Math.max(maxY, series.getMaxY()); - } + minX = Math.min(minX, series.getMinX()); + minY = Math.min(minY, series.getMinY()); + maxX = Math.max(maxX, series.getMaxX()); + maxY = Math.max(maxY, series.getMaxY()); } Log.d(CalculatorPlotActivity.class.getName(), "min x: " + minX + ", min y: " + minY + ", max x: " + maxX + ", max y: " + maxY); @@ -187,14 +165,15 @@ public class CalculatorPlotActivity extends Activity { }); graphContainer.addView(graphicalView); - updateDataSets(chart); + updateDataSets(chart, 100); } - private boolean checkMinMaxValue(@NotNull Double value) { - return !value.equals(MathHelper.NULL_VALUE); - } private void updateDataSets(@NotNull final XYChart chart) { + updateDataSets(chart, EVAL_DELAY_MILLIS); + } + + private void updateDataSets(@NotNull final XYChart chart, long millisToWait) { pendingOperation.setObject(new Runnable() { @Override public void run() { @@ -206,17 +185,17 @@ public class CalculatorPlotActivity extends Activity { //Log.d(CalculatorPlotActivity.class.getName(), "x = [" + dr.getXAxisMin() + ", " + dr.getXAxisMax() + "], y = [" + dr.getYAxisMin() + ", " + dr.getYAxisMax() + "]"); - final XYSeries realSeries = chart.getDataset().getSeriesAt(0); + final MyXYSeries realSeries = (MyXYSeries)chart.getDataset().getSeriesAt(0); - final XYSeries imagSeries; + final MyXYSeries imagSeries; if (chart.getDataset().getSeriesCount() > 1) { - imagSeries = chart.getDataset().getSeriesAt(1); + imagSeries = (MyXYSeries)chart.getDataset().getSeriesAt(1); } else { - imagSeries = new XYSeries(getImagFunctionName(CalculatorPlotActivity.this.variable)); + imagSeries = new MyXYSeries(getImagFunctionName(CalculatorPlotActivity.this.variable), DEFAULT_NUMBER_OF_STEPS * 2); } try { - if (addXY(dr.getXAxisMin(), dr.getXAxisMax(), expression, variable, realSeries, imagSeries, true)) { + if (PlotUtils.addXY(dr.getXAxisMin(), dr.getXAxisMax(), expression, variable, realSeries, imagSeries, true, DEFAULT_NUMBER_OF_STEPS)) { if (chart.getDataset().getSeriesCount() <= 1) { chart.getDataset().addSeries(imagSeries); chart.getRenderer().addSeriesRenderer(createImagRenderer()); @@ -237,7 +216,7 @@ public class CalculatorPlotActivity extends Activity { }); - new Handler().postDelayed(pendingOperation.getObject(), EVAL_DELAY_MILLIS); + new Handler().postDelayed(pendingOperation.getObject(), millisToWait); } @NotNull @@ -254,10 +233,10 @@ public class CalculatorPlotActivity extends Activity { private final static MutableObject pendingOperation = new MutableObject(); private static XYChart prepareChart(final double minValue, final double maxValue, @NotNull final Generic expression, @NotNull final Constant variable) { - final XYSeries realSeries = new XYSeries(getRealFunctionName(expression, variable)); - final XYSeries imagSeries = new XYSeries(getImagFunctionName(variable)); + final MyXYSeries realSeries = new MyXYSeries(getRealFunctionName(expression, variable), DEFAULT_NUMBER_OF_STEPS * 2); + final MyXYSeries imagSeries = new MyXYSeries(getImagFunctionName(variable), DEFAULT_NUMBER_OF_STEPS * 2); - boolean imagExists = addXY(minValue, maxValue, expression, variable, realSeries, imagSeries, false); + boolean imagExists = PlotUtils.addXY(minValue, maxValue, expression, variable, realSeries, imagSeries, false, DEFAULT_NUMBER_OF_STEPS); final XYMultipleSeriesDataset data = new XYMultipleSeriesDataset(); data.addSeries(realSeries); @@ -286,111 +265,6 @@ public class CalculatorPlotActivity extends Activity { return imagRenderer; } - private static boolean addXY(double minValue, double maxValue, Generic expression, Constant variable, @NotNull XYSeries realSeries, @NotNull XYSeries imagSeries, boolean addExtra) { - boolean imagExists = false; - - double min = Math.min(minValue, maxValue); - double max = Math.max(minValue, maxValue); - double dist = max - min; - if (addExtra) { - min = min - dist; - max = max + dist; - } - - final int numberOfSteps = DEFAULT_NUMBER_OF_STEPS; - final double step = Math.max( dist / numberOfSteps, 0.000000001); - - Double prevRealY = null; - Double prevX = null; - Double prevImagY = null; - - final MutableInt realSeriesI = new MutableInt(0); - final MutableInt imagSeriesI = new MutableInt(0); - - double x = min; - while (x <= max) { - - boolean needToCalculateRealY = needToCalculate(realSeries, step, x, realSeriesI); - - if (needToCalculateRealY) { - final Complex c = calculatorExpression(expression, variable, x); - Double y = prepareY(c.realPart()); - if (y != null) { - addSingularityPoint(realSeries, prevX, x, prevRealY, y); - realSeries.add(x, y); - prevRealY = y; - prevX = x; - } - - boolean needToCalculateImagY = needToCalculate(imagSeries, step, x, imagSeriesI); - if (needToCalculateImagY) { - y = prepareY(c.imaginaryPart()); - if (y != null) { - addSingularityPoint(imagSeries, prevX, x, prevImagY, y); - imagSeries.add(x, y); - prevImagY = y; - prevX = x; - } - if (c.imaginaryPart() != 0d) { - imagExists = true; - } - } - } else { - boolean needToCalculateImagY = needToCalculate(imagSeries, step, x, imagSeriesI); - if (needToCalculateImagY) { - final Complex c = calculatorExpression(expression, variable, x); - Double y = prepareY(c.imaginaryPart()); - if (y != null) { - addSingularityPoint(imagSeries, prevX, x, prevImagY, y); - imagSeries.add(x, y); - prevImagY = y; - prevX = x; - } - if (c.imaginaryPart() != 0d) { - imagExists = true; - } - } - } - - x += step; - } - - sortSeries(realSeries); - if (imagExists) { - sortSeries(imagSeries); - } - - return imagExists; - } - - @NotNull - private static Complex calculatorExpression(@NotNull Generic expression, @NotNull Constant variable, double x) { - return unwrap(expression.substitute(variable, Expression.valueOf(x)).numeric()); - } - - // todo serso: UNABLE TO PLOT i/ln(t)!!! - - private static void addSingularityPoint(@NotNull XYSeries series, @Nullable Double prevX, @NotNull Double x, @Nullable Double prevY, @NotNull Double y) { - if (prevX != null && prevY != null) { - if ( (Math.abs(y) > 0d && Math.abs(prevY / y) > MAX_Y_DIFF) || (Math.abs(prevY) > 0d && Math.abs(y / prevY) > MAX_Y_DIFF)) { - //Log.d(CalculatorPlotActivity.class.getName(), "Singularity! Prev point: (" + prevX + ", " + prevY + "), current point: (" +x+ ", " + y +")" ); - //Log.d(CalculatorPlotActivity.class.getName(), String.valueOf(prevX + Math.abs(x - prevX) / 2) + ", null"); - series.add( prevX + Math.abs(x - prevX) / 2, MathHelper.NULL_VALUE); - } - } - } - - private static boolean needToCalculate(@NotNull XYSeries series, double step, double x, @NotNull MutableInt i) { - boolean needToCalculateY = true; - for ( ; i.intValue() < series.getItemCount(); i.increment() ){ - if ( Math.abs(x - series.getX(i.intValue())) < step ) { - needToCalculateY = false; - break; - } - } - return needToCalculateY; - } - @Override public Object onRetainNonConfigurationInstance() { return new PlotBoundaries(chart.getRenderer()); @@ -464,27 +338,6 @@ public class CalculatorPlotActivity extends Activity { } } - private static void sortSeries(@NotNull XYSeries series) { - final List values = new ArrayList(series.getItemCount()); - - for (int i = 0; i < series.getItemCount(); i++) { - values.add(new Point(series.getX(i), series.getY(i))); - } - - Collections.sort(values, new Comparator() { - @Override - public int compare(Point point, Point point1) { - return Double.compare(point.getX(), point1.getX()); - } - }); - - Log.d(CalculatorPlotActivity.class.getName(), "Points for " + series.getTitle()); - series.clear(); - for (Point value : values) { - series.add(value.getX(), value.getY()); - Log.d(CalculatorPlotActivity.class.getName(), "x = " + value.getX() + ", y = " + value.getY()); - } - } @NotNull private static XYSeriesRenderer createCommonRenderer() { @@ -497,37 +350,6 @@ public class CalculatorPlotActivity extends Activity { return renderer; } - @Nullable - private static Double prepareY(double y) { - if (Double.isNaN(y)) { - return null; - } else { - return y; - } - } - - @NotNull - private static Complex unwrap(@Nullable Generic numeric) { - if (numeric instanceof JsclInteger) { - return Complex.valueOf(((JsclInteger) numeric).intValue(), 0d); - } else if (numeric instanceof NumericWrapper) { - return unwrap(((NumericWrapper) numeric).content()); - } else { - throw new ArithmeticException(); - } - } - - @NotNull - private static Complex unwrap(@Nullable Numeric content) { - if (content instanceof Real) { - return Complex.valueOf(((Real) content).doubleValue(), 0d); - } else if (content instanceof Complex) { - return ((Complex) content); - } else { - throw new ArithmeticException(); - } - } - public static class Input implements Serializable { diff --git a/src/main/java/org/solovyev/android/calculator/plot/MyXYSeries.java b/src/main/java/org/solovyev/android/calculator/plot/MyXYSeries.java new file mode 100644 index 00000000..e9d3f2df --- /dev/null +++ b/src/main/java/org/solovyev/android/calculator/plot/MyXYSeries.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.calculator.plot; + +import org.achartengine.model.XYSeries; +import org.achartengine.util.MathHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * User: serso + * Date: 12/5/11 + * Time: 8:43 PM + */ + +/** + * BEST SOLUTION IS TO MODIFY LIBRARY CLASS + * NOTE: this class is a copy of XYSeries with some modifications: + * 1. Possibility to insert point in th emiddle og the range + */ + +public class MyXYSeries extends XYSeries { + /** + * The series title. + */ + private String mTitle; + /** + * A list to contain the values for the X axis. + */ + private List mX = new ArrayList(); + /** + * A list to contain the values for the Y axis. + */ + private List mY = new ArrayList(); + /** + * The minimum value for the X axis. + */ + private double mMinX = MathHelper.NULL_VALUE; + /** + * The maximum value for the X axis. + */ + private double mMaxX = -MathHelper.NULL_VALUE; + /** + * The minimum value for the Y axis. + */ + private double mMinY = MathHelper.NULL_VALUE; + /** + * The maximum value for the Y axis. + */ + private double mMaxY = -MathHelper.NULL_VALUE; + /** + * The scale number for this series. + */ + private int mScaleNumber; + + /** + * Builds a new XY series. + * + * @param title the series title. + */ + public MyXYSeries(String title) { + this(title, 10); + } + + public MyXYSeries(String title, int initialCapacity) { + super(title, 0); + + this.mX = new ArrayList(initialCapacity); + this.mY = new ArrayList(initialCapacity); + + mTitle = title; + mScaleNumber = 0; + + initRange(); + } + + public int getScaleNumber() { + return mScaleNumber; + } + + /** + * Initializes the range for both axes. + */ + private void initRange() { + mMinX = MathHelper.NULL_VALUE; + mMaxX = -MathHelper.NULL_VALUE; + mMinY = MathHelper.NULL_VALUE; + mMaxY = -MathHelper.NULL_VALUE; + int length = getItemCount(); + for (int k = 0; k < length; k++) { + double x = getX(k); + double y = getY(k); + updateRange(x, y); + } + } + + /** + * Updates the range on both axes. + * + * @param x the new x value + * @param y the new y value + */ + private void updateRange(double x, double y) { + mMinX = Math.min(mMinX, x); + mMaxX = Math.max(mMaxX, x); + mMinY = Math.min(mMinY, y); + mMaxY = Math.max(mMaxY, y); + } + + /** + * Returns the series title. + * + * @return the series title + */ + public String getTitle() { + return mTitle; + } + + /** + * Sets the series title. + * + * @param title the series title + */ + public void setTitle(String title) { + mTitle = title; + } + + /** + * Adds a new value to the series. + * + * @param x the value for the X axis + * @param y the value for the Y axis + */ + public synchronized void add(double x, double y) { + boolean added = false; + for (int i = 0; i < mX.size(); i++ ) { + if ( mX.get(i) > x ) { + mX.add(i, x); + mY.add(i, y); + added = true; + break; + } + } + + if ( !added ) { + mX.add(x); + mY.add(y); + } + + updateRange(x, y); + } + + + public boolean needToAdd(double density, double x) { + boolean result = true; + + for (Double x1 : mX) { + if (Math.abs(x - x1) < density) { + result = false; + break; + } + } + + return result; + } + + /** + * Removes an existing value from the series. + * + * @param index the index in the series of the value to remove + */ + public synchronized void remove(int index) { + double removedX = mX.remove(index); + double removedY = mY.remove(index); + if (removedX == mMinX || removedX == mMaxX || removedY == mMinY || removedY == mMaxY) { + initRange(); + } + } + + /** + * Removes all the existing values from the series. + */ + public synchronized void clear() { + mX.clear(); + mY.clear(); + initRange(); + } + + /** + * Returns the X axis value at the specified index. + * + * @param index the index + * @return the X value + */ + public synchronized double getX(int index) { + return mX.get(index); + } + + /** + * Returns the Y axis value at the specified index. + * + * @param index the index + * @return the Y value + */ + public synchronized double getY(int index) { + return mY.get(index); + } + + /** + * Returns the series item count. + * + * @return the series item count + */ + public synchronized int getItemCount() { + return mX == null ? 0 : mX.size(); + } + + /** + * Returns the minimum value on the X axis. + * + * @return the X axis minimum value + */ + public double getMinX() { + return mMinX; + } + + /** + * Returns the minimum value on the Y axis. + * + * @return the Y axis minimum value + */ + public double getMinY() { + return mMinY; + } + + /** + * Returns the maximum value on the X axis. + * + * @return the X axis maximum value + */ + public double getMaxX() { + return mMaxX; + } + + /** + * Returns the maximum value on the Y axis. + * + * @return the Y axis maximum value + */ + public double getMaxY() { + return mMaxY; + } +} + diff --git a/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java b/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java new file mode 100644 index 00000000..b3b9e2f8 --- /dev/null +++ b/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.calculator.plot; + +import jscl.math.Expression; +import jscl.math.Generic; +import jscl.math.JsclInteger; +import jscl.math.NumericWrapper; +import jscl.math.function.Constant; +import jscl.math.numeric.Complex; +import jscl.math.numeric.Numeric; +import jscl.math.numeric.Real; +import org.achartengine.util.MathHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * User: serso + * Date: 12/5/11 + * Time: 8:58 PM + */ +public final class PlotUtils { + + private static final double MAX_Y_DIFF = Math.pow(10, 6); + + // not intended for instantiation + private PlotUtils() { + throw new AssertionError(); + } + + public static boolean addXY(double minValue, + double maxValue, + @NotNull Generic expression, + @NotNull Constant variable, + @NotNull MyXYSeries realSeries, + @NotNull MyXYSeries imagSeries, + boolean addExtra, + int numberOfSteps) { + + boolean imagExists = false; + + double min = Math.min(minValue, maxValue); + double max = Math.max(minValue, maxValue); + double dist = max - min; + if (addExtra) { + min = min - dist; + max = max + dist; + } + + final double step = Math.max( dist / numberOfSteps, 0.000000001); + + Double prevRealY = null; + Double prevX = null; + Double prevImagY = null; + + double x = min; + while (x <= max) { + + boolean needToCalculateRealY = realSeries.needToAdd(step, x); + + if (needToCalculateRealY) { + final Complex c = calculatorExpression(expression, variable, x); + Double y = prepareY(c.realPart()); + if (y != null) { + addSingularityPoint(realSeries, prevX, x, prevRealY, y); + realSeries.add(x, y); + prevRealY = y; + prevX = x; + } + + boolean needToCalculateImagY = imagSeries.needToAdd(step, x); + if (needToCalculateImagY) { + y = prepareY(c.imaginaryPart()); + if (y != null) { + addSingularityPoint(imagSeries, prevX, x, prevImagY, y); + imagSeries.add(x, y); + prevImagY = y; + prevX = x; + } + if (c.imaginaryPart() != 0d) { + imagExists = true; + } + } + } else { + boolean needToCalculateImagY = imagSeries.needToAdd(step, x); + if (needToCalculateImagY) { + final Complex c = calculatorExpression(expression, variable, x); + Double y = prepareY(c.imaginaryPart()); + if (y != null) { + addSingularityPoint(imagSeries, prevX, x, prevImagY, y); + imagSeries.add(x, y); + prevImagY = y; + prevX = x; + } + if (c.imaginaryPart() != 0d) { + imagExists = true; + } + } + } + + x += step; + } + + return imagExists; + } + + @NotNull + public static Complex calculatorExpression(@NotNull Generic expression, @NotNull Constant variable, double x) { + return unwrap(expression.substitute(variable, Expression.valueOf(x)).numeric()); + } + + public static void addSingularityPoint(@NotNull MyXYSeries series, @Nullable Double prevX, @NotNull Double x, @Nullable Double prevY, @NotNull Double y) { + if (prevX != null && prevY != null) { + // y or prevY should be more than 1d because if they are too small false singularity may occur (e.g., 1/0.000000000000000001) + if ( (Math.abs(y) >= 1d && Math.abs(prevY / y) > MAX_Y_DIFF) || (Math.abs(prevY) >= 1d && Math.abs(y / prevY) > MAX_Y_DIFF)) { + //Log.d(CalculatorPlotActivity.class.getName(), "Singularity! Prev point: (" + prevX + ", " + prevY + "), current point: (" +x+ ", " + y +")" ); + //Log.d(CalculatorPlotActivity.class.getName(), String.valueOf(prevX + Math.abs(x - prevX) / 2) + ", null"); + series.add(prevX + Math.abs(x - prevX) / 2, MathHelper.NULL_VALUE); + } + } + } + + @Nullable + public static Double prepareY(double y) { + if (Double.isNaN(y)) { + return null; + } else { + return y; + } + } + + @NotNull + public static Complex unwrap(@Nullable Generic numeric) { + if (numeric instanceof JsclInteger) { + return Complex.valueOf(((JsclInteger) numeric).intValue(), 0d); + } else if (numeric instanceof NumericWrapper) { + return unwrap(((NumericWrapper) numeric).content()); + } else { + throw new ArithmeticException(); + } + } + + @NotNull + public static Complex unwrap(@Nullable Numeric content) { + if (content instanceof Real) { + return Complex.valueOf(((Real) content).doubleValue(), 0d); + } else if (content instanceof Complex) { + return ((Complex) content); + } else { + throw new ArithmeticException(); + } + } +} diff --git a/src/test/java/org/solovyev/android/calculator/plot/PlotUtilsTest.java b/src/test/java/org/solovyev/android/calculator/plot/PlotUtilsTest.java new file mode 100644 index 00000000..7314ce9b --- /dev/null +++ b/src/test/java/org/solovyev/android/calculator/plot/PlotUtilsTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2009-2011. Created by serso aka se.solovyev. + * For more information, please, contact se.solovyev@gmail.com + * or visit http://se.solovyev.org + */ + +package org.solovyev.android.calculator.plot; + +import jscl.math.Expression; +import jscl.math.function.Constant; +import junit.framework.Assert; +import org.achartengine.model.XYSeries; +import org.jetbrains.annotations.NotNull; +import org.junit.Test; + +/** + * User: serso + * Date: 12/5/11 + * Time: 9:07 PM + */ + +public class PlotUtilsTest { + + @Test + public void testAddXY() throws Exception { + MyXYSeries series = new MyXYSeries("test_01", 100); + PlotUtils.addXY(-10, 10, Expression.valueOf("asin(t)"), new Constant("t"), series, new MyXYSeries("test_01_imag"), false, 200); + testAscSeries(series); + PlotUtils.addXY(-1, 1, Expression.valueOf("asin(t)"), new Constant("t"), series, new MyXYSeries("test_01_imag"), true, 200); + testAscSeries(series); + + series = new MyXYSeries("test_02", 1000); + PlotUtils.addXY(-10, 10, Expression.valueOf("1/t"), new Constant("t"), series, new MyXYSeries("test_01_imag"), false, 1000); + testAscSeries(series); + PlotUtils.addXY(-1, 1, Expression.valueOf("1/t"), new Constant("t"), series, new MyXYSeries("test_01_imag"), true, 1000); + testAscSeries(series); + + } + + public void testAscSeries(@NotNull XYSeries series) { + for ( int i = 0; i < series.getItemCount(); i++ ) { + if (i > 1) { + Assert.assertTrue(series.getX(i - 1) + " > " +series.getX(i) + " at " + i + " of " + series.getItemCount(), series.getX(i - 1) <= series.getX(i)); + } + } + } +}