From def00244a59682088c590034405e491df4c16641 Mon Sep 17 00:00:00 2001 From: Sergey Solovyev Date: Thu, 1 Dec 2011 17:57:42 +0400 Subject: [PATCH] plotting --- AndroidManifest.xml | 19 +- res/layout-land/calc_plot_view.xml | 48 ++++ res/layout-port/calc_plot_view.xml | 48 ++++ res/values/strings.xml | 2 + .../calculator/ApplicationContext.java | 23 ++ .../calculator/CalculatorActivity.java | 2 +- .../CalculatorActivityLauncher.java | 41 +-- .../calculator/CalculatorPlotActivity.java | 254 +++++++++++++++++- .../jscl/FromJsclNumericTextProcessor.java | 2 +- .../android/view/AutoResizeTextView.java | 11 +- .../android/view/prefs/ResourceCache.java | 66 ++++- .../android/view/widgets/NumberPicker.java | 2 +- 12 files changed, 454 insertions(+), 64 deletions(-) create mode 100644 res/layout-land/calc_plot_view.xml create mode 100644 res/layout-port/calc_plot_view.xml create mode 100644 src/main/java/org/solovyev/android/calculator/ApplicationContext.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 63493307..5e2d0bb4 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -12,7 +12,8 @@ a:targetSdkVersion="8"/> + a:label="@string/c_app_name" + a:name=".ApplicationContext"> @@ -30,8 +31,7 @@ - + a:configChanges="orientation|keyboardHidden"/> - + a:configChanges="orientation|keyboardHidden"/> - + a:configChanges="orientation|keyboardHidden"/> - + a:configChanges="orientation|keyboardHidden"/> - + a:label="@string/c_plot_graph"/> \ No newline at end of file diff --git a/res/layout-land/calc_plot_view.xml b/res/layout-land/calc_plot_view.xml new file mode 100644 index 00000000..a9380099 --- /dev/null +++ b/res/layout-land/calc_plot_view.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout-port/calc_plot_view.xml b/res/layout-port/calc_plot_view.xml new file mode 100644 index 00000000..1ca5792f --- /dev/null +++ b/res/layout-port/calc_plot_view.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 6ea43e89..1f3a2be3 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -277,5 +277,7 @@ Check the \'Round result\' preference in application settings - it should be tur Infinite loop is detected in expression Graph + From + To diff --git a/src/main/java/org/solovyev/android/calculator/ApplicationContext.java b/src/main/java/org/solovyev/android/calculator/ApplicationContext.java new file mode 100644 index 00000000..2d440493 --- /dev/null +++ b/src/main/java/org/solovyev/android/calculator/ApplicationContext.java @@ -0,0 +1,23 @@ +package org.solovyev.android.calculator; + +import org.jetbrains.annotations.NotNull; + +/** + * User: serso + * Date: 12/1/11 + * Time: 1:21 PM + */ +public class ApplicationContext extends android.app.Application { + + @NotNull + private static ApplicationContext instance; + + public ApplicationContext() { + instance = this; + } + + @NotNull + public static ApplicationContext getInstance() { + return instance; + } +} diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java b/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java index 536f969a..229dc526 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java @@ -74,7 +74,7 @@ public class CalculatorActivity extends Activity implements FontSizeAdjuster, Sh super.onCreate(savedInstanceState); setLayout(preferences); - ResourceCache.instance.initCaptions(R.string.class, this); + ResourceCache.instance.initCaptions(ApplicationContext.getInstance(), R.string.class); firstTimeInit(preferences); vibrator = (Vibrator) this.getSystemService(VIBRATOR_SERVICE); diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java b/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java index 4c06922f..9954faa5 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java @@ -16,7 +16,9 @@ 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; @@ -56,47 +58,12 @@ public class CalculatorActivityLauncher { } public static void plotGraph(@NotNull final Context context, @NotNull Generic generic, @NotNull Constant constant) throws ArithmeticException { - - final XYSeries series = new XYSeries(generic.toString()); - - final double min = -10; - final double max = 10; - final double step = 0.5; - double x = min; - while (x <= max) { - Generic numeric = generic.substitute(constant, Expression.valueOf(x)).numeric(); - series.add(x, unwrap(numeric)); - x += step; - } - final XYMultipleSeriesDataset data = new XYMultipleSeriesDataset(); - data.addSeries(series); - final XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(); - renderer.addSeriesRenderer(new XYSeriesRenderer()); - final Intent intent = ChartFactory.getLineChartIntent(context, data, renderer); + final Intent intent = new Intent(); + intent.putExtra(CalculatorPlotActivity.INPUT, new CalculatorPlotActivity.Input(generic.toString(), constant.getName())); intent.setClass(context, CalculatorPlotActivity.class); context.startActivity(intent); } - private static double unwrap(Generic numeric) { - if ( numeric instanceof JsclInteger) { - return ((JsclInteger) numeric).intValue(); - } else if ( numeric instanceof NumericWrapper ) { - return unwrap(((NumericWrapper) numeric).content()); - } else { - throw new ArithmeticException(); - } - } - - private static double unwrap(Numeric content) { - if (content instanceof Real) { - return ((Real) content).doubleValue(); - } else if ( content instanceof Complex) { - return ((Complex) content).realPart(); - } else { - throw new ArithmeticException(); - } - } - public static void createVar(@NotNull final Context context, @NotNull CalculatorModel calculatorModel) { if (calculatorModel.getDisplay().isValid() ) { final String varValue = calculatorModel.getDisplay().getText().toString(); diff --git a/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java b/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java index 26dc7939..ff0f0564 100644 --- a/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java +++ b/src/main/java/org/solovyev/android/calculator/CalculatorPlotActivity.java @@ -6,12 +6,262 @@ package org.solovyev.android.calculator; -import org.achartengine.GraphicalActivity; +import android.app.Activity; +import android.graphics.Color; +import android.os.Bundle; +import android.os.Handler; +import android.view.ViewGroup; +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.ParseException; +import org.achartengine.ChartFactory; +import org.achartengine.GraphicalView; +import org.achartengine.chart.AbstractChart; +import org.achartengine.chart.LineChart; +import org.achartengine.model.XYMultipleSeriesDataset; +import org.achartengine.model.XYSeries; +import org.achartengine.renderer.BasicStroke; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.achartengine.renderer.XYSeriesRenderer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.solovyev.android.view.widgets.NumberPicker; +import org.solovyev.common.utils.MutableObject; + +import java.io.Serializable; /** * User: serso * Date: 12/1/11 * Time: 12:40 AM */ -public class CalculatorPlotActivity extends GraphicalActivity{ +public class CalculatorPlotActivity extends Activity { + + private static final int DEFAULT_NUMBER_OF_STEPS = 100; + + private static final int DEFAULT_MIN_NUMBER = -10; + + private static final int DEFAULT_MAX_NUMBER = 10; + + public static final String INPUT = "org.solovyev.android.calculator.CalculatorPlotActivity_input"; + + public static final long EVAL_DELAY_MILLIS = 1000; + + /** + * The encapsulated graphical view. + */ + private GraphicalView graphicalView; + + @NotNull + private Generic expression; + + @NotNull + private Constant variable; + + private double minValue = DEFAULT_MIN_NUMBER; + + private double maxValue = DEFAULT_MAX_NUMBER; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + + final Input input = (Input) extras.getSerializable(INPUT); + + try { + this.expression = Expression.valueOf(input.getExpression()); + this.variable = new Constant(input.getVariableName()); + + String title = extras.getString(ChartFactory.TITLE); + if (title == null) { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } else if (title.length() > 0) { + setTitle(title); + } + + setContentView(R.layout.calc_plot_view); + + setGraphicalView(minValue, maxValue); + + final NumberPicker minXNumberPicker = (NumberPicker)findViewById(R.id.plot_x_min_value); + final NumberPicker maxXNumberPicker = (NumberPicker)findViewById(R.id.plot_x_max_value); + + minXNumberPicker.setRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + minXNumberPicker.setCurrent(DEFAULT_MIN_NUMBER); + maxXNumberPicker.setRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + maxXNumberPicker.setCurrent(DEFAULT_MAX_NUMBER); + + + minXNumberPicker.setOnChangeListener(new BoundariesChangeListener(true)); + maxXNumberPicker.setOnChangeListener(new BoundariesChangeListener(false)); + + } catch (ParseException e) { + Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); + finish(); + } + } + + private void setGraphicalView(final double minValue, final double maxValue) { + final ViewGroup graphContainer = (ViewGroup) findViewById(R.id.plot_graph_container); + + if (graphicalView != null) { + graphContainer.removeView(graphicalView); + } + + graphicalView = new GraphicalView(this, prepareChart(minValue, maxValue, expression, variable)); + graphContainer.addView(graphicalView); + } + + @NotNull + private final static MutableObject pendingOperation = new MutableObject(); + + private class BoundariesChangeListener implements NumberPicker.OnChangedListener { + + private boolean min; + + private BoundariesChangeListener(boolean min) { + this.min = min; + } + + + @Override + public void onChanged(NumberPicker picker, int oldVal, final int newVal) { + if (min) { + minValue = newVal; + } else { + maxValue = newVal; + } + + pendingOperation.setObject(new Runnable() { + @Override + public void run() { + // allow only one runner at one time + synchronized (pendingOperation) { + //lock all operations with history + if (pendingOperation.getObject() == this) { + // actually nothing shall be logged while text operations are done + setGraphicalView(CalculatorPlotActivity.this.minValue, CalculatorPlotActivity.this.maxValue); + } + } + } + }); + + new Handler().postDelayed(pendingOperation.getObject(), EVAL_DELAY_MILLIS); + } + } + + private static AbstractChart prepareChart(final double minValue, final double maxValue, @NotNull final Generic expression, @NotNull final Constant variable) { + final XYSeries realSeries = new XYSeries(expression.toString()); + final XYSeries imagSeries = new XYSeries("Im(" + expression.toString() + ")"); + + boolean imagExists = false; + + final double min = Math.min(minValue, maxValue); + final double max = Math.max(minValue, maxValue); + final int numberOfSteps = DEFAULT_NUMBER_OF_STEPS; + final double step = Math.max((max - min) / numberOfSteps, 0.001); + double x = min; + while (x <= max) { + Generic numeric = expression.substitute(variable, Expression.valueOf(x)).numeric(); + final Complex c = unwrap(numeric); + realSeries.add(x, prepareY(c.realPart())); + imagSeries.add(x, prepareY(c.imaginaryPart())); + if (c.imaginaryPart() != 0d) { + imagExists = true; + } + x += step; + } + + final XYMultipleSeriesDataset data = new XYMultipleSeriesDataset(); + data.addSeries(realSeries); + if (imagExists) { + data.addSeries(imagSeries); + } + + final XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer(); + renderer.setZoomEnabled(false); + renderer.setZoomEnabled(false, false); + renderer.addSeriesRenderer(createCommonRenderer()); + renderer.setPanEnabled(false); + renderer.setPanEnabled(false, false); + if (imagExists) { + final XYSeriesRenderer imagRenderer = createCommonRenderer(); + imagRenderer.setStroke(BasicStroke.DOTTED); + renderer.addSeriesRenderer(imagRenderer); + } + + return new LineChart(data, renderer); + } + + @NotNull + private static XYSeriesRenderer createCommonRenderer() { + final XYSeriesRenderer renderer = new XYSeriesRenderer(); + renderer.setColor(Color.WHITE); + renderer.setStroke(BasicStroke.SOLID); + return renderer; + } + + private static double prepareY(double y) { + if (Double.isNaN(y) || Double.isInfinite(y)) { + return 0d; + } 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 { + + @NotNull + private String expression; + + @NotNull + private String variableName; + + public Input(@NotNull String expression, @NotNull String variableName) { + this.expression = expression; + this.variableName = variableName; + } + + @NotNull + public String getExpression() { + return expression; + } + + @NotNull + public String getVariableName() { + return variableName; + } + } } diff --git a/src/main/java/org/solovyev/android/calculator/jscl/FromJsclNumericTextProcessor.java b/src/main/java/org/solovyev/android/calculator/jscl/FromJsclNumericTextProcessor.java index 88b6410c..a03206b5 100644 --- a/src/main/java/org/solovyev/android/calculator/jscl/FromJsclNumericTextProcessor.java +++ b/src/main/java/org/solovyev/android/calculator/jscl/FromJsclNumericTextProcessor.java @@ -6,7 +6,7 @@ package org.solovyev.android.calculator.jscl; -import jscl.text.Messages; +import jscl.text.msg.Messages; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.solovyev.android.calculator.math.MathType; diff --git a/src/main/java/org/solovyev/android/view/AutoResizeTextView.java b/src/main/java/org/solovyev/android/view/AutoResizeTextView.java index 317f8f0e..1840209e 100644 --- a/src/main/java/org/solovyev/android/view/AutoResizeTextView.java +++ b/src/main/java/org/solovyev/android/view/AutoResizeTextView.java @@ -29,6 +29,8 @@ public class AutoResizeTextView extends TextView { // Minimum text size for this text view public static final float MIN_TEXT_SIZE = 20; + private float initialTextSize = 100; + // Interface for resize notifications public interface OnTextResizeListener { public void onTextResize(TextView textView, float oldSize, float newSize); @@ -207,7 +209,8 @@ public class AutoResizeTextView extends TextView { Log.d(this.getClass().getName(), "Old text size: " + oldTextSize); // If there is a max text size set, use the lesser of that and the default text size - float newTextSize = 100; + // todo serso: +2 is a workaround => to be checked boundary constraints + float newTextSize = initialTextSize + 2; int newTextHeight; @@ -227,7 +230,7 @@ public class AutoResizeTextView extends TextView { if (newTextSize <= minTextSize) { break; } - newTextSize = Math.max(newTextSize - 2, minTextSize); + newTextSize = Math.max(newTextSize - 1, minTextSize); newTextHeight = getTextRect(text, textPaint, width, newTextSize); logDimensions(newTextSize, newTextHeight); } @@ -236,7 +239,7 @@ public class AutoResizeTextView extends TextView { if (newTextSize <= minTextSize) { break; } - newTextSize = Math.max(newTextSize + 2, minTextSize); + newTextSize = Math.max(newTextSize + 1, minTextSize); newTextHeight = getTextRect(text, textPaint, width, newTextSize); logDimensions(newTextSize, newTextHeight); } @@ -247,6 +250,8 @@ public class AutoResizeTextView extends TextView { } } + initialTextSize = newTextSize; + // If we had reached our minimum text size and still don't fit, append an ellipsis if (addEllipsis && newTextSize == minTextSize && newTextHeight > height) { // Draw using a static layout diff --git a/src/main/java/org/solovyev/android/view/prefs/ResourceCache.java b/src/main/java/org/solovyev/android/view/prefs/ResourceCache.java index cd4bf37f..9dcf45fb 100644 --- a/src/main/java/org/solovyev/android/view/prefs/ResourceCache.java +++ b/src/main/java/org/solovyev/android/view/prefs/ResourceCache.java @@ -7,11 +7,14 @@ package org.solovyev.android.view.prefs; import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; import android.util.Log; import android.view.View; import android.widget.Button; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.solovyev.android.calculator.CalculatorActivity; import org.solovyev.android.view.widgets.DragButton; import java.lang.reflect.Field; @@ -33,7 +36,13 @@ public enum ResourceCache { // ids of buttons in R.class private List buttonIds = null; - private static final Map> captions = new HashMap>(); + // first map: key: language id, value: map of captions and translations + // second mal: key: caption id, value: translation + private final Map> captions = new HashMap>(); + + private Class resourceClass; + + private Context context; public List getDragButtonIds() { return dragButtonIds; @@ -43,11 +52,28 @@ public enum ResourceCache { return buttonIds; } - public void initCaptions(@NotNull Class resourceClass, @NotNull Activity activity) { - final Locale locale = Locale.getDefault(); + /** + * Method load captions for default locale using android R class + * @param context STATIC CONTEXT + * @param resourceClass class of captions in android (SUBCLASS of R class) + */ + public void initCaptions(@NotNull Context context, @NotNull Class resourceClass) { + initCaptions(context, resourceClass, Locale.getDefault()); + } - if (!captions.containsKey(locale.getLanguage())) { + /** + * Method load captions for specified locale using android R class + * @param context STATIC CONTEXT + * @param resourceClass class of captions in android (SUBCLASS of R class) + * @param locale language to be used for translation + */ + public void initCaptions(@NotNull Context context, @NotNull Class resourceClass, @NotNull Locale locale) { + assert this.resourceClass == null || this.resourceClass.equals(resourceClass); + this.context = context; + this.resourceClass = resourceClass; + + if (!initialized(locale)) { final Map captionsByLanguage = new HashMap(); for (Field field : resourceClass.getDeclaredFields()) { @@ -55,7 +81,7 @@ public enum ResourceCache { if (Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers)) { try { int captionId = field.getInt(resourceClass); - captionsByLanguage.put(field.getName(), activity.getString(captionId)); + captionsByLanguage.put(field.getName(), context.getString(captionId)); } catch (IllegalAccessException e) { Log.e(ResourceCache.class.getName(), e.getMessage()); } @@ -66,13 +92,39 @@ public enum ResourceCache { } } + private boolean initialized(@NotNull Locale locale) { + return captions.containsKey(locale.getLanguage()); + } + + /** + * @param captionId id of caption to be translated + * @return translation by caption id in default language, null if no translation in default language present + */ @Nullable public String getCaption(@NotNull String captionId) { - final Locale locale = Locale.getDefault(); + return getCaption(captionId, Locale.getDefault()); + } - final Map captionsByLanguage = captions.get(locale.getLanguage()); + + /** + * @param captionId id of caption to be translated + * @param locale language to be used for translation + * @return translation by caption id in specified language, null if no translation in specified language present + */ + @Nullable + public String getCaption(@NotNull String captionId, @NotNull final Locale locale) { + Map captionsByLanguage = captions.get(locale.getLanguage()); if (captionsByLanguage != null) { return captionsByLanguage.get(captionId); + } else { + assert resourceClass != null && context != null; + + initCaptions(context, resourceClass, locale); + + captionsByLanguage = captions.get(locale.getLanguage()); + if (captionsByLanguage != null) { + return captionsByLanguage.get(captionId); + } } return null; diff --git a/src/main/java/org/solovyev/android/view/widgets/NumberPicker.java b/src/main/java/org/solovyev/android/view/widgets/NumberPicker.java index 61d427ae..728161a0 100644 --- a/src/main/java/org/solovyev/android/view/widgets/NumberPicker.java +++ b/src/main/java/org/solovyev/android/view/widgets/NumberPicker.java @@ -412,7 +412,7 @@ public class NumberPicker extends LinearLayout { } private static final char[] DIGIT_CHARACTERS = new char[] { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' + '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; private NumberPickerButton mIncrementButton;