From 5f7ee1e64e7b13d314d2d0d3428ef2166444b295 Mon Sep 17 00:00:00 2001 From: Sergey Solovyev Date: Mon, 31 Dec 2012 00:37:14 +0400 Subject: [PATCH] new plotter --- .../calculator/CalculatorDisplayMenuItem.java | 17 +- .../android/calculator/plot/PlotInput.java | 32 +- android-app/misc/lib/arity-2.1.6.jar | Bin 0 -> 57481 bytes android-app/pom.xml | 8 + .../java/arity/calculator/Calculator.java | 496 +++++++++++++++ .../arity/calculator/CalculatorEditable.java | 85 +++ .../src/main/java/arity/calculator/Data.java | 145 +++++ .../src/main/java/arity/calculator/Defs.java | 62 ++ .../src/main/java/arity/calculator/FPS.java | 22 + .../java/arity/calculator/FileHandler.java | 62 ++ .../main/java/arity/calculator/GLView.java | 183 ++++++ .../java/arity/calculator/Graph2dView.java | 566 ++++++++++++++++++ .../main/java/arity/calculator/Graph3d.java | 264 ++++++++ .../java/arity/calculator/Graph3dView.java | 249 ++++++++ .../main/java/arity/calculator/GraphView.java | 30 + .../src/main/java/arity/calculator/Help.java | 16 + .../main/java/arity/calculator/History.java | 112 ++++ .../java/arity/calculator/HistoryEntry.java | 34 ++ .../arity/calculator/MotionEventWrap.java | 21 + .../arity/calculator/MotionEventWrapNew.java | 19 + .../main/java/arity/calculator/ShowGraph.java | 69 +++ .../java/arity/calculator/TouchHandler.java | 79 +++ .../src/main/java/arity/calculator/Util.java | 70 +++ .../java/arity/calculator/ZoomTracker.java | 53 ++ .../calculator/AndroidCalculatorLogger.java | 4 +- .../calculator/CalculatorActivity.java | 3 +- .../CalculatorActivityLauncher.java | 11 +- .../calculator/CalculatorApplication.java | 10 + .../about/CalculatorFragmentType.java | 2 + .../plot/AbstractCalculatorPlotFragment.java | 525 ++++++++++++++++ .../plot/CalculatorArityPlotFragment.java | 131 ++++ .../plot/CalculatorPlotActivity.java | 8 +- .../plot/CalculatorPlotFragment.java | 499 ++------------- .../android/calculator/plot/PlotUtils.java | 17 +- .../android/calculator/CalculatorUtils.java | 3 +- 35 files changed, 3447 insertions(+), 460 deletions(-) create mode 100755 android-app/misc/lib/arity-2.1.6.jar create mode 100755 android-app/src/main/java/arity/calculator/Calculator.java create mode 100755 android-app/src/main/java/arity/calculator/CalculatorEditable.java create mode 100755 android-app/src/main/java/arity/calculator/Data.java create mode 100755 android-app/src/main/java/arity/calculator/Defs.java create mode 100755 android-app/src/main/java/arity/calculator/FPS.java create mode 100755 android-app/src/main/java/arity/calculator/FileHandler.java create mode 100755 android-app/src/main/java/arity/calculator/GLView.java create mode 100755 android-app/src/main/java/arity/calculator/Graph2dView.java create mode 100755 android-app/src/main/java/arity/calculator/Graph3d.java create mode 100755 android-app/src/main/java/arity/calculator/Graph3dView.java create mode 100755 android-app/src/main/java/arity/calculator/GraphView.java create mode 100755 android-app/src/main/java/arity/calculator/Help.java create mode 100755 android-app/src/main/java/arity/calculator/History.java create mode 100755 android-app/src/main/java/arity/calculator/HistoryEntry.java create mode 100755 android-app/src/main/java/arity/calculator/MotionEventWrap.java create mode 100755 android-app/src/main/java/arity/calculator/MotionEventWrapNew.java create mode 100755 android-app/src/main/java/arity/calculator/ShowGraph.java create mode 100755 android-app/src/main/java/arity/calculator/TouchHandler.java create mode 100755 android-app/src/main/java/arity/calculator/Util.java create mode 100755 android-app/src/main/java/arity/calculator/ZoomTracker.java create mode 100644 android-app/src/main/java/org/solovyev/android/calculator/plot/AbstractCalculatorPlotFragment.java create mode 100644 android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorArityPlotFragment.java diff --git a/android-app-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayMenuItem.java b/android-app-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayMenuItem.java index 7867c261..1a54fa49 100644 --- a/android-app-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayMenuItem.java +++ b/android-app-core/src/main/java/org/solovyev/android/calculator/CalculatorDisplayMenuItem.java @@ -9,7 +9,9 @@ import org.solovyev.android.calculator.jscl.JsclOperation; import org.solovyev.android.calculator.plot.PlotInput; import org.solovyev.android.calculator.view.NumeralBaseConverterDialog; import org.solovyev.android.menu.LabeledMenuItem; -import org.solovyev.common.collections.CollectionsUtils; + +import java.util.ArrayList; +import java.util.List; /** * User: Solovyev_S @@ -82,10 +84,17 @@ public enum CalculatorDisplayMenuItem implements LabeledMenuItem variables = new ArrayList(CalculatorUtils.getNotSystemConstants(generic)); + final Constant xVariable = variables.get(0); - Locator.getInstance().getCalculator().fireCalculatorEvent(CalculatorEventType.plot_graph, PlotInput.newInstance(generic, constant), context); + final Constant yVariable; + if ( variables.size() > 1 ) { + yVariable = variables.get(1); + } else { + yVariable = null; + } + + Locator.getInstance().getCalculator().fireCalculatorEvent(CalculatorEventType.plot_graph, PlotInput.newInstance(generic, xVariable, yVariable), context); } @Override diff --git a/android-app-core/src/main/java/org/solovyev/android/calculator/plot/PlotInput.java b/android-app-core/src/main/java/org/solovyev/android/calculator/plot/PlotInput.java index 864bcc78..5f17a629 100644 --- a/android-app-core/src/main/java/org/solovyev/android/calculator/plot/PlotInput.java +++ b/android-app-core/src/main/java/org/solovyev/android/calculator/plot/PlotInput.java @@ -3,6 +3,7 @@ package org.solovyev.android.calculator.plot; import jscl.math.Generic; import jscl.math.function.Constant; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * User: serso @@ -15,19 +16,25 @@ public class PlotInput { private Generic function; @NotNull - private Constant constant; + private Constant xVariable; + + @Nullable + private Constant yVariable; public PlotInput() { } - private PlotInput(@NotNull Generic function, @NotNull Constant constant) { - this.function = function; - this.constant = constant; - } - @NotNull - public static PlotInput newInstance(@NotNull Generic function, @NotNull Constant constant) { - return new PlotInput(function, constant); + public static PlotInput newInstance(@NotNull Generic function, + @NotNull Constant xVariable, + @Nullable Constant yVariable) { + PlotInput result = new PlotInput(); + + result.function = function; + result.xVariable = xVariable; + result.yVariable = yVariable; + + return result; } @NotNull @@ -36,7 +43,12 @@ public class PlotInput { } @NotNull - public Constant getConstant() { - return constant; + public Constant getXVariable() { + return xVariable; + } + + @Nullable + public Constant getYVariable() { + return yVariable; } } diff --git a/android-app/misc/lib/arity-2.1.6.jar b/android-app/misc/lib/arity-2.1.6.jar new file mode 100755 index 0000000000000000000000000000000000000000..ec3f021df6705087fe57a6006704f3a264b73f20 GIT binary patch literal 57481 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 G{q{fLZhQj( literal 0 HcmV?d00001 diff --git a/android-app/pom.xml b/android-app/pom.xml index f8f5e968..b9dc5dae 100644 --- a/android-app/pom.xml +++ b/android-app/pom.xml @@ -111,6 +111,14 @@ 0.7.1 + + arity + arity + 2.1.6 + system + ${project.basedir}/misc/lib/arity-2.1.6.jar + + admob admob diff --git a/android-app/src/main/java/arity/calculator/Calculator.java b/android-app/src/main/java/arity/calculator/Calculator.java new file mode 100755 index 00000000..13ae3df2 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Calculator.java @@ -0,0 +1,496 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import org.javia.arity.*; +import org.javia.arity.Util; +import org.solovyev.android.calculator.Locator; + +import java.util.ArrayList; + +public class Calculator extends Activity implements TextWatcher, + View.OnKeyListener, + View.OnClickListener, + AdapterView.OnItemClickListener, + SharedPreferences.OnSharedPreferenceChangeListener +{ + static final char MINUS = '\u2212', TIMES = '\u00d7', DIV = '\u00f7', SQRT = '\u221a', PI = '\u03c0', + UP_ARROW = '\u21e7', DN_ARROW = '\u21e9', ARROW = '\u21f3'; + + private static final int MSG_INPUT_CHANGED = 1; + private static final String INFINITY = "Infinity"; + private static final String INFINITY_UNICODE = "\u221e"; + + static Symbols symbols = new Symbols(); + static Function function; + + private TextView result; + private EditText input; + private ListView historyView; + private Graph2dView graphView; + private Graph3dView graph3dView; + private History history; + private int nDigits = 0; + private boolean pendingClearResult; + private boolean isAlphaVisible; + static ArrayList graphedFunction; + static Defs defs; + private ArrayList auxFuncs = new ArrayList(); + static boolean useHighQuality3d = true; + + private static final char[][] ALPHA = { + {'q', 'w', '=', ',', ';', SQRT, '!', '\''}, + {'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k'}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', 'l'}, + }; + + private static final char[][] DIGITS = { + {'7', '8', '9', '%', '^', ARROW}, + {'4', '5', '6','(', ')', 'C'}, + {'1', '2', '3', TIMES, DIV, 'E'}, + {'0', '0', '.', '+', MINUS, 'E'}, + }; + + private static final char[][] DIGITS2 = { + {'0', '.', '+', MINUS, TIMES, DIV, '^', '(', ')', 'C'}, + {'1', '2', '3', '4', '5', '6', '7', '8', '9', 'E'}, + }; + + /* + private static final char[][] DIGITS3 = { + {'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'}, + {'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', PI}, + {'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '=', '%'}, + {'0', '.', '+', MINUS, TIMES, DIV, '^', '(', ')', 'C'}, + {'1', '2', '3', '4', '5', '6', '7', '8', '9', 'E'}, + }; + */ + + public void onConfigurationChanged(Configuration config) { + super.onConfigurationChanged(config); + internalConfigChange(config); + } + + private void internalConfigChange(Configuration config) { + /*setContentView(R.layout.main); + graphView = (GraphView) findViewById(R.id.graph); + graph3dView = (Graph3dView) findViewById(R.id.graph3d); + historyView = (ListView) findViewById(R.id.history); + + final boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE; + // final boolean hasKeyboard = config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; + + alpha = (KeyboardView) findViewById(R.id.alpha); + digits = (KeyboardView) findViewById(R.id.digits); + if (isLandscape) { + digits.init(DIGITS2, false, true); + isAlphaVisible = false; + } else { + alpha.init(ALPHA, false, false); + digits.init(DIGITS, true, true); + updateAlpha(); + } + + result = (TextView) findViewById(R.id.result); + + Editable oldText = input != null ? input.getText() : null; + input = (EditText) findViewById(R.id.input); + input.setOnKeyListener(this); + input.addTextChangedListener(this); + input.setEditableFactory(new CalculatorEditable.Factory()); + input.setInputType(0); + changeInput(history.getText()); + if (oldText != null) { + input.setText(oldText); + } + input.requestFocus(); + graphView.setOnClickListener(this); + graph3dView.setOnClickListener(this); + if (historyView != null) { + historyView.setAdapter(adapter); + historyView.setOnItemClickListener(this); + }*/ + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + history = new History(this); + internalConfigChange(getResources().getConfiguration()); + + defs = new Defs(this, symbols); + if (history.fileNotFound) { + String[] init = { + "sqrt(pi)\u00f70.5!", + "e^(i\u00d7pi)", + "ln(e^100)", + "sin(x)", + "x^2" + }; + nDigits = 10; + for (String s : init) { + onEnter(s); + } + nDigits = 0; + } + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + String value = prefs.getString("quality", null); + if (value == null) { + useHighQuality3d = Build.VERSION.SDK_INT >= 5; + prefs.edit().putString("quality", useHighQuality3d ? "high" : "low").commit(); + } else { + useHighQuality3d = value.equals("high"); + } + } + + public void onPause() { + super.onPause(); + graph3dView.onPause(); + history.updateEdited(input.getText().toString()); + history.save(); + defs.save(); + } + + public void onResume() { + super.onResume(); + graph3dView.onResume(); + } + + //OnSharedPreferenceChangeListener + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if (key.equals("quality")) { + useHighQuality3d = prefs.getString(key, "high").equals("high"); + // Calculator.log("useHigh quality changed to " + useHighQuality3d); + } + } + + //OnClickListener + public void onClick(View target) { + if (target == graphView || target == graph3dView) { + startActivity(new Intent(this, ShowGraph.class)); + } + } + + // OnItemClickListener + public void onItemClick(AdapterView parent, View view, int pos, long id) { + history.moveToPos(pos, input.getText().toString()); + changeInput(history.getText()); + } + + // TextWatcher + public void afterTextChanged(Editable s) { + // handler.removeMessages(MSG_INPUT_CHANGED); + // handler.sendEmptyMessageDelayed(MSG_INPUT_CHANGED, 250); + evaluate(); + /* + if (pendingClearResult && s.length() != 0) { + if (!(s.length() == 4 && s.toString().startsWith("ans"))) { + result.setText(null); + } + showGraph(null); + pendingClearResult = false; + } + */ + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + + // OnKeyListener + public boolean onKey(View v, int keyCode, KeyEvent event) { + int action = event.getAction(); + if (action == KeyEvent.ACTION_DOWN) { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + doEnter(); + break; + + case KeyEvent.KEYCODE_DPAD_UP: + onUp(); + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + onDown(); + break; + default: + return false; + } + return true; + } else { + switch (keyCode) { + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + return true; + } + return false; + } + } + + /* + private Handler handler = new Handler() { + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_INPUT_CHANGED: + // String text = input.getText().toString(); + evaluate(); + } + } + }; + */ + + static void log(String mes) { + if (mes != null) { + Locator.getInstance().getLogger().debug(null, mes); + } + } + + void evaluate() { + evaluate(input.getText().toString()); + } + + private String formatEval(Complex value) { + if (nDigits == 0) { + nDigits = getResultSpace(); + } + String res = Util.complexToString(value, nDigits, 2); + return res.replace(INFINITY, INFINITY_UNICODE); + } + + private void evaluate(String text) { + // log("evaluate " + text); + if (text.length() == 0) { + result.setEnabled(false); + return; + } + + auxFuncs.clear(); + int end = -1; + do { + text = text.substring(end+1); + end = text.indexOf(';'); + String slice = end == -1 ? text : text.substring(0, end); + try { + Function f = symbols.compile(slice); + auxFuncs.add(f); + } catch (SyntaxException e) { + continue; + } + } while (end != -1); + + graphedFunction = auxFuncs; + int size = auxFuncs.size(); + if (size == 0) { + result.setEnabled(false); + return; + } else if (size == 1) { + Function f = auxFuncs.get(0); + int arity = f.arity(); + // Calculator.log("res " + f); + if (arity == 1 || arity == 2) { + result.setText(null); + showGraph(f); + } else if (arity == 0) { + result.setText(formatEval(f.evalComplex())); + result.setEnabled(true); + showGraph(null); + } else { + result.setText("function"); + result.setEnabled(true); + showGraph(null); + } + } else { + graphView.setFunctions(auxFuncs); + if (graphView.getVisibility() != View.VISIBLE) { + if (isAlphaVisible) { + isAlphaVisible = false; + updateAlpha(); + } + result.setVisibility(View.GONE); + historyView.setVisibility(View.GONE); + graph3dView.setVisibility(View.GONE); + graph3dView.onPause(); + graphView.setVisibility(View.VISIBLE); + } + } + } + + private int getResultSpace() { + int width = result.getWidth() - result.getTotalPaddingLeft() - result.getTotalPaddingRight(); + float oneDigitWidth = result.getPaint().measureText("5555555555") / 10f; + return (int) (width / oneDigitWidth); + } + + private void updateAlpha() { + + } + + private StringBuilder oneChar = new StringBuilder(" "); + void onKey(char key) { + if (key == 'E') { + doEnter(); + } else if (key == 'C') { + doBackspace(); + } else if (key == ARROW) { + isAlphaVisible = !isAlphaVisible; + updateAlpha(); + } else { + int cursor = input.getSelectionStart(); + oneChar.setCharAt(0, key); + input.getText().insert(cursor, oneChar); + } + } + + private void showGraph(Function f) { + if (f == null) { + if (historyView.getVisibility() != View.VISIBLE) { + graphView.setVisibility(View.GONE); + graph3dView.setVisibility(View.GONE); + graph3dView.onPause(); + historyView.setVisibility(View.VISIBLE); + result.setVisibility(View.VISIBLE); + } + } else { + // graphedFunction = f; + if (f.arity() == 1) { + graphView.setFunction(f); + if (graphView.getVisibility() != View.VISIBLE) { + if (isAlphaVisible) { + isAlphaVisible = false; + updateAlpha(); + } + result.setVisibility(View.GONE); + historyView.setVisibility(View.GONE); + graph3dView.setVisibility(View.GONE); + graph3dView.onPause(); + graphView.setVisibility(View.VISIBLE); + } + } else { + graph3dView.setFunction(f); + if (graph3dView.getVisibility() != View.VISIBLE) { + if (isAlphaVisible) { + isAlphaVisible = false; + updateAlpha(); + } + result.setVisibility(View.GONE); + historyView.setVisibility(View.GONE); + graphView.setVisibility(View.GONE); + graph3dView.setVisibility(View.VISIBLE); + graph3dView.onResume(); + } + } + } + } + + void onEnter() { + onEnter(input.getText().toString()); + } + + void onEnter(String text) { + boolean historyChanged = false; + try { + FunctionAndName fan = symbols.compileWithName(text); + if (fan.name != null) { + symbols.define(fan); + defs.add(text); + } + Function f = fan.function; + int arity = f.arity(); + Complex value = null; + if (arity == 0) { + value = f.evalComplex(); + symbols.define("ans", value); + } + historyChanged = arity == 0 ? + history.onEnter(text, formatEval(value)) : + history.onEnter(text, null); + } catch (SyntaxException e) { + historyChanged = history.onEnter(text, null); + } + showGraph(null); + + if (text.length() == 0) { + result.setText(null); + } + changeInput(history.getText()); + } + + private void changeInput(String newInput) { + input.setText(newInput); + input.setSelection(newInput.length()); + /* + if (newInput.length() > 0) { + result.setText(null); + } else { + pendingClearResult = true; + } + */ + /* + if (result.getText().equals("function")) { + result.setText(null); + } + */ + } + + /* + private void updateChecked() { + int pos = history.getListPos(); + if (pos >= 0) { + log("check " + pos); + historyView.setItemChecked(pos, true); + adapter.notifyDataSetInvalidated(); + } + } + */ + + void onUp() { + if (history.moveUp(input.getText().toString())) { + changeInput(history.getText()); + // updateChecked(); + } + } + + void onDown() { + if (history.moveDown(input.getText().toString())) { + changeInput(history.getText()); + // updateChecked(); + } + } + + private static final KeyEvent + KEY_DEL = new KeyEvent(0, KeyEvent.KEYCODE_DEL), + KEY_ENTER = new KeyEvent(0, KeyEvent.KEYCODE_ENTER); + + void doEnter() { + onEnter(); + } + + void doBackspace() { + input.dispatchKeyEvent(KEY_DEL); + } + + +} diff --git a/android-app/src/main/java/arity/calculator/CalculatorEditable.java b/android-app/src/main/java/arity/calculator/CalculatorEditable.java new file mode 100755 index 00000000..ea1bfc64 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/CalculatorEditable.java @@ -0,0 +1,85 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.text.Editable; +import android.text.SpannableStringBuilder; + +class CalculatorEditable extends SpannableStringBuilder { + static class Factory extends Editable.Factory { + public Editable newEditable(CharSequence source) { + return new CalculatorEditable(source); + } + } + + static final char MINUS = '\u2212', TIMES = '\u00d7', DIV = '\u00f7'; + private boolean isRec; + + public CalculatorEditable(CharSequence source) { + super(source); + } + + public SpannableStringBuilder replace(int start, int end, CharSequence buf, int bufStart, int bufEnd) { + if (isRec || bufEnd - bufStart != 1) { + return super.replace(start, end, buf, bufStart, bufEnd); + } else { + isRec = true; + try { + char c = buf.charAt(bufStart); + return internalReplace(start, end, c); + } finally { + isRec = false; + } + } + } + + private boolean isOperator(char c) { + return "\u2212\u00d7\u00f7+-/*=^,".indexOf(c) != -1; + } + + private SpannableStringBuilder internalReplace(int start, int end, char c) { + switch (c) { + case '-': c = MINUS; break; + case '*': c = TIMES; break; + case '/': c = DIV; break; + } + if (c == '.') { + int p = start - 1; + while (p >= 0 && Character.isDigit(charAt(p))) { + --p; + } + if (p >= 0 && charAt(p) == '.') { + return super.replace(start, end, ""); + } + } + + char prevChar = start > 0 ? charAt(start-1) : '\0'; + + if (c == MINUS && prevChar == MINUS) { + return super.replace(start, end, ""); + } + + if (isOperator(c)) { + while (isOperator(prevChar) && + (c != MINUS || prevChar == '+')) { + --start; + prevChar = start > 0 ? charAt(start-1) : '\0'; + } + } + + //don't allow leading operator + * / + if (start == 0 && isOperator(c)) { // && c != MINUS + return super.replace(start, end, "ans" + c); + } + + //allow at most one '=' + if (c == '=') { + for (int pos = 0; pos < start; ++pos) { + if (charAt(pos) == '=') { + return super.replace(start, end, ""); + } + } + } + return super.replace(start, end, "" + c); + } +} diff --git a/android-app/src/main/java/arity/calculator/Data.java b/android-app/src/main/java/arity/calculator/Data.java new file mode 100755 index 00000000..4f32c8fe --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Data.java @@ -0,0 +1,145 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +class Data { + float[] xs = new float[4]; + float[] ys = new float[4]; + int size = 0; + int allocSize = 4; + + void swap(Data o) { + float savex[] = o.xs; + float savey[] = o.ys; + int ssize = o.size; + int salloc = o.allocSize; + + o.xs = xs; + o.ys = ys; + o.size = size; + o.allocSize = allocSize; + + xs = savex; + ys = savey; + size = ssize; + allocSize = salloc; + } + + void push(float x, float y) { + if (size >= allocSize) { + makeSpace(size+1); + } + // Calculator.log("push " + size + ' ' + x + ' ' + y); + xs[size] = x; + ys[size] = y; + ++size; + } + + private void makeSpace(int sizeNeeded) { + int oldAllocSize = allocSize; + while (sizeNeeded > allocSize) { + allocSize += allocSize; + } + if (oldAllocSize != allocSize) { + float[] a = new float[allocSize]; + System.arraycopy(xs, 0, a, 0, size); + xs = a; + a = new float[allocSize]; + System.arraycopy(ys, 0, a, 0, size); + ys = a; + } + } + + float topX() { + return xs[size-1]; + } + + float topY() { + return ys[size-1]; + } + + float firstX() { + return xs[0]; + } + + float firstY() { + return ys[0]; + } + + void pop() { + --size; + } + + boolean empty() { + return size == 0; + } + + void clear() { + size = 0; + } + + void eraseBefore(float x) { + int pos = 0; + while (pos < size && xs[pos] < x) { + ++pos; + } + --pos; + if (pos > 0) { + size -= pos; + System.arraycopy(xs, pos, xs, 0, size); + System.arraycopy(ys, pos, ys, 0, size); + } + } + + void eraseAfter(float x) { + int pos = size-1; + while (pos >= 0 && x < xs[pos]) { + --pos; + } + ++pos; + if (pos < size-1) { + size = pos+1; + } + } + + int findPosAfter(float x, float y) { + int pos = 0; + while (pos < size && xs[pos] <= x) { + ++pos; + } + if (Float.isNaN(y)) { + while (pos < size && ys[pos] != ys[pos]) { + ++pos; + } + } + // Calculator.log("pos " + pos); + return pos; + } + + void append(Data d) { + makeSpace(size + d.size); + int pos = d.findPosAfter(xs[size-1], ys[size-1]); + /* + while (pos < d.size && d.xs[pos] <= last) { + ++pos; + } + if (last != last) { + while (pos < d.size && d.ys[pos] != d.ys[pos]) { + ++pos; + } + } + */ + System.arraycopy(d.xs, pos, xs, size, d.size-pos); + System.arraycopy(d.ys, pos, ys, size, d.size-pos); + size += d.size-pos; + } + + public String toString() { + StringBuilder b = new StringBuilder(); + b.append(size).append(": "); + for (int i = 0; i < size; ++i) { + b.append(xs[i]).append(", "); + } + return b.toString(); + } +} diff --git a/android-app/src/main/java/arity/calculator/Defs.java b/android-app/src/main/java/arity/calculator/Defs.java new file mode 100755 index 00000000..dfa3ae76 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Defs.java @@ -0,0 +1,62 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import org.javia.arity.Symbols; +import org.javia.arity.SyntaxException; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; + +class Defs extends FileHandler { + private static final int SIZE_LIMIT = 50; + ArrayList lines = new ArrayList(); + private Symbols symbols; + + Defs(Context context, Symbols symbols) { + super(context, "defs", 1); + this.symbols = symbols; + symbols.pushFrame(); + load(); + } + + void clear() { + lines.clear(); + symbols.popFrame(); + symbols.pushFrame(); + } + + int size() { + return lines.size(); + } + + void doRead(DataInputStream is) throws IOException { + int size = is.readInt(); + for (int i = 0; i < size; ++i) { + String line = is.readUTF(); + lines.add(line); + try { + symbols.define(symbols.compileWithName(line)); + } catch (SyntaxException e) { + // ignore + } + } + } + + void doWrite(DataOutputStream os) throws IOException { + os.writeInt(lines.size()); + for (String s : lines) { + os.writeUTF(s); + } + } + + void add(String text) { + if (lines.size() >= SIZE_LIMIT) { + lines.remove(0); + } + lines.add(text); + } +} diff --git a/android-app/src/main/java/arity/calculator/FPS.java b/android-app/src/main/java/arity/calculator/FPS.java new file mode 100755 index 00000000..fbd743be --- /dev/null +++ b/android-app/src/main/java/arity/calculator/FPS.java @@ -0,0 +1,22 @@ +package arity.calculator; + +class FPS { + private int drawCnt; + private long lastTime; + private int fps; + + boolean incFrame() { + if (--drawCnt > 0) { + return false; + } + drawCnt = 100; + long now = System.currentTimeMillis(); + fps = Math.round(100000f / (now - lastTime)); + lastTime = now; + return true; + } + + int getValue() { + return fps; + } +} diff --git a/android-app/src/main/java/arity/calculator/FileHandler.java b/android-app/src/main/java/arity/calculator/FileHandler.java new file mode 100755 index 00000000..f520d982 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/FileHandler.java @@ -0,0 +1,62 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import java.io.*; + +abstract class FileHandler { + private String fileName; + private Context context; + private int version; + boolean fileNotFound; + + private DataInputStream openInput() throws IOException { + try { + return new DataInputStream(new BufferedInputStream(context.openFileInput(fileName), 256)); + } catch (FileNotFoundException e) { + fileNotFound = true; + return null; + } + } + + private DataOutputStream openOutput() throws IOException { + return new DataOutputStream(new BufferedOutputStream(context.openFileOutput(fileName, 0), 256)); + } + + FileHandler(Context context, String fileName, int version) { + this.context = context; + this.fileName = fileName; + this.version = version; + } + + void load() { + try { + DataInputStream is = openInput(); + if (is != null) { + int readVersion = is.readInt(); + if (readVersion != version) { + throw new IllegalStateException("invalid version " + readVersion); + } + doRead(is); + is.close(); + } + } catch (IOException e) { + throw new RuntimeException("" + e); + } + } + + void save() { + try { + DataOutputStream os = openOutput(); + os.writeInt(version); + doWrite(os); + os.close(); + } catch (IOException e) { + throw new RuntimeException("" + e); + } + } + + abstract void doRead(DataInputStream is) throws IOException; + abstract void doWrite(DataOutputStream os) throws IOException; +} diff --git a/android-app/src/main/java/arity/calculator/GLView.java b/android-app/src/main/java/arity/calculator/GLView.java new file mode 100755 index 00000000..15f3798e --- /dev/null +++ b/android-app/src/main/java/arity/calculator/GLView.java @@ -0,0 +1,183 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Handler; +import android.os.Message; +import android.util.AttributeSet; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import javax.microedition.khronos.egl.*; +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +abstract class GLView extends SurfaceView implements SurfaceHolder.Callback { + private boolean hasSurface; + private boolean paused; + private EGL10 egl; + private EGLDisplay display; + private EGLConfig config; + private EGLSurface surface; + private EGLContext eglContext; + private GL11 gl; + protected int width, height; + private boolean mIsLooping; + + abstract void onDrawFrame(GL10 gl); + abstract void onSurfaceCreated(GL10 gl, int width, int height); + + public String captureScreenshot() { + Bitmap bitmap = getRawPixels(gl, width, height); + Util.bitmapBGRtoRGB(bitmap, width, height); + return Util.saveBitmap(bitmap, GraphView.SCREENSHOT_DIR, "calculator"); + } + + private static Bitmap getRawPixels(GL10 gl, int width, int height) { + int size = width * height; + ByteBuffer buf = ByteBuffer.allocateDirect(size * 4); + buf.order(ByteOrder.nativeOrder()); + gl.glReadPixels(0, 0, width, height, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, buf); + int data[] = new int[size]; + buf.asIntBuffer().get(data); + buf = null; + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + bitmap.setPixels(data, size-width, -width, 0, 0, width, height); + return bitmap; + } + + private Handler handler = new Handler() { + public void handleMessage(Message msg) { + glDraw(); + } + }; + + public GLView(Context context) { + super(context); + init(); + } + + public GLView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + SurfaceHolder holder = getHolder(); + holder.setType(SurfaceHolder.SURFACE_TYPE_GPU); + holder.addCallback(this); + } + + public void onResume() { + Calculator.log("onResume " + this); + paused = false; + if (hasSurface) { + initGL(); + } + } + + public void onPause() { + Calculator.log("onPause " + this); + deinitGL(); + } + + private void initGL() { + egl = (EGL10) EGLContext.getEGL(); + display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); + int[] ver = new int[2]; + egl.eglInitialize(display, ver); + + int[] configSpec = {EGL10.EGL_NONE}; + EGLConfig[] configOut = new EGLConfig[1]; + int[] nConfig = new int[1]; + egl.eglChooseConfig(display, configSpec, configOut, 1, nConfig); + config = configOut[0]; + eglContext = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, null); + surface = egl.eglCreateWindowSurface(display, config, getHolder(), null); + egl.eglMakeCurrent(display, surface, surface, eglContext); + gl = (GL11) eglContext.getGL(); + onSurfaceCreated(gl, width, height); + requestDraw(); + } + + private void deinitGL() { + paused = true; + if (display != null) { + egl.eglMakeCurrent(display, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT); + egl.eglDestroySurface(display, surface); + egl.eglDestroyContext(display, eglContext); + egl.eglTerminate(display); + + egl = null; + config = null; + eglContext = null; + surface = null; + display = null; + gl = null; + } + } + + protected void glDraw() { + if (hasSurface && !paused) { + onDrawFrame(gl); + if (!egl.eglSwapBuffers(display, surface)) { + Calculator.log("swapBuffers error " + egl.eglGetError()); + } + if (egl.eglGetError() == EGL11.EGL_CONTEXT_LOST) { + Calculator.log("egl context lost " + this); + paused = true; + } + if (mIsLooping) { + requestDraw(); + } + } + } + + public void surfaceCreated(SurfaceHolder holder) { + Calculator.log("surfaceCreated " + this); + } + + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + Calculator.log("surfaceChanged " + format + ' ' + this); + this.width = width; + this.height = height; + boolean doInit = !hasSurface && !paused; + hasSurface = true; + if (doInit) { + initGL(); + } + } + + public void surfaceDestroyed(SurfaceHolder holder) { + Calculator.log("surfaceDestroyed " + this); + hasSurface = false; + deinitGL(); + } + + public void startLooping() { + if (!mIsLooping) { + Calculator.log("start looping"); + mIsLooping = true; + glDraw(); + } + } + + public void stopLooping() { + if (mIsLooping) { + Calculator.log("stop looping"); + mIsLooping = false; + } + } + + public boolean isLooping() { + return mIsLooping; + } + + public void requestDraw() { + handler.sendEmptyMessage(1); + } +} diff --git a/android-app/src/main/java/arity/calculator/Graph2dView.java b/android-app/src/main/java/arity/calculator/Graph2dView.java new file mode 100755 index 00000000..b54847ab --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Graph2dView.java @@ -0,0 +1,566 @@ +// Copyright (C) 2009-2010 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import android.graphics.*; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.widget.Scroller; +import android.widget.ZoomButtonsController; +import org.javia.arity.Function; + +import java.util.ArrayList; +import java.util.List; + +public class Graph2dView extends View implements + GraphView, + ZoomButtonsController.OnZoomListener, + TouchHandler.TouchHandlerInterface { + + private int width, height; + private Matrix matrix = new Matrix(); + private Paint paint = new Paint(), textPaint = new Paint(), fillPaint = new Paint(); + private ArrayList funcs = new ArrayList(); + private Data next = new Data(), endGraph = new Data(); + private Data graphs[] = {new Data(), new Data(), new Data(), new Data(), new Data()}; + private static final int GRAPHS_SIZE = 5; + private float gwidth = 8; + private float currentX, currentY; + private float lastMinX; + private Scroller scroller; + private float boundMinY, boundMaxY; + protected ZoomButtonsController zoomController = new ZoomButtonsController(this); + private ZoomTracker zoomTracker = new ZoomTracker(); + private TouchHandler touchHandler; + private float lastTouchX, lastTouchY; + + private static final int + COL_AXIS = 0xff00a000, + COL_GRID = 0xff004000, + COL_TEXT = 0xff00ff00; + + private static final int COL_GRAPH[] = {0xffffffff, 0xff00ffff, 0xffffff00, 0xffff00ff, 0xff80ff80}; + + private static final int + COL_ZOOM = 0x40ffffff, + COL_ZOOM_TEXT1 = 0xd0ffffff, + COL_ZOOM_TEXT2 = 0x30ffffff; + + public Graph2dView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public Graph2dView(Context context) { + super(context); + touchHandler = new TouchHandler(this); + init(context); + } + + private void init(Context context) { + zoomController.setOnZoomListener(this); + scroller = new Scroller(context); + paint.setAntiAlias(false); + textPaint.setAntiAlias(true); + } + + public String captureScreenshot() { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565); + Canvas canvas = new Canvas(bitmap); + onDraw(canvas); + return Util.saveBitmap(bitmap, GraphView.SCREENSHOT_DIR, "calculator"); + } + + private void clearAllGraph() { + for (int i = 0; i < GRAPHS_SIZE; ++i) { + graphs[i].clear(); + } + } + + public void setFunctions(List fs) { + funcs.clear(); + for (Function f : fs) { + int arity = f.arity(); + if (arity == 0 || arity == 1) { + funcs.add(f); + } + } + clearAllGraph(); + invalidate(); + } + + public void setFunction(Function f) { + funcs.clear(); + if (f != null) { + funcs.add(f); + } + clearAllGraph(); + invalidate(); + } + + public void onVisibilityChanged(boolean visible) { + } + + public void onZoom(boolean zoomIn) { + if (zoomIn) { + if (canZoomIn()) { + gwidth /= 2; + invalidateGraphs(); + } + } else { + if (canZoomOut()) { + gwidth *= 2; + invalidateGraphs(); + } + } + zoomController.setZoomInEnabled(canZoomIn()); + zoomController.setZoomOutEnabled(canZoomOut()); + } + + public void onResume() { + } + + public void onPause() { + } + + public void onDetachedFromWindow() { + zoomController.setVisible(false); + super.onDetachedFromWindow(); + } + + protected void onSizeChanged(int w, int h, int ow, int oh) { + width = w; + height = h; + clearAllGraph(); + // points = new float[w+w]; + } + + protected void onDraw(Canvas canvas) { + if (funcs.size() == 0) { + return; + } + if (scroller.computeScrollOffset()) { + final float scale = gwidth / width; + currentX = scroller.getCurrX() * scale; + currentY = scroller.getCurrY() * scale; + if (!scroller.isFinished()) { + invalidate(); + } + } + drawGraph(canvas); + } + + private float eval(Function f, float x) { + float v = (float) f.eval(x); + // Calculator.log("eval " + x + "; " + v); + if (v < -10000f) { + return -10000f; + } + if (v > 10000f) { + return 10000f; + } + return v; + } + + // distance from (x,y) to the line (x1,y1) to (x2,y2), squared, multiplied by 4 + /* + private float distance(float x1, float y1, float x2, float y2, float x, float y) { + float dx = x2 - x1; + float dy = y2 - y1; + float mx = x - x1; + float my = y - y1; + float up = dx*my - dy*mx; + return up*up*4/(dx*dx + dy*dy); + } + */ + + // distance as above when x==(x1+x2)/2. + private float distance2(float x1, float y1, float x2, float y2, float y) { + final float dx = x2 - x1; + final float dy = y2 - y1; + final float up = dx * (y1 + y2 - y - y); + return up * up / (dx * dx + dy * dy); + } + + private void computeGraph(Function f, float minX, float maxX, float minY, float maxY, Data graph) { + if (f.arity() == 0) { + float v = (float) f.eval(); + if (v < -10000f) { + v = -10000f; + } + if (v > 10000f) { + v = 10000f; + } + graph.clear(); + graph.push(minX, v); + graph.push(maxX, v); + return; + } + + final float scale = width / gwidth; + final float maxStep = 15.8976f / scale; + final float minStep = .05f / scale; + float ythresh = 1 / scale; + ythresh = ythresh * ythresh; + // next.clear(); + // endGraph.clear(); + if (!graph.empty()) { + // Calculator.log("last " + lastMinX + " min " + minX); + if (minX >= lastMinX) { + graph.eraseBefore(minX); + } else { + graph.eraseAfter(maxX); + maxX = Math.min(maxX, graph.firstX()); + graph.swap(endGraph); + } + } + if (graph.empty()) { + graph.push(minX, eval(f, minX)); + } + float leftX, leftY; + float rightX = graph.topX(), rightY = graph.topY(); + int nEval = 1; + while (true) { + leftX = rightX; + leftY = rightY; + if (leftX > maxX) { + break; + } + if (next.empty()) { + float x = leftX + maxStep; + next.push(x, eval(f, x)); + ++nEval; + } + rightX = next.topX(); + rightY = next.topY(); + next.pop(); + + if (leftY != leftY && rightY != rightY) { // NaN + continue; + } + + float dx = rightX - leftX; + float middleX = (leftX + rightX) / 2; + float middleY = eval(f, middleX); + ++nEval; + boolean middleIsOutside = (middleY < leftY && middleY < rightY) || (leftY < middleY && rightY < middleY); + if (dx < minStep) { + // Calculator.log("minStep"); + if (middleIsOutside) { + graph.push(rightX, Float.NaN); + } + graph.push(rightX, rightY); + continue; + } + if (middleIsOutside && ((leftY < minY && rightY > maxY) || (leftY > maxY && rightY < minY))) { + graph.push(rightX, Float.NaN); + graph.push(rightX, rightY); + // Calculator.log("+-inf"); + continue; + } + + if (!middleIsOutside) { + /* + float diff = leftY + rightY - middleY - middleY; + float dy = rightY - leftY; + float dx2 = dx*dx; + float distance = dx2*diff*diff/(dx2+dy*dy); + */ + // Calculator.log("" + dx + ' ' + leftY + ' ' + middleY + ' ' + rightY + ' ' + distance + ' ' + ythresh); + if (distance2(leftX, leftY, rightX, rightY, middleY) < ythresh) { + graph.push(rightX, rightY); + continue; + } + } + next.push(rightX, rightY); + next.push(middleX, middleY); + rightX = leftX; + rightY = leftY; + } + if (!endGraph.empty()) { + graph.append(endGraph); + } + long t2 = System.currentTimeMillis(); + // Calculator.log("graph points " + graph.size + " evals " + nEval + " time " + (t2-t1)); + + next.clear(); + endGraph.clear(); + } + + private static Path path = new Path(); + + private Path graphToPath(Data graph) { + boolean first = true; + int size = graph.size; + float[] xs = graph.xs; + float[] ys = graph.ys; + path.rewind(); + for (int i = 0; i < size; ++i) { + float y = ys[i]; + float x = xs[i]; + // Calculator.log("path " + x + ' ' + y); + if (y == y) { // !NaN + if (first) { + path.moveTo(x, y); + first = false; + } else { + path.lineTo(x, y); + } + } else { + first = true; + } + } + return path; + } + + private static final float NTICKS = 15; + + private static float stepFactor(float w) { + float f = 1; + while (w / f > NTICKS) { + f *= 10; + } + while (w / f < NTICKS / 10) { + f /= 10; + } + float r = w / f; + if (r < NTICKS / 5) { + return f / 5; + } else if (r < NTICKS / 2) { + return f / 2; + } else { + return f; + } + } + + private static StringBuilder b = new StringBuilder(); + private static char[] buf = new char[20]; + + private static StringBuilder format(float fv) { + int pos = 0; + boolean addDot = false; + int v = Math.round(fv * 100); + boolean isNeg = v < 0; + v = isNeg ? -v : v; + for (int i = 0; i < 2; ++i) { + int digit = v % 10; + v /= 10; + if (digit != 0 || addDot) { + buf[pos++] = (char) ('0' + digit); + addDot = true; + } + } + if (addDot) { + buf[pos++] = '.'; + } + if (v == 0) { + buf[pos++] = '0'; + } + while (v != 0) { + buf[pos++] = (char) ('0' + (v % 10)); + v /= 10; + } + if (isNeg) { + buf[pos++] = '-'; + } + b.setLength(0); + b.append(buf, 0, pos); + b.reverse(); + return b; + } + + private void drawGraph(Canvas canvas) { + long t1 = System.currentTimeMillis(); + float minX = getXMin(); + float maxX = getXMax(minX); + float ywidth = gwidth * height / width; + float minY = currentY - ywidth / 2; + float maxY = minY + ywidth; + if (minY < boundMinY || maxY > boundMaxY) { + float halfw = ywidth / 2; + boundMinY = minY - halfw; + boundMaxY = maxY + halfw; + clearAllGraph(); + } + + canvas.drawColor(0xff000000); + + paint.setStrokeWidth(0); + paint.setAntiAlias(false); + paint.setStyle(Paint.Style.STROKE); + + final float h2 = height / 2f; + final float scale = width / gwidth; + + float x0 = -minX * scale; + boolean drawYAxis = true; + if (x0 < 25) { + x0 = 25; + // drawYAxis = false; + } else if (x0 > width - 3) { + x0 = width - 3; + // drawYAxis = false; + } + float y0 = maxY * scale; + if (y0 < 3) { + y0 = 3; + } else if (y0 > height - 15) { + y0 = height - 15; + } + + final float tickSize = 3; + final float y2 = y0 + tickSize; + paint.setColor(COL_GRID); + float step = stepFactor(gwidth); + // Calculator.log("width " + gwidth + " step " + step); + float v = ((int) (minX / step)) * step; + textPaint.setColor(COL_TEXT); + textPaint.setTextSize(12); + textPaint.setTextAlign(Paint.Align.CENTER); + float stepScale = step * scale; + for (float x = (v - minX) * scale; x <= width; x += stepScale, v += step) { + canvas.drawLine(x, 0, x, height, paint); + if (!(-.001f < v && v < .001f)) { + StringBuilder b = format(v); + canvas.drawText(b, 0, b.length(), x, y2 + 10, textPaint); + } + } + + final float x1 = x0 - tickSize; + v = ((int) (minY / step)) * step; + textPaint.setTextAlign(Paint.Align.RIGHT); + for (float y = height - (v - minY) * scale; y >= 0; y -= stepScale, v += step) { + canvas.drawLine(0, y, width, y, paint); + if (!(-.001f < v && v < .001f)) { + StringBuilder b = format(v); + canvas.drawText(b, 0, b.length(), x1, y + 4, textPaint); + } + } + + paint.setColor(COL_AXIS); + if (drawYAxis) { + canvas.drawLine(x0, 0, x0, height, paint); + } + canvas.drawLine(0, y0, width, y0, paint); + + matrix.reset(); + matrix.preTranslate(-currentX, -currentY); + matrix.postScale(scale, -scale); + matrix.postTranslate(width / 2, height / 2); + + paint.setStrokeWidth(0); + paint.setAntiAlias(false); + + int n = Math.min(funcs.size(), GRAPHS_SIZE); + for (int i = 0; i < n; ++i) { + computeGraph(funcs.get(i), minX, maxX, boundMinY, boundMaxY, graphs[i]); + Path path = graphToPath(graphs[i]); + path.transform(matrix); + paint.setColor(COL_GRAPH[i]); + canvas.drawPath(path, paint); + } + lastMinX = minX; + } + + private float getXMax(float minX) { + return minX + gwidth; + } + + private float getXMax() { + return getXMax(getXMin()); + } + + private float getXMin() { + return currentX - gwidth / 2; + } + + private boolean canZoomIn() { + return gwidth > 1f; + } + + private boolean canZoomOut() { + return gwidth < 50; + } + + private void invalidateGraphs() { + clearAllGraph(); + boundMinY = boundMaxY = 0; + invalidate(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return touchHandler != null ? touchHandler.onTouchEvent(event) : super.onTouchEvent(event); + } + + public void onTouchDown(float x, float y) { + zoomController.setVisible(true); + if (!scroller.isFinished()) { + scroller.abortAnimation(); + } + lastTouchX = x; + lastTouchY = y; + } + + public void onTouchMove(float x, float y) { + float deltaX = x - lastTouchX; + float deltaY = y - lastTouchY; + if (deltaX < -1 || deltaX > 1 || deltaY < -1 || deltaY > 1) { + scroll(-deltaX, deltaY); + lastTouchX = x; + lastTouchY = y; + invalidate(); + } + } + + public void onTouchUp(float x, float y) { + final float scale = width / gwidth; + float sx = -touchHandler.velocityTracker.getXVelocity(); + float sy = touchHandler.velocityTracker.getYVelocity(); + final float asx = Math.abs(sx); + final float asy = Math.abs(sy); + if (asx < asy / 3) { + sx = 0; + } else if (asy < asx / 3) { + sy = 0; + } + scroller.fling(Math.round(currentX * scale), + Math.round(currentY * scale), + Math.round(sx), Math.round(sy), -10000, 10000, -10000, 10000); + invalidate(); + } + + public void onTouchZoomDown(float x1, float y1, float x2, float y2) { + zoomTracker.start(gwidth, x1, y1, x2, y2); + } + + public void onTouchZoomMove(float x1, float y1, float x2, float y2) { + if (!zoomTracker.update(x1, y1, x2, y2)) { + return; + } + float targetGwidth = zoomTracker.value; + if (targetGwidth > .25f && targetGwidth < 200) { + gwidth = targetGwidth; + } + // scroll(-zoomTracker.moveX, zoomTracker.moveY); + invalidateGraphs(); + // Calculator.log("zoom redraw"); + } + + private void scroll(float deltaX, float deltaY) { + final float scale = gwidth / width; + float dx = deltaX * scale; + float dy = deltaY * scale; + final float adx = Math.abs(dx); + final float ady = Math.abs(dy); + if (adx < ady / 3) { + dx = 0; + } else if (ady < adx / 3) { + dy = 0; + } + currentX += dx; + currentY += dy; + } +} diff --git a/android-app/src/main/java/arity/calculator/Graph3d.java b/android-app/src/main/java/arity/calculator/Graph3d.java new file mode 100755 index 00000000..2f15ca63 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Graph3d.java @@ -0,0 +1,264 @@ +// Copyright (C) 2009-2010 Mihai Preda + +package arity.calculator; + +import org.javia.arity.Function; + +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.nio.ShortBuffer; + +class Graph3d { + private final int N = Calculator.useHighQuality3d ? 36 : 24; + private ShortBuffer verticeIdx; + private FloatBuffer vertexBuf; + private ByteBuffer colorBuf; + private int vertexVbo, colorVbo, vertexElementVbo; + private boolean useVBO; + private int nVertex; + + Graph3d(GL11 gl) { + short[] b = new short[N*N]; + int p = 0; + for (int i = 0; i < N; i++) { + short v = 0; + for (int j = 0; j < N; v += N+N, j+=2) { + b[p++] = (short)(v+i); + b[p++] = (short)(v+N+N-1-i); + } + v = (short) (N*(N-2)); + i++; + for (int j = N-1; j >= 0; v -= N+N, j-=2) { + b[p++] = (short)(v+N+N-1-i); + b[p++] = (short)(v+i); + } + } + verticeIdx = buildBuffer(b); + + String extensions = gl.glGetString(GL10.GL_EXTENSIONS); + useVBO = extensions.indexOf("vertex_buffer_object") != -1; + Calculator.log("VBOs support: " + useVBO + " version " + gl.glGetString(GL10.GL_VERSION)); + + if (useVBO) { + int[] out = new int[3]; + gl.glGenBuffers(3, out, 0); + vertexVbo = out[0]; + colorVbo = out[1]; + vertexElementVbo = out[2]; + } + } + + private static FloatBuffer buildBuffer(float[] b) { + ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 2); + bb.order(ByteOrder.nativeOrder()); + FloatBuffer sb = bb.asFloatBuffer(); + sb.put(b); + sb.position(0); + return sb; + } + + private static ShortBuffer buildBuffer(short[] b) { + ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 1); + bb.order(ByteOrder.nativeOrder()); + ShortBuffer sb = bb.asShortBuffer(); + sb.put(b); + sb.position(0); + return sb; + } + + private static ByteBuffer buildBuffer(byte[] b) { + ByteBuffer bb = ByteBuffer.allocateDirect(b.length << 1); + bb.order(ByteOrder.nativeOrder()); + bb.put(b); + bb.position(0); + return bb; + } + + public void update(GL11 gl, Function f, float zoom) { + final int NTICK = Calculator.useHighQuality3d ? 5 : 0; + final float size = 4*zoom; + final float minX = -size, maxX = size, minY = -size, maxY = size; + + Calculator.log("update VBOs " + vertexVbo + ' ' + colorVbo + ' ' + vertexElementVbo); + nVertex = N*N+6+8 + NTICK*6; + int nFloats = nVertex * 3; + float vertices[] = new float[nFloats]; + byte colors[] = new byte[nVertex << 2]; + if (f != null) { + Calculator.log("Graph3d update"); + float sizeX = maxX - minX; + float sizeY = maxY - minY; + float stepX = sizeX / (N-1); + float stepY = sizeY / (N-1); + int pos = 0; + double sum = 0; + float y = minY; + float x = minX - stepX; + int nRealPoints = 0; + for (int i = 0; i < N; i++, y+=stepY) { + float xinc = (i & 1) == 0 ? stepX : -stepX; + x += xinc; + for (int j = 0; j < N; ++j, x+=xinc, pos+=3) { + float z = (float) f.eval(x, y); + vertices[pos] = x; + vertices[pos+1] = y; + vertices[pos+2] = z; + if (z == z) { // not NAN + sum += z * z; + ++nRealPoints; + } + } + } + float maxAbs = (float) Math.sqrt(sum / nRealPoints); + maxAbs *= .9f; + maxAbs = Math.min(maxAbs, 15); + maxAbs = Math.max(maxAbs, .001f); + + final int limitColor = N*N*4; + for (int i = 0, j = 2; i < limitColor; i+=4, j+=3) { + float z = vertices[j]; + if (z == z) { + final float a = z / maxAbs; + final float abs = a < 0 ? -a : a; + colors[i] = floatToByte(a); + colors[i+1] = floatToByte(1-abs*.3f); + colors[i+2] = floatToByte(-a); + colors[i+3] = (byte) 255; + } else { + vertices[j] = 0; + z = 0; + colors[i] = 0; + colors[i+1] = 0; + colors[i+2] = 0; + colors[i+3] = 0; + } + } + } + int base = N*N*3; + int colorBase = N*N*4; + int p = base; + final int baseSize = 2; + for (int i = -baseSize; i <= baseSize; i+=2*baseSize) { + vertices[p] = i; vertices[p+1] = -baseSize; vertices[p+2] = 0; + p += 3; + vertices[p] = i; vertices[p+1] = baseSize; vertices[p+2] = 0; + p += 3; + vertices[p] = -baseSize; vertices[p+1] = i; vertices[p+2] = 0; + p += 3; + vertices[p] = baseSize; vertices[p+1] = i; vertices[p+2] = 0; + p += 3; + } + for (int i = colorBase; i < colorBase+8*4; i += 4) { + colors[i] = 0; + colors[i+1] = 0; + colors[i+2] = (byte) 255; + colors[i+3] = (byte) 255; + } + base += 8*3; + colorBase += 8*4; + + final float unit = 2; + final float axis[] = { + 0, 0, 0, + unit, 0, 0, + 0, 0, 0, + 0, unit, 0, + 0, 0, 0, + 0, 0, unit, + }; + System.arraycopy(axis, 0, vertices, base, 6*3); + for (int i = colorBase; i < colorBase+6*4; i+=4) { + colors[i] = (byte) 255; + colors[i+1] = (byte) 255; + colors[i+2] = (byte) 255; + colors[i+3] = (byte) 255; + } + base += 6*3; + colorBase += 6*4; + + p = base; + final float tick = .03f; + final float offset = .01f; + for (int i = 1; i <= NTICK; ++i) { + vertices[p] = i-tick; + vertices[p+1] = -offset; + vertices[p+2] = -offset; + + vertices[p+3] = i+tick; + vertices[p+4] = offset; + vertices[p+5] = offset; + p += 6; + + vertices[p] = -offset; + vertices[p+1] = i-tick; + vertices[p+2] = -offset; + + vertices[p+3] = offset; + vertices[p+4] = i+tick; + vertices[p+5] = offset; + p += 6; + + vertices[p] = -offset; + vertices[p+1] = -offset; + vertices[p+2] = i-tick; + + vertices[p+3] = offset; + vertices[p+4] = offset; + vertices[p+5] = i+tick; + p += 6; + + } + for (int i = colorBase+NTICK*6*4-1; i >= colorBase; --i) { + colors[i] = (byte) 255; + } + + vertexBuf = buildBuffer(vertices); + colorBuf = buildBuffer(colors); + + if (useVBO) { + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexVbo); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, vertexBuf.capacity()*4, vertexBuf, GL11.GL_STATIC_DRAW); + vertexBuf = null; + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, colorVbo); + gl.glBufferData(GL11.GL_ARRAY_BUFFER, colorBuf.capacity(), colorBuf, GL11.GL_STATIC_DRAW); + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0); + colorBuf = null; + + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, vertexElementVbo); + gl.glBufferData(GL11.GL_ELEMENT_ARRAY_BUFFER, verticeIdx.capacity()*2, verticeIdx, GL11.GL_STATIC_DRAW); + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0); + } + } + + private byte floatToByte(float v) { + return (byte) (v <= 0 ? 0 : v >= 1 ? 255 : (int)(v*255)); + } + + public void draw(GL11 gl) { + if (useVBO) { + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, vertexVbo); + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, 0); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, colorVbo); + gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, 0); + + gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, 0); + // gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, N*N); + + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, vertexElementVbo); + gl.glDrawElements(GL10.GL_LINE_STRIP, N*N, GL10.GL_UNSIGNED_SHORT, 0); + gl.glBindBuffer(GL11.GL_ELEMENT_ARRAY_BUFFER, 0); + } else { + gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuf); + gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, colorBuf); + gl.glDrawElements(GL10.GL_LINE_STRIP, N*N, GL10.GL_UNSIGNED_SHORT, verticeIdx); + } + final int N2 = N*N; + gl.glDrawArrays(GL10.GL_LINE_STRIP, 0, N2); + gl.glDrawArrays(GL10.GL_LINES, N2, nVertex - N2); + } +} diff --git a/android-app/src/main/java/arity/calculator/Graph3dView.java b/android-app/src/main/java/arity/calculator/Graph3dView.java new file mode 100755 index 00000000..41880c6a --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Graph3dView.java @@ -0,0 +1,249 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import android.opengl.Matrix; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.widget.ZoomButtonsController; +import org.javia.arity.Function; + +import javax.microedition.khronos.opengles.GL10; +import javax.microedition.khronos.opengles.GL11; + +public class Graph3dView extends GLView implements + GraphView, + ZoomButtonsController.OnZoomListener, + TouchHandler.TouchHandlerInterface { + + private float lastTouchX, lastTouchY; + private TouchHandler touchHandler; + private ZoomButtonsController zoomController = new ZoomButtonsController(this); + private float zoomLevel = 1, targetZoom, zoomStep = 0, currentZoom; + private FPS fps = new FPS(); + private Graph3d graph; + + public Graph3dView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public Graph3dView(Context context) { + super(context); + touchHandler = new TouchHandler(this); + init(); + } + + private void init() { + startLooping(); + zoomController.setOnZoomListener(this); + + Matrix.setIdentityM(matrix1, 0); + Matrix.rotateM(matrix1, 0, -75, 1, 0, 0); + } + + public void onVisibilityChanged(boolean visible) { + } + + public void onZoom(boolean zoomIn) { + boolean changed = false; + if (zoomIn) { + if (canZoomIn(zoomLevel)) { + targetZoom = zoomLevel * .625f; + zoomStep = -zoomLevel / 40; + changed = true; + } + } else { + if (canZoomOut(zoomLevel)) { + targetZoom = zoomLevel * 1.6f; + zoomStep = zoomLevel / 20; + changed = true; + } + } + if (changed) { + zoomController.setZoomInEnabled(canZoomIn(targetZoom)); + zoomController.setZoomOutEnabled(canZoomOut(targetZoom)); + if (!shouldRotate()) { + setRotation(0, 0); + } + startLooping(); + } + } + + @Override + protected void glDraw() { + if ((zoomStep < 0 && zoomLevel > targetZoom) || + (zoomStep > 0 && zoomLevel < targetZoom)) { + zoomLevel += zoomStep; + } else if (zoomStep != 0) { + zoomStep = 0; + zoomLevel = targetZoom; + isDirty = true; + if (!shouldRotate()) { + stopLooping(); + } + } + super.glDraw(); + } + + private boolean canZoomIn(float zoom) { + return zoom > .2f; + } + + private boolean canZoomOut(float zoom) { + return zoom < 5; + } + + @Override + public void onDetachedFromWindow() { + zoomController.setVisible(false); + super.onDetachedFromWindow(); + } + + public void onTouchDown(float x, float y) { + zoomController.setVisible(true); + stopLooping(); + lastTouchX = x; + lastTouchY = y; + } + + public void onTouchMove(float x, float y) { + float deltaX = x - lastTouchX; + float deltaY = y - lastTouchY; + if (deltaX > 1 || deltaX < -1 || deltaY > 1 || deltaY < -1) { + setRotation(deltaX, deltaY); + glDraw(); + lastTouchX = x; + lastTouchY = y; + } + } + + public void onTouchUp(float x, float y) { + float vx = touchHandler.velocityTracker.getXVelocity(); + float vy = touchHandler.velocityTracker.getYVelocity(); + // Calculator.log("velocity " + vx + ' ' + vy); + setRotation(vx / 100, vy / 100); + if (shouldRotate()) { + startLooping(); + } + } + + public void onTouchZoomDown(float x1, float y1, float x2, float y2) { + + } + + public void onTouchZoomMove(float x1, float y1, float x2, float y2) { + + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + return touchHandler != null ? touchHandler.onTouchEvent(event) : super.onTouchEvent(event); + } + + // ---- + + private float[] matrix1 = new float[16], matrix2 = new float[16], matrix3 = new float[16]; + private float angleX, angleY; + private boolean isDirty; + private Function function; + private static final float DISTANCE = 15f; + + void setRotation(float x, float y) { + angleX = x; + angleY = y; + } + + boolean shouldRotate() { + final float limit = .5f; + return angleX < -limit || angleX > limit || angleY < -limit || angleY > limit; + } + + public void setFunction(Function f) { + function = f; + zoomLevel = 1; + isDirty = true; + } + + @Override + public void onSurfaceCreated(GL10 gl, int width, int height) { + gl.glDisable(GL10.GL_DITHER); + gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT, GL10.GL_FASTEST); + gl.glClearColor(0, 0, 0, 1); + gl.glShadeModel(Calculator.useHighQuality3d ? GL10.GL_SMOOTH : GL10.GL_FLAT); + gl.glDisable(GL10.GL_LIGHTING); + graph = new Graph3d((GL11) gl); + isDirty = true; + angleX = .5f; + angleY = 0; + + gl.glViewport(0, 0, width, height); + initFrustum(gl, DISTANCE * zoomLevel); + currentZoom = zoomLevel; + } + + @Override + public void onDrawFrame(GL10 gl10) { + GL11 gl = (GL11) gl10; + if (currentZoom != zoomLevel) { + initFrustum(gl, DISTANCE * zoomLevel); + currentZoom = zoomLevel; + } + if (isDirty) { + graph.update(gl, function, zoomLevel); + isDirty = false; + } + + if (fps.incFrame()) { + Calculator.log("f/s " + fps.getValue()); + } + + gl.glClear(GL10.GL_COLOR_BUFFER_BIT); + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glLoadIdentity(); + gl.glTranslatef(0, 0, -DISTANCE * zoomLevel); + + Matrix.setIdentityM(matrix2, 0); + float ax = Math.abs(angleX); + float ay = Math.abs(angleY); + if (ay * 3 < ax) { + Matrix.rotateM(matrix2, 0, angleX, 0, 1, 0); + } else if (ax * 3 < ay) { + Matrix.rotateM(matrix2, 0, angleY, 1, 0, 0); + } else { + if (ax > ay) { + Matrix.rotateM(matrix2, 0, angleX, 0, 1, 0); + Matrix.rotateM(matrix2, 0, angleY, 1, 0, 0); + } else { + Matrix.rotateM(matrix2, 0, angleY, 1, 0, 0); + Matrix.rotateM(matrix2, 0, angleX, 0, 1, 0); + } + } + Matrix.multiplyMM(matrix3, 0, matrix2, 0, matrix1, 0); + gl.glMultMatrixf(matrix3, 0); + System.arraycopy(matrix3, 0, matrix1, 0, 16); + graph.draw(gl); + } + + private void initFrustum(GL10 gl, float distance) { + gl.glMatrixMode(GL10.GL_PROJECTION); + gl.glLoadIdentity(); + float near = distance * (1 / 3f); + float far = distance * 3f; + float dimen = near / 5f; + float h = dimen * height / width; + gl.glFrustumf(-dimen, dimen, -h, h, near, far); + gl.glMatrixMode(GL10.GL_MODELVIEW); + gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); + gl.glEnableClientState(GL10.GL_COLOR_ARRAY); + } + + private void printMatrix(float[] m, String name) { + StringBuffer b = new StringBuffer(); + for (int i = 0; i < 16; ++i) { + b.append(m[i]).append(' '); + } + Calculator.log(name + ' ' + b.toString()); + } +} diff --git a/android-app/src/main/java/arity/calculator/GraphView.java b/android-app/src/main/java/arity/calculator/GraphView.java new file mode 100755 index 00000000..01d2fabc --- /dev/null +++ b/android-app/src/main/java/arity/calculator/GraphView.java @@ -0,0 +1,30 @@ +// Copyright (C) 2009-2010 Mihai Preda + +package arity.calculator; + +import org.javia.arity.Function; + +public interface GraphView { + + static final String SCREENSHOT_DIR = "/screenshots"; + + public void setFunction(Function f); + + public void onPause(); + public void onResume(); + + public String captureScreenshot(); + + void setId(int id); + + /* + ********************************************************************** + * + * CUSTOMIZATION + * + ********************************************************************** + */ + +/* void setBgColor(int color); + void setAxisColor(int color);*/ +} diff --git a/android-app/src/main/java/arity/calculator/Help.java b/android-app/src/main/java/arity/calculator/Help.java new file mode 100755 index 00000000..a4fc0a4c --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Help.java @@ -0,0 +1,16 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.app.Activity; +import android.os.Bundle; +import android.webkit.WebView; + +public class Help extends Activity { + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + WebView view = new WebView(this); + setContentView(view); + view.loadUrl("file:///android_asset/help.html"); + } +} diff --git a/android-app/src/main/java/arity/calculator/History.java b/android-app/src/main/java/arity/calculator/History.java new file mode 100755 index 00000000..3bda9c8f --- /dev/null +++ b/android-app/src/main/java/arity/calculator/History.java @@ -0,0 +1,112 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.content.Context; +import java.io.*; +import java.util.ArrayList; + +class History extends FileHandler { + private static final int SIZE_LIMIT = 30; + ArrayList entries = new ArrayList(); + int pos; + HistoryEntry aboveTop = new HistoryEntry("", ""); + + + History(Context context) { + super(context, "history", 1); + load(); + } + + void clear() { + entries.clear(); + pos = 0; + } + + int size() { + return entries.size(); + } + + void doRead(DataInputStream is) throws IOException { + aboveTop = new HistoryEntry(is); + int loadSize = is.readInt(); + for (int i = 0; i < loadSize; ++i) { + entries.add(new HistoryEntry(is)); + } + pos = entries.size(); + } + + void doWrite(DataOutputStream os) throws IOException { + aboveTop.save(os); + os.writeInt(entries.size()); + for (HistoryEntry entry : entries) { + entry.save(os); + } + } + + private HistoryEntry currentEntry() { + if (pos < entries.size()) { + return entries.get(pos); + } else { + return aboveTop; + } + } + + int getListPos() { + return entries.size() - 1 - pos; + } + + boolean onEnter(String text, String result) { + if (result == null) { + result = ""; + } + currentEntry().onEnter(); + pos = entries.size(); + if (text.length() == 0) { + return false; + } + if (entries.size() > 0) { + HistoryEntry top = entries.get(entries.size()-1); + if (text.equals(top.line) && result.equals(top.result)) { + return false; + } + } + if (entries.size() > SIZE_LIMIT) { + entries.remove(0); + } + entries.add(new HistoryEntry(text, result)); + pos = entries.size(); + return true; + } + + void moveToPos(int listPos, String text) { + currentEntry().editLine = text; + pos = entries.size() - listPos - 1; + } + + void updateEdited(String text) { + currentEntry().editLine = text; + } + + boolean moveUp(String text) { + updateEdited(text); + if (pos >= entries.size()) { + return false; + } + ++pos; + return true; + } + + boolean moveDown(String text) { + updateEdited(text); + if (pos <= 0) { + return false; + } + --pos; + return true; + } + + String getText() { + return currentEntry().editLine; + } +} diff --git a/android-app/src/main/java/arity/calculator/HistoryEntry.java b/android-app/src/main/java/arity/calculator/HistoryEntry.java new file mode 100755 index 00000000..7a6079ef --- /dev/null +++ b/android-app/src/main/java/arity/calculator/HistoryEntry.java @@ -0,0 +1,34 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import java.io.*; + +class HistoryEntry { + String line, editLine, result; + + HistoryEntry(DataInputStream is) throws IOException { + line = is.readUTF(); + editLine = is.readUTF(); + if (editLine.length() == 0) { + editLine = line; + } + result = is.readUTF(); + } + + HistoryEntry(String text, String result) { + line = text; + editLine = text; + this.result = result == null ? "" : result; + } + + void save(DataOutputStream os) throws IOException { + os.writeUTF(line); + os.writeUTF(editLine.equals(line) ? "" : editLine); + os.writeUTF(result); + } + + void onEnter() { + editLine = line; + } +} diff --git a/android-app/src/main/java/arity/calculator/MotionEventWrap.java b/android-app/src/main/java/arity/calculator/MotionEventWrap.java new file mode 100755 index 00000000..cb3c5b9e --- /dev/null +++ b/android-app/src/main/java/arity/calculator/MotionEventWrap.java @@ -0,0 +1,21 @@ +// Copyright (C) 2010 Mihai Preda + +package arity.calculator; + +import android.view.MotionEvent; + +class MotionEventWrap { + private static final boolean IS_API_5 = Util.SDK_VERSION >= 5; + + static int getPointerCount(MotionEvent event) { + return IS_API_5 ? MotionEventWrapNew.getPointerCount(event) : 1; + } + + static float getX(MotionEvent event, int idx) { + return IS_API_5 ? MotionEventWrapNew.getX(event, idx) : 0; + } + + static float getY(MotionEvent event, int idx) { + return IS_API_5 ? MotionEventWrapNew.getX(event, idx) : 0; + } +} diff --git a/android-app/src/main/java/arity/calculator/MotionEventWrapNew.java b/android-app/src/main/java/arity/calculator/MotionEventWrapNew.java new file mode 100755 index 00000000..06dbd9ae --- /dev/null +++ b/android-app/src/main/java/arity/calculator/MotionEventWrapNew.java @@ -0,0 +1,19 @@ +// Copyright (C) 2010 Mihai Preda + +package arity.calculator; + +import android.view.MotionEvent; + +class MotionEventWrapNew { + static int getPointerCount(MotionEvent event) { + return event.getPointerCount(); + } + + static float getX(MotionEvent event, int idx) { + return event.getX(idx); + } + + static float getY(MotionEvent event, int idx) { + return event.getY(idx); + } +} diff --git a/android-app/src/main/java/arity/calculator/ShowGraph.java b/android-app/src/main/java/arity/calculator/ShowGraph.java new file mode 100755 index 00000000..4fd627f1 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/ShowGraph.java @@ -0,0 +1,69 @@ +// Copyright (C) 2009 Mihai Preda + +package arity.calculator; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import org.javia.arity.Function; + +import java.util.ArrayList; + +public class ShowGraph extends Activity { + + private GraphView view; + + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + ArrayList funcs = Calculator.graphedFunction; + if (funcs == null) { + finish(); + return; + } + int size = funcs.size(); + if (size == 1) { + Function f = funcs.get(0); + view = f.arity() == 1 ? new Graph2dView(this) : new Graph3dView(this); + view.setFunction(f); + } else { + view = new Graph2dView(this); + ((Graph2dView) view).setFunctions(funcs); + } + setContentView((View) view); + } + + protected void onPause() { + super.onPause(); + view.onPause(); + } + + protected void onResume() { + super.onResume(); + view.onResume(); + } + +/* public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + (new MenuInflater(this)).inflate(R.menu.graph, menu); + return true; + } + + public boolean onOptionsItemSelected(MenuItem item) { + super.onOptionsItemSelected(item); + switch (item.getItemId()) { + case R.id.capture_screenshot: + String fileName = view.captureScreenshot(); + if (fileName != null) { + Toast.makeText(this, "screenshot saved as \n" + fileName, Toast.LENGTH_LONG).show(); + Intent i = new Intent(Intent.ACTION_VIEW); + i.setDataAndType(Uri.fromFile(new File(fileName)), "image/png"); + startActivity(i); + } + break; + + default: + return false; + } + return true; + }*/ +} diff --git a/android-app/src/main/java/arity/calculator/TouchHandler.java b/android-app/src/main/java/arity/calculator/TouchHandler.java new file mode 100755 index 00000000..bbaa7aca --- /dev/null +++ b/android-app/src/main/java/arity/calculator/TouchHandler.java @@ -0,0 +1,79 @@ +// Copyright (C) 2009-2010 Mihai Preda + +package arity.calculator; + +import android.view.MotionEvent; +import android.view.VelocityTracker; + +class TouchHandler { + static interface TouchHandlerInterface { + void onTouchDown(float x, float y); + void onTouchMove(float x, float y); + void onTouchUp(float x, float y); + void onTouchZoomDown(float x1, float y1, float x2, float y2); + void onTouchZoomMove(float x1, float y1, float x2, float y2); + } + + VelocityTracker velocityTracker = VelocityTracker.obtain(); + + private boolean isAfterZoom; + private TouchHandlerInterface listener; + + TouchHandler(TouchHandlerInterface listener) { + this.listener = listener; + } + + public boolean onTouchEvent(MotionEvent event) { + // Calculator.log("touch " + event + ' ' + event.getPointerCount() + event.getPointerId(0)); + + int fullAction = event.getAction(); + int action = fullAction & MotionEvent.ACTION_MASK; + int pointer = (fullAction & MotionEvent.ACTION_POINTER_ID_MASK) >> MotionEvent.ACTION_POINTER_ID_SHIFT; + float x = event.getX(); + float y = event.getY(); + int nPoints = MotionEventWrap.getPointerCount(event); + + switch (action) { + case MotionEvent.ACTION_DOWN: + isAfterZoom = false; + velocityTracker.clear(); + velocityTracker.addMovement(event); + listener.onTouchDown(x, y); + break; + + case MotionEvent.ACTION_MOVE: + if (nPoints == 1) { + if (isAfterZoom) { + velocityTracker.clear(); + listener.onTouchDown(x, y); + isAfterZoom = false; + } + velocityTracker.addMovement(event); + listener.onTouchMove(x, y); + } else if (nPoints == 2) { + listener.onTouchZoomMove(x, y, MotionEventWrap.getX(event, 1), MotionEventWrap.getY(event, 1)); + } + break; + + case MotionEvent.ACTION_UP: + velocityTracker.addMovement(event); + velocityTracker.computeCurrentVelocity(1000); + listener.onTouchUp(x, y); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + if (nPoints == 2) { + listener.onTouchZoomDown(x, y, MotionEventWrap.getX(event, 1), MotionEventWrap.getY(event, 1)); + } + break; + + case MotionEvent.ACTION_POINTER_UP: + if (nPoints == 2) { + isAfterZoom = true; + } + break; + } + return true; + } + +} diff --git a/android-app/src/main/java/arity/calculator/Util.java b/android-app/src/main/java/arity/calculator/Util.java new file mode 100755 index 00000000..6ec6b48e --- /dev/null +++ b/android-app/src/main/java/arity/calculator/Util.java @@ -0,0 +1,70 @@ +// Copyright (C) 2009-2010 Mihai Preda + +package arity.calculator; + +import android.graphics.Bitmap; +import android.os.Environment; +import android.os.Build; + +import java.nio.ShortBuffer; +import java.io.*; + +class Util { + public static final int SDK_VERSION = getSdkVersion(); + + private static int getSdkVersion() { + try { + return Integer.parseInt(Build.VERSION.SDK); + } catch (NumberFormatException e) { + Calculator.log("invalid SDK " + Build.VERSION.SDK); + return 3; + } + } + + static String saveBitmap(Bitmap bitmap, String dir, String baseName) { + try { + File sdcard = Environment.getExternalStorageDirectory(); + File pictureDir = new File(sdcard, dir); + pictureDir.mkdirs(); + File f = null; + for (int i = 1; i < 200; ++i) { + String name = baseName + i + ".png"; + f = new File(pictureDir, name); + if (!f.exists()) { + break; + } + } + if (!f.exists()) { + String name = f.getAbsolutePath(); + FileOutputStream fos = new FileOutputStream(name); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos); + fos.flush(); + fos.close(); + return name; + } + } catch (Exception e) { + Calculator.log("exception saving screenshot: " + e); + } finally { + /* + if (fos != null) { + fos.close(); + } + */ + } + return null; + } + + static void bitmapBGRtoRGB(Bitmap bitmap, int width, int height) { + int size = width * height; + short data[] = new short[size]; + ShortBuffer buf = ShortBuffer.wrap(data); + bitmap.copyPixelsToBuffer(buf); + for (int i = 0; i < size; ++i) { + //BGR-565 to RGB-565 + short v = data[i]; + data[i] = (short) (((v&0x1f) << 11) | (v&0x7e0) | ((v&0xf800) >> 11)); + } + buf.rewind(); + bitmap.copyPixelsFromBuffer(buf); + } +} diff --git a/android-app/src/main/java/arity/calculator/ZoomTracker.java b/android-app/src/main/java/arity/calculator/ZoomTracker.java new file mode 100755 index 00000000..81857327 --- /dev/null +++ b/android-app/src/main/java/arity/calculator/ZoomTracker.java @@ -0,0 +1,53 @@ +// Copyright (C) 2010 Mihai Preda + +package arity.calculator; + +class ZoomTracker { + private float sx1, sy1, sx2, sy2; + private float initialDist; + private float initialValue; + + float value; + float moveX, moveY; + + void start(float value, float x1, float y1, float x2, float y2) { + sx1 = x1; + sy1 = y1; + sx2 = x2; + sy2 = y2; + initialDist = distance(x1, y1, x2, y2); + initialValue = value; + } + + boolean update(float x1, float y1, float x2, float y2) { + final float LIMIT = 1.5f; + if (Math.abs(x1 - sx1) < LIMIT && Math.abs(y1 - sy1) < LIMIT && + Math.abs(x2 - sx2) < LIMIT && Math.abs(y2 - sy2) < LIMIT) { + return false; + } + moveX = common(x1, sx1, x2, sx2); + moveY = common(y1, sy1, y2, sy2); + float dist = distance(x1, y1, x2, y2); + value = initialDist / dist * initialValue; + sx1 = x1; + sx2 = x2; + sy1 = y1; + sy2 = y2; + return true; + } + + private float distance(float x1, float y1, float x2, float y2) { + final float dx = x1-x2; + final float dy = y1-y2; + // return (float) Math.sqrt(dx*dx+dy*dy); + return Math.max(dx*dx, dy*dy); + } + + private float common(float x1, float sx1, float x2, float sx2) { + float dx1 = x1 - sx1; + float dx2 = x2 - sx2; + return (dx1 < 0 && dx2 < 0) ? Math.max(dx1, dx2) : + (dx1 > 0 && dx2 > 0) ? Math.min(dx1, dx2): + 0; + } +} diff --git a/android-app/src/main/java/org/solovyev/android/calculator/AndroidCalculatorLogger.java b/android-app/src/main/java/org/solovyev/android/calculator/AndroidCalculatorLogger.java index 2aba58c6..4bb0a7fa 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/AndroidCalculatorLogger.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/AndroidCalculatorLogger.java @@ -12,7 +12,7 @@ import org.jetbrains.annotations.Nullable; public class AndroidCalculatorLogger implements CalculatorLogger { @NotNull - private static final String TAG = AndroidCalculatorLogger.class.getSimpleName(); + private static final String TAG = "Calculatorpp"; @Override public void debug(@Nullable String tag, @NotNull String message) { @@ -21,7 +21,7 @@ public class AndroidCalculatorLogger implements CalculatorLogger { @NotNull private String getTag(@Nullable String tag) { - return tag != null ? tag : TAG; + return tag != null ? TAG + "/" + tag : TAG; } @Override diff --git a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java index 003692d9..adb76513 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivity.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import org.solovyev.android.AndroidUtils; import org.solovyev.android.calculator.about.CalculatorFragmentType; import org.solovyev.android.calculator.about.CalculatorReleaseNotesFragment; +import org.solovyev.android.calculator.plot.CalculatorPlotActivity; import org.solovyev.android.fragments.FragmentUtils; import org.solovyev.android.prefs.Preference; import org.solovyev.common.equals.EqualsTool; @@ -63,7 +64,7 @@ public class CalculatorActivity extends SherlockFragmentActivity implements Shar activityHelper.addTab(this, CalculatorFragmentType.variables, null, R.id.main_second_pane); activityHelper.addTab(this, CalculatorFragmentType.functions, null, R.id.main_second_pane); activityHelper.addTab(this, CalculatorFragmentType.operators, null, R.id.main_second_pane); - activityHelper.addTab(this, CalculatorFragmentType.plotter, null, R.id.main_second_pane); + activityHelper.addTab(this, CalculatorPlotActivity.getPlotterFragmentType(), null, R.id.main_second_pane); activityHelper.addTab(this, CalculatorFragmentType.faq, null, R.id.main_second_pane); } else { getSupportActionBar().hide(); diff --git a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java index 14d8a89d..10e7a93c 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorActivityLauncher.java @@ -23,6 +23,7 @@ import org.solovyev.android.calculator.help.CalculatorHelpActivity; import org.solovyev.android.calculator.history.CalculatorHistoryActivity; import org.solovyev.android.calculator.math.edit.*; import org.solovyev.android.calculator.matrix.CalculatorMatrixActivity; +import org.solovyev.android.calculator.plot.AbstractCalculatorPlotFragment; import org.solovyev.android.calculator.plot.CalculatorPlotActivity; import org.solovyev.android.calculator.plot.CalculatorPlotFragment; import org.solovyev.android.calculator.plot.PlotInput; @@ -99,10 +100,14 @@ public final class CalculatorActivityLauncher implements CalculatorEventListener context.startActivity(intent); } - public static void plotGraph(@NotNull final Context context, @NotNull Generic generic, @NotNull Constant constant){ + public static void plotGraph(@NotNull final Context context, + @NotNull Generic generic, + @NotNull Constant xVariable, + @Nullable Constant yVariable){ final Intent intent = new Intent(); intent.putExtra(ChartFactory.TITLE, context.getString(R.string.c_graph)); - intent.putExtra(CalculatorPlotFragment.INPUT, new CalculatorPlotFragment.Input(generic.toString(), constant.getName())); + final AbstractCalculatorPlotFragment.Input input = new CalculatorPlotFragment.Input(generic.toString(), xVariable.getName(), yVariable == null ? null : yVariable.getName()); + intent.putExtra(CalculatorPlotFragment.INPUT, input); intent.setClass(context, CalculatorPlotActivity.class); AndroidUtils2.addFlags(intent, false, context); context.startActivity(intent); @@ -214,7 +219,7 @@ public final class CalculatorActivityLauncher implements CalculatorEventListener App.getInstance().getUiThreadExecutor().execute(new Runnable() { @Override public void run() { - plotGraph(context, plotInput.getFunction(), plotInput.getConstant()); + plotGraph(context, plotInput.getFunction(), plotInput.getXVariable(), plotInput.getYVariable()); } }); break; diff --git a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java index 1e603087..80282a87 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/CalculatorApplication.java @@ -3,6 +3,7 @@ package org.solovyev.android.calculator; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; import net.robotmedia.billing.BillingController; @@ -65,8 +66,12 @@ public class CalculatorApplication extends android.app.Application implements Sh ********************************************************************** */ + @NotNull private final List listeners = new ArrayList(); + @NotNull + protected final Handler uiHandler = new Handler(); + /* ********************************************************************** * @@ -186,6 +191,11 @@ public class CalculatorApplication extends android.app.Application implements Sh return new CalculatorFragmentHelperImpl(layoutId, titleResId, listenersOnCreate); } + @NotNull + public Handler getUiHandler() { + return uiHandler; + } + /* ********************************************************************** * diff --git a/android-app/src/main/java/org/solovyev/android/calculator/about/CalculatorFragmentType.java b/android-app/src/main/java/org/solovyev/android/calculator/about/CalculatorFragmentType.java index 8163b697..2f48afdb 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/about/CalculatorFragmentType.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/about/CalculatorFragmentType.java @@ -13,6 +13,7 @@ import org.solovyev.android.calculator.math.edit.CalculatorFunctionsFragment; import org.solovyev.android.calculator.math.edit.CalculatorOperatorsFragment; import org.solovyev.android.calculator.math.edit.CalculatorVarsFragment; import org.solovyev.android.calculator.matrix.CalculatorMatrixEditFragment; +import org.solovyev.android.calculator.plot.CalculatorArityPlotFragment; import org.solovyev.android.calculator.plot.CalculatorPlotFragment; /** @@ -31,6 +32,7 @@ public enum CalculatorFragmentType { functions(CalculatorFunctionsFragment.class, R.layout.math_entities_fragment, R.string.c_functions), operators(CalculatorOperatorsFragment.class, R.layout.math_entities_fragment, R.string.c_operators), plotter(CalculatorPlotFragment.class, R.layout.plot_fragment, R.string.c_graph), + plotter_2(CalculatorArityPlotFragment.class, R.layout.plot_fragment, R.string.c_graph), about(CalculatorAboutFragment.class, R.layout.about_fragment, R.string.c_about), faq(CalculatorHelpFaqFragment.class, R.layout.help_faq_fragment, R.string.c_faq), hints(CalculatorHelpHintsFragment.class, R.layout.help_hints_fragment, R.string.c_hints), diff --git a/android-app/src/main/java/org/solovyev/android/calculator/plot/AbstractCalculatorPlotFragment.java b/android-app/src/main/java/org/solovyev/android/calculator/plot/AbstractCalculatorPlotFragment.java new file mode 100644 index 00000000..85112169 --- /dev/null +++ b/android-app/src/main/java/org/solovyev/android/calculator/plot/AbstractCalculatorPlotFragment.java @@ -0,0 +1,525 @@ +package org.solovyev.android.calculator.plot; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.support.v4.app.FragmentActivity; +import android.util.Log; +import android.view.View; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuInflater; +import com.actionbarsherlock.view.MenuItem; +import jscl.math.Expression; +import jscl.math.Generic; +import jscl.math.function.Constant; +import jscl.text.ParseException; +import org.achartengine.renderer.XYMultipleSeriesRenderer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.solovyev.android.calculator.*; +import org.solovyev.android.menu.ActivityMenu; +import org.solovyev.android.menu.IdentifiableMenuItem; +import org.solovyev.android.menu.ListActivityMenu; +import org.solovyev.android.sherlock.menu.SherlockMenuHelper; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +/** + * User: serso + * Date: 12/30/12 + * Time: 3:09 PM + */ +public abstract class AbstractCalculatorPlotFragment extends CalculatorFragment implements CalculatorEventListener { + + /* + ********************************************************************** + * + * CONSTANTS + * + ********************************************************************** + */ + + protected static final String TAG = "CalculatorPlotFragment"; + + public static final String INPUT = "plotter_input"; + + protected static final String PLOT_BOUNDARIES = "plot_boundaries"; + + private static final int DEFAULT_MIN_NUMBER = -10; + + private static final int DEFAULT_MAX_NUMBER = 10; + + /* + ********************************************************************** + * + * FIELDS + * + ********************************************************************** + */ + + @Nullable + private Input input; + + private int bgColor; + + // thread for applying UI changes + @NotNull + private final Handler uiHandler = new Handler(); + + @NotNull + private PreparedInput preparedInput; + + @NotNull + private ActivityMenu fragmentMenu = ListActivityMenu.fromResource(R.menu.plot_menu, PlotMenu.class, SherlockMenuHelper.getInstance()); + + // thread which calculated data for graph view + @NotNull + private final Executor plotExecutor = Executors.newSingleThreadExecutor(); + + @NotNull + private final CalculatorEventHolder lastEventHolder = new CalculatorEventHolder(CalculatorUtils.createFirstEventDataId()); + + + public AbstractCalculatorPlotFragment() { + super(CalculatorApplication.getInstance().createFragmentHelper(R.layout.plot_fragment, R.string.c_graph, false)); + } + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Bundle arguments = getArguments(); + + if (arguments != null) { + input = (CalculatorPlotFragment.Input) arguments.getSerializable(INPUT); + } + + if (input == null) { + this.bgColor = getResources().getColor(R.color.cpp_pane_background); + } else { + this.bgColor = getResources().getColor(android.R.color.transparent); + } + + setHasOptionsMenu(true); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + if (input == null) { + this.preparedInput = prepareInputFromDisplay(Locator.getInstance().getDisplay().getViewState(), savedInstanceState); + } else { + this.preparedInput = prepareInput(input, true, savedInstanceState); + } + } + + @Override + public void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + + final PlotBoundaries plotBoundaries = getPlotBoundaries(); + if (plotBoundaries != null) { + out.putSerializable(PLOT_BOUNDARIES, plotBoundaries); + } + } + + @Nullable + protected abstract PlotBoundaries getPlotBoundaries(); + + @Override + public void onResume() { + super.onResume(); + + createChart(preparedInput); + createGraphicalView(getView(), preparedInput); + } + + @Override + public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, @NotNull CalculatorEventType calculatorEventType, @Nullable final Object data) { + if (calculatorEventType.isOfType(CalculatorEventType.display_state_changed)) { + PreparedInput preparedInput = getPreparedInput(); + if (!preparedInput.isFromInputArgs()) { + + final CalculatorEventHolder.Result result = this.lastEventHolder.apply(calculatorEventData); + if (result.isNewAfter()) { + preparedInput = prepareInputFromDisplay(((CalculatorDisplayChangeEventData) data).getNewValue(), null); + this.preparedInput = preparedInput; + + + final PreparedInput finalPreparedInput = preparedInput; + getUiHandler().post(new Runnable() { + @Override + public void run() { + + if (!finalPreparedInput.isError()) { + createChart(finalPreparedInput); + + final View view = getView(); + if (view != null) { + createGraphicalView(view, finalPreparedInput); + } + } else { + onError(); + } + } + }); + } + + } + } + } + + protected abstract void onError(); + + protected abstract void createGraphicalView(@NotNull View view, @NotNull PreparedInput preparedInput); + + protected abstract void createChart(@NotNull PreparedInput preparedInput); + + + protected double getMaxValue(@Nullable PlotBoundaries plotBoundaries) { + return plotBoundaries == null ? DEFAULT_MAX_NUMBER : plotBoundaries.getXMax(); + } + + protected double getMinValue(@Nullable PlotBoundaries plotBoundaries) { + return plotBoundaries == null ? DEFAULT_MIN_NUMBER : plotBoundaries.getXMin(); + } + + /* + ********************************************************************** + * + * GETTERS + * + ********************************************************************** + */ + + @NotNull + public Handler getUiHandler() { + return uiHandler; + } + + @NotNull + public PreparedInput getPreparedInput() { + return preparedInput; + } + + public int getBgColor() { + return bgColor; + } + + @NotNull + public Executor getPlotExecutor() { + return plotExecutor; + } + + /* + ********************************************************************** + * + * MENU + * + ********************************************************************** + */ + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + + final FragmentActivity activity = this.getActivity(); + if (activity != null) { + fragmentMenu.onCreateOptionsMenu(activity, menu); + } + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + final FragmentActivity activity = this.getActivity(); + if (activity != null) { + fragmentMenu.onPrepareOptionsMenu(activity, menu); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + return super.onOptionsItemSelected(item) || fragmentMenu.onOptionsItemSelected(this.getActivity(), item); + } + + /* + ********************************************************************** + * + * STATIC + * + ********************************************************************** + */ + + @NotNull + protected static PreparedInput prepareInputFromDisplay(@NotNull CalculatorDisplayViewState displayState, @Nullable Bundle savedInstanceState) { + try { + if (displayState.isValid() && displayState.getResult() != null) { + final Generic expression = displayState.getResult(); + if (CalculatorUtils.isPlotPossible(expression, displayState.getOperation())) { + final List variables = new ArrayList(CalculatorUtils.getNotSystemConstants(expression)); + final Constant xVariable = variables.get(0); + + final Constant yVariable; + if ( variables.size() > 1 ) { + yVariable = variables.get(1); + } else { + yVariable = null; + } + + final Input input = new Input(expression.toString(), xVariable.getName(), yVariable == null ? null : yVariable.getName()); + return prepareInput(input, false, savedInstanceState); + } + } + } catch (RuntimeException e) { + Log.e(TAG, e.getLocalizedMessage(), e); + } + + return PreparedInput.newErrorInstance(false); + } + + @NotNull + private static PreparedInput prepareInput(@NotNull Input input, boolean fromInputArgs, @Nullable Bundle savedInstanceState) { + PreparedInput result; + + try { + final PreparedExpression preparedExpression = ToJsclTextProcessor.getInstance().process(input.getExpression()); + final Generic expression = Expression.valueOf(preparedExpression.getExpression()); + final Constant xVar = new Constant(input.getXVariableName()); + + final Constant yVar; + if (input.getYVariableName() != null) { + yVar = new Constant(input.getYVariableName()); + } else { + yVar = null; + } + + PlotBoundaries plotBoundaries = null; + if (savedInstanceState != null) { + plotBoundaries = (PlotBoundaries) savedInstanceState.getSerializable(PLOT_BOUNDARIES); + } + + if ( plotBoundaries == null ) { + plotBoundaries = PlotBoundaries.newDefaultInstance(); + } + + result = PreparedInput.newInstance(input, expression, xVar, yVar, fromInputArgs, plotBoundaries); + } catch (ParseException e) { + result = PreparedInput.newErrorInstance(fromInputArgs); + Locator.getInstance().getNotifier().showMessage(e); + } catch (CalculatorParseException e) { + result = PreparedInput.newErrorInstance(fromInputArgs); + Locator.getInstance().getNotifier().showMessage(e); + } + + return result; + } + + private static enum PlotMenu implements IdentifiableMenuItem { + + preferences(R.id.menu_plot_settings) { + @Override + public void onClick(@NotNull MenuItem data, @NotNull Context context) { + context.startActivity(new Intent(context, CalculatorPlotPreferenceActivity.class)); + } + }; + + private final int itemId; + + private PlotMenu(int itemId) { + this.itemId = itemId; + } + + + @NotNull + @Override + public Integer getItemId() { + return itemId; + } + } + + public static final class PlotBoundaries implements Serializable { + + private double xMin; + private double xMax; + private double yMin; + private double yMax; + + public PlotBoundaries() { + } + + public PlotBoundaries(@NotNull XYMultipleSeriesRenderer renderer) { + this.xMin = renderer.getXAxisMin(); + this.yMin = renderer.getYAxisMin(); + this.xMax = renderer.getXAxisMax(); + this.yMax = renderer.getYAxisMax(); + } + + public double getXMin() { + return xMin; + } + + public double getXMax() { + return xMax; + } + + public double getYMin() { + return yMin; + } + + public double getYMax() { + return yMax; + } + + @Override + public String toString() { + return "PlotBoundaries{" + + "yMax=" + yMax + + ", yMin=" + yMin + + ", xMax=" + xMax + + ", xMin=" + xMin + + '}'; + } + + @NotNull + public static PlotBoundaries newDefaultInstance() { + PlotBoundaries plotBoundaries = new PlotBoundaries(); + plotBoundaries.xMin = DEFAULT_MIN_NUMBER; + plotBoundaries.yMin = DEFAULT_MIN_NUMBER; + plotBoundaries.xMax = DEFAULT_MAX_NUMBER; + plotBoundaries.yMax = DEFAULT_MAX_NUMBER; + return plotBoundaries; + } + } + + public static class PreparedInput { + + @Nullable + private Input input; + + @Nullable + private Generic expression; + + @Nullable + private Constant xVariable; + + @Nullable + private Constant yVariable; + + private boolean fromInputArgs; + + @NotNull + private PlotBoundaries plotBoundaries = PlotBoundaries.newDefaultInstance(); + + private PreparedInput() { + } + + @NotNull + public static PreparedInput newInstance(@NotNull Input input, + @NotNull Generic expression, + @NotNull Constant xVariable, + @Nullable Constant yVariable, + boolean fromInputArgs, + @NotNull PlotBoundaries plotBoundaries) { + final PreparedInput result = new PreparedInput(); + + result.input = input; + result.expression = expression; + result.xVariable = xVariable; + result.yVariable = yVariable; + result.fromInputArgs = fromInputArgs; + result.plotBoundaries = plotBoundaries; + + return result; + } + + @NotNull + public static PreparedInput newErrorInstance(boolean fromInputArgs) { + final PreparedInput result = new PreparedInput(); + + result.input = null; + result.expression = null; + result.xVariable = null; + result.yVariable = null; + result.fromInputArgs = fromInputArgs; + + return result; + } + + public boolean isFromInputArgs() { + return fromInputArgs; + } + + @Nullable + public Input getInput() { + return input; + } + + @Nullable + public Generic getExpression() { + return expression; + } + + @NotNull + public PlotBoundaries getPlotBoundaries() { + return plotBoundaries; + } + + @Nullable + public Constant getXVariable() { + return xVariable; + } + + @Nullable + public Constant getYVariable() { + return yVariable; + } + + public boolean isError() { + return input == null || expression == null || xVariable == null; + } + } + + public static class Input implements Serializable { + + @NotNull + private String expression; + + @NotNull + private String xVariableName; + + @Nullable + private String yVariableName; + + public Input(@NotNull String expression, + @NotNull String xVariableName, + @Nullable String yVariableName) { + this.expression = expression; + this.xVariableName = xVariableName; + this.yVariableName = yVariableName; + } + + @NotNull + public String getExpression() { + return expression; + } + + @NotNull + public String getXVariableName() { + return xVariableName; + } + + @Nullable + public String getYVariableName() { + return yVariableName; + } + } +} diff --git a/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorArityPlotFragment.java b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorArityPlotFragment.java new file mode 100644 index 00000000..7ee927ea --- /dev/null +++ b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorArityPlotFragment.java @@ -0,0 +1,131 @@ +package org.solovyev.android.calculator.plot; + +import android.view.View; +import android.view.ViewGroup; +import arity.calculator.Graph2dView; +import arity.calculator.Graph3dView; +import arity.calculator.GraphView; +import jscl.math.Generic; +import jscl.math.function.Constant; +import org.javia.arity.Complex; +import org.javia.arity.Function; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.solovyev.android.calculator.R; + +import java.util.ArrayList; +import java.util.List; + +/** + * User: serso + * Date: 12/30/12 + * Time: 4:43 PM + */ +public class CalculatorArityPlotFragment extends AbstractCalculatorPlotFragment { + + @Nullable + private GraphView graphView; + + @Nullable + @Override + protected PlotBoundaries getPlotBoundaries() { + if ( graphView != null ) { + // todo serso: return plot boundaries + return null; + } else { + return null; + } + } + + @Override + protected void createGraphicalView(@NotNull View root, @NotNull PreparedInput preparedInput) { + // remove old + final ViewGroup graphContainer = (ViewGroup) root.findViewById(R.id.main_fragment_layout); + + if (graphView instanceof View) { + graphContainer.removeView((View) graphView); + } + + if (!preparedInput.isError()) { + final Generic expression = preparedInput.getExpression(); + final Constant xVariable = preparedInput.getXVariable(); + final Constant yVariable = preparedInput.getYVariable(); + + final int arity = yVariable == null ? 1 : 2; + + final List functions = new ArrayList(); + functions.add(new Function() { + @Override + public int arity() { + return arity; + } + + @Override + public double eval(double x) { + return PlotUtils.calculatorExpression(expression, xVariable, x).realPart(); + } + + @Override + public double eval(double x, double y) { + return PlotUtils.calculatorExpression(expression, xVariable, x, yVariable, y).realPart(); + } + + @Override + public Complex eval(Complex x) { + jscl.math.numeric.Complex result = PlotUtils.calculatorExpression(expression, xVariable, x.re); + return new Complex(result.realPart(), result.imaginaryPart()); + } + + @Override + public Complex eval(Complex x, Complex y) { + jscl.math.numeric.Complex result = PlotUtils.calculatorExpression(expression, xVariable, x.re, yVariable, y.re); + return new Complex(result.realPart(), result.imaginaryPart()); + } + }); + + if (functions.size() == 1) { + final Function f = functions.get(0); + graphView = f.arity() == 1 ? new Graph2dView(getActivity()) : new Graph3dView(getActivity()); + graphView.setFunction(f); + } else { + graphView = new Graph2dView(this.getActivity()); + ((Graph2dView) graphView).setFunctions(functions); + } + + graphContainer.addView((View) graphView); + } else { + onError(); + } + } + + @Override + protected void createChart(@NotNull PreparedInput preparedInput) { + } + + + @Override + public void onResume() { + super.onResume(); + if (this.graphView != null) { + this.graphView.onResume(); + } + } + + @Override + protected void onError() { + final View root = getView(); + if (root != null && graphView instanceof View) { + final ViewGroup graphContainer = (ViewGroup) root.findViewById(R.id.main_fragment_layout); + graphContainer.removeView((View) graphView); + } + this.graphView = null; + } + + @Override + public void onPause() { + super.onPause(); + if (this.graphView != null) { + this.graphView.onPause(); + } + } +} diff --git a/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotActivity.java b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotActivity.java index 439f2543..ceaaadd4 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotActivity.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotActivity.java @@ -3,6 +3,7 @@ package org.solovyev.android.calculator.plot; import android.app.ActionBar; import android.content.Intent; import android.os.Bundle; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.solovyev.android.calculator.CalculatorFragmentActivity; import org.solovyev.android.calculator.R; @@ -29,6 +30,11 @@ public class CalculatorPlotActivity extends CalculatorFragmentActivity { } getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD); - getActivityHelper().setFragment(this, CalculatorFragmentType.plotter, arguments, R.id.main_layout); + getActivityHelper().setFragment(this, getPlotterFragmentType(), arguments, R.id.main_layout); + } + + @NotNull + public static CalculatorFragmentType getPlotterFragmentType() { + return CalculatorFragmentType.plotter_2; } } diff --git a/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotFragment.java b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotFragment.java index cbb0996b..610550ab 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotFragment.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/plot/CalculatorPlotFragment.java @@ -6,23 +6,12 @@ package org.solovyev.android.calculator.plot; -import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Handler; import android.preference.PreferenceManager; -import android.support.v4.app.FragmentActivity; -import android.util.Log; import android.view.View; import android.view.ViewGroup; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; -import jscl.math.Expression; import jscl.math.Generic; import jscl.math.function.Constant; -import jscl.text.ParseException; import org.achartengine.chart.XYChart; import org.achartengine.model.XYMultipleSeriesDataset; import org.achartengine.model.XYSeries; @@ -32,47 +21,16 @@ import org.achartengine.tools.ZoomEvent; import org.achartengine.tools.ZoomListener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.solovyev.android.calculator.CalculatorApplication; -import org.solovyev.android.calculator.CalculatorDisplayChangeEventData; -import org.solovyev.android.calculator.CalculatorDisplayViewState; -import org.solovyev.android.calculator.CalculatorEventData; -import org.solovyev.android.calculator.CalculatorEventHolder; -import org.solovyev.android.calculator.CalculatorEventListener; -import org.solovyev.android.calculator.CalculatorEventType; -import org.solovyev.android.calculator.CalculatorFragment; -import org.solovyev.android.calculator.CalculatorParseException; import org.solovyev.android.calculator.CalculatorPreferences; -import org.solovyev.android.calculator.CalculatorUtils; -import org.solovyev.android.calculator.Locator; -import org.solovyev.android.calculator.PreparedExpression; import org.solovyev.android.calculator.R; -import org.solovyev.android.calculator.ToJsclTextProcessor; -import org.solovyev.android.menu.ActivityMenu; -import org.solovyev.android.menu.IdentifiableMenuItem; -import org.solovyev.android.menu.ListActivityMenu; -import org.solovyev.android.sherlock.menu.SherlockMenuHelper; import org.solovyev.common.MutableObject; -import org.solovyev.common.collections.CollectionsUtils; - -import java.io.Serializable; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; /** * User: serso * Date: 12/1/11 * Time: 12:40 AM */ -public class CalculatorPlotFragment extends CalculatorFragment implements CalculatorEventListener { - - private static final String TAG = CalculatorPlotFragment.class.getSimpleName(); - - private static final int DEFAULT_MIN_NUMBER = -10; - - private static final int DEFAULT_MAX_NUMBER = 10; - - public static final String INPUT = "plotter_input"; - private static final String PLOT_BOUNDARIES = "plot_boundaries"; +public class CalculatorPlotFragment extends AbstractCalculatorPlotFragment { public static final long EVAL_DELAY_MILLIS = 200; @@ -85,153 +43,42 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul @Nullable private MyGraphicalView graphicalView; - // thread which calculated data for graph view - @NotNull - private final Executor plotExecutor = Executors.newSingleThreadExecutor(); + protected void createChart(@NotNull PreparedInput preparedInput) { + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); + final Boolean interpolate = CalculatorPreferences.Graph.interpolate.getPreference(preferences); + final GraphLineColor realLineColor = CalculatorPreferences.Graph.lineColorReal.getPreference(preferences); + final GraphLineColor imagLineColor = CalculatorPreferences.Graph.lineColorImag.getPreference(preferences); - // thread for applying UI changes - @NotNull - private final Handler uiHandler = new Handler(); - - @NotNull - private PreparedInput preparedInput; + //noinspection ConstantConditions + try { + this.chart = PlotUtils.prepareChart(getMinValue(null), getMaxValue(null), preparedInput.getExpression(), preparedInput.getXVariable(), getBgColor(), interpolate, realLineColor.getColor(), imagLineColor.getColor()); + } catch (ArithmeticException e) { + PlotUtils.handleArithmeticException(e, CalculatorPlotFragment.this); + } + } @Nullable - private Input input; - - @NotNull - private final CalculatorEventHolder lastEventHolder = new CalculatorEventHolder(CalculatorUtils.createFirstEventDataId()); - - private int bgColor; - - @NotNull - private ActivityMenu fragmentMenu = ListActivityMenu.fromResource(R.menu.plot_menu, PlotMenu.class, SherlockMenuHelper.getInstance()); - - public CalculatorPlotFragment() { - super(CalculatorApplication.getInstance().createFragmentHelper(R.layout.plot_fragment, R.string.c_graph, false)); - } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - final Bundle arguments = getArguments(); - - if (arguments != null) { - input = (Input) arguments.getSerializable(INPUT); - } - - if (input == null) { - this.bgColor = getResources().getColor(R.color.cpp_pane_background); - } else { - this.bgColor = getResources().getColor(android.R.color.transparent); - } - - setHasOptionsMenu(true); - } - - @NotNull - private static PreparedInput prepareInputFromDisplay(@NotNull CalculatorDisplayViewState displayState, @Nullable Bundle savedInstanceState) { - try { - if (displayState.isValid() && displayState.getResult() != null) { - final Generic expression = displayState.getResult(); - if (CalculatorUtils.isPlotPossible(expression, displayState.getOperation())) { - final Constant constant = CollectionsUtils.getFirstCollectionElement(CalculatorUtils.getNotSystemConstants(expression)); - - final Input input = new Input(expression.toString(), constant.getName()); - return prepareInput(input, false, savedInstanceState); - } - } - } catch (RuntimeException e) { - Log.e(TAG, e.getLocalizedMessage(), e); - } - - return PreparedInput.newErrorInstance(false); - } - - @NotNull - private static PreparedInput prepareInput(@NotNull Input input, boolean fromInputArgs, @Nullable Bundle savedInstanceState) { - PreparedInput result; - - try { - final PreparedExpression preparedExpression = ToJsclTextProcessor.getInstance().process(input.getExpression()); - final Generic expression = Expression.valueOf(preparedExpression.getExpression()); - final Constant variable = new Constant(input.getVariableName()); - - PlotBoundaries plotBoundaries = null; - if ( savedInstanceState != null ) { - plotBoundaries = (PlotBoundaries)savedInstanceState.getSerializable(PLOT_BOUNDARIES); - } - - result = PreparedInput.newInstance(input, expression, variable, fromInputArgs, plotBoundaries); - } catch (ParseException e) { - result = PreparedInput.newErrorInstance(fromInputArgs); - Locator.getInstance().getNotifier().showMessage(e); - } catch (CalculatorParseException e) { - result = PreparedInput.newErrorInstance(fromInputArgs); - Locator.getInstance().getNotifier().showMessage(e); - } - - return result; - } - - private void createChart() { - if (!preparedInput.isError()) { - final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); - final Boolean interpolate = CalculatorPreferences.Graph.interpolate.getPreference(preferences); - final GraphLineColor realLineColor = CalculatorPreferences.Graph.lineColorReal.getPreference(preferences); - final GraphLineColor imagLineColor = CalculatorPreferences.Graph.lineColorImag.getPreference(preferences); - - //noinspection ConstantConditions - try { - this.chart = PlotUtils.prepareChart(getMinValue(null), getMaxValue(null), preparedInput.getExpression(), preparedInput.getVariable(), bgColor, interpolate, realLineColor.getColor(), imagLineColor.getColor()); - } catch (ArithmeticException e) { - PlotUtils.handleArithmeticException(e, CalculatorPlotFragment.this); - } - } else { - onError(); - } - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - - if (input == null) { - this.preparedInput = prepareInputFromDisplay(Locator.getInstance().getDisplay().getViewState(), savedInstanceState); - } else { - this.preparedInput = prepareInput(input, true, savedInstanceState); - } - } - - @Override - public void onSaveInstanceState(Bundle out) { - super.onSaveInstanceState(out); - + protected PlotBoundaries getPlotBoundaries() { if (chart != null) { - out.putSerializable(PLOT_BOUNDARIES, new PlotBoundaries(chart.getRenderer())); + return new PlotBoundaries(chart.getRenderer()); + } else { + return null; } } - @Override - public void onResume() { - super.onResume(); - - createChart(); - createGraphicalView(getView(), this.preparedInput.getPlotBoundaries()); - } - - private void createGraphicalView(@NotNull View root, @Nullable PlotBoundaries plotBoundaries) { + protected void createGraphicalView(@NotNull View root, @Nullable PreparedInput preparedInput) { final ViewGroup graphContainer = (ViewGroup) root.findViewById(R.id.main_fragment_layout); if (graphicalView != null) { graphContainer.removeView(graphicalView); } - if (!preparedInput.isError()) { + if (!getPreparedInput().isError()) { final XYChart chart = this.chart; assert chart != null; + final PlotBoundaries plotBoundaries = preparedInput.getPlotBoundaries(); double minValue = getMinValue(plotBoundaries); double maxValue = getMaxValue(plotBoundaries); @@ -249,20 +96,20 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul maxY = Math.max(maxY, series.getMaxY()); } - if (plotBoundaries == null) { + if (preparedInput == null) { chart.getRenderer().setXAxisMin(Math.max(minX, minValue)); chart.getRenderer().setYAxisMin(Math.max(minY, minValue)); chart.getRenderer().setXAxisMax(Math.min(maxX, maxValue)); chart.getRenderer().setYAxisMax(Math.min(maxY, maxValue)); } else { - chart.getRenderer().setXAxisMin(plotBoundaries.xMin); - chart.getRenderer().setYAxisMin(plotBoundaries.yMin); - chart.getRenderer().setXAxisMax(plotBoundaries.xMax); - chart.getRenderer().setYAxisMax(plotBoundaries.yMax); + chart.getRenderer().setXAxisMin(plotBoundaries.getXMin()); + chart.getRenderer().setYAxisMin(plotBoundaries.getYMin()); + chart.getRenderer().setXAxisMax(plotBoundaries.getXMax()); + chart.getRenderer().setYAxisMax(plotBoundaries.getYMax()); } graphicalView = new MyGraphicalView(this.getActivity(), chart); - graphicalView.setBackgroundColor(this.bgColor); + graphicalView.setBackgroundColor(this.getBgColor()); graphicalView.addZoomListener(new ZoomListener() { @Override @@ -292,14 +139,6 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul } - private double getMaxValue(@Nullable PlotBoundaries plotBoundaries) { - return plotBoundaries == null ? DEFAULT_MAX_NUMBER : plotBoundaries.xMax; - } - - private double getMinValue(@Nullable PlotBoundaries plotBoundaries) { - return plotBoundaries == null ? DEFAULT_MIN_NUMBER : plotBoundaries.xMin; - } - private void updateDataSets(@NotNull final XYChart chart) { updateDataSets(chart, EVAL_DELAY_MILLIS); @@ -309,10 +148,13 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this.getActivity()); final GraphLineColor imagLineColor = CalculatorPreferences.Graph.lineColorImag.getPreference(preferences); - final Generic expression = preparedInput.getExpression(); - final Constant variable = preparedInput.getVariable(); + final PreparedInput preparedInput = getPreparedInput(); - if (expression != null && variable != null) { + final Generic expression = preparedInput.getExpression(); + final Constant variable = preparedInput.getXVariable(); + final MyGraphicalView graphicalView = this.graphicalView; + + if (expression != null && variable != null && graphicalView != null) { pendingOperation.setObject(new Runnable() { @Override public void run() { @@ -321,43 +163,43 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul //lock all operations with history if (pendingOperation.getObject() == this) { - plotExecutor.execute(new Runnable() { + getPlotExecutor().execute(new Runnable() { @Override public void run() { - final XYMultipleSeriesRenderer dr = chart.getRenderer(); + final XYMultipleSeriesRenderer dr = chart.getRenderer(); - final XYMultipleSeriesDataset dataset = chart.getDataset(); - if (dataset != null && dr != null) { - final MyXYSeries realSeries = (MyXYSeries) dataset.getSeriesAt(0); + final XYMultipleSeriesDataset dataset = chart.getDataset(); + if (dataset != null && dr != null) { + final MyXYSeries realSeries = (MyXYSeries) dataset.getSeriesAt(0); - if (realSeries != null) { - final MyXYSeries imagSeries; - if (dataset.getSeriesCount() > 1) { - imagSeries = (MyXYSeries) dataset.getSeriesAt(1); - } else { - imagSeries = new MyXYSeries(PlotUtils.getImagFunctionName(variable), PlotUtils.DEFAULT_NUMBER_OF_STEPS * 2); - } + if (realSeries != null) { + final MyXYSeries imagSeries; + if (dataset.getSeriesCount() > 1) { + imagSeries = (MyXYSeries) dataset.getSeriesAt(1); + } else { + imagSeries = new MyXYSeries(PlotUtils.getImagFunctionName(variable), PlotUtils.DEFAULT_NUMBER_OF_STEPS * 2); + } - try { - if (PlotUtils.addXY(dr.getXAxisMin(), dr.getXAxisMax(), expression, variable, realSeries, imagSeries, true, PlotUtils.DEFAULT_NUMBER_OF_STEPS)) { - if (dataset.getSeriesCount() <= 1) { - dataset.addSeries(imagSeries); - dr.addSeriesRenderer(PlotUtils.createImagRenderer(imagLineColor.getColor())); - } - } - } catch (ArithmeticException e) { - PlotUtils.handleArithmeticException(e, CalculatorPlotFragment.this); - } + try { + if (PlotUtils.addXY(dr.getXAxisMin(), dr.getXAxisMax(), expression, variable, realSeries, imagSeries, true, PlotUtils.DEFAULT_NUMBER_OF_STEPS)) { + if (dataset.getSeriesCount() <= 1) { + dataset.addSeries(imagSeries); + dr.addSeriesRenderer(PlotUtils.createImagRenderer(imagLineColor.getColor())); + } + } + } catch (ArithmeticException e) { + PlotUtils.handleArithmeticException(e, CalculatorPlotFragment.this); + } - uiHandler.post(new Runnable() { - @Override - public void run() { - graphicalView.repaint(); - } - }); - } - } - } + getUiHandler().post(new Runnable() { + @Override + public void run() { + graphicalView.repaint(); + } + }); + } + } + } }); } } @@ -366,37 +208,12 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul } - uiHandler.postDelayed(pendingOperation.getObject(), millisToWait); + getUiHandler().postDelayed(pendingOperation.getObject(), millisToWait); } @NotNull private final MutableObject pendingOperation = new MutableObject(); - @Override - public void onCalculatorEvent(@NotNull CalculatorEventData calculatorEventData, @NotNull CalculatorEventType calculatorEventType, @Nullable final Object data) { - if (calculatorEventType.isOfType(CalculatorEventType.display_state_changed)) { - if (!preparedInput.isFromInputArgs()) { - - final CalculatorEventHolder.Result result = this.lastEventHolder.apply(calculatorEventData); - if (result.isNewAfter()) { - this.preparedInput = prepareInputFromDisplay(((CalculatorDisplayChangeEventData) data).getNewValue(), null); - createChart(); - - uiHandler.post(new Runnable() { - @Override - public void run() { - final View view = getView(); - if (view != null) { - createGraphicalView(view, preparedInput.getPlotBoundaries()); - } - } - }); - } - - } - } - } - /* public void zoomInClickHandler(@NotNull View v) { this.graphicalView.zoomIn(); @@ -406,196 +223,8 @@ public class CalculatorPlotFragment extends CalculatorFragment implements Calcul this.graphicalView.zoomOut(); }*/ - /* - ********************************************************************** - * - * MENU - * - ********************************************************************** - */ - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - super.onCreateOptionsMenu(menu, inflater); - - final FragmentActivity activity = this.getActivity(); - if (activity != null) { - fragmentMenu.onCreateOptionsMenu(activity, menu); - } - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - super.onPrepareOptionsMenu(menu); - - final FragmentActivity activity = this.getActivity(); - if (activity != null) { - fragmentMenu.onPrepareOptionsMenu(activity, menu); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - return super.onOptionsItemSelected(item) || fragmentMenu.onOptionsItemSelected(this.getActivity(), item); - } public void onError() { this.chart = null; } - - /* - ********************************************************************** - * - * STATIC - * - ********************************************************************** - */ - - private static enum PlotMenu implements IdentifiableMenuItem { - - preferences(R.id.menu_plot_settings) { - @Override - public void onClick(@NotNull MenuItem data, @NotNull Context context) { - context.startActivity(new Intent(context, CalculatorPlotPreferenceActivity.class)); - } - }; - - private final int itemId; - - private PlotMenu(int itemId) { - this.itemId = itemId; - } - - - @NotNull - @Override - public Integer getItemId() { - return itemId; - } - } - - public static final class PlotBoundaries implements Serializable { - - private double xMin; - private double xMax; - private double yMin; - private double yMax; - - public PlotBoundaries() { - } - - public PlotBoundaries(@NotNull XYMultipleSeriesRenderer renderer) { - this.xMin = renderer.getXAxisMin(); - this.yMin = renderer.getYAxisMin(); - this.xMax = renderer.getXAxisMax(); - this.yMax = renderer.getYAxisMax(); - } - - @Override - public String toString() { - return "PlotBoundaries{" + - "yMax=" + yMax + - ", yMin=" + yMin + - ", xMax=" + xMax + - ", xMin=" + xMin + - '}'; - } - } - - public static class PreparedInput { - - @Nullable - private Input input; - - @Nullable - private Generic expression; - - @Nullable - private Constant variable; - - private boolean fromInputArgs; - - @Nullable - private PlotBoundaries plotBoundaries = null; - - private PreparedInput() { - } - - @NotNull - public static PreparedInput newInstance(@NotNull Input input, @NotNull Generic expression, @NotNull Constant variable, boolean fromInputArgs, @Nullable PlotBoundaries plotBoundaries) { - final PreparedInput result = new PreparedInput(); - - result.input = input; - result.expression = expression; - result.variable = variable; - result.fromInputArgs = fromInputArgs; - result.plotBoundaries = plotBoundaries; - - return result; - } - - @NotNull - public static PreparedInput newErrorInstance(boolean fromInputArgs) { - final PreparedInput result = new PreparedInput(); - - result.input = null; - result.expression = null; - result.variable = null; - result.fromInputArgs = fromInputArgs; - - return result; - } - - public boolean isFromInputArgs() { - return fromInputArgs; - } - - @Nullable - public Input getInput() { - return input; - } - - @Nullable - public Generic getExpression() { - return expression; - } - - @Nullable - public PlotBoundaries getPlotBoundaries() { - return plotBoundaries; - } - - @Nullable - public Constant getVariable() { - return variable; - } - - public boolean isError() { - return input == null || expression == null || variable == null; - } - } - - 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/android-app/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java b/android-app/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java index c531ec99..7e9c8dfb 100644 --- a/android-app/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java +++ b/android-app/src/main/java/org/solovyev/android/calculator/plot/PlotUtils.java @@ -202,7 +202,7 @@ public final class PlotUtils { return renderer; } - static void handleArithmeticException(@NotNull ArithmeticException e, @NotNull CalculatorPlotFragment calculatorPlotFragment) { + static void handleArithmeticException(@NotNull ArithmeticException e, @NotNull AbstractCalculatorPlotFragment calculatorPlotFragment) { String message = e.getLocalizedMessage(); if (StringUtils.isEmpty(message)) { message = e.getMessage(); @@ -371,9 +371,20 @@ public final class PlotUtils { } @NotNull - public static Complex calculatorExpression(@NotNull Generic expression, @NotNull Constant variable, double x) { + public static Complex calculatorExpression(@NotNull Generic expression, @NotNull Constant xVar, double x) { try { - return unwrap(expression.substitute(variable, Expression.valueOf(x)).numeric()); + return unwrap(expression.substitute(xVar, Expression.valueOf(x)).numeric()); + } catch (RuntimeException e) { + return NaN; + } + } + + @NotNull + public static Complex calculatorExpression(@NotNull Generic expression, @NotNull Constant xVar, double x, @NotNull Constant yVar, double y) { + try { + Generic tmp = expression.substitute(xVar, Expression.valueOf(x)); + tmp = tmp.substitute(yVar, Expression.valueOf(y)); + return unwrap(tmp.numeric()); } catch (RuntimeException e) { return NaN; } diff --git a/core/src/main/java/org/solovyev/android/calculator/CalculatorUtils.java b/core/src/main/java/org/solovyev/android/calculator/CalculatorUtils.java index c33552ad..cd30ac70 100644 --- a/core/src/main/java/org/solovyev/android/calculator/CalculatorUtils.java +++ b/core/src/main/java/org/solovyev/android/calculator/CalculatorUtils.java @@ -45,7 +45,8 @@ public final class CalculatorUtils { boolean result = false; if (operation == JsclOperation.simplify) { - if (getNotSystemConstants(expression).size() == 1) { + int size = getNotSystemConstants(expression).size(); + if (size == 1 || size == 2) { result = true; } }