├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── me
│ │ └── scai
│ │ ├── handwriting
│ │ ├── AbstractToken.java
│ │ ├── CAbstractWrittenTokenSet.java
│ │ ├── CHandWritingTokenImageData.java
│ │ ├── CStroke.java
│ │ ├── CWrittenToken.java
│ │ ├── CWrittenTokenSet.java
│ │ ├── CWrittenTokenSetNoStroke.java
│ │ ├── HandwritingEngineUserAction.java
│ │ ├── NodeToken.java
│ │ ├── Rectangle.java
│ │ ├── StateStack.java
│ │ ├── StrokeCurator.java
│ │ ├── StrokeCuratorConfig.java
│ │ ├── StrokeCuratorConfigurable.java
│ │ ├── StrokeCuratorState.java
│ │ ├── StrokeCuratorUserAction.java
│ │ ├── TokenDegeneracy.java
│ │ ├── TokenRecogEngine.java
│ │ ├── TokenRecogEngineIMG.java
│ │ ├── TokenRecogEngineSDV.java
│ │ ├── TokenRecogOutput.java
│ │ ├── TokenUuidUtils.java
│ │ ├── ml
│ │ │ ├── DataSet.java
│ │ │ ├── DataSetWithStringLabels.java
│ │ │ ├── MachineLearningHelper.java
│ │ │ ├── README.md
│ │ │ └── TrainTokenRecogEngineSDV.java
│ │ ├── remote
│ │ │ ├── TokenRecogRemoteEngine.java
│ │ │ ├── TokenRecogRemoteEngineException.java
│ │ │ └── TokenRecogRemoteEngineImpl.java
│ │ ├── tokens
│ │ │ ├── TokenFileSettings.java
│ │ │ └── TokenSettings.java
│ │ └── utils
│ │ │ ├── DataIOHelper.java
│ │ │ └── LimitedStack.java
│ │ ├── parsetree
│ │ ├── GeometricShortcut.java
│ │ ├── GraphicalProduction.java
│ │ ├── GraphicalProductionSet.java
│ │ ├── HandwritingEngineException.java
│ │ ├── ITokenSetParser.java
│ │ ├── MathHelper.java
│ │ ├── Node.java
│ │ ├── NodeAnalyzer.java
│ │ ├── ParseTreeBiaser.java
│ │ ├── ParseTreeMathTexifier.java
│ │ ├── ParseTreeMatrixProcessor.java
│ │ ├── ParseTreeStringizer.java
│ │ ├── TerminalSet.java
│ │ ├── TextHelper.java
│ │ ├── TokenSet2NodeTokenParser.java
│ │ ├── TokenSetParser.java
│ │ ├── TokenSetParserException.java
│ │ ├── TokenSetParserOutput.java
│ │ ├── evaluation
│ │ │ ├── ArgumentRange.java
│ │ │ ├── DefiniteIntegralTerm.java
│ │ │ ├── EvaluatorHelper.java
│ │ │ ├── FunctionArgumentList.java
│ │ │ ├── FunctionSigmaPiIntegralTerm.java
│ │ │ ├── FunctionTerm.java
│ │ │ ├── ParseTreeEvaluator.java
│ │ │ ├── ParseTreeEvaluatorHelper.java
│ │ │ ├── PiTerm.java
│ │ │ ├── PlatoVarMap.java
│ │ │ ├── SigmaPiIntegralTerm.java
│ │ │ ├── SigmaTerm.java
│ │ │ ├── Undefined.java
│ │ │ ├── UniformArgumentRange.java
│ │ │ ├── UniformIntegralArgumentRange.java
│ │ │ ├── ValueUnion.java
│ │ │ ├── exceptions
│ │ │ │ ├── DivisionByZeroException.java
│ │ │ │ ├── InvalidArgumentForMatrixOperation.java
│ │ │ │ ├── LogarithmOfNonPositiveException.java
│ │ │ │ ├── ParseTreeEvaluatorException.java
│ │ │ │ ├── ParseTreeMathException.java
│ │ │ │ ├── SquareRootOfNegativeException.java
│ │ │ │ ├── UndefinedFunctionException.java
│ │ │ │ ├── UnexpectedTypeException.java
│ │ │ │ ├── UnsupportedMatrixExponentException.java
│ │ │ │ └── ZeroToZerothPowerException.java
│ │ │ ├── matrix
│ │ │ │ └── MatrixHelper.java
│ │ │ └── program
│ │ │ │ └── ProgramKeyword.java
│ │ ├── geometry
│ │ │ ├── AlignRelation.java
│ │ │ ├── GeometricRelation.java
│ │ │ ├── GeometryHelper.java
│ │ │ ├── HeightRelation.java
│ │ │ ├── NodeInternalGeometry.java
│ │ │ ├── PositionRelation.java
│ │ │ ├── SpacingRelation.java
│ │ │ └── WidthRelation.java
│ │ └── scientific
│ │ │ └── ScientificConstants.java
│ │ └── plato
│ │ ├── engine
│ │ ├── HandwritingEngine.java
│ │ ├── HandwritingEngineImpl.java
│ │ └── HandwritingEngineState.java
│ │ ├── helpers
│ │ ├── CStrokeJsonHelper.java
│ │ ├── CWrittenTokenJsonHelper.java
│ │ └── CWrittenTokenSetJsonHelper.java
│ │ └── state
│ │ └── PlatoState.java
└── resources
│ ├── config
│ ├── productions.txt
│ ├── stroke_curator_config.json
│ └── terminals.json
│ └── token_engine
│ └── token_engine.sdv.sz0_whr1_ns1.ser
└── test
├── java
└── me
│ └── scai
│ ├── handwriting
│ ├── TestHelper.java
│ ├── Test_CStroke.java
│ ├── Test_CWrittenToken.java
│ ├── Test_CWrittenTokenSet.java
│ ├── Test_NodeToken.java
│ ├── Test_Rectangle.java
│ ├── Test_StateStack.java
│ ├── Test_StrokeCurator.java
│ ├── Test_StrokeCuratorConfig.java
│ ├── TokenSetJsonTestHelper.java
│ └── utils
│ │ ├── Test_CHandWritingTokenImageData.java
│ │ └── Test_LimitedStack.java
│ ├── parsetree
│ ├── Test_GraphicalProductionJsonConversion.java
│ ├── Test_GraphicalProductionSet.java
│ ├── Test_MathHelper.java
│ ├── Test_NodeAnalyzer.java
│ ├── Test_ParseTreeMathTexifier.java
│ ├── Test_QADataSet.java
│ ├── Test_SerializeNode.java
│ ├── Test_TerminalSet.java
│ ├── Test_TokenSetParser.java
│ ├── evaluation
│ │ └── Test_ValueUnion.java
│ └── geometry
│ │ ├── Test_GeometricRelationJsonConversion.java
│ │ └── Test_GeometryHelper.java
│ └── plato
│ └── engine
│ └── Test_HandwritingEngineImpl.java
└── resources
├── config
└── stroke_curator_config_test_1.json
├── token_engine
└── token_engine.sdv.sz0_whr1_ns1.ser
└── tokens
├── no_source_equal_separator.im
├── token_no_source.wt
├── token_with_source_and_unicode_token.wt
└── with_source_colon_separator.im
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated files
2 | gen/
3 | bin/
4 |
5 | # Built app files
6 | *.apk
7 | *.ap_
8 |
9 | # Files for dex VM
10 | *.dex
11 |
12 | # Java class files
13 | *.class
14 |
15 | # Local configuration files (sdk paths, etc)
16 | local.properties
17 |
18 | # Proguard folder generated by Eclipse
19 | proguard/
20 |
21 | # Intellij project files
22 | *.iml
23 | *.ipr
24 | *.iws
25 | .idea/
26 |
27 | .classpath
28 | .project
29 | .settings/
30 |
31 | # Backup files in graph_lang
32 | graph_lang/*backup*
33 |
34 | target/
35 |
36 | jars/
37 |
38 |
39 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | install:
4 | - mkdir /home/travis/java-worker-pool
5 | - git clone https://github.com/Glyphoid/java-worker-pool.git /home/travis/java-worker-pool
6 | - mvn -f /home/travis/java-worker-pool/pom.xml clean install -DskipTests
7 | - mkdir /home/travis/java-web-utils
8 | - git clone https://github.com/Glyphoid/java-web-utils.git /home/travis/java-web-utils
9 | - mvn -f /home/travis/java-web-utils/pom.xml clean install -DskipTests
10 | - mkdir /home/travis/tokensets
11 | - git clone https://github.com/Glyphoid/tokensets /home/travis/tokensets
12 |
13 | script:
14 | - mvn test -B -DtokenSetPathPrefix=/home/travis/tokensets/TS_ -DnoLargeTokenSetParserTest=true
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015-2016 Shanqing Cai
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # math-handwriting-lib: A parser and evaluator of handwritten mathematical formulae (Java)
2 |
3 | [](https://www.codacy.com/app/shanqing-cai/math-handwriting-lib?utm_source=github.com&utm_medium=referral&utm_content=Glyphoid/math-handwriting-lib&utm_campaign=badger)
4 | [](https://travis-ci.org/Glyphoid/math-handwriting-lib)
5 |
6 | See:
7 | * [Web GUI Demo](http://scai.io/glyphoid/)
8 | * [Demo videos (YouTube)](https://www.youtube.com/watch?v=9LFmDcpyZ0w&list=PLcUSYoM0otQi4qCaO5uzluG8ww69kgepc)
9 |
10 | This library is for recognition of handwritten mathematical expresssions in the **online** fashion, i.e., through the utilization of stroke representations of handwritten symbols.
11 |
12 | This is the core part of Glyphoid.
13 |
14 | List of currently supported math notation syntaxes:
15 |
16 | 1. Numerical values: Decimal numbers, negative/positive signs
17 | 2. Basic arithemtics: Addition, subtraction, multplication
18 | 3. Fractions
19 | 4. Exponentiation
20 | 5. Square root
21 | 6. Parentheses (Limited support so far)
22 | 7. Variable definition and arithmetics: Symbol names including Latin and Greek letters, with or without suffixes
23 | 8. Function definition and evaluation: Multi-argument function supported
24 | 9. Matrices and vectors: Sparse matrix notations supported, addition, multiplication, transpose, inverse
25 | 10. Certain common elementary functions: e.g., sin, cos, log, exp
26 | 11. Certain matrix functions: det, rank
27 | 12. Summation (Sigma) and product (Pi) expressions
28 | 13. Definite integrals
29 | 14. Numerical comparisons (e.g., >, <, =)
30 | 15. Logical AND / OR expressions
31 | 16. Common mathematical and scientific constants, such as pi, e and c
32 | 17. Incremental parsing: [Example](https://youtu.be/SlsEhwm3Whk?t=147)
33 | 18. Piecewise functions: [Example](https://youtu.be/SlsEhwm3Whk?t=316)
34 |
35 | ## Build and Test
36 | 1. Make sure that you have the following installed:
37 | 1. Java 7 or above
38 | 2. Apache Maven
39 | 2. Download and build the [Glyphoid Java Worker Pool Porject](https://github.com/Glyphoid/java-worker-pool)
40 | 3. Download and build the [Glyphoid Java Web Utilites Porject](https://github.com/Glyphoid/java-web-utils)
41 | 3. Get the token set data from the [tokensets repository](https://github.com/Glyphoid/tokensets). These are required by the unit tests of math-handwriting-lib.
42 | 4. cd to the root directory of math-handwriting-lib (i.e., where the pom.xml is)
43 | 5. On Linux and Mac, execute Maven clean build and local-repository installation:
44 |
45 | `mvn clean install -DtokenSetPathPrefix=${TOKENSETS_DATA_DIR}/TS_`
46 |
47 | On Windows, do
48 |
49 | `mvn clean install -DtokenSetPathPrefix=${TOKENSETS_DATA_DIR}\TS_`
50 |
51 | The environment variable `tokenSetPathPrefix` tells the unit tests in math-handwriting-lib the location of the test token set files. `TOKENSETS_DATA_DIR` is the path to the above-mentioned tokensets repository.
52 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 | me.scai.plato
4 | math-handwriting-lib
5 | 0.1.3-SNAPSHOT
6 |
7 |
8 |
9 |
10 | src
11 |
12 | **/*.java
13 |
14 |
15 |
16 |
17 |
18 |
19 | maven-compiler-plugin
20 | 3.1
21 |
22 | 1.7
23 | 1.7
24 |
25 |
26 |
27 |
28 | maven-surefire-plugin
29 | 2.18.1
30 |
31 |
32 | test
33 |
34 |
35 |
36 | **/Test_*.java
37 |
38 |
39 |
40 |
41 |
42 | org.eluder.coveralls
43 | coveralls-maven-plugin
44 | 4.1.0
45 |
46 | guess
47 |
48 |
49 |
50 |
51 |
52 | org.apache.maven.plugins
53 | maven-jar-plugin
54 | 3.0.2
55 |
56 |
57 |
58 | true
59 | lib/
60 | me.scai.handwriting.ml.TrainTokenRecogEngineSDV
61 |
62 |
63 |
64 |
65 |
66 |
67 | org.codehaus.mojo
68 | exec-maven-plugin
69 |
70 | me.scai.handwriting.ml.TrainTokenRecogEngineSDV
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | me.scai.utilities
80 | worker-pool
81 | 1.0-SNAPSHOT
82 |
83 |
84 |
85 | net.sourceforge.argparse4j
86 | argparse4j
87 | 0.7.0
88 |
89 |
90 |
91 | org.encog
92 | encog-core
93 | 3.3.0
94 |
95 |
96 |
97 | commons-lang
98 | commons-lang
99 | 2.6
100 |
101 |
102 |
103 | me.scai.network
104 | web-utils
105 | 1.0-SNAPSHOT
106 |
107 |
108 |
109 | junit
110 | junit
111 | test
112 | 4.12
113 |
114 |
115 |
116 | com.google.code.gson
117 | gson
118 | 1.4
119 |
120 |
121 |
122 | gov.nist.math
123 | jama
124 | 1.0.3
125 |
126 |
127 |
128 | org.jscience
129 | jscience
130 | 4.3.1
131 |
132 |
133 | org.javolution
134 | javolution
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/AbstractToken.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import me.scai.parsetree.TerminalSet;
4 |
5 | import java.util.List;
6 |
7 | public abstract class AbstractToken {
8 | /* Member variables */
9 | protected float [] tokenBounds = new float[4];
10 | public float width = 0f;
11 | public float height = 0f;
12 |
13 | /* The type of a token, according to the terminal set, if applicable.
14 | * For a node token, the terminal type will be null */
15 | public List tokenTermTypes = null;
16 |
17 | /* Constructor */
18 | public AbstractToken() {
19 | initializeTokenBounds();
20 | }
21 |
22 | /* Methods */
23 | /* Abstract methods */
24 |
25 | /**
26 | * Get the result of the token-level recognition or token set-level parsing
27 | */
28 | public abstract String getRecogResult();
29 |
30 | /** Set the result of the token-level recogniton or token set-level parsing */
31 | public abstract void setRecogResult(String rw);
32 |
33 |
34 | public abstract float getCentralX();
35 | public abstract float getCentralY();
36 | public abstract void getTokenTerminalType(TerminalSet termSet);
37 |
38 | /* Concrete methods */
39 | /* Get the bounds: min_x, min_y, max_x, max_y */
40 | public float [] getBounds() {
41 | return tokenBounds;
42 | }
43 |
44 | public void setBounds(float [] bounds) {
45 | this.tokenBounds = bounds;
46 | }
47 |
48 | protected void initializeTokenBounds() {
49 | /* Initialize the token bounds: [min_x, min_y, max_x, max_y] */
50 | tokenBounds[0] = Float.POSITIVE_INFINITY;
51 | tokenBounds[1] = Float.POSITIVE_INFINITY;
52 | tokenBounds[2] = Float.NEGATIVE_INFINITY;
53 | tokenBounds[3] = Float.NEGATIVE_INFINITY;
54 | }
55 |
56 | public void clear() {
57 | initializeTokenBounds();
58 |
59 | width = 0f;
60 | height = 0f;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/CAbstractWrittenTokenSet.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.io.FileNotFoundException;
4 | import java.io.IOException;
5 | import java.io.PrintWriter;
6 | import java.util.List;
7 |
8 | public abstract class CAbstractWrittenTokenSet {
9 | /* Member variables */
10 | protected int nt = 0; /* Number of tokens */
11 | protected String [] tokenNames = null;
12 |
13 | /* Geometric bounds of the token set */
14 | protected float min_x = Float.POSITIVE_INFINITY;
15 | protected float min_y = Float.POSITIVE_INFINITY;
16 | protected float max_x = Float.NEGATIVE_INFINITY;
17 | protected float max_y = Float.NEGATIVE_INFINITY;
18 |
19 | /* Constructor */
20 | public CAbstractWrittenTokenSet() {} // For deserialization
21 |
22 | /* Get the number of tokens */
23 | public int nTokens() {
24 | return nt;
25 | }
26 |
27 | /* Test if the set is empty */
28 | public boolean empty() {
29 | return (nt == 0);
30 | }
31 |
32 | /* Add a new token */
33 | public void addOneToken() {
34 | nt++;
35 | }
36 |
37 | /* Set char set: the set of possible token names.
38 | * For example, these can be used in conjunction with recogPs. */
39 | public void setTokenNames(String [] t_tokenNames) {
40 | tokenNames = t_tokenNames;
41 | }
42 |
43 | public String [] getTokenNames() {
44 | return tokenNames;
45 | }
46 |
47 | public String getTokenName(int i) {
48 | if (tokenNames == null) {
49 | return null;
50 | } else {
51 | /* TODO: bound check */
52 | return tokenNames[i];
53 | }
54 | }
55 |
56 | public void deleteOneToken() {
57 | nt--;
58 | }
59 |
60 | public float [] getSetBounds() {
61 | float [] bnds = new float[4];
62 |
63 | bnds[0] = min_x;
64 | bnds[1] = min_y;
65 | bnds[2] = max_x;
66 | bnds[3] = max_y;
67 |
68 | return bnds;
69 | }
70 |
71 | public int getNumTokens() {
72 | return nt;
73 | }
74 |
75 | /* *** Abstract methods *** */
76 | protected abstract void calcBounds();
77 | protected abstract void clear();
78 |
79 | public abstract float [] getTokenBounds(int i);
80 | public abstract float [] getTokenBounds(int [] is);
81 |
82 | public abstract List getTokenTermTypes(int i);
83 |
84 | public abstract float [] setTokenBounds(int i, final float [] newBounds);
85 |
86 | }
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/CHandWritingTokenImageData.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.File;
5 | import java.io.FileInputStream;
6 | import java.io.IOException;
7 | import java.io.InputStreamReader;
8 |
9 | public class CHandWritingTokenImageData {
10 | public String tokenName = null;
11 | public int nw = 0;
12 | public int nh = 0;
13 | public int nStrokes = 0; /* Number of strokes */
14 | public float w;
15 | public float h;
16 |
17 | public Double[] imData = null;
18 |
19 | public CHandWritingTokenImageData() {
20 | };
21 |
22 | public CHandWritingTokenImageData(String t_tokenName) {
23 | tokenName = t_tokenName;
24 | }
25 |
26 | public CHandWritingTokenImageData(String t_tokenName, int t_nw, int t_nh) {
27 | tokenName = t_tokenName;
28 | nw = t_nw;
29 | nh = t_nh;
30 | }
31 |
32 | public static CHandWritingTokenImageData readImFile(File f,
33 | boolean bIncludeTokenSize,
34 | boolean bIncludeTokenWHRatio,
35 | boolean bIncludeTokenNumStrokes)
36 | throws IOException {
37 | final String TOKEN_NAME_LABEL = "Token name: ";
38 | final String N_W_LABEL = "n_w";
39 | final String N_H_LABEL = "n_h";
40 | final String NS_LABEL = "ns";
41 | final String W_LABEL = "w";
42 | final String H_LABEL = "h";
43 | final String SOURCE_DEVICE_LABEL = "source";
44 | final String BRUSH_WIDTH_LABEL = "b_w";
45 |
46 | CHandWritingTokenImageData dat = new CHandWritingTokenImageData();
47 |
48 | FileInputStream fin = null;
49 | BufferedReader in = null;
50 | try {
51 | fin = new FileInputStream(f);
52 | in = new BufferedReader(new InputStreamReader(fin));
53 | } catch (IOException e) {
54 | throw new IOException();
55 | }
56 |
57 | String line = null;
58 | int line_n = 0;
59 |
60 | /* Number of double values before the start of the image data */
61 | int nExtra = 0;
62 | if (bIncludeTokenSize) {
63 | nExtra += 2;
64 | }
65 | if (bIncludeTokenWHRatio) {
66 | nExtra += 1;
67 | }
68 | if (bIncludeTokenNumStrokes) {
69 | nExtra += 1;
70 | }
71 |
72 | /* Current 3: w, h, whr and ns */
73 | //int dcnt = 4;
74 | int dcnt = 0;
75 |
76 | double w = -1.0; /* Token width */
77 | double h = -1.0; /* Token height */
78 |
79 | while ((line = in.readLine()) != null) {
80 | if (line_n == 0) {
81 | if (line.startsWith(TOKEN_NAME_LABEL)) {
82 | dat.tokenName = line.replaceFirst(TOKEN_NAME_LABEL, "");
83 | } else {
84 | in.close();
85 | throw new IOException("Cannot find expected prefix " + TOKEN_NAME_LABEL);
86 | }
87 | } else if (line_n == 1) {
88 | if (line.startsWith(N_W_LABEL)) {
89 | String[] items = line.split(" ");
90 | dat.nw = Integer.parseInt(items[items.length - 1]);
91 | } else {
92 | in.close();
93 | throw new IOException("Cannot find expected prefix " + N_W_LABEL);
94 | }
95 | } else if (line_n == 2) {
96 | if (line.startsWith(N_H_LABEL)) {
97 | String[] items = line.split(" ");
98 | dat.nh = Integer.parseInt(items[items.length - 1]);
99 |
100 | /* Allocate space */
101 | dat.imData = new Double[nExtra + dat.nw * dat.nh];
102 | } else {
103 | in.close();
104 | throw new IOException("Cannot find expected prefix " + N_H_LABEL);
105 | }
106 | } else if (line_n == 3) {
107 | if (line.startsWith(NS_LABEL)) { /* Line: number of strokes */
108 | String[] items = line.split(" ");
109 | dat.nStrokes = Integer.parseInt(items[items.length - 1]);
110 |
111 | //dat.imData[3] = (double) dat.nStrokes;
112 | if ( bIncludeTokenNumStrokes )
113 | dat.imData[dcnt++] = (double) dat.nStrokes;
114 | //extraFeatures.add((double) dat.nStrokes);
115 | } else {
116 | in.close();
117 | throw new IOException("Cannot find expected prefix " + NS_LABEL);
118 | }
119 | } else if (line_n == 4) {
120 | if (line.startsWith(W_LABEL)) {
121 | String[] items = line.split(" ");
122 | w = Double.parseDouble(items[items.length - 1]);
123 | dat.w = (float) w;
124 | //dat.imData[0] = w;
125 | if ( bIncludeTokenSize )
126 | dat.imData[dcnt++] = w;
127 | //extraFeatures.add(w);
128 | } else {
129 | in.close();
130 | throw new IOException("Cannot find expected prefix " + W_LABEL);
131 | }
132 | } else if (line_n == 5) {
133 | if (line.startsWith(H_LABEL)) {
134 | String[] items = line.split(" ");
135 | h = Double.parseDouble(items[items.length - 1]);
136 | dat.h = (float) h;
137 |
138 | if ( bIncludeTokenSize )
139 | dat.imData[dcnt++] = h;
140 |
141 | if ( bIncludeTokenWHRatio ) {
142 | if ( w != -1.0 ) {
143 | if (h == 0.0) {
144 | dat.imData[dcnt++] = 100.0; // Prevent infinity values
145 | } else {
146 | dat.imData[dcnt++] = w / h;
147 | }
148 | }
149 | else {
150 | System.err.println("w value has not been retrieved yet");
151 | }
152 | }
153 | } else {
154 | in.close();
155 | throw new IOException("Cannot find expected prefix " + H_LABEL);
156 | }
157 | } else if (line_n >= 6) {
158 | if (line.startsWith(SOURCE_DEVICE_LABEL) ||
159 | line.startsWith(BRUSH_WIDTH_LABEL)) {
160 | continue;
161 | // TODO(cais): The source device (e.g., ipad, desktop_chrome) and brush width information
162 | // are currently not used. Use them as features in the future.
163 | }
164 |
165 | String[] strs = line.split(" ");
166 | for (int i = 0; i < strs.length; ++i) {
167 | dat.imData[dcnt++] = Double.parseDouble(strs[i]);
168 |
169 | if (dat.imData[dcnt - 1] < 0) {
170 | throw new IllegalStateException("Encountered negative stroke image data");
171 | }
172 | }
173 | }
174 |
175 | line_n++;
176 | }
177 |
178 | in.close();
179 |
180 | // if (dcnt != dat.imData.length) {
181 | // in.close();
182 | // throw new IOException();
183 | // }
184 |
185 | return dat;
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/CStroke.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.util.LinkedList;
4 |
5 | /* CStroke: class for supporting a single, continuous stroke */
6 | public class CStroke {
7 | /* Member variables */
8 | private LinkedList xs = new LinkedList();
9 | private LinkedList ys = new LinkedList();
10 | private boolean bNormalized = false;
11 |
12 | public float min_x = Float.POSITIVE_INFINITY, max_x = Float.NEGATIVE_INFINITY; /* Record bounds */
13 | public float min_y = Float.POSITIVE_INFINITY, max_y = Float.NEGATIVE_INFINITY; /* Record bounds */
14 | /* ~Member variables */
15 |
16 | /* Constructor: no initial point: empty initially */
17 | public CStroke() {
18 |
19 | }
20 |
21 | /* Constructor: supply the initial x and y coordinates */
22 | public CStroke(float x, float y) {
23 | addPoint(x, y);
24 | }
25 |
26 | /* Copy constructor */
27 | public CStroke(CStroke s0) {
28 | xs = new LinkedList();
29 | ys = new LinkedList();
30 |
31 | for (int i = 0; i < s0.xs.size(); ++i) {
32 | xs.add(s0.xs.get(i));
33 | ys.add(s0.ys.get(i));
34 | }
35 |
36 | bNormalized = s0.bNormalized;
37 |
38 | min_x = s0.min_x;
39 | max_x = s0.max_x;
40 | min_y = s0.min_y;
41 | max_y = s0.max_y;
42 | }
43 |
44 | /* Add a single point */
45 | public void addPoint(Float x, Float y) {
46 | xs.add(x);
47 | ys.add(y);
48 |
49 | if ( x < min_x ) {
50 | min_x = x;
51 | }
52 | if ( x > max_x ) {
53 | max_x = x;
54 | }
55 | if ( y < min_y ) {
56 | min_y = y;
57 | }
58 | if ( y > max_y ) {
59 | max_y = y;
60 | }
61 | }
62 |
63 | /* Get the number of points */
64 | public int nPoints() {
65 | return xs.size();
66 | }
67 |
68 | /* Normalize axes */
69 | public void normalizeAxes(float min_x, float max_x,
70 | float min_y, float max_y) {
71 | float rng_x = max_x - min_x;
72 | float rng_y = max_y - min_y;
73 | for (int i = 0; i < xs.size(); ++i) {
74 | if (rng_x == 0.0f) {
75 | xs.set(i, 0.0f); /* Edge case: Zero width */
76 | }
77 | else {
78 | xs.set(i, (xs.get(i) - min_x) / rng_x);
79 | }
80 |
81 | if (rng_y == 0.0f) {
82 | ys.set(i, 0.0f); /* Edge case: Zero height */
83 | }
84 | else {
85 | ys.set(i, (ys.get(i) - min_y) / rng_y);
86 | }
87 | }
88 |
89 | bNormalized = true;
90 | }
91 |
92 | /* Fill imageMap: do this only after calling normalizeAxes()
93 | * Input arguments: im - 1D array, size must be equal to h * w
94 | * h - height
95 | * w - width
96 | * Storage in im: x varies the fastest
97 | * x increase: left to right;
98 | * y increase: top to bottom */
99 | public void fillImageMap(int [] im, int w, int h) {
100 | if ( im.length != h * w )
101 | System.err.println("UNEXPECTED_INT_ARRAY_SIZE");
102 |
103 | if ( !bNormalized )
104 | System.err.println("CSTROKE_UNNORMALIZED: CStroke object has not been normalized when fillImageMap is called!");
105 |
106 | int prev_ix = 0;
107 | int prev_iy = 0;
108 | for (int i = 0; i < xs.size(); ++i) {
109 | int ix = (int)(xs.get(i) * w);
110 | if ( ix == w )
111 | ix--;
112 |
113 | int iy = (int)(ys.get(i) * h);
114 | if ( iy == h )
115 | iy--;
116 |
117 | if ( i == 0 ) { /* First step */
118 | int ind = iy * w + ix;
119 | im[ind] = 1;
120 | }
121 | else {
122 | int x_step = ix - prev_ix;
123 | int y_step = iy - prev_iy;
124 |
125 | if ( x_step == 0 && y_step == 0 )
126 | continue;
127 |
128 | int abs_x_step = (x_step > 0) ? x_step : -x_step;
129 | int abs_y_step = (y_step > 0) ? y_step : -y_step;
130 |
131 | if ( abs_x_step > abs_y_step ) {
132 | /* x is the driver */
133 | int inc = (x_step > 0) ? 1 : -1;
134 | int x = prev_ix + inc;
135 | while ( true ) {
136 | double f = ( (double) x - (double) prev_ix ) / (double) x_step;
137 | int y = (int) ((double) prev_iy + f * y_step);
138 | if ( x == w ) x--;
139 | if ( y == h ) y--;
140 |
141 | int ind = y * w + x;
142 | im[ind] = 1;
143 |
144 | if ( x == ix )
145 | break;
146 |
147 | x += inc;
148 | }
149 |
150 | }
151 | else {
152 | /* x is the driver */
153 | int inc = (y_step > 0) ? 1 : -1;
154 | int y = prev_iy + inc;
155 | while ( true ) {
156 | double f = ( (double) y - (double) prev_iy ) / (double) y_step;
157 | int x = (int) ((double) prev_ix + f * x_step);
158 | if ( x == w ) x--;
159 | if ( y == h ) y--;
160 |
161 | int ind = y * w + x;
162 | im[ind] = 1;
163 |
164 | if ( y == iy )
165 | break;
166 |
167 | y += inc;
168 | }
169 |
170 | }
171 |
172 | }
173 |
174 | prev_ix = ix;
175 | prev_iy = iy;
176 | }
177 | }
178 |
179 | /* Get a float array of the xs and ys coordinates */
180 | public float[] getXs() {
181 | float [] oxs = new float[xs.size()];
182 |
183 | for (int i = 0; i < xs.size(); ++i) {
184 | oxs[i] = xs.get(i);
185 | }
186 |
187 | return oxs;
188 | }
189 |
190 | public float[] getYs() {
191 | float [] oys = new float[ys.size()];
192 |
193 | for (int i = 0; i < ys.size(); ++i) {
194 | oys[i] = ys.get(i);
195 | }
196 |
197 | return oys;
198 | }
199 |
200 | @Override
201 | public String toString() {
202 | String s = new String();
203 |
204 | s += "Stroke (np=" + xs.size() + "):\n\txs=[";
205 | for (int i = 0; i < xs.size(); ++i) {
206 | s += String.format("%.3f", xs.get(i));
207 | if ( i != xs.size() - 1 )
208 | s += ", ";
209 | else
210 | s += "]";
211 | }
212 |
213 | s += "\n\tys=[";
214 | for (int i = 0; i < ys.size(); ++i) {
215 | s += String.format("%.3f", ys.get(i));
216 | if ( i != ys.size() - 1 )
217 | s += ", ";
218 | else
219 | s += "]";
220 | }
221 |
222 | return s;
223 | }
224 |
225 | public boolean isNormalized() {
226 | return bNormalized;
227 | }
228 | }
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/HandwritingEngineUserAction.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | public enum HandwritingEngineUserAction {
4 | AddStroke,
5 | RemoveLastToken,
6 | RemoveToken,
7 | RemoveTokens,
8 | MoveToken,
9 | MoveTokens,
10 | MergeStrokesAsToken,
11 | ForceSetTokenName,
12 | ClearStrokes,
13 | ParseTokenSubset;
14 |
15 | // private String commandString; // String for HTTP requests
16 | //
17 | // HandwritingEngineUserAction(String commandString) {
18 | // this.commandString = commandString;
19 | // }
20 | //
21 | // @Override
22 | // public String toString() {
23 | // return this.commandString;
24 | // }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/NodeToken.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import me.scai.parsetree.GraphicalProductionSet;
4 | import me.scai.parsetree.Node;
5 | import me.scai.parsetree.TerminalSet;
6 |
7 | import java.util.LinkedList;
8 | import java.util.List;
9 |
10 | /**
11 | * A token that is a parsed node
12 | */
13 | public class NodeToken extends AbstractToken {
14 | /* Members */
15 | private Node node;
16 | private CAbstractWrittenTokenSet wtSet;
17 |
18 | private List matchingGraphicalProductionIndices; // TODO: Do not make public just for the sake of serialization/desrialization
19 |
20 | private String parsingResult;
21 |
22 | /* Constructors */
23 | public NodeToken(Node node, CAbstractWrittenTokenSet wtSet) {
24 | // Set the bounds of the node
25 | node.setBounds(wtSet.getSetBounds());
26 |
27 | // Make sure that tokenNames are available
28 | if (wtSet instanceof CWrittenTokenSetNoStroke && wtSet.tokenNames == null) {
29 | CWrittenTokenSetNoStroke wtSetNS = (CWrittenTokenSetNoStroke) wtSet;
30 |
31 | String[] tokenNames = new String[wtSet.getNumTokens()];
32 | for (int i = 0; i < wtSetNS.tokens.size(); ++i) {
33 | tokenNames[i] = wtSetNS.tokens.get(i).getRecogResult();
34 | }
35 |
36 | wtSet.setTokenNames(tokenNames);
37 | }
38 |
39 | if (node.getBounds() == null) {
40 | throw new IllegalArgumentException("node bounds not available");
41 | }
42 |
43 | this.node = node;
44 | this.wtSet = wtSet;
45 |
46 | this.tokenBounds = this.node.getBounds();
47 | this.width = this.tokenBounds[2] - this.tokenBounds[0];
48 | this.height = this.tokenBounds[3] - this.tokenBounds[1];
49 |
50 | }
51 |
52 | /* Methods */
53 | @Override
54 | public float getCentralX() {
55 | return 0.5f * (tokenBounds[2] + tokenBounds[0]);
56 | }
57 |
58 | @Override
59 | public float getCentralY() {
60 | return 0.5f * (tokenBounds[3] + tokenBounds[1]);
61 | }
62 |
63 | @Override
64 | public void getTokenTerminalType(TerminalSet termSet) {
65 | tokenTermTypes = null;
66 | }
67 |
68 | @Override
69 | public String getRecogResult() {
70 | return parsingResult;
71 | }
72 |
73 | @Override
74 | public void setRecogResult(String pr) {
75 | parsingResult = pr;
76 | }
77 |
78 | public Node getNode() {
79 | return node;
80 | }
81 |
82 | public CAbstractWrittenTokenSet getTokenSet() {
83 | return wtSet;
84 | }
85 |
86 | public boolean isPotentiallyTerminal() {
87 | return wtSet.getNumTokens() == 1;
88 | // It is potentially a terminal if and only if the written token set has only one terminal
89 | }
90 |
91 | /**
92 | * Get all the matching graphical production indices inside a given GraphicalProductionSet
93 | * @param gpSet
94 | * @return
95 | */
96 | public List getMatchingGraphicalProductionIndices(GraphicalProductionSet gpSet) {
97 | if (matchingGraphicalProductionIndices == null) { // Use caching
98 | matchingGraphicalProductionIndices = new LinkedList<>();
99 |
100 | Node nd = node;
101 |
102 | while (nd != null) {
103 | String prodSumString = nd.prodSumString;
104 |
105 | if (gpSet.prodSumStrings.contains(prodSumString)) {
106 | matchingGraphicalProductionIndices.add(gpSet.prodSumStrings.indexOf(prodSumString));
107 | }
108 |
109 | if (nd.ch == null) {
110 | break;
111 | }
112 |
113 | if (nd.ch.length == 1) {
114 | nd = nd.ch[0];
115 | } else {
116 | break;
117 | }
118 | }
119 | }
120 |
121 | return matchingGraphicalProductionIndices;
122 |
123 | }
124 |
125 | /**
126 | * Get the direct, single-lineage, descendant of the node that has the specified production LHS
127 | * @param lhs Production LHS
128 | * @return null if such a descendant does not exist.
129 | * The descendant node if it exists.
130 | */
131 | public Node getSingleLineageDirectDescendant(String lhs) {
132 | if (lhs == null) {
133 | throw new IllegalArgumentException("lhs is null and hence invalid");
134 | }
135 |
136 | Node nd = this.node;
137 |
138 | while (nd.ch != null && nd.ch.length == 1) {
139 | if (lhs.equals(nd.ch[0].lhs)) {
140 | return nd.ch[0];
141 | }
142 |
143 | nd = nd.ch[0];
144 | }
145 |
146 | return null;
147 | }
148 |
149 | public List getMatchingGraphicalProductionIndices() {
150 | return matchingGraphicalProductionIndices;
151 | }
152 |
153 | public void setMatchingGraphicalProductionIndices(List matchingGraphicalProductionIndices) {
154 | this.matchingGraphicalProductionIndices = matchingGraphicalProductionIndices;
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/Rectangle.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import me.scai.parsetree.MathHelper;
4 |
5 | public class Rectangle {
6 | private float xMin, yMin, xMax, yMax;
7 |
8 | /* Methods */
9 |
10 | /* Constructors */
11 | public Rectangle(float [] bnds) {
12 | xMin = bnds[0];
13 | yMin = bnds[1];
14 | xMax = bnds[2];
15 | yMax = bnds[3];
16 | }
17 |
18 | public Rectangle(float t_xMin, float t_yMin, float t_xMax, float t_yMax) {
19 | xMin = t_xMin;
20 | yMin = t_yMin;
21 | xMax = t_xMax;
22 | yMax = t_yMax;
23 | }
24 |
25 | public Rectangle(CWrittenToken wt) {
26 | float [] bnds = wt.getBounds();
27 |
28 | xMin = bnds[0];
29 | yMin = bnds[1];
30 | xMax = bnds[2];
31 | yMax = bnds[3];
32 | }
33 |
34 | public Rectangle(CAbstractWrittenTokenSet wts) {
35 | float [] bnds = wts.getSetBounds();
36 |
37 | xMin = bnds[0];
38 | yMin = bnds[1];
39 | xMax = bnds[2];
40 | yMax = bnds[3];
41 | }
42 |
43 | public Rectangle(CAbstractWrittenTokenSet wts, int [] inds) {
44 | xMin = Float.POSITIVE_INFINITY;
45 | yMin = Float.POSITIVE_INFINITY;
46 | xMax = Float.NEGATIVE_INFINITY;
47 | yMax = Float.NEGATIVE_INFINITY;
48 |
49 | if ( inds.length == 0 ) {
50 | throw new RuntimeException("Index to tokens is empty.");
51 | }
52 |
53 | for (int i = 0; i < inds.length; ++i) {
54 | float [] bnds = wts.getTokenBounds(inds[i]);
55 |
56 | if (xMin > bnds[0])
57 | xMin = bnds[0];
58 | if (yMin > bnds[1])
59 | yMin = bnds[1];
60 | if (xMax < bnds[2])
61 | xMax = bnds[2];
62 | if (yMax < bnds[3])
63 | yMax = bnds[3];
64 |
65 | }
66 | }
67 |
68 | public float getCentralX() {
69 | return (xMin + xMax) * 0.5f;
70 | }
71 |
72 | public float getCentralY() {
73 | return (yMin + yMax) * 0.5f;
74 | }
75 |
76 | public void mergeWith(Rectangle rct) {
77 | float new_xMin, new_yMin, new_xMax, new_yMax;
78 |
79 | new_xMin = MathHelper.min(xMin, rct.xMin);
80 | new_yMin = MathHelper.min(yMin, rct.yMin);
81 | new_xMax = MathHelper.max(xMax, rct.xMax);
82 | new_yMax = MathHelper.max(yMax, rct.yMax);
83 |
84 | xMin = new_xMin;
85 | yMin = new_yMin;
86 | xMax = new_xMax;
87 | yMax = new_yMax;
88 | }
89 |
90 |
91 | /* Geometric relations */
92 | public boolean isCenterWestOf(float x) {
93 | return ((xMin + xMax) * 0.5f) < x;
94 | }
95 |
96 | public boolean isCenterEastOf(float x) {
97 | return ((xMin + xMax) * 0.5f) > x;
98 | }
99 |
100 | public boolean isCenterNorthOf(float y) {
101 | return ((yMin + yMax) * 0.5f) < y;
102 | }
103 |
104 | public boolean isCenterSouthOf(float y) {
105 | return ((yMin + yMax) * 0.5f) > y;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/StateStack.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import com.google.gson.JsonObject;
4 | import me.scai.handwriting.utils.LimitedStack;
5 | import me.scai.plato.state.PlatoState;
6 |
7 | public class StateStack {
8 | /* Constants */
9 |
10 | /* Member variables */
11 | private LimitedStack limitedStack;
12 |
13 | private int capacity;
14 | private int stackPointer;
15 |
16 | /* Constructors */
17 | public StateStack(int capacity) {
18 | this.capacity = capacity;
19 | this.limitedStack = new LimitedStack<>(capacity);
20 |
21 | this.stackPointer = 0;
22 | }
23 |
24 | public void push(T state) {
25 | /* Pop out all the states above the stack pointer */
26 | int nToPop = stackPointer;
27 |
28 | for (int n = 0; n < nToPop; ++n) {
29 | limitedStack.pop();
30 | }
31 |
32 | limitedStack.push(state);
33 |
34 | stackPointer = 0;
35 | }
36 |
37 | public void undo() {
38 | if (canUndo()) {
39 | stackPointer++;
40 | assert stackPointer >= 0 && stackPointer <= limitedStack.size();
41 | } else {
42 | throw new IllegalStateException("No more state to undo");
43 | }
44 | }
45 |
46 | public void redo() {
47 | if (canRedo()) {
48 | stackPointer--;
49 | assert stackPointer >= 0 && stackPointer <= limitedStack.size();
50 | } else {
51 | throw new IllegalStateException("No more state to redo");
52 | }
53 | }
54 |
55 | public boolean canUndo() {
56 | return stackPointer < limitedStack.size();
57 | }
58 |
59 | public boolean canRedo() {
60 | return stackPointer > 0;
61 | }
62 |
63 | // public StrokeCuratorUserAction getLastUserAction() {
64 | public String getLastUserAction() {
65 | if ( stackPointer < limitedStack.size() ) {
66 | return limitedStack.get(stackPointer).getUserAction();
67 | } else {
68 | return null;
69 | }
70 | }
71 |
72 | public JsonObject getLastSerializedState() {
73 | if ( stackPointer < limitedStack.size() ) {
74 | return limitedStack.get(stackPointer).getState();
75 | } else {
76 | return null;
77 | }
78 | }
79 |
80 | public T getLastState() {
81 | if ( stackPointer < limitedStack.size() ) {
82 | return limitedStack.get(stackPointer);
83 | } else {
84 | return null;
85 | }
86 | }
87 |
88 | public int getCapacity() {
89 | return capacity;
90 | }
91 |
92 | public boolean isEmpty() {
93 | return limitedStack.isEmpty();
94 | }
95 |
96 | public int getSize() {
97 | return limitedStack.size();
98 | }
99 |
100 | /**
101 | * Get the number of states that have been undone, but are still in the stack and can potentially be redone.
102 | * @return
103 | */
104 | public int getUndoneSize() {
105 | return stackPointer;
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/StrokeCurator.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import com.google.gson.JsonObject;
4 |
5 | import java.util.List;
6 |
7 | public interface StrokeCurator {
8 |
9 | /* Add a new stroke */
10 | void addStroke(CStroke s);
11 |
12 | /* Merge a number of strokes as a token. These strokes may have already been
13 | * incorporated in other tokens, in which case the proper removal and plucking
14 | * of old tokens need to take place. */
15 | void mergeStrokesAsToken(int [] indices);
16 |
17 | /* Move a token
18 | * @returns old bounds
19 | */
20 | public float[] moveToken(int tokenIdx, float [] newBounds);
21 |
22 | /* Remove the last stroke */
23 | // TODO
24 | // void removeLastStroke();
25 |
26 | /**
27 | * Remove i-th token
28 | * @param idxToken: Index to the token
29 | * @return Indices to constituent strokes that made up the removed token */
30 | int [] removeToken(int idxToken);
31 |
32 | /**
33 | * Remove the last token
34 | * @return Indices to constituent strokes that made up the removed token */
35 | int [] removeLastToken();
36 |
37 | /* Delete all strokes */
38 | void clear();
39 |
40 | /* Get the set of written tokens (CWrittenTokenSet) */
41 | CWrittenTokenSet getTokenSet();
42 |
43 | List getTokenUuids();
44 |
45 | String getTokenUuid(int tokenIdx);
46 |
47 | /* Get the number of strokes that have been added */
48 | int getNumStrokes();
49 |
50 | /* Get the number of recognized tokens */
51 | int getNumTokens();
52 |
53 | /* Test and see if the stroke set is currently empty */
54 | boolean isEmpty();
55 |
56 | /* Getters for status information: written tokens and their recognition results */
57 | CWrittenTokenSet getWrittenTokenSet();
58 | List getWrittenTokenRecogWinners();
59 | List getWrittenTokenRecogPs();
60 |
61 | /* Get the constituent stroke indicies */
62 | List getWrittenTokenConstStrokeIndices();
63 |
64 | /* Force setting the recognition winner */
65 | void forceSetRecogWinner(int tokenIdx, String recogWinner);
66 |
67 | /* Serialization methods */
68 | /* Get serialized form of the strokes */
69 | List getSerializedStrokes();
70 |
71 | String getSerializedTokenSet();
72 |
73 | String getSerializedConstStrokeIndices();
74 |
75 | /* State and stack */
76 | JsonObject getStateSerialization();
77 | String getStateSerializationString();
78 |
79 | /**
80 | * Get the last user action;
81 | *
82 | * @return
83 | */
84 | StrokeCuratorUserAction getLastUserAction();
85 | void undoUserAction();
86 | void redoUserAction();
87 |
88 | boolean canUndoUserAction();
89 | boolean canRedoUserAction();
90 |
91 | /* Injection of serialized state */
92 | void injectSerializedState(JsonObject json);
93 |
94 | /**
95 | * Get all possible toke names (NOT display names)
96 | */
97 | List getAllTokenNames();
98 |
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/StrokeCuratorState.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import com.google.gson.JsonObject;
4 | import me.scai.plato.state.PlatoState;
5 |
6 | public class StrokeCuratorState implements PlatoState {
7 | private StrokeCuratorUserAction userAction;
8 | private JsonObject state;
9 |
10 | /* Constructors */
11 | public StrokeCuratorState(StrokeCuratorUserAction userAction, JsonObject state) {
12 | this.userAction = userAction;
13 | this.state = state;
14 | }
15 |
16 | /* Implementing interface methods */
17 | @Override
18 | public String getUserAction() {
19 | return userAction.toString();
20 | }
21 |
22 | @Override
23 | public JsonObject getState() {
24 | return state;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/StrokeCuratorUserAction.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import me.scai.plato.state.PlatoState;
4 |
5 | public enum StrokeCuratorUserAction {
6 | // AddStroke("add-stroke"),
7 | // MoveToken("move-token"),
8 | // RemoveLastToken("remove-last-token"),
9 | // RemoveToken("remove-token"),
10 | // MergeStrokesAsToken("merge-strokes-as-token"),
11 | // ForceSetTokenName("force-set-token-name"),
12 | // ClearStrokes("clear"),
13 | // GetGraphicalProductions("get-graphical-productions");
14 | AddStroke,
15 | MoveToken,
16 | RemoveLastToken,
17 | RemoveToken,
18 | MergeStrokesAsToken,
19 | ForceSetTokenName,
20 | ClearStrokes,
21 | GetGraphicalProductions;
22 |
23 | // private String commandString; // String for HTTP requests
24 |
25 | // StrokeCuratorUserAction(String commandString) {
26 | // this.commandString = commandString;
27 | // }
28 |
29 | // @Override
30 | // public String toString() {
31 | // return this.commandString;
32 | // }
33 |
34 | // public static StrokeCuratorUserAction fromString(String s) {
35 | // if (s.equals("clear")) {
36 | // return ClearStrokes;
37 | // } else {
38 | // if (s.charAt(0) >= 'A' && s.charAt(0) <= 'Z') {
39 | // return StrokeCuratorUserAction.valueOf(s);
40 | // } else {
41 | // String as = "";
42 | //
43 | // String[] items = s.split("-");
44 | // for (String item : items) {
45 | // String ts = "";
46 | // ts += item.charAt(0);
47 | //
48 | // as += ts.toUpperCase() + item.substring(1, item.length());
49 | // }
50 | //
51 | // return StrokeCuratorUserAction.valueOf(as);
52 | // }
53 | //
54 | // }
55 | // }
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/TokenDegeneracy.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.util.Map;
4 | import java.util.HashMap;
5 | import java.util.Set;
6 | import java.util.HashSet;
7 |
8 | import com.google.gson.Gson;
9 | import com.google.gson.JsonElement;
10 | import com.google.gson.JsonObject;
11 |
12 | public class TokenDegeneracy {
13 | // private static final String RESOURCES_DIR = "resources";
14 | // private static final String RESOURCES_CONFIG_DIR = "config";
15 | // private static final String TOKEN_DEGENERACY_CONFIG_FN = "token_degeneracy.json";
16 |
17 | private static final Gson gson = new Gson();
18 |
19 | private Map tab = new HashMap<>();
20 | private Map > altMap = new HashMap<>(); /* Full map of alternatives */
21 |
22 | /* Constructor */
23 | public TokenDegeneracy(JsonObject obj) {
24 | for (Map.Entry kv : obj.entrySet()) {
25 | String tokenName = kv.getKey();
26 | String substituteTokenName = kv.getValue().getAsString();
27 |
28 | if (tokenName.equals(substituteTokenName)) {
29 | throw new RuntimeException("The token name \"" + tokenName + "\" and its alternative \"" + substituteTokenName + "\" are identical, which is not allowed in a token degeneracy table");
30 | }
31 |
32 | tab.put(tokenName, substituteTokenName);
33 | }
34 |
35 | prepareAlternatives();
36 | }
37 |
38 | /* Get the degenerated token name */
39 | public String getDegenerated(String tokenName) {
40 | String degen = tab.get(tokenName);
41 |
42 | if (degen != null) {
43 | // System.out.println("\"" + tokenName + "\" --> \"" + degen + "\"");
44 | }
45 |
46 | return (degen == null) ? tokenName : degen;
47 | }
48 |
49 | /* Prepare the full map of alternatives */
50 | private void prepareAlternatives() {
51 | for (Map.Entry entry : tab.entrySet()) {
52 | String t0 = entry.getKey();
53 | String t1 = entry.getValue();
54 |
55 | if (!altMap.containsKey(t0)) {
56 | altMap.put(t0, new HashSet());
57 | }
58 | altMap.get(t0).add(t1);
59 |
60 | if (!altMap.containsKey(t1)) {
61 | altMap.put(t1, new HashSet());
62 | }
63 | altMap.get(t1).add(t0);
64 | }
65 | }
66 |
67 | /* Get all alternatives of a token */
68 | public Set getAlternatives(String token) {
69 | return altMap.get(token);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/TokenRecogEngine.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.io.File;
4 | import java.io.FilenameFilter;
5 | import java.io.IOException;
6 | import java.io.Serializable;
7 | import java.util.ArrayList;
8 | import java.lang.reflect.Method;
9 | import java.lang.reflect.InvocationTargetException;
10 | import java.util.List;
11 |
12 | import org.encog.neural.networks.BasicNetwork;
13 |
14 | public abstract class TokenRecogEngine implements Serializable {
15 | protected static final long serialVersionUID = 1L;
16 |
17 | protected final static boolean bDebug = false;
18 | protected final static String wt_file_prefix = "L_";
19 | protected final static String wt_file_suffix = ".wt";
20 | protected final static String im_file_prefix = "L_";
21 | protected final static String im_file_suffix = ".im";
22 |
23 | /* Feature settings */
24 | protected boolean bIncludeTokenSize;
25 | protected boolean bIncludeTokenWHRatio;
26 | protected boolean bIncludeTokenNumStrokes;
27 |
28 | /* Network and training settings */
29 | int hiddenLayer1_size;
30 | int hiddenLayer2_size;
31 | boolean useTanh;
32 |
33 | /* Training strategy */
34 | protected final double strategyError = 0.25;
35 | protected final int strategyCycles = 100;
36 | protected int trainMaxIter = 100;
37 | //private double trainSeconds = 10.0;
38 | protected double trainThreshErrRate = 0.01;
39 |
40 | protected BasicNetwork bnet = null;
41 |
42 | protected static final int trainingPercent_loadData = 10;
43 | protected int trainingProgPercent = 0;
44 |
45 | /* The set of all token names (unique, e.g., a, b, c, ...) */
46 | public ArrayList tokenNames = null;
47 |
48 | /* Inner interface */
49 | /* Inner interface: Progress bar updater */
50 | public Object trainingProgressDisplay;
51 | /* e.g., an instance of android.app.ProgressDialog */
52 |
53 | public interface ProgBarUpdater {
54 | public void update(); /* To be overridden in derived classes */
55 | }
56 |
57 | protected transient ProgBarUpdater progBarUpdater = null;
58 |
59 | /* Constructors (to be overridden) */
60 | public TokenRecogEngine() {}
61 |
62 | public TokenRecogEngine(int t_hiddenLayer1_size,
63 | int t_hiddenLayer2_size,
64 | int t_maxIter,
65 | double t_threshErrRate) {}
66 |
67 | /* Non-abstract methods */
68 | /* Get the i-th token name */
69 | public String getTokenName(int i) {
70 | return tokenNames.get(i);
71 | }
72 |
73 | public List getAllTokenNames() {
74 | return tokenNames;
75 | }
76 |
77 | public int getTrainMaxIter() {
78 | return trainMaxIter;
79 | }
80 |
81 | public double getTrainThreshErrRate() {
82 | return trainThreshErrRate;
83 | }
84 |
85 | /* Re-format all .im files */
86 | public void reFormatImFiles(String inDirName, int imgW, int imgH) {
87 | File inDir = new File(inDirName);
88 |
89 | File [] files = inDir.listFiles(new FilenameFilter() {
90 | @Override
91 | public boolean accept(File dir, String name) {
92 | return (name.startsWith(wt_file_prefix) && name.endsWith(wt_file_suffix));
93 | }
94 | });
95 |
96 | if ( files.length == 0 )
97 | return;
98 |
99 | for (int i = 0; i < files.length; ++i) {
100 | CWrittenToken t_wToken = new CWrittenToken();
101 |
102 | String letter = null;
103 | try {
104 | letter = t_wToken.readFromFile(files[i]);
105 | }
106 | catch (IOException e) {
107 |
108 | }
109 |
110 | //t_wToken.normalizeAxes();
111 |
112 | /* Get the name of the .im file */
113 | String t_path = inDirName.endsWith("/") ? inDirName : inDirName + "/";
114 | String imFN = t_path + files[i].getName().replace(wt_file_suffix, im_file_suffix);
115 | System.out.println("Processing file: " + imFN);
116 |
117 | float [] wh = null;
118 | try {
119 | wh = CWrittenToken.getTokenWidthHeightFromImFile(imFN);
120 | }
121 | catch (IOException e) {
122 | /* TODO */
123 | }
124 | t_wToken.width = wh[0];
125 | t_wToken.height = wh[1];
126 | t_wToken.bNormalized = true;
127 |
128 | try {
129 | t_wToken.writeImgFile(imFN, letter, imgW, imgH);
130 | }
131 | catch (IOException e) {
132 |
133 | }
134 | }
135 | }
136 |
137 | /* Function for setting a new progBarUpdater */
138 | public void setProgBarUpdater(ProgBarUpdater pbu) {
139 | progBarUpdater = pbu;
140 | }
141 |
142 | public void setTrainingProgDialog(Object trainProgDisplay) {
143 | trainingProgressDisplay = trainProgDisplay;
144 |
145 | setProgBarUpdater(new ProgBarUpdater() {
146 | public void update() {
147 | if ( trainingProgressDisplay.getClass().getName() == "android.app.ProgressDialog" ) {
148 | try {
149 | Method method = trainingProgressDisplay.getClass().getMethod("setProgress");
150 | method.invoke(trainingProgressDisplay, trainingProgPercent);
151 | }
152 | catch ( NoSuchMethodException nsme ) {
153 | throw new RuntimeException("Encountered NoSuchMethodException");
154 | }
155 | catch ( InvocationTargetException ite ) {
156 | throw new RuntimeException("Encountered InvocationTargetException");
157 | }
158 | catch ( IllegalAccessException iae ) {
159 | throw new RuntimeException("Encountered IllegalAccessException");
160 | }
161 | }
162 | // trainingProgDialog.setProgress(trainingProgPercent);
163 | }
164 | });
165 | }
166 |
167 | /* Abstract methods */
168 | public abstract void train(String inDirName, String outDataDirName);
169 |
170 | public abstract boolean isReadyToRecognize();
171 |
172 | public abstract void setFeatures(int [] ivs, boolean [] bvs);
173 |
174 | public abstract boolean isTokenHardCoded(CWrittenToken wt);
175 |
176 | public abstract int recognize(CWrittenToken wt, double [] outPs);
177 |
178 | public boolean isIncludeTokenSize() {
179 | return bIncludeTokenSize;
180 | }
181 |
182 | public boolean isIncludeTokenWHRatio() {
183 | return bIncludeTokenWHRatio;
184 | }
185 |
186 | public boolean isIncludeTokenNumStrokes() {
187 | return bIncludeTokenNumStrokes;
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/TokenRecogOutput.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 | import java.util.List;
4 |
5 | public class TokenRecogOutput {
6 | private String winner;
7 | private float maxP;
8 |
9 | private List candidateNames;
10 | private List candidatePs;
11 |
12 | public TokenRecogOutput(String winner, float maxP, List candidateNames, List candiatePs) {
13 | this.winner = winner;
14 | this.maxP = maxP;
15 | this.candidateNames = candidateNames;
16 | this.candidatePs = candiatePs;
17 | }
18 |
19 | public String getWinner() {
20 | return winner;
21 | }
22 |
23 | public float getMaxP() {
24 | return maxP;
25 | }
26 |
27 | public List getCandidateNames() {
28 | return candidateNames;
29 | }
30 |
31 | public List getCandidatePs() {
32 | return candidatePs;
33 | }
34 |
35 | public double[] getCandidatePsAsDoubleArray() {
36 | double[] ps = new double[candidatePs.size()];
37 |
38 | for (int i = 0; i < candidatePs.size(); ++i) {
39 | ps[i] = (double) candidatePs.get(i);
40 | }
41 |
42 | return ps;
43 | }
44 |
45 | public float[] getCandidatePsAsFloatArray() {
46 | float[] ps = new float[candidatePs.size()];
47 |
48 | for (int i = 0; i < candidatePs.size(); ++i) {
49 | ps[i] = (float) candidatePs.get(i);
50 | }
51 |
52 | return ps;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/TokenUuidUtils.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting;
2 |
3 |
4 | import org.apache.commons.lang.RandomStringUtils;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | public class TokenUuidUtils {
10 | public static String getRandomTokenUuid() {
11 | return RandomStringUtils.random(8, true, true);
12 | }
13 |
14 | public static List getRandomTokenUuids(int n) {
15 | List r = new ArrayList<>();
16 |
17 | ((ArrayList) r).ensureCapacity(n);
18 |
19 | for (int i = 0; i < n; ++i) {
20 | r.add(getRandomTokenUuid());
21 | }
22 |
23 | return r;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/ml/DataSet.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.ml;
2 |
3 | import java.util.ArrayList;
4 | import java.util.HashSet;
5 | import java.util.List;
6 | import java.util.Set;
7 |
8 | public class DataSet {
9 | /* Members */
10 | private List X;
11 | private List y;
12 |
13 | private List labelNames;
14 |
15 | /* Constructor */
16 | public DataSet() {
17 | X = new ArrayList<>();
18 | y = new ArrayList<>();
19 | }
20 |
21 | /* Methods */
22 | public void addAll(DataSet that) {
23 | this.X.addAll(that.X);
24 | this.y.addAll(that.y);
25 | }
26 |
27 | public void addSample(float[] tX, int ty) {
28 | X.add(tX);
29 | y.add(ty);
30 | }
31 |
32 | public int numSamples() {
33 | assert(y.size() == X.size());
34 | return y.size();
35 | }
36 |
37 | public boolean isEmpty() {
38 | return numSamples() == 0;
39 | }
40 |
41 | public List getX() {
42 | return X;
43 | }
44 |
45 | public List getY() {
46 | return y;
47 | }
48 |
49 | public Set getYSet() {
50 | Set ySet = new HashSet<>();
51 |
52 | ySet.addAll(y);
53 |
54 | return ySet;
55 | }
56 |
57 | public int numUniqueYs() {
58 | return getYSet().size();
59 | }
60 |
61 | public List getLabelNames() {
62 | return labelNames;
63 | }
64 |
65 | public void setLabelNames(List labelNames) {
66 | this.labelNames = labelNames;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/ml/DataSetWithStringLabels.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.ml;
2 |
3 | import java.util.ArrayList;
4 | import java.util.HashSet;
5 | import java.util.List;
6 | import java.util.Set;
7 |
8 | public class DataSetWithStringLabels {
9 | /* Members */
10 | private List X;
11 | private List y;
12 |
13 | private List labelNames;
14 |
15 | /* Constructor */
16 | public DataSetWithStringLabels() {
17 | X = new ArrayList<>();
18 | y = new ArrayList<>();
19 | }
20 |
21 | /* Methods */
22 | public void addAll(DataSetWithStringLabels that) {
23 | this.X.addAll(that.X);
24 | this.y.addAll(that.y);
25 | }
26 |
27 | public void addSample(float[] tX, String ty) {
28 | X.add(tX);
29 | y.add(ty);
30 | }
31 |
32 | public int numSamples() {
33 | assert(y.size() == X.size());
34 | return y.size();
35 | }
36 |
37 | public boolean isEmpty() {
38 | return numSamples() == 0;
39 | }
40 |
41 | public List getX() {
42 | return X;
43 | }
44 |
45 | public List getY() {
46 | return y;
47 | }
48 |
49 | public Set getYSet() {
50 | Set ySet = new HashSet<>();
51 |
52 | ySet.addAll(y);
53 |
54 | return ySet;
55 | }
56 |
57 | public int numUniqueYs() {
58 | return getYSet().size();
59 | }
60 |
61 | public List getLabelNames() {
62 | return labelNames;
63 | }
64 |
65 | public void setLabelNames(List labelNames) {
66 | this.labelNames = labelNames;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/ml/README.md:
--------------------------------------------------------------------------------
1 | # Using the machine-learning (ML) module of Glyphoid math-handwriting-lib
2 |
3 | Below are steps to train the token engine.
4 |
5 | As preparatory steps, install git, Java 7+ and Maven 3+.
6 |
7 | ```bash
8 | # Clone and build Glyphoid/java-worker-pool
9 | git clone https://github.com/Glyphoid/java-worker-pool.git
10 | pushd java-worker-pool
11 | mvn clean install
12 | popd
13 |
14 | # Clone and build Glyphoid/java-web-utils
15 | git clone https://github.com/Glyphoid/java-web-utils.git
16 | pushd java-web-utils
17 | mvn clean install
18 | popd
19 |
20 | mkdir /tmp/glyphoid
21 |
22 | # Download token data from git hub.
23 | pushd /tmp/glyphoid
24 | git clone https://github.com/Glyphoid/token-data.git
25 | mkdir intm_data
26 | mkdir token_engine
27 | popd
28 |
29 | # Build and run math-handwriting-engine's TrainTokenRecogEngineSDV main class.
30 | git clone https://github.com/Glyphoid/math-handwriting-lib.git
31 | pushd math-handwriting-lib
32 |
33 | export TOKEN_DIR="/tmp/glyphoid/token-data"
34 | export DATA_DIR="/tmp/glyphoid/intm_data"
35 | export ENGINE_DIR="/tmp/glyphoid/token_engine"
36 | export NEW_IMAGE_SIZE=32
37 |
38 | mvn package -DskipTests && mvn exec:java \
39 | -Dexec.args="--token_dir ${TOKEN_DIR} "\
40 | "--data_dir ${DATA_DIR} "\
41 | "--engine_dir ${ENGINE_DIR} "\
42 | "--new_image_size ${NEW_IMAGE_SIZE}"
43 |
44 | popd
45 | ```
46 |
47 | If you just need to generate the intermediate data files without training the
48 | token engine with the encog-based java code, you can use the flag
49 | `--generate_intermediate_data_only`:
50 |
51 | ```bash
52 | mvn package -DskipTests && mvn exec:java \
53 | -Dexec.args="--token_dir ${TOKEN_DIR} "\
54 | "--data_dir ${DATA_DIR} "\
55 | "--engine_dir ${ENGINE_DIR} "\
56 | "--new_image_size ${NEW_IMAGE_SIZE} "
57 | "--generate_intermediate_data_only true"
58 | ```
59 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/remote/TokenRecogRemoteEngine.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.remote;
2 |
3 | import me.scai.handwriting.CWrittenToken;
4 | import me.scai.handwriting.TokenRecogOutput;
5 |
6 | public interface TokenRecogRemoteEngine {
7 | /**
8 | * Recognize CWrittenToken
9 | * @param wt Written token instance
10 | * @return Recognizer output
11 | */
12 | TokenRecogOutput recognize(CWrittenToken wt) throws TokenRecogRemoteEngineException;
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/remote/TokenRecogRemoteEngineException.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.remote;
2 |
3 | public class TokenRecogRemoteEngineException extends Exception {
4 | public TokenRecogRemoteEngineException(String msg) {
5 | super(msg);
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/remote/TokenRecogRemoteEngineImpl.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.remote;
2 |
3 | import com.google.api.client.http.GenericUrl;
4 | import com.google.gson.JsonArray;
5 | import com.google.gson.JsonObject;
6 | import com.google.gson.JsonPrimitive;
7 | import me.scai.handwriting.CWrittenToken;
8 | import me.scai.handwriting.TokenRecogOutput;
9 | import me.scai.handwriting.ml.MachineLearningHelper;
10 | import me.scai.handwriting.tokens.TokenSettings;
11 | import me.scai.network.webutils.JsonWebClient;
12 | import me.scai.network.webutils.exceptions.AllAttemptsFailedException;
13 |
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | public class TokenRecogRemoteEngineImpl implements TokenRecogRemoteEngine {
18 | /* Constants */
19 | private static final String HTTP_METHOD = "POST";
20 |
21 | /* Member variables */
22 | private GenericUrl url;
23 | private TokenSettings tokenSettings;
24 |
25 | /* Constructor */
26 | public TokenRecogRemoteEngineImpl(String url, TokenSettings tokenSettings) {
27 | this.url = new GenericUrl(url);
28 | this.tokenSettings = tokenSettings;
29 | }
30 |
31 | @Override
32 | public TokenRecogOutput recognize(CWrittenToken wt)
33 | throws TokenRecogRemoteEngineException {
34 | float[] x = MachineLearningHelper.getSdveVector(wt, tokenSettings);
35 |
36 | // Construct JSON object for request
37 | JsonArray featureVector = new JsonArray();
38 | for (int i = 0; i < x.length; ++i) {
39 | featureVector.add(new JsonPrimitive(x[i]));
40 | }
41 |
42 | JsonObject reqObj = new JsonObject();
43 | reqObj.add("featureVector", featureVector);
44 |
45 | JsonObject respObj = null;
46 | try {
47 | respObj = JsonWebClient.sendRequestAndGetResponseWithRepeats(url, HTTP_METHOD, reqObj);
48 | } catch (AllAttemptsFailedException e) {
49 | throw new TokenRecogRemoteEngineException("All retries have failed: " + e.getMessage());
50 | }
51 |
52 | // Create the return object
53 | String winner = respObj.get("winnerTokenName").getAsString();
54 |
55 | List candidateNames = new ArrayList<>();
56 | List candidatePs = new ArrayList<>();
57 |
58 | JsonArray recogPs = respObj.get("recogPVals").getAsJsonArray();
59 | for (int i = 0; i < recogPs.size(); ++i) {
60 | JsonArray candArray = recogPs.get(i).getAsJsonArray();
61 |
62 | candidateNames.add(candArray.get(0).getAsString());
63 | candidatePs.add(candArray.get(1).getAsFloat());
64 | }
65 |
66 | // Assume: Descending order of P-value
67 | TokenRecogOutput output = new TokenRecogOutput(winner, candidatePs.get(0), candidateNames, candidatePs);
68 |
69 | return output;
70 | }
71 |
72 | public static void main(String[] args) { //DEBUG
73 | final String url = "http://127.0.0.1:11610/glyphoid/token-recog";
74 |
75 | // TODO: Hard-coded tokens and token denegeracy
76 | TokenSettings tokenSettings = new TokenSettings(false, true, true, null, 16, 4, null);
77 |
78 | String testJSON = "{\"numStrokes\":2,\"strokes\":{\"0\":{\"numPoints\":22,\"x\":[106,109,120,127,136,150,168,205,246,267,285,325,342,357,370,384,415,427,439,441,448,443],\"y\":[182,184,185,187,188,190,193,199,205,206,209,212,214,215,217,217,218,218,218,220,220,220]},\"1\":{\"numPoints\":23,\"x\":[284,282,279,278,276,276,276,276,276,276,277,277,279,279,280,280,280,282,282,282,281,281,281],\"y\":[75,75,82,89,98,110,124,151,164,181,196,212,242,257,271,281,292,307,310,314,323,328,329]}}}";
79 |
80 | CWrittenToken wt = new CWrittenToken(testJSON);
81 |
82 | TokenRecogRemoteEngine remoteEngine = new TokenRecogRemoteEngineImpl(url, tokenSettings);
83 |
84 | try {
85 | remoteEngine.recognize(wt);
86 | } catch (TokenRecogRemoteEngineException e) {
87 | //TODO
88 | }
89 | }
90 |
91 | }
92 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/tokens/TokenFileSettings.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.tokens;
2 |
3 | public class TokenFileSettings {
4 | public static final String WT_FILE_PREFIX = "L_";
5 | public static final String WT_FILE_SUFFIX = ".wt";
6 | public static final String IM_FILE_PREFIX = "L_";
7 | public static final String IM_FILE_SUFFIX = ".im";
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/tokens/TokenSettings.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.tokens;
2 |
3 | import me.scai.handwriting.TokenDegeneracy;
4 |
5 | import java.util.Arrays;
6 |
7 | public class TokenSettings {
8 | /* Member variables */
9 | private boolean includeTokenSize;
10 | private boolean includeTokenWHRatio;
11 | private boolean includeTokenNumStrokes;
12 |
13 | private String [] hardCodedTokens;
14 |
15 | // Number of discrete points per token used when generating stroke direction vector (SDV)
16 | private int npPerStroke;
17 |
18 | // Maximum number of strokes
19 | private int maxNumStrokes;
20 |
21 | private TokenDegeneracy tokenDegeneracy;
22 |
23 | /* Constructor */
24 | public TokenSettings(boolean includeTokenSize,
25 | boolean includeTokenWHRatio,
26 | boolean includeTokenNumStrokes,
27 | String[] hardCodedTokens,
28 | int npPerStroke,
29 | int maxNumStrokes,
30 | TokenDegeneracy tokenDegeneracy) {
31 | this.includeTokenSize = includeTokenSize;
32 | this.includeTokenWHRatio = includeTokenWHRatio;
33 | this.includeTokenNumStrokes = includeTokenNumStrokes;
34 | this.hardCodedTokens = hardCodedTokens;
35 | this.npPerStroke = npPerStroke;
36 | this.maxNumStrokes = maxNumStrokes;
37 | this.tokenDegeneracy = tokenDegeneracy;
38 | }
39 |
40 | /* Getters */
41 | public boolean isIncludeTokenSize() {
42 | return includeTokenSize;
43 | }
44 |
45 | public boolean isIncludeTokenWHRatio() {
46 | return includeTokenWHRatio;
47 | }
48 |
49 | public boolean isIncludeTokenNumStrokes() {
50 | return includeTokenNumStrokes;
51 | }
52 |
53 | public String[] getHardCodedTokens() {
54 | return hardCodedTokens;
55 | }
56 |
57 | public int getNpPerStroke() {
58 | return npPerStroke;
59 | }
60 |
61 | public int getMaxNumStrokes() {
62 | return maxNumStrokes;
63 | }
64 |
65 | public TokenDegeneracy getTokenDegeneracy() {
66 | return tokenDegeneracy;
67 | }
68 |
69 | /* Other methods */
70 | public boolean isTokenHardCoded(String token) {
71 | if (hardCodedTokens == null) {
72 | return false;
73 | } else {
74 | return Arrays.asList(hardCodedTokens).indexOf(token) != -1;
75 | }
76 |
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/utils/DataIOHelper.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.utils;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.io.PrintWriter;
6 | import java.util.List;
7 |
8 | public class DataIOHelper {
9 | public static void printFloatDataToCsvFile(List data, File f) {
10 | PrintWriter pw = null;
11 | try {
12 | pw = new PrintWriter(f);
13 | for (float[] dataRow : data) {
14 | for (int i = 0; i < dataRow.length; ++i) {
15 | pw.printf("%.9f", dataRow[i]);
16 | if (i < dataRow.length - 1) {
17 | pw.print(",");
18 | }
19 | }
20 | pw.print("\n");
21 | }
22 |
23 | } catch (IOException e) {
24 | throw new RuntimeException(e.getMessage());
25 | } finally {
26 | pw.close();
27 | }
28 | }
29 |
30 | public static void printLabelsDataToOneHotCsvFile(List labels, int nLabels, File f) {
31 | PrintWriter pw = null;
32 | try {
33 | pw = new PrintWriter(f);
34 | for (int label : labels) {
35 | for (int i = 0; i < nLabels; ++i) {
36 | pw.print(i == label ? "1" : "0");
37 |
38 | if (i < nLabels - 1) {
39 | pw.print(",");
40 | }
41 | }
42 |
43 | pw.print("\n");
44 | }
45 |
46 | } catch (IOException e) {
47 | throw new RuntimeException(e.getMessage());
48 | } finally {
49 | pw.close();
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/handwriting/utils/LimitedStack.java:
--------------------------------------------------------------------------------
1 | package me.scai.handwriting.utils;
2 |
3 |
4 | import java.util.LinkedList;
5 |
6 | public class LimitedStack {
7 | private LinkedList stack = new LinkedList<>();
8 | private int capacity;
9 |
10 | /* Constructors */
11 | public LimitedStack(int capacity) {
12 | if (capacity < 0) {
13 | throw new IllegalArgumentException("Negative capacity is illegal");
14 | }
15 |
16 | this.capacity = capacity;
17 | }
18 |
19 | public void push(T obj) {
20 | stack.push(obj);
21 |
22 | if (stack.size() > capacity) {
23 | stack.removeLast();
24 | }
25 | }
26 |
27 | public T pop() {
28 | return stack.pop();
29 | }
30 |
31 | public T peek() {
32 | return stack.peek();
33 | }
34 |
35 | public T get(int i) {
36 | return stack.get(i);
37 | }
38 |
39 | public boolean isEmpty() {
40 | return stack.isEmpty();
41 | }
42 |
43 | public int getCapacity() {
44 | return capacity;
45 | }
46 |
47 | public int size() {
48 | return stack.size();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/HandwritingEngineException.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | /**
4 | * Created by scai on 5/8/2015.
5 | */
6 | public class HandwritingEngineException extends Exception {
7 | public HandwritingEngineException(String msg) {
8 | super(msg);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/ITokenSetParser.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | //import me.scai.handwriting.CAbstractWrittenTokenSet;
4 | import me.scai.handwriting.CWrittenTokenSetNoStroke;
5 | import me.scai.handwriting.NodeToken;
6 |
7 | public interface ITokenSetParser {
8 | /**
9 | * Parse a token set and return the node
10 | * @param tokenSet The input token set
11 | * @return
12 | * @throws TokenSetParserException
13 | * @throws InterruptedException
14 | */
15 | Node parse(CWrittenTokenSetNoStroke tokenSet)
16 | throws TokenSetParserException, InterruptedException; /* Return reference to root node */
17 |
18 | /**
19 | * Enable or disabled production;
20 | * @param prodIdx: 0-based production index as it appears in the production configuration file (e.g,. productions.txt)
21 | */
22 | void setProductionEnabled(int prodIdx);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/Node.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import java.util.Arrays;
4 |
5 | public class Node {
6 | // private float x_min = 0.0f, y_min = 0.0f, x_max = 0.0f, y_max = 0.0f; /* Location information */
7 | private boolean isTerminal = true;
8 | public String lhs;
9 | public String prodSumString; /* Production summary string. See GraphicalProduction.sumString */
10 | public String termName; /* Terminal name: applies only to terminal nodes, e.g., EPS, 3 */
11 | // public String auxTermName; /* Used by classes such as the evaluator to store auxiliary info
12 | // such as internal variable names during function calls */
13 | public String [] rhsTypes; /* Child types: applies only to non-terminal nodes */
14 |
15 | private float [] bounds; /* {x_min, y_min, x_max, y_max} */
16 |
17 | private float geometricScore = 0.0f;
18 |
19 | int nc = 0; /* Number of children */
20 |
21 | Node p = null; /* Parent */
22 | public Node [] ch = null; /* Children */
23 |
24 | /* Constructors */
25 | /* Default constructor: terminal node */
26 | public Node() {
27 | isTerminal = true;
28 | prodSumString = null;
29 |
30 | nc = 0;
31 | ch = null;
32 | p = null;
33 | }
34 |
35 | /* Terminal node with the parent specified */
36 | // public Node(Node t_p) {
37 | // this();
38 | // p = t_p;
39 | // }
40 |
41 | /* Non-terminal (NT) node with production summary string, parent and children specified */
42 | public Node(String t_lhs, String t_prodSumString, Node t_p, Node [] t_ch) {
43 | assert(t_prodSumString.length() >= 0);
44 |
45 | lhs = t_lhs;
46 | isTerminal = false;
47 | prodSumString = t_prodSumString;
48 | p = t_p;
49 | nc = t_ch.length;
50 | ch = t_ch;
51 | }
52 |
53 | /* Copy constructor */
54 | public Node(Node n0) {
55 | this.isTerminal = n0.isTerminal;
56 | this.lhs = n0.lhs;
57 | this.prodSumString = n0.prodSumString;
58 | this.termName = n0.termName;
59 |
60 | if (n0.rhsTypes != null) {
61 | this.rhsTypes = Arrays.copyOf(n0.rhsTypes, n0.rhsTypes.length);
62 | }
63 | if (n0.bounds != null) {
64 | this.bounds = Arrays.copyOf(n0.bounds, n0.bounds.length);
65 | }
66 |
67 | this.geometricScore = n0.geometricScore;
68 | this.nc = n0.nc;
69 |
70 | // this.p = n0.p;
71 | /* TODO: This may be problematic. We want p to point to the copy, not the original. */
72 |
73 | if (n0.ch != null) {
74 | this.ch = new Node[n0.ch.length];
75 |
76 | for (int i = 0; i < this.ch.length; ++i) {
77 | if (n0.ch[i] != null) {
78 | this.ch[i] = new Node(n0.ch[i]);
79 | this.ch[i].p = this; /* Order of things matter. See comment above. */
80 | }
81 | }
82 | }
83 | }
84 |
85 | /* Non-Terminal (NT) node with production summary string specified */
86 | public Node(String t_lhs, String t_prodSumString, String [] t_rhsTypes) {
87 | lhs = t_lhs;
88 | isTerminal = false;
89 | prodSumString = t_prodSumString;
90 | rhsTypes = t_rhsTypes;
91 | p = null;
92 | // nc = 0;
93 | nc = t_rhsTypes.length;
94 | ch = new Node[t_rhsTypes.length];
95 | }
96 |
97 | /* Terminal (T) node with production summary string specified */
98 | public Node(String t_lhs, String t_prodSumString, String t_termName, float [] t_bounds) {
99 | lhs = t_lhs;
100 | isTerminal = true; /* Will be set to false when addChild() is called */
101 | prodSumString = t_prodSumString;
102 |
103 | if (TerminalSet.isTerminalNameType(t_termName)) {
104 | termName = TerminalSet.getTerminalNameTypeTokenName(t_termName);
105 | }
106 | else {
107 | termName = t_termName;
108 | }
109 | p = null;
110 | nc = 0;
111 | ch = null;
112 |
113 | bounds = t_bounds;
114 | }
115 |
116 | /* Non-terminal (NT) node with production summary string and children specified */
117 | public Node(String t_lhs, String t_prodSumString, Node [] t_ch) {
118 | assert(t_prodSumString.length() >= 0);
119 |
120 | lhs = t_lhs;
121 | isTerminal = false;
122 | prodSumString = t_prodSumString;
123 | p = null;
124 | nc = t_ch.length;
125 | ch = t_ch;
126 | }
127 |
128 | /* Property getters */
129 | public boolean isTerminal() {
130 | return isTerminal;
131 | }
132 |
133 | public int numChildren() {
134 | return nc;
135 | }
136 |
137 | public void setChild(int ic, Node child) {
138 | ch[ic] = child;
139 | }
140 |
141 | // public void addChild(Node newChild) {
142 | // if ( isTerminal )
143 | // isTerminal = false;
144 | //
145 | // Node [] chOld = ch;
146 | // if ( chOld == null )
147 | // ch = new Node[1];
148 | // else
149 | // ch = new Node[chOld.length + 1];
150 | //
151 | // for (int i = 0; i < ch.length - 1; ++i)
152 | // ch[i] = chOld[i];
153 | // ch[ch.length - 1] = newChild;
154 | //
155 | // nc++;
156 | // }
157 |
158 | public void setRHSTypes(String [] t_rhsTypes) {
159 | rhsTypes = t_rhsTypes;
160 | }
161 |
162 | public String [] getRHSTypes() {
163 | return rhsTypes;
164 | }
165 |
166 | @Override
167 | public String toString() {
168 | String s = "Node (";
169 | if ( prodSumString != null ) {
170 | s += prodSumString;
171 | }
172 | s += ")";
173 |
174 | if ( isTerminal ) {
175 | s += "(T: " + termName + ")";
176 | }
177 | else {
178 | s += "(NT)";
179 | }
180 |
181 | return s;
182 | }
183 |
184 | public void setGeometricScore(float gs) {
185 | geometricScore = gs;
186 | }
187 |
188 | public float getGeometricScore() {
189 | return geometricScore;
190 | }
191 |
192 | public void setBounds(float[] bounds) {
193 | this.bounds = bounds;
194 | }
195 |
196 | public float [] getBounds() {
197 | return bounds;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/NodeAnalyzer.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import me.scai.parsetree.geometry.GeometryHelper;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | public class NodeAnalyzer {
9 | /**
10 | * Get all graphical productions that the node can represent.
11 | * For example if the grammar consists of:
12 | * root --> gpA,
13 | * gpA --> gpB,
14 | * gpB --> gpC,
15 | * gpC --> gpD,
16 | * gpD --> [terminalA, terminalB]
17 | * then a node of the typca: root --> gpA --> gpB --> grpC --> grD can potentially represent
18 | * gpA, gpB, gpC or gpD.
19 | * Algorithm: starting from the root node, follow the node tree until we reach the first node
20 | * which has more than one children or the only child is a terminal. Add all the nodes visited
21 | * to the list to be returned
22 | * @param node Input node
23 | * @return The list of valid productions LHS (TODO: replace the string types with appropriate types for LHS)
24 | */
25 | public static List getValidProductionLHS(Node node) {
26 | List validProds = new ArrayList<>();
27 |
28 | Node n = node;
29 |
30 | while ( !n.isTerminal() &&
31 | n.numChildren() == 1 &&
32 | !n.ch[0].isTerminal() ) {
33 | validProds.add(n.lhs);
34 |
35 | n = n.ch[0];
36 | }
37 |
38 | validProds.add(n.lhs);
39 |
40 | return validProds;
41 | }
42 |
43 | /**
44 | * Calculate the bounds of a node, generally non-terminal. The algorithm merges the
45 | * bounds of all terminal tokens that belong to this node.
46 | * @param node The node
47 | * @return The total (merged) bounds of the node.
48 | * Side effect: It also calls the setBounds method of the node if the node is non-terminal.
49 | */
50 | public static float[] calcNodeBounds(Node node) {
51 | if (node.isTerminal()) {
52 | assert node.getBounds() != null;
53 |
54 | return node.getBounds();
55 | } else {
56 | float[] mergedBounds = GeometryHelper.getInitBounds();
57 |
58 | for (Node child : node.ch) {
59 | float[] childBounds = calcNodeBounds(child);
60 | mergedBounds = GeometryHelper.mergeBounds(mergedBounds, childBounds);
61 | }
62 |
63 | node.setBounds(mergedBounds);
64 |
65 | return mergedBounds;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/ParseTreeMathTexifier.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | public class ParseTreeMathTexifier {
7 | /* Member variables */
8 | /* Constants */
9 | public final static String parsingErrString = "[Parsing failed: Syntax error]";
10 | public final static String MATH_TEXIFICATION_FAILED_STRING = "[Conversion to Math Tex failed]";
11 |
12 | private Map sumString2MathTexInstrMap = new HashMap<>();/* For stringization to Math TeX noatation */
13 | private Map terminal2TexNotationMap = new HashMap<>();
14 | /* ~Member variables */
15 |
16 | /* Constructor */
17 | public ParseTreeMathTexifier(final GraphicalProductionSet gpSet,
18 | final TerminalSet termSet) {
19 | sumString2MathTexInstrMap.clear();
20 |
21 | for (int i = 0; i < gpSet.prods.size(); ++i) {
22 | GraphicalProduction gp = gpSet.prods.get(i);
23 |
24 | String t_sumString = gp.sumString;
25 | String [] t_instr = gp.mathTexInstr;
26 |
27 | sumString2MathTexInstrMap.put(t_sumString, t_instr);
28 | }
29 |
30 | /* Connect map for Math TeX notations */
31 | terminal2TexNotationMap = termSet.token2TexNotationMap;
32 | }
33 |
34 | /* Get the function name out of a instruction item such as:
35 | * "GET_VAR_TEX_NOTATION(n0)" */
36 | static String getTexFunctionName(String item) {
37 | if (item.indexOf("(") == -1 || item.indexOf(")") == -1) {
38 | return null;
39 | }
40 | else {
41 | String funcName = item.substring(0, item.indexOf("("));
42 |
43 | return funcName;
44 | }
45 | }
46 |
47 | /* Get the function name out of a instruction item such as:
48 | * "GET_VAR_TEX_NOTATION(n0)" */
49 | static int [] getTexFunctionArgIndices(String item) {
50 | if (item.indexOf("(") == -1 || item.indexOf(")") == -1) {
51 | return null;
52 | } else if (item.indexOf(")") == item.indexOf("(") + 1) { // Empty arg list
53 | return null;
54 | } else {
55 | String funcArgsStr = item.substring(item.indexOf("(") + 1, item.indexOf(")"));
56 |
57 | String [] funcArgs = funcArgsStr.split(",");
58 | int [] argIndices = new int[funcArgs.length];
59 |
60 | for (int i = 0; i < argIndices.length; ++i) {
61 | String funcArg = funcArgs[i].trim();
62 |
63 | argIndices[i] = Integer.parseInt(funcArg.replace("n", ""));
64 | }
65 |
66 | return argIndices;
67 | }
68 | }
69 |
70 | /* Input: n: root of the parse tree */
71 | /* Currently based on recursion. */
72 | public String texify(Node n) {
73 | if ( n == null ) {
74 | return MATH_TEXIFICATION_FAILED_STRING;
75 | }
76 |
77 | String s = "";
78 |
79 | String prodSumString = n.prodSumString;
80 | String [] instr = sumString2MathTexInstrMap.get(prodSumString);
81 | if ( instr == null ) {
82 | throw new RuntimeException("Cannot find the stringization instruction for: "
83 | + n.prodSumString);
84 | }
85 |
86 | for (int i = 0; i < instr.length; ++i) {
87 | String instrItem = instr[i];
88 |
89 | String texFunctionName = getTexFunctionName(instrItem);
90 |
91 | if ( texFunctionName != null ) { /* Special string */
92 | int[] chIndices = getTexFunctionArgIndices(instrItem);
93 |
94 | switch (texFunctionName) {
95 | case "GET_TEX_VAR_NOTATION":
96 | s += getTexVarNotation(n.ch[chIndices[0]].termName);
97 | break;
98 | case "GET_TEX_PLUS_OP":
99 | s += getTexPlusOp(n.ch[chIndices[0]].termName);
100 | break;
101 | case "GET_TEX_MINUS_OP":
102 | s += getTexMinusOp(n.ch[chIndices[0]].termName);
103 | break;
104 | case "GET_TEX_MULTIPLY_OP":
105 | s += getTexMultiplyOp(n.ch[chIndices[0]].termName);
106 | break;
107 | case "GET_TEX_ASSIGN_OP":
108 | s += getTexAssignOp(n.ch[chIndices[0]].termName);
109 | break;
110 | case "SPACE":
111 | s += " ";
112 | break;
113 | case "LINE_BREAK":
114 | s += "\\\\\n";
115 | break;
116 | default:
117 | throw new RuntimeException("Unrecognized function name for TeXification: \"" + texFunctionName + "\"");
118 | }
119 | // s += invokeTexFunction(texFunctionName, texFunctionNodeIdx); /* TODO: Use reflection */
120 | } else if ( instrItem.startsWith("n") ) { /* String content from the children nodes */
121 | int iNode = Integer.parseInt( instrItem.substring(1, instrItem.length()) );
122 | if ( iNode < 0 || iNode >= n.nc ) {
123 | throw new RuntimeException("Node index (" + iNode
124 | + ") exceeds number of children ("
125 | + n.nc + ")");
126 | }
127 |
128 | if ( n.ch[iNode].isTerminal() ) {
129 | s += getTexNotation(n.ch[iNode].termName);
130 | }
131 | else {
132 | s += texify(n.ch[iNode]);
133 | }
134 | }
135 | else { /* Hard-coded string content */
136 | s += instrItem;
137 | }
138 |
139 | }
140 |
141 | return s;
142 | }
143 |
144 | /* Get the Math TeX notation from terminal name */
145 | private String getTexNotation(String term) {
146 | boolean contains = terminal2TexNotationMap.containsKey(term);
147 |
148 | if (contains) {
149 | return terminal2TexNotationMap.get(term);
150 | }
151 | else {
152 | return term;
153 | }
154 | }
155 |
156 | private String getTexVarNotation(String term) {
157 | return getTexNotation(term);
158 | }
159 |
160 | private String getTexMultiplyOp(String term) {
161 | if (term.equals("*")) {
162 | return "\\ast";
163 | }
164 | else if (term.equals("X")) {
165 | return "\\times";
166 | }
167 | else {
168 | return term;
169 | }
170 | }
171 |
172 | private String getTexPlusOp(String term) {
173 | return term;
174 | }
175 |
176 | private String getTexMinusOp(String term) {
177 | return term;
178 | }
179 |
180 | private String getTexAssignOp(String term) {
181 | return term;
182 | }
183 |
184 | }
185 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/ParseTreeStringizer.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | public class ParseTreeStringizer {
7 |
8 | /* Constants */
9 | public final static String STRINGIZATION_FAILED_STRING = "[Stringization failed due to syntax error]";
10 |
11 | /* Member variables */
12 | private Map sumString2InstrMap = new HashMap<>(); /* For stringization to plain computer math notation */
13 | private Map specialStringMap = new HashMap<>();
14 |
15 | private Map specialTerminalNameMap = new HashMap<>();
16 |
17 | /* ~Member variables */
18 |
19 | /* Methods */
20 |
21 | /* Constructor */
22 | public ParseTreeStringizer(final GraphicalProductionSet gpSet) {
23 | sumString2InstrMap.clear();
24 | specialStringMap.clear();
25 |
26 | /* Create map of special strings: Strinigization instructions */
27 | specialStringMap.put("_SPACE_", " ");
28 | specialStringMap.put("_UNDERSCORE_", "_");
29 | specialStringMap.put("_OPEN_PAREN_", "(");
30 | specialStringMap.put("_CLOSE_PAREN_", ")");
31 | specialStringMap.put("_LOGICAL_AND_", "&&");
32 | specialStringMap.put("_LOGICAL_OR_", "||");
33 | specialStringMap.put("_IF_", "if");
34 |
35 | /* Create map of special mapping from terminal names in grammar definition to those in the
36 | * stringization results */ ;
37 | // TODO: Externalize
38 | specialTerminalNameMap.put("lt", "<");
39 | specialTerminalNameMap.put("gt", ">");
40 | specialTerminalNameMap.put("lte", "<=");
41 | specialTerminalNameMap.put("gte", ">=");
42 |
43 | for (int i = 0; i < gpSet.prods.size(); ++i) {
44 | GraphicalProduction gp = gpSet.prods.get(i);
45 |
46 | String t_sumString = gp.sumString;
47 | String [] t_instr = gp.stringizeInstr;
48 |
49 | sumString2InstrMap.put(t_sumString, t_instr);
50 | }
51 | }
52 |
53 | /* Input: n: root of the parse tree */
54 | /* Currently based on recursion. */
55 | public String stringize(Node n) {
56 | if ( n == null ) {
57 | return STRINGIZATION_FAILED_STRING;
58 | }
59 |
60 | String s = "";
61 |
62 | String prodSumString = n.prodSumString;
63 | String [] instr = sumString2InstrMap.get(prodSumString);
64 | if ( instr == null ) {
65 | throw new RuntimeException("Cannot find the stringization instruction for: "
66 | + n.prodSumString);
67 | }
68 |
69 | for (int i = 0; i < instr.length; ++i) {
70 | if ( specialStringMap.containsKey(instr[i]) ) { /* Special string */
71 | s += specialStringMap.get(instr[i]);
72 | }
73 | else if ( instr[i].startsWith("n") ) { /* String content from the children nodes */
74 | int iNode = Integer.parseInt( instr[i].substring(1, instr[i].length()) );
75 | if ( iNode < 0 || iNode >= n.nc ) {
76 | throw new RuntimeException("Node index (" + iNode
77 | + ") exceeds number of children ("
78 | + n.nc + ")");
79 | }
80 |
81 | if ( n.ch[iNode].isTerminal() ) {
82 | Node chNode = n.ch[iNode];
83 | final String termName = chNode.termName;
84 | final String mappedTermName = specialTerminalNameMap.containsKey(termName) ?
85 | specialTerminalNameMap.get(termName) :
86 | termName;
87 |
88 | s += mappedTermName;
89 | }
90 | else {
91 | s += stringize(n.ch[iNode]);
92 | }
93 | }
94 | else { /* Hard-coded string content */
95 | s += instr[i];
96 | }
97 |
98 | }
99 |
100 | return s;
101 | }
102 |
103 | /* ~Methods */
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/TextHelper.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import java.io.BufferedReader;
4 | import java.io.File;
5 | import java.io.FileInputStream;
6 | import java.io.FileNotFoundException;
7 | import java.io.IOException;
8 | import java.io.InputStreamReader;
9 | import java.net.URL;
10 | import java.util.ArrayList;
11 | import java.util.Arrays;
12 |
13 | public class TextHelper {
14 | public static String readTextFile(String fileName)
15 | throws IOException {
16 | // return new String(Files.readAllBytes(Paths.get(fileName)), StandardCharsets.UTF_8); /* Java 7 approach, which doesn't seem to work in Android 4.1 */
17 | File file = new File(fileName);
18 | FileInputStream fis = new FileInputStream(file);
19 | byte [] data = new byte[(int) file.length()];
20 | fis.read(data);
21 | fis.close();
22 |
23 | return new String(data, "UTF-8");
24 | }
25 |
26 | public static String readTextFileAtUrl(final URL url)
27 | throws IOException {
28 | BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
29 | StringBuilder sb = new StringBuilder();
30 |
31 | String inputLine;
32 | while ((inputLine = br.readLine()) != null) {
33 | sb.append(inputLine + "\n");
34 | }
35 |
36 | br.close();
37 | return sb.toString();
38 |
39 | }
40 |
41 |
42 |
43 | private static String [] readLinesTrimmedNoCommmentFromBufferedReader(final BufferedReader in, final String commentString)
44 | throws IOException {
45 | ArrayList lineList = new ArrayList();
46 | String line;
47 | while ( in.ready() ) {
48 | line = in.readLine();
49 | if (line == null) {
50 | continue;
51 | }
52 |
53 | line = line.trim();
54 |
55 | if ( line.startsWith(commentString) )
56 | continue;
57 |
58 | if ( line.contains(commentString) ) {
59 | String [] items = line.split(commentString);
60 | line = items[0].trim();
61 | }
62 |
63 | lineList.add(line);
64 | }
65 |
66 | in.close();
67 |
68 | String [] lines = new String[lineList.size()];
69 |
70 | lineList.toArray(lines);
71 |
72 | return lines;
73 | }
74 |
75 | /* Read lines from a file name, with comments removed and white spaces trimmd */
76 | public static String [] readLinesTrimmedNoComment(final String fileName, final String commentString)
77 | throws FileNotFoundException, IOException {
78 | File wtsFile = new File(fileName);
79 | if ( !wtsFile.isFile() )
80 | throw new FileNotFoundException("Cannot find file for reading: " + fileName);
81 |
82 | FileInputStream fin = null;
83 | BufferedReader in = null;
84 | try {
85 | fin = new FileInputStream(wtsFile);
86 | in = new BufferedReader(new InputStreamReader(fin));
87 | }
88 | catch ( IOException e ) {
89 | throw new IOException("IOException during reading of text file: " + fileName);
90 | }
91 |
92 | return readLinesTrimmedNoCommmentFromBufferedReader(in, commentString);
93 | }
94 |
95 | /* Read lines from a file name, with comments removed and white spaces trimmd */
96 | public static String [] readLinesTrimmedNoCommentFromUrl(final URL fileUrl, final String commentString)
97 | throws IOException {
98 | BufferedReader in = null;
99 | try {
100 | in = new BufferedReader(new InputStreamReader(fileUrl.openStream()));
101 | }
102 | catch ( IOException e ) {
103 | throw new IOException("IOException during reading from URL: \"" + fileUrl + "\"");
104 | }
105 |
106 | return readLinesTrimmedNoCommmentFromBufferedReader(in, commentString);
107 | }
108 |
109 |
110 | public static String [] removeTrailingEmptyLines(String [] lines) {
111 | ArrayList linesList = new ArrayList(Arrays.asList(lines));
112 | boolean bEndEmptyLine = linesList.get(linesList.size() - 1).equals("");
113 | while ( bEndEmptyLine ) {
114 | linesList.remove(linesList.size() - 1);
115 |
116 | bEndEmptyLine = linesList.get(linesList.size() - 1).equals("");
117 | }
118 |
119 | String [] newLines = new String[linesList.size()];
120 | linesList.toArray(newLines);
121 |
122 | return newLines;
123 | }
124 |
125 | public static int [] findAll(final String s, final String subs) {
126 | if ( s.length() == 0 || subs.length() == 0 )
127 | return null;
128 |
129 | ArrayList indices = new ArrayList();
130 | int i = 0;
131 | int len = subs.length();
132 |
133 | int i0;
134 | while ( (i0 = s.substring(i, s.length()).indexOf(subs, i)) != -1 ) {
135 | indices.add(i0 + i);
136 | i += s.indexOf(subs, i) + len;
137 | }
138 |
139 | int [] r = new int[indices.size()];
140 | for (int n = 0; n < indices.size(); ++n)
141 | r[n] = indices.get(n);
142 |
143 | return r;
144 | }
145 |
146 | public static int numInstances(final String s, final String subs) {
147 | if ( s.length() == 0 || subs.length() == 0 )
148 | return 0;
149 |
150 | int nInstances = 0;
151 | int i = 0;
152 | int len = subs.length();
153 |
154 | while ( s.indexOf(subs, i) != -1 ) {
155 | nInstances++;
156 | i += s.indexOf(subs, i) + len;
157 | }
158 |
159 | return nInstances;
160 |
161 | }
162 | }
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/TokenSet2NodeTokenParser.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | import me.scai.handwriting.AbstractToken;
4 | import me.scai.handwriting.CWrittenTokenSetNoStroke;
5 | import me.scai.handwriting.NodeToken;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | public class TokenSet2NodeTokenParser {
11 | // Member variables
12 | final private ITokenSetParser parser;
13 | final private ParseTreeStringizer stringizer;
14 |
15 | // Constructor
16 | public TokenSet2NodeTokenParser(ITokenSetParser parser, ParseTreeStringizer stringizer) {
17 | this.parser = parser;
18 | this.stringizer = stringizer;
19 | }
20 |
21 | // Methods
22 |
23 | /**
24 | * Parse an entire token set and return a node token
25 | * @param tokenSet The input token set. It will not be modified.
26 | * @return The NodeToken, with node, wtSet, recognition, and possibly other fields set.
27 | */
28 | public NodeToken parse2NodeToken(CWrittenTokenSetNoStroke tokenSet)
29 | throws TokenSetParserException, InterruptedException {
30 | Node node = parser.parse(tokenSet);
31 |
32 | if (node == null) {
33 | throw new TokenSetParserException("Generation of NodeToken from token set failed due to failure to parse the token set");
34 | }
35 |
36 | NodeToken nodeToken = new NodeToken(node, tokenSet);
37 |
38 | // Add the recognition result
39 | nodeToken.setRecogResult(stringizer.stringize(node));
40 |
41 | return nodeToken;
42 | }
43 |
44 | /**
45 | * Parse a subset of the tokens in a token set and turn them into a NodeToken. The remaining tokens are
46 | * preserved. The NodeToken is placed at the front of the token list of the token set, after all the other node
47 | * tokens that already exist.
48 | * @param tokenSet Input token set. It is not meant to be modified. Instead, a new token set will be returned.
49 | * @param tokenIndices Indices to the tokens to be parsed into a node token (0-based)
50 | * @param nodeTokenConstituentUuids UUIDs of the tokens the make up the new resultant NodeToken (if subset parsing is successful)
51 | * and the rest of the returned token set.
52 | * Optional (can be null). Passed as reference if not null.
53 | * @return Token set with the NodeToken and the un-parsed tokens included
54 | */
55 | public CWrittenTokenSetNoStroke parseAsNodeToken(CWrittenTokenSetNoStroke tokenSet,
56 | int[] tokenIndices,
57 | List> nodeTokenConstituentUuids)
58 | throws TokenSetParserException, InterruptedException{
59 |
60 |
61 |
62 | CWrittenTokenSetNoStroke subsetToParse = tokenSet.fromSubset(tokenIndices);
63 |
64 | NodeToken nodeToken = parse2NodeToken(subsetToParse);
65 |
66 | if (nodeToken != null) {
67 | final int origNumTokens = tokenSet.getNumTokens();
68 | final int newNumTokens = origNumTokens - tokenIndices.length + 1;
69 |
70 | AbstractToken[] newTokens = new AbstractToken[newNumTokens];
71 | List> constituentTokenUuids = new ArrayList<>();
72 |
73 | int counter = 0;
74 | newTokens[counter++] = nodeToken;
75 |
76 | // Constituent token UUIDs of the new node token
77 | List firstNodeTokenUuids = new ArrayList<>();
78 | for (int tokenIndex : tokenIndices) {
79 | List uuids = tokenSet.getConstituentTokenUuids(tokenIndex);
80 | firstNodeTokenUuids.addAll(uuids); // Note: This flattens nested node tokens. TODO: Is this appropriate?
81 | }
82 | constituentTokenUuids.add(firstNodeTokenUuids);
83 |
84 | for (int i = 0; i < origNumTokens; ++i) {
85 | if (MathHelper.find(tokenIndices, i).length == 0) { // This token is outside the subset parsed this time
86 | newTokens[counter++] = tokenSet.tokens.get(i); // TODO: Concurrency issue?
87 | constituentTokenUuids.add(tokenSet.getConstituentTokenUuids(i));
88 | }
89 | }
90 |
91 | assert (counter == newNumTokens);
92 |
93 | if (nodeTokenConstituentUuids != null) {
94 | assert(nodeTokenConstituentUuids.isEmpty());
95 | nodeTokenConstituentUuids.addAll(constituentTokenUuids);
96 | }
97 |
98 | return CWrittenTokenSetNoStroke.from(newTokens, constituentTokenUuids);
99 |
100 | } else {
101 | throw new TokenSetParserException("Subset parsing failed");
102 | }
103 | }
104 |
105 | /**
106 | * Convenience form of three-arg parseAsNodeToken
107 | * @param tokenSet
108 | * @param tokenIndices
109 | * @return
110 | * @throws TokenSetParserException
111 | * @throws InterruptedException
112 | */
113 | public CWrittenTokenSetNoStroke parseAsNodeToken(CWrittenTokenSetNoStroke tokenSet,
114 | int[] tokenIndices)
115 | throws TokenSetParserException, InterruptedException {
116 |
117 | return parseAsNodeToken(tokenSet, tokenIndices, null);
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/TokenSetParserException.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | public class TokenSetParserException extends Exception {
4 | public TokenSetParserException() {
5 | super();
6 | }
7 |
8 | public TokenSetParserException(String message) {
9 | super(message);
10 | }
11 | }
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/TokenSetParserOutput.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree;
2 |
3 | public class TokenSetParserOutput {
4 | /* TODO: Add parse tree */
5 | private String stringizerOutput;
6 | private String evaluatorOutput;
7 | private String mathTex;
8 |
9 | private String errorMsg;
10 |
11 | public TokenSetParserOutput(String stringizerOutput, String evaluatorOutput, String mathTex) {
12 | this.stringizerOutput = stringizerOutput;
13 | this.evaluatorOutput = evaluatorOutput;
14 | this.mathTex = mathTex;
15 | }
16 |
17 | public TokenSetParserOutput(String errorMsg) {
18 | this.errorMsg = errorMsg;
19 | }
20 |
21 | /* Getters */
22 | public String getStringizerOutput() {
23 | return stringizerOutput;
24 | }
25 |
26 | public String getEvaluatorOutput() {
27 | return evaluatorOutput;
28 | }
29 |
30 | public String getMathTex() {
31 | return mathTex;
32 | }
33 |
34 | public String getErrorMsg() {
35 | return errorMsg;
36 | }
37 |
38 | /* Setters */
39 | public void setStringizerOutput(String stringizerOutput) {
40 | this.stringizerOutput = stringizerOutput;
41 | }
42 |
43 | public void setEvaluatorOutput(String evaluatorOutput) {
44 | this.evaluatorOutput = evaluatorOutput;
45 | }
46 |
47 | public void setMathTex(String mathTex) {
48 | this.mathTex = mathTex;
49 | }
50 |
51 | public void setErrorMsg(String errorMsg) {
52 | this.errorMsg = errorMsg;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/evaluation/ArgumentRange.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree.evaluation;
2 |
3 | import java.util.List;
4 |
5 | interface ArgumentRange {
6 | public List getValues(); /* TODO: Parameterize the type */
7 | }
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/evaluation/DefiniteIntegralTerm.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree.evaluation;
2 |
3 | import me.scai.parsetree.evaluation.exceptions.ParseTreeEvaluatorException;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | public class DefiniteIntegralTerm extends SigmaPiIntegralTerm {
9 | public DefiniteIntegralTerm(String tFunctionName,
10 | FunctionArgumentList tArgList,
11 | List tArgRanges) {
12 | super(tFunctionName, tArgList, tArgRanges);
13 | }
14 |
15 | @Override
16 | public Object evaluate(ParseTreeEvaluator evaluator,
17 | List tempArgNames)
18 | throws ParseTreeEvaluatorException {
19 | if (tempArgNames.size() != argList.numArgs()) {
20 | throw new RuntimeException("Incorrect number of temporary argument names");
21 | }
22 |
23 | if (!isDefined()) {
24 | throw new RuntimeException(
25 | "The body of this definite integral is not defined"); /* TODO: More specific exception type */
26 | }
27 |
28 | int numArgs = argList.numArgs();
29 | List argSymbols = argList.getSymbolNames(); /* TODO: Use escape arg names such as _arg_1 */
30 |
31 | /* Obtain the value lists for all arguments */
32 | ArrayList> allArgVals = new ArrayList>();
33 | allArgVals.ensureCapacity(numArgs);
34 |
35 | double integUnitSize = 1.0; /* Integral unit size: General enough to support multiple integrals */
36 | ArrayList intervals = new ArrayList<>();
37 | intervals.ensureCapacity(numArgs);
38 | for (int i = 0; i < numArgs; ++i) {
39 | allArgVals.add(argumentRanges.get(i).getValues());
40 |
41 | intervals.add(((UniformIntegralArgumentRange) argumentRanges.get(0)).getInterval());
42 | integUnitSize *= intervals.get(intervals.size() - 1);
43 | }
44 |
45 | /* "Functionize" the body, i.e., replace the argument symbols with
46 | * special ones like "__stack0_funcArg1__"
47 | */
48 | int funcStackHeight = evaluator.getFuncStackHeight();
49 | EvaluatorHelper.functionizeBody(this.evalBody, funcStackHeight, argSymbols);
50 |
51 | double sum = 0.0;
52 | int numVals = allArgVals.get(0).size();
53 |
54 | if (numArgs > 1) {
55 | throw new IllegalStateException("Multiple definite integral is not implemented yet");
56 | }
57 |
58 | // Trapezoid method for 1D cases
59 | // TODO: Multi-dimensional integration
60 | // Handle the first point
61 | for (int j = 0; j < numArgs; ++j) {
62 | String argSymbol = tempArgNames.get(j);
63 | double argVal = allArgVals.get(j).get(0);
64 |
65 | evaluator.variable_assign_value(argSymbol, argVal);
66 | }
67 | Object out = evaluator.eval(this.evalBody);
68 | double leftY = (double) out;
69 |
70 | // Handle all remaining points
71 | for (int i = 0; i < numVals; ++i) {
72 | for (int j = 0; j < numArgs; ++j) {
73 | String argSymbol = tempArgNames.get(j);
74 | double argVal = allArgVals.get(j).get(i) + intervals.get(j);
75 |
76 | evaluator.variable_assign_value(argSymbol, argVal);
77 | }
78 |
79 | out = evaluator.eval(this.evalBody);
80 | double rightY = (double) out;
81 |
82 | sum += ((leftY + rightY) * integUnitSize / 2.0);
83 |
84 | leftY = rightY;
85 | }
86 |
87 | return sum;
88 | }
89 |
90 | @Override
91 | public String toString() {
92 | StringBuilder sb = new StringBuilder("DefiniteIntegralTerm: ");
93 | sb.append("(");
94 |
95 | int nArgs = argNames.size();
96 | for (int i = 0; i < nArgs; ++i) {
97 | sb.append(argNames.get(i));
98 | if (i < nArgs - 1) {
99 | sb.append(", ");
100 | }
101 | }
102 | sb.append(")");
103 |
104 | return sb.toString();
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/evaluation/EvaluatorHelper.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree.evaluation;
2 |
3 | import java.util.List;
4 | import java.util.regex.Pattern;
5 | import java.util.regex.Matcher;
6 |
7 | import me.scai.parsetree.Node;
8 |
9 | public class EvaluatorHelper {
10 | static final Pattern argNamePattern = Pattern.compile("__stack[0-9]+__funcArg[0-9]+__.*__");
11 |
12 | public static String genInternalFuncArgName(int stackHeight, int argIdx, String argOrigName) {
13 | // return String.format("__funcArg%d__", argIdx);
14 | // return genFuncArgName(stackHeight - 1, argIdx);
15 | return genFuncArgName(stackHeight - 1, argIdx, argOrigName);
16 | }
17 |
18 | public static String genFuncArgName(int stackPos, int argIdx, String argOrigName) {
19 | if (argOrigName != null) {
20 | return String.format("__stack%d__funcArg%d__%s__", stackPos, argIdx, argOrigName);
21 | } else {
22 | return String.format("__stack%d__funcArg%d__", stackPos, argIdx);
23 | }
24 | }
25 |
26 | /**
27 | * Get first encountered terminal name
28 | * @param n
29 | * @return
30 | */
31 | public static String getFirstTermName(Node n) {
32 | if (n.isTerminal()) {
33 | return n.termName;
34 | } else {
35 | for (int i = 0; i < n.ch.length; ++i) {
36 | return getFirstTermName(n.ch[i]);
37 | }
38 | }
39 |
40 | return null;
41 | }
42 |
43 | // public static String [] genFuncArgNames(int stackPos, int numArgs) {
44 | // String [] funcArgNames = new String[numArgs];
45 | //
46 | // for (int i = 0; i < numArgs; ++i) {
47 | // funcArgNames[i] = genFuncArgName(stackPos, i);
48 | // }
49 | //
50 | // return funcArgNames;
51 | // }
52 |
53 | public static String [] genFuncArgNames(int stackPos, FunctionArgumentList argList) {
54 | String [] funcArgNames = new String[argList.numArgs()];
55 |
56 | for (int i = 0; i < argList.numArgs(); ++i) {
57 | if (argList.args.get(i).getClass() == String.class){
58 | String argName = (String) argList.args.get(i); // TODO: Is this kosher?
59 | funcArgNames[i] = genFuncArgName(stackPos, i, argName);
60 | } else {
61 | funcArgNames[i] = genFuncArgName(stackPos, i, null);
62 | }
63 |
64 | }
65 |
66 | return funcArgNames;
67 | }
68 |
69 | /* Get argument index from temporary internal argument names such as
70 | * "__funcArg0__".
71 | *
72 | * @return -1 if the argument name is not temporary internal
73 | * int >= 0 if the argument name is temporary internal
74 | * */
75 | public static int getArgIdx(String name) {
76 | Matcher matcher = argNamePattern.matcher(name);
77 | // Matcher partMatcher = argNamePatternPart.matcher(name);
78 |
79 | if ( !(matcher.matches()) ) {
80 | return -1;
81 | } else {
82 | String[] parts = matcher.group(0).split("__");
83 | return Integer.valueOf(parts[2].replace("funcArg", ""));
84 | // String match = matcher.group(0);
85 | // int idx = match.indexOf("_funcArg"); // TODO: Better approach
86 | // String part = match.substring(idx);
87 | // return Integer.valueOf(part.replace("_funcArg", "").replace("__", ""));
88 | //
89 | // return Integer.valueOf(match.replace("__funcArg", "").replace("__", ""));
90 | }
91 | }
92 |
93 | /* Replace the VARIALBE --> VARIALBE_SYMBOL nodes in the parse tree
94 | * with special symbol names such as __funcArg0__ and __funcArg1__.
95 | */
96 | public static void functionizeBody(Node node, int stackHeight, List origArgNames) {
97 | if (stackHeight <= 0) {
98 | throw new IllegalStateException("Unexpected call to functionizeBody() while the function call stack is empty");
99 | }
100 |
101 | // TODO: Accommodate nested sigma/pi/integ: Do a first pass to determine what variable names are already functionized
102 | // TODO: Perhaps the functionziation needs to take into account the current stack position
103 |
104 | /* Implementation uses recursion */
105 | if (node.isTerminal()) {
106 | if (node.lhs.equals("VARIABLE") &&
107 | origArgNames.contains(node.termName)) {
108 | int argIdx = origArgNames.indexOf(node.termName);
109 | node.termName = genInternalFuncArgName(stackHeight, argIdx, node.termName); //TODO: Remove
110 | // node.auxTermName = genInternalFuncArgName(argIdx);
111 | }
112 | }
113 | else {
114 | /* Call recursively */
115 | int nc = node.numChildren();
116 | for (int i = 0; i < nc; ++i) {
117 | functionizeBody(node.ch[i], stackHeight, origArgNames);
118 | }
119 | }
120 |
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/main/java/me/scai/parsetree/evaluation/FunctionArgumentList.java:
--------------------------------------------------------------------------------
1 | package me.scai.parsetree.evaluation;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | import Jama.Matrix;
7 |
8 | public class FunctionArgumentList {
9 | /* enums */
10 | public enum ArgumentType {
11 | Symbol, Value
12 | }
13 |
14 | /* ~enums */
15 |
16 | /* Member variables */
17 | List