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 00000000..ec3f021d Binary files /dev/null and b/android-app/misc/lib/arity-2.1.6.jar differ 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; } }