├── .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 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8b5edfb1a70646c7b83d7ea83de18be2)](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 | [![Build & Test on Travis CI](https://travis-ci.org/Glyphoid/math-handwriting-lib.svg?branch=master)](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 args = new ArrayList(); 18 | List argTypes = new ArrayList(); 19 | 20 | /* ~Member variables */ 21 | 22 | /* Constructors */ 23 | public FunctionArgumentList(Object arg) { 24 | append(arg); 25 | } 26 | 27 | public void append(Object arg) { 28 | if (arg.getClass().equals(String.class)) { 29 | args.add(arg); 30 | argTypes.add(ArgumentType.Symbol); 31 | } else if (arg.getClass().equals(Double.class) 32 | || arg.getClass().equals(Matrix.class)) { /* 33 | * TODO: equals function 34 | * class 35 | */ 36 | if (arg.getClass().equals(Double.class)) { 37 | args.add((Double) arg); 38 | } else { 39 | args.add((Matrix) arg); 40 | } 41 | argTypes.add(ArgumentType.Value); 42 | } else { 43 | throw new RuntimeException("Unsupport argument type: " 44 | + arg.getClass()); 45 | } 46 | } 47 | 48 | public int numArgs() { 49 | return args.size(); 50 | } 51 | 52 | public Object get(int i) { 53 | return args.get(i); 54 | } 55 | 56 | public ArgumentType getType(int i) { 57 | return argTypes.get(i); 58 | } 59 | 60 | public boolean allSymbols() { 61 | for (ArgumentType argType : argTypes) { 62 | if (argType != ArgumentType.Symbol) { 63 | return false; 64 | } 65 | } 66 | 67 | return true; 68 | } 69 | 70 | public boolean allValues() { 71 | for (ArgumentType argType : argTypes) { 72 | if (argType != ArgumentType.Value) { 73 | return false; 74 | } 75 | } 76 | 77 | return true; 78 | } 79 | 80 | public List getSymbolNames() { 81 | if (!allSymbols()) { 82 | throw new RuntimeException( 83 | "Not all items in the argument list are symbols"); /* 84 | * Throw 85 | * an 86 | * error 87 | * ? 88 | */ 89 | } else { 90 | List argNames = new ArrayList(); 91 | 92 | for (Object arg : args) { 93 | argNames.add((String) arg); 94 | } 95 | 96 | return argNames; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/FunctionSigmaPiIntegralTerm.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import java.util.List; 4 | 5 | import me.scai.parsetree.Node; 6 | 7 | /* Parent class for functions, sigma summation, pi production and integral terms */ 8 | public abstract class FunctionSigmaPiIntegralTerm { /* For the lack of a better name ... */ 9 | /* Member variables */ 10 | protected FunctionArgumentList argList; 11 | protected List argNames; 12 | 13 | Node body; /* Function body that preserves the original variable names */ 14 | Node evalBody; /* Function body for evaluation purpose. Variable names are internal temporary ones */ 15 | /* ~Member variables */ 16 | 17 | /* Constructor */ 18 | public FunctionSigmaPiIntegralTerm(FunctionArgumentList tArgList) { 19 | argList = tArgList; /* TODO: Check to see if allSymbols() is true */ 20 | 21 | if (argList.allSymbols()) { 22 | argNames = argList.getSymbolNames(); 23 | } 24 | } 25 | 26 | /* Concrete methods */ 27 | public void defineBody(Node tBody) { 28 | body = tBody; 29 | evalBody = new Node(tBody); /* Use the copy constructor of Node */ 30 | /* TODO: This is probably not the best option. Why would you duplicate and 31 | * DFS-traverse a function body just to accommodate variable name changes? 32 | * Can build a cache of the argument name nodes to speed up the 33 | * name changes and avoid duplication. 34 | */ 35 | } 36 | 37 | public boolean isDefined() { 38 | return (body != null); 39 | } 40 | 41 | // /* Abstract methods */ 42 | // abstract Object evaluate(ParseTreeEvaluator evaluator, 43 | // FunctionArgumentList argValueList) 44 | // throws ParseTreeEvaluatorException; 45 | 46 | public FunctionArgumentList getArgumentList() { 47 | return argList; 48 | } 49 | 50 | public void setArgNames(List tArgNames) { 51 | this.argNames = tArgNames; 52 | } 53 | 54 | public List getArgNames() { 55 | return this.argNames; 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/FunctionTerm.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import me.scai.parsetree.ParseTreeStringizer; 4 | import me.scai.parsetree.evaluation.exceptions.ParseTreeEvaluatorException; 5 | 6 | import java.util.List; 7 | 8 | public class FunctionTerm extends FunctionSigmaPiIntegralTerm { 9 | /* Member variables */ 10 | protected String functionName; 11 | 12 | /* Constructor */ 13 | public FunctionTerm(String tFunctionName, FunctionArgumentList tArgList) { 14 | super(tArgList); 15 | 16 | functionName = tFunctionName; 17 | } 18 | 19 | /* Methods */ 20 | /* Getters and setters */ 21 | public String getFunctionName() { 22 | return functionName; 23 | } 24 | 25 | /* Evaluation: 26 | * @param evluator: The instance of ParseTreeEvalautor used in this evaluation operation 27 | * @param tempArgNaes: Temporary argument names 28 | * @param argValueLIst: List of values for the arguments */ 29 | public Object evaluate(ParseTreeEvaluator evaluator, 30 | List tempArgNames, 31 | FunctionArgumentList argValueList) 32 | throws ParseTreeEvaluatorException { 33 | if (!isDefined()) { 34 | throw new RuntimeException( 35 | "The body of this function is not defined"); 36 | /* TODO: More specific exception type */ 37 | } 38 | 39 | if (!argValueList.allValues()) { 40 | throw new RuntimeException( 41 | "Not all items in the argument list are values"); 42 | /* TODO: More specific exception type */ 43 | } 44 | 45 | if (argValueList.numArgs() != argList.numArgs()) { 46 | throw new RuntimeException("Argument list length mismatch"); 47 | /* TODO: More specific exception type */ 48 | } 49 | 50 | if (tempArgNames.size() != argList.numArgs()) { 51 | throw new RuntimeException("Argument temporary names length mismatch"); 52 | /* TODO: More specific exception type */ 53 | } 54 | 55 | /* "Functionize" the body */ 56 | int funcStackHeight = evaluator.getFuncStackHeight(); 57 | EvaluatorHelper.functionizeBody(this.evalBody, funcStackHeight, this.getArgNames()); 58 | 59 | int numArgs = argList.numArgs(); 60 | // List argSymbols = argList.getSymbolNames(); 61 | List argSymbols = tempArgNames; 62 | 63 | /* Set the values of the arguments, using the temporary variable names */ 64 | for (int i = 0; i < numArgs; ++i) { 65 | String argSymbol = argSymbols.get(i); 66 | Object argValue = argValueList.get(i); 67 | 68 | evaluator.variable_assign_value(argSymbol, argValue); 69 | /* TODO: Assign matrix-type values */ 70 | } 71 | 72 | /* TODO: Replace the symbol node in the body that match argument names with 73 | * special nodes. */ 74 | 75 | Object out = evaluator.eval(evalBody); 76 | 77 | /* Recover argument names */ 78 | // this.setArgNames(origFuncArgNames); 79 | 80 | return out; 81 | } 82 | 83 | /** 84 | * Stringize the full content of the function definition, including the name, arg list and body, 85 | * @return Full description string 86 | */ 87 | public String getFullDefinition(ParseTreeStringizer stringizer) { 88 | String fullDef = toString(); 89 | 90 | fullDef += " := " + stringizer.stringize(evalBody); 91 | 92 | return fullDef; 93 | } 94 | 95 | @Override 96 | public String toString() { 97 | StringBuilder sb = new StringBuilder("function: "); 98 | sb.append(functionName); 99 | sb.append("("); 100 | 101 | int nArgs = argNames.size(); 102 | for (int i = 0; i < nArgs; ++i) { 103 | sb.append(argNames.get(i)); 104 | if (i < nArgs - 1) { 105 | sb.append(", "); 106 | } 107 | } 108 | sb.append(")"); 109 | 110 | return sb.toString(); 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/ParseTreeEvaluatorHelper.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import me.scai.parsetree.evaluation.exceptions.ParseTreeEvaluatorException; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | 7 | public class ParseTreeEvaluatorHelper { 8 | /** 9 | * Try to find the root cause of an invocation target exception 10 | * @param t The invocation target exception 11 | * @return If the root cause can't be found, null 12 | * If found, the root cause itself 13 | */ 14 | public static ParseTreeEvaluatorException findRootCause(InvocationTargetException t) { 15 | Throwable tt = t; 16 | 17 | while (tt.getCause() != null) { 18 | if (tt.getCause() instanceof ParseTreeEvaluatorException) { 19 | return (ParseTreeEvaluatorException) tt.getCause(); 20 | } else { 21 | tt = tt.getCause(); 22 | } 23 | } 24 | 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/PiTerm.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 | class PiTerm extends SigmaPiIntegralTerm { 9 | public PiTerm(String tFunctionName, FunctionArgumentList tArgList, 10 | List tArgRanges) { 11 | super(tFunctionName, tArgList, tArgRanges); 12 | } 13 | 14 | /* Implementation of abstract methods */ 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 function 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 | double prod = 1.0; 31 | 32 | /* Obtain the value lists for all arguments */ 33 | ArrayList> allArgVals = new ArrayList>(); 34 | allArgVals.ensureCapacity(numArgs); 35 | 36 | for (int i = 0; i < numArgs; ++i) { 37 | allArgVals.add(argumentRanges.get(i).getValues()); 38 | } 39 | 40 | /* "Functionize" the body, i.e., replace the argument symbols with 41 | * special ones like "__stack0_funcArg1__" 42 | */ 43 | int funcStackHeight = evaluator.getFuncStackHeight(); 44 | EvaluatorHelper.functionizeBody(this.evalBody, funcStackHeight, argSymbols); 45 | 46 | int numVals = allArgVals.get(0).size(); 47 | for (int i = 0; i < numVals; ++i) { 48 | for (int j = 0; j < numArgs; ++j) { 49 | // String argSymbol = argSymbols.get(j); 50 | String argSymbol = tempArgNames.get(j); 51 | double argVal = allArgVals.get(j).get(i); 52 | 53 | evaluator.variable_assign_value(argSymbol, argVal); 54 | } 55 | 56 | Object out = evaluator.eval(this.evalBody); 57 | double outVal = (double) out; 58 | /* TODO: Handle situations in which this isn't satisfied */ 59 | prod *= outVal; 60 | } 61 | 62 | return prod; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | StringBuilder sb = new StringBuilder("PiTerm: "); 68 | sb.append("("); 69 | 70 | int nArgs = argNames.size(); 71 | for (int i = 0; i < nArgs; ++i) { 72 | sb.append(argNames.get(i)); 73 | if (i < nArgs - 1) { 74 | sb.append(", "); 75 | } 76 | } 77 | sb.append(")"); 78 | 79 | return sb.toString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/PlatoVarMap.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public class PlatoVarMap { 9 | private Map varMap; 10 | 11 | /* Constructors */ 12 | public PlatoVarMap() { 13 | this.varMap = new HashMap(); 14 | } 15 | 16 | public PlatoVarMap(Map varMap) { 17 | this.varMap = varMap; 18 | } 19 | 20 | /* Methods */ 21 | public List getVarNamesSorted() { 22 | // List varNamesSorted = new ArrayList(); 23 | // ((ArrayList) varNamesSorted).ensureCapacity(varMap.size()); 24 | String[] varNamesSorted = new String[varMap.size()]; 25 | 26 | int i = 0; 27 | for (Map.Entry varEntry : varMap.entrySet()) { 28 | varNamesSorted[i++] = varEntry.getKey(); 29 | } 30 | 31 | Arrays.sort(varNamesSorted); 32 | 33 | return Arrays.asList(varNamesSorted); 34 | } 35 | 36 | public void addVar(String varName, ValueUnion varValue) { 37 | varMap.put(varName, varValue); 38 | } 39 | 40 | public int numVars() { 41 | return varMap.size(); 42 | } 43 | 44 | public boolean containsVarName(String varName) { 45 | if (varMap.containsKey(varName)) { 46 | return true; 47 | } else { 48 | if ( varName.indexOf("funcArg") != -1 && 49 | varName.substring(varName.indexOf("funcArg")).indexOf("__") != 50 | varName.substring(varName.indexOf("funcArg")).lastIndexOf("__") ) { 51 | String[] parts = varName.split("__"); 52 | String varName1 = String.format("__%s__%s__", parts[1], parts[2]); 53 | 54 | return varMap.containsKey(varName1); 55 | } else { 56 | return false; 57 | } 58 | 59 | } 60 | } 61 | 62 | public ValueUnion getVarValue(String varName) { 63 | if (varMap.containsKey(varName)) { 64 | return varMap.get(varName); 65 | } else { 66 | if ( varName.contains("funcArg") && 67 | varName.substring(varName.indexOf("funcArg")).indexOf("__") != 68 | varName.substring(varName.indexOf("funcArg")).lastIndexOf("__") ) { 69 | String[] parts = varName.split("__"); 70 | String varName1 = String.format("__%s__%s__", parts[1], parts[2]); 71 | 72 | if (varMap.containsKey(varName1)) { 73 | return varMap.get(varName1); 74 | } else { 75 | return null; 76 | } 77 | } else { 78 | return null; 79 | } 80 | 81 | } 82 | 83 | // if (varMap.containsKey(varName)) { 84 | // 85 | // } else { 86 | // return null; 87 | // } 88 | } 89 | 90 | public void removeVar(String varName) { 91 | if (varName == null) { 92 | throw new IllegalStateException("Encountered null variable name"); 93 | } 94 | if (varMap.containsKey(varName)) { 95 | varMap.remove(varName); 96 | } 97 | } 98 | 99 | public void clear() { 100 | varMap.clear(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/SigmaPiIntegralTerm.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import me.scai.parsetree.evaluation.exceptions.ParseTreeEvaluatorException; 4 | 5 | import java.util.List; 6 | 7 | /* Parent class for sigma summation, pi product and integral terms */ 8 | abstract class SigmaPiIntegralTerm extends FunctionSigmaPiIntegralTerm { 9 | /* Member variables */ 10 | List argumentRanges; 11 | 12 | /* ~Member variables */ 13 | 14 | /* Constructor */ 15 | public SigmaPiIntegralTerm(String tFunctionName, FunctionArgumentList tArgList, 16 | List tArgRanges) { 17 | super(tArgList); 18 | 19 | if (tArgList.numArgs() != tArgRanges.size()) { 20 | throw new RuntimeException("Mismatch between number of arguments (" 21 | + tArgList.numArgs() + ") and number of ranges (" 22 | + tArgRanges.size() + ")"); 23 | } 24 | 25 | /* Check to make sure that the lengths all match */ 26 | if (tArgRanges.size() > 0) { 27 | int rangeLen = tArgRanges.get(0).getValues().size(); 28 | 29 | for (int i = 1; i < tArgRanges.size(); ++i) { 30 | int rangeLen1 = tArgRanges.get(i).getValues().size(); 31 | 32 | if (rangeLen1 != rangeLen) { 33 | throw new RuntimeException( 34 | "Mismatch between the length of value ranges (" 35 | + rangeLen + " != " + rangeLen1 + ")"); 36 | } 37 | } 38 | } 39 | 40 | this.argumentRanges = tArgRanges; 41 | } 42 | 43 | public abstract Object evaluate(ParseTreeEvaluator evaluator, 44 | List tempArgNames) 45 | throws ParseTreeEvaluatorException; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/SigmaTerm.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 | class SigmaTerm extends SigmaPiIntegralTerm { 9 | public SigmaTerm(String tFunctionName, FunctionArgumentList tArgList, 10 | List tArgRanges) { 11 | super(tFunctionName, tArgList, tArgRanges); 12 | } 13 | 14 | /* Implementation of abstract methods */ 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 function 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 | double sum = 0.0; 31 | 32 | /* Obtain the value lists for all arguments */ 33 | ArrayList> allArgVals = new ArrayList>(); 34 | allArgVals.ensureCapacity(numArgs); 35 | 36 | for (int i = 0; i < numArgs; ++i) { 37 | allArgVals.add(argumentRanges.get(i).getValues()); 38 | } 39 | 40 | /* "Functionize" the body, i.e., replace the argument symbols with 41 | * special ones like "__stack0_funcArg1__" 42 | */ 43 | int funcStackHeight = evaluator.getFuncStackHeight(); 44 | EvaluatorHelper.functionizeBody(this.evalBody, funcStackHeight, argSymbols); 45 | 46 | int numVals = allArgVals.get(0).size(); 47 | for (int i = 0; i < numVals; ++i) { 48 | for (int j = 0; j < numArgs; ++j) { 49 | // String argSymbol = argSymbols.get(j); 50 | String argSymbol = tempArgNames.get(j); 51 | double argVal = allArgVals.get(j).get(i); 52 | 53 | evaluator.variable_assign_value(argSymbol, argVal); 54 | } 55 | 56 | Object out = evaluator.eval(this.evalBody); 57 | double outVal = (double) out; 58 | /* TODO: Handle situations in which this isn't satisfied */ 59 | sum += outVal; 60 | } 61 | 62 | return sum; 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | StringBuilder sb = new StringBuilder("SigmaTerm: "); 68 | sb.append("("); 69 | 70 | int nArgs = argNames.size(); 71 | for (int i = 0; i < nArgs; ++i) { 72 | sb.append(argNames.get(i)); 73 | if (i < nArgs - 1) { 74 | sb.append(", "); 75 | } 76 | } 77 | sb.append(")"); 78 | 79 | return sb.toString(); 80 | } 81 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/Undefined.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | public class Undefined { 4 | /* Singleton instance */ 5 | private static Undefined instance; 6 | 7 | /* Constructor: private as per singleton pattern */ 8 | private Undefined() {} 9 | 10 | /* Singleton access */ 11 | public static Undefined getInstance() { 12 | if (instance == null) { 13 | instance = new Undefined(); 14 | } 15 | 16 | return instance; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "undefined"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/UniformArgumentRange.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | class UniformArgumentRange implements ArgumentRange { 7 | /* Member variables */ 8 | double minVal; 9 | double maxVal; 10 | double interval; 11 | 12 | /* Constructor*/ 13 | public UniformArgumentRange(double tMinVal, double tInterval, double tMaxVal) { 14 | this.minVal = tMinVal; 15 | this.interval = tInterval; 16 | this.maxVal = tMaxVal; 17 | } 18 | 19 | 20 | 21 | /* Methods */ 22 | @Override 23 | public List getValues() { 24 | ArrayList vs = new ArrayList(); 25 | 26 | /* Edge cases */ 27 | if ((maxVal - minVal) * interval < 0.0) { 28 | return vs; 29 | } 30 | if (interval == 0.0 && maxVal - minVal != 0.0) { 31 | return vs; 32 | } 33 | 34 | int estimLength = (int) ((maxVal - minVal) / interval) + 1; 35 | vs.ensureCapacity(estimLength); 36 | 37 | double v = minVal; 38 | while ((interval > 0.0) && v <= maxVal || (interval < 0.0) 39 | && v >= maxVal) { 40 | vs.add(v); 41 | v += interval; 42 | } 43 | 44 | return vs; 45 | } 46 | 47 | public double getInterval() { 48 | return interval; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/UniformIntegralArgumentRange.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | public class UniformIntegralArgumentRange implements ArgumentRange { 7 | /* Member variables */ 8 | private double lowerBound; 9 | private double upperBound; 10 | private int numIntervals; 11 | 12 | private double interval; 13 | private ArrayList values; 14 | 15 | /* Constructor*/ 16 | public UniformIntegralArgumentRange(double lowerBound, double upperBound, int numIntervals) { 17 | if (numIntervals <= 0) { 18 | throw new IllegalArgumentException("Negative or zero value in nIntervals"); 19 | } 20 | 21 | this.lowerBound = lowerBound; 22 | this.upperBound = upperBound; 23 | this.numIntervals = numIntervals; 24 | 25 | values = new ArrayList<>(); 26 | values.ensureCapacity(numIntervals); 27 | 28 | interval = (upperBound - lowerBound) / (double) numIntervals; 29 | double x0 = lowerBound; 30 | values.add(x0); 31 | 32 | for (int i = 1; i < this.numIntervals; ++i) { 33 | values.add(values.get(i - 1) + interval); 34 | } 35 | } 36 | 37 | /* Methods */ 38 | 39 | /** 40 | * This returns the lower values of the trapezoids 41 | * @return 42 | */ 43 | @Override 44 | public List getValues() { 45 | return values; 46 | } 47 | 48 | public int getNumInterval() { 49 | return numIntervals; 50 | } 51 | 52 | public double getInterval() { 53 | return interval; 54 | } 55 | 56 | public double getLowerBound() { 57 | return lowerBound; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/ValueUnion.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation; 2 | 3 | import Jama.Matrix; 4 | import me.scai.parsetree.ParseTreeStringizer; 5 | import me.scai.parsetree.evaluation.matrix.MatrixHelper; 6 | import org.jscience.physics.amount.Amount; 7 | 8 | public class ValueUnion { 9 | public enum ValueType { 10 | Undefined, // Undefined type, used in case such as: unsatisfied logical predicament in an "if" expression 11 | Boolean, 12 | Double, 13 | Matrix, 14 | UserFunction, 15 | PhysicalQuantity 16 | } 17 | 18 | private ValueType valueType; 19 | private Object value; 20 | private String description = ""; 21 | 22 | /* Constructors */ 23 | /* Constructor for Undefined type */ 24 | public ValueUnion(Undefined undefined) { 25 | assert(undefined != null); 26 | 27 | valueType = ValueType.Undefined; 28 | value = undefined; 29 | } 30 | 31 | /* Constructor for Boolean type */ 32 | public ValueUnion(boolean bv) { 33 | valueType = ValueType.Boolean; 34 | value = bv; 35 | } 36 | 37 | /* Constructor for double type */ 38 | public ValueUnion(double dv) { 39 | valueType = ValueType.Double; 40 | value = dv; 41 | } 42 | 43 | public ValueUnion(double dv, String description) { 44 | valueType = ValueType.Double; 45 | value = dv; 46 | this.description = description; 47 | } 48 | 49 | public ValueUnion(Matrix mv) { 50 | valueType = ValueType.Matrix; 51 | value = mv; 52 | } 53 | 54 | public ValueUnion(FunctionTerm funcTerm) { 55 | valueType = ValueType.UserFunction; 56 | value = funcTerm; 57 | } 58 | 59 | public ValueUnion(Amount physicalAmount) { 60 | valueType = ValueType.PhysicalQuantity; 61 | value = physicalAmount; 62 | } 63 | 64 | public ValueUnion(final Amount physicalAmount, final String description) { 65 | this.valueType = ValueType.PhysicalQuantity; 66 | this.value = physicalAmount; 67 | this.description = description; 68 | } 69 | 70 | /* Value getters */ 71 | public Object get() { 72 | return value; 73 | } 74 | 75 | // Note: The Undefined type does not require a dedicated value getter because the value always points to the same 76 | // singleton object 77 | 78 | public boolean getBoolean() { 79 | if (valueType == ValueType.Boolean) { 80 | return (Boolean) value; 81 | } else { 82 | throw new RuntimeException("Incorrect value type for getDouble(): " + ValueType.Double.toString()); 83 | } 84 | } 85 | 86 | public double getDouble() { 87 | if (valueType == ValueType.Double) { 88 | return (Double) value; 89 | } else if (valueType == ValueType.PhysicalQuantity) { 90 | Amount valueAmount = (Amount) value; 91 | return valueAmount.getEstimatedValue(); 92 | } else { 93 | throw new RuntimeException("Incorrect value type for getDouble(): " + ValueType.Double.toString()); 94 | } 95 | 96 | } 97 | 98 | public Matrix getMatrix() { 99 | if (valueType != ValueType.Matrix) { 100 | throw new RuntimeException("Incorrect value type"); 101 | } 102 | 103 | return (Matrix) value; 104 | } 105 | 106 | public FunctionTerm getUserFunction() { 107 | if (valueType != ValueType.UserFunction) { 108 | throw new RuntimeException("Incorrect value type"); 109 | } 110 | 111 | return (FunctionTerm) value; 112 | } 113 | 114 | public Amount getPhysicalQuantity() { 115 | if (valueType != ValueType.PhysicalQuantity) { 116 | throw new RuntimeException("Incorrect value type"); 117 | } 118 | 119 | return (Amount) value; 120 | } 121 | 122 | public String getDescription() { 123 | return description; 124 | } 125 | 126 | public ValueType getValueType() { 127 | return valueType; 128 | } 129 | 130 | public String getValueString(ParseTreeStringizer stringizer) { 131 | String valStr; 132 | 133 | if (valueType == ValueType.Boolean) { 134 | valStr = Boolean.toString((Boolean) value); 135 | 136 | } else if (valueType == ValueType.Double) { 137 | valStr = Double.toString((Double) value); 138 | 139 | } else if (valueType == ValueType.Matrix) { 140 | valStr = MatrixHelper.matrix2String((Matrix) value); 141 | 142 | } else if (valueType == ValueType.UserFunction) { 143 | assert(valueType != null); 144 | 145 | valStr = ((FunctionTerm) value).getFullDefinition(stringizer); 146 | 147 | } else if (valueType == ValueType.PhysicalQuantity) { 148 | valStr = ((Amount) value).toString(); 149 | 150 | } else { 151 | throw new IllegalStateException("Encountered unexpected value type when trying to generate string representation of ValueUnion"); 152 | } 153 | 154 | return valStr; 155 | } 156 | 157 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/DivisionByZeroException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class DivisionByZeroException extends ParseTreeMathException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public DivisionByZeroException() { 7 | super("Division by zero"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/InvalidArgumentForMatrixOperation.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class InvalidArgumentForMatrixOperation extends ParseTreeMathException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public InvalidArgumentForMatrixOperation(String op) { 7 | super("Invalid argument type for matrix operation \"" + op + "\""); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/LogarithmOfNonPositiveException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class LogarithmOfNonPositiveException extends ParseTreeMathException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public LogarithmOfNonPositiveException() { 7 | super("Logarithm of a non-positive number (complex number support is not available yet)"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/ParseTreeEvaluatorException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class ParseTreeEvaluatorException extends Exception { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public ParseTreeEvaluatorException(String msg) { 7 | super("Evaluation error: " + msg); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/ParseTreeMathException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | class ParseTreeMathException extends ParseTreeEvaluatorException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public ParseTreeMathException(String msg) { super(msg); } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/SquareRootOfNegativeException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class SquareRootOfNegativeException extends ParseTreeMathException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public SquareRootOfNegativeException() { 7 | super("Square root of a negative number (complex number support is not available yet)"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/UndefinedFunctionException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class UndefinedFunctionException extends ParseTreeEvaluatorException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public UndefinedFunctionException(String funcName) { 7 | super("Undefined function \"" + funcName + "\""); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/UnexpectedTypeException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | /** 4 | * Created by shanqing on 1/7/16. 5 | */ 6 | class UnexpectedTypeException extends ParseTreeEvaluatorException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | public UnexpectedTypeException(String msg) { super(msg); } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/UnsupportedMatrixExponentException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | /** 4 | * Created by shanqing on 1/7/16. 5 | */ 6 | public class UnsupportedMatrixExponentException extends ParseTreeMathException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | public UnsupportedMatrixExponentException() { 10 | super("Unsupported exponent on matrix base"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/exceptions/ZeroToZerothPowerException.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.exceptions; 2 | 3 | public class ZeroToZerothPowerException extends ParseTreeMathException { 4 | private static final long serialVersionUID = 1L; 5 | 6 | public ZeroToZerothPowerException() { super("Zero to the zeroth power"); } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/matrix/MatrixHelper.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.matrix; 2 | 3 | import Jama.Matrix; 4 | 5 | public class MatrixHelper { 6 | /* Convert a matrix to a string */ 7 | public static String matrix2String(Matrix m) { 8 | StringBuilder sb = new StringBuilder(); 9 | sb.append("["); 10 | 11 | int nr = m.getRowDimension(); 12 | int nc = m.getColumnDimension(); 13 | for (int i = 0; i < nr; ++i) { 14 | for (int j = 0; j < nc; ++j) { 15 | sb.append("" + m.get(i, j)); 16 | if (j < nc - 1) { 17 | sb.append(", "); 18 | } 19 | } 20 | 21 | if (i < nr - 1) { 22 | sb.append("; "); 23 | } 24 | } 25 | 26 | sb.append("]"); 27 | return sb.toString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/evaluation/program/ProgramKeyword.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.evaluation.program; 2 | 3 | public enum ProgramKeyword { 4 | If, 5 | While 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/geometry/GeometricRelation.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import me.scai.handwriting.CAbstractWrittenTokenSet; 4 | 5 | public abstract class GeometricRelation { 6 | protected int [] idxTested; /* Indices to the tested tokens */ 7 | public int [] idxInRel; /* Indices to the in-relation-to tokens */ 8 | /* Note that we use arrays to represent both the tested and the in-relation-to 9 | * tokens, in order to make it sufficiently general. In most simple cases, 10 | * there should be only only one tested token and only one in-relation-to token. 11 | * Note that these are indices to items in GraphicalProduction rhs, not 12 | * tokens in the token set. The indices of the tokens in the token set are 13 | * specified as input arguments to eval(). 14 | */ 15 | 16 | /* Get the tested indices */ 17 | public int [] getIdxTested() { 18 | return idxTested; 19 | } 20 | 21 | public int getNTested() { 22 | return idxTested.length; 23 | } 24 | 25 | /* Get the in-relation-to indices */ 26 | public int [] getIdxInRel() { 27 | return idxInRel; 28 | } 29 | 30 | public int getNInRel() { 31 | return idxInRel.length; 32 | } 33 | 34 | protected String [] splitInputString(String str) { 35 | String [] items; 36 | if ( (str.contains("(") && !str.contains(")")) || 37 | (str.contains("(") && !str.contains(")")) ) 38 | throw new IllegalArgumentException("Unbalanced bracket order in string defining geometric relation"); 39 | 40 | 41 | if ( str.contains("(") && str.contains(")") ) { 42 | int idxLB = str.indexOf("("); 43 | int idxRB = str.indexOf(")"); 44 | 45 | if ( idxLB > idxRB ) 46 | throw new IllegalArgumentException("Wrong bracket order in string defining geometric relation"); 47 | 48 | str = str.substring(0, str.length() - 1); /* Strip the right bracket */ 49 | items = str.split("\\("); 50 | } 51 | else { 52 | items = new String[2]; 53 | items[0] = str; 54 | } 55 | 56 | return items; 57 | } 58 | 59 | /* eval: Test if the geometric relation is true */ 60 | /* Output is a float number between 0 and 1 */ 61 | /* 0: 100% not true; 1: 100% true */ 62 | /* "ti" stands for token index */ 63 | //public abstract float eval(CWrittenTokenSet wts, int [] tiTested, int [] tiInRel); /* To remove? */ 64 | public abstract void parseString(String str, int t_idxTested); 65 | public abstract float verify(CAbstractWrittenTokenSet wtsTested, float [] bndsInRel); 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/geometry/HeightRelation.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import me.scai.handwriting.CAbstractWrittenTokenSet; 5 | 6 | /* HeightRelation */ 7 | public class HeightRelation extends GeometricRelation { 8 | public enum HeightRelationType { 9 | HeightRelationLess, 10 | HeightRelationEqual, 11 | HeightRelationGreater, 12 | } 13 | 14 | /* Member variables */ 15 | private HeightRelation() {} 16 | 17 | @Expose 18 | HeightRelationType heightRelationType; 19 | 20 | /* Constructor */ 21 | public HeightRelation(HeightRelationType hrt, int t_idxTested, int t_idxInRel) { 22 | heightRelationType = hrt; 23 | 24 | idxTested = new int[1]; 25 | idxTested[0] = t_idxTested; 26 | 27 | idxInRel = new int[1]; 28 | idxInRel[0] = t_idxInRel; 29 | } 30 | 31 | 32 | @Override 33 | public float verify(CAbstractWrittenTokenSet wtsTested, float [] bndsInRel) { 34 | float [] bndsTested = wtsTested.getSetBounds(); 35 | if ( bndsInRel.length != 4 ) 36 | throw new IllegalArgumentException("tiTested does not have length 1"); 37 | 38 | float hTested = bndsTested[3] - bndsTested[1]; 39 | float hInRel = bndsInRel[3] - bndsInRel[1]; 40 | float hMean = (hTested + hInRel) * 0.5f; 41 | 42 | float v; 43 | if ( heightRelationType == HeightRelationType.HeightRelationEqual ) { 44 | v = 1.0f - Math.abs(hTested - hInRel) / hMean; 45 | if ( v > 0.75f ) /* Slack */ 46 | v = 1.0f; 47 | } 48 | else if ( heightRelationType == HeightRelationType.HeightRelationGreater ) { 49 | v = (hTested - hInRel) / hInRel; 50 | if ( v > 0.5f ) 51 | v = 1.0f; 52 | } 53 | else /* heightRelationType == HeightRelationType.HeightRelationLess */ { 54 | v = (hInRel - hTested) / (hInRel * 0.1f); /* TODO: Remove ad hoc coefficients */ 55 | // if ( v > 0.1f ) 56 | // v = 1.0f; 57 | } 58 | 59 | if ( v > 1.0f ) 60 | v = 1.0f; 61 | else if ( v < 0.0f ) 62 | v = 0.0f; 63 | 64 | return v; 65 | } 66 | 67 | @Override 68 | public void parseString(String str, int t_idxTested) { 69 | String [] items = splitInputString(str); 70 | 71 | heightRelationType = HeightRelationType.valueOf(items[0]); 72 | 73 | idxTested = new int[1]; 74 | idxTested[0] = t_idxTested; 75 | 76 | if ( items[1] != null ) { 77 | idxInRel = new int[1]; 78 | idxInRel[0] = Integer.parseInt(items[1]); 79 | } 80 | 81 | } 82 | 83 | /* Factory method */ 84 | public static HeightRelation createFromString(String str, int t_idxTested) { 85 | HeightRelation r = new HeightRelation(); 86 | 87 | r.parseString(str, t_idxTested); 88 | return r; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/geometry/NodeInternalGeometry.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import java.util.List; 4 | import java.util.ArrayList; 5 | import java.util.LinkedList; 6 | 7 | import me.scai.parsetree.Node; 8 | import me.scai.parsetree.TerminalSet; 9 | 10 | public class NodeInternalGeometry { 11 | /* Member variables */ 12 | private TerminalSet termSet; 13 | public final static List majorTerminalTypes = new ArrayList(); 14 | /* ~Member variables */ 15 | 16 | /* Constructor */ 17 | public NodeInternalGeometry(TerminalSet tTermSet) { 18 | termSet = tTermSet; 19 | 20 | majorTerminalTypes.add("DIGIT"); 21 | majorTerminalTypes.add("VARIABLE_SYMBOL"); 22 | } 23 | 24 | /* Methods */ 25 | 26 | /* Get the sizes of all major tokens (as defined in "majorTokenTypes") under the node */ 27 | public List getMajorTokenBounds(Node node) { 28 | /* Uses a recursive algorithm */ 29 | if ( node == null ) { 30 | return null; 31 | } 32 | 33 | List b = new LinkedList(); 34 | 35 | for (int i = 0; i < node.ch.length; ++i) { 36 | Node chNode = node.ch[i]; /* Child node */ 37 | 38 | if (chNode.isTerminal()) { 39 | 40 | List termTypes = termSet.getTypeOfToken(chNode.termName); 41 | boolean typeMatch = false; 42 | 43 | if (termTypes != null) { 44 | for (String termType : termTypes) { 45 | if (majorTerminalTypes.contains(termType)) { 46 | typeMatch = true; 47 | break; 48 | } 49 | } 50 | } 51 | 52 | if (typeMatch) { 53 | b.add(chNode.getBounds()); 54 | } 55 | } 56 | else { 57 | List tBounds = getMajorTokenBounds(chNode); 58 | b.addAll(tBounds); 59 | } 60 | } 61 | 62 | return b; 63 | } 64 | 65 | public float getMaxMajorTokenWidth(Node node) { 66 | float maxWidth = Float.NEGATIVE_INFINITY; 67 | 68 | List allBounds = getMajorTokenBounds(node); 69 | if (allBounds == null) { 70 | return maxWidth; 71 | } 72 | 73 | for (float [] bounds : allBounds) { 74 | float w = bounds[3] - bounds[1]; 75 | 76 | if (w > maxWidth) { 77 | maxWidth = w; 78 | } 79 | } 80 | 81 | return maxWidth; 82 | } 83 | 84 | public boolean isTerminalTypeMajor(List termTypes) { 85 | boolean match = false; 86 | 87 | if (termTypes != null) { 88 | for (String termType : termTypes) { 89 | if (majorTerminalTypes.contains(termType)) { 90 | match = true; 91 | break; 92 | } 93 | } 94 | } 95 | 96 | return match; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/geometry/WidthRelation.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import com.google.gson.annotations.Expose; 4 | import me.scai.handwriting.CAbstractWrittenTokenSet; 5 | 6 | /* WidthRelation */ 7 | public class WidthRelation extends GeometricRelation { 8 | public enum WidthRelationType { 9 | WidthRelationLess, 10 | WidthRelationEqual, 11 | WidthRelationGreater, 12 | } 13 | 14 | /* Member variables */ 15 | @Expose 16 | WidthRelationType widthRelationType; 17 | 18 | /* Constructor */ 19 | private WidthRelation() {} 20 | 21 | public WidthRelation(WidthRelationType hrt, int t_idxTested, int t_idxInRel) { 22 | widthRelationType = hrt; 23 | 24 | idxTested = new int[1]; 25 | idxTested[0] = t_idxTested; 26 | 27 | idxInRel = new int[1]; 28 | idxInRel[0] = t_idxInRel; 29 | } 30 | 31 | 32 | @Override 33 | public float verify(CAbstractWrittenTokenSet wtsTested, float [] bndsInRel) { 34 | float [] bndsTested = wtsTested.getSetBounds(); 35 | if ( bndsInRel.length != 4 ) 36 | throw new IllegalArgumentException("tiTested does not have length 1"); 37 | 38 | float wTested = bndsTested[3] - bndsTested[1]; 39 | float wInRel = bndsInRel[3] - bndsInRel[1]; 40 | float wMean = (wTested + wInRel) * 0.5f; 41 | 42 | float v; 43 | if ( widthRelationType == WidthRelationType.WidthRelationEqual ) { 44 | v = 1.0f - Math.abs(wTested - wInRel) / wMean; 45 | if ( v > 0.75f ) { /* Slack */ 46 | v = 1.0f; 47 | } 48 | } else if ( widthRelationType == WidthRelationType.WidthRelationGreater ) { 49 | v = (wTested - wInRel) / wInRel; 50 | if ( v > 0.5f ) { 51 | v = 1.0f; 52 | } 53 | } else { 54 | assert widthRelationType == WidthRelationType.WidthRelationLess; 55 | 56 | v = (wInRel - wTested) / wInRel; 57 | if ( v > 0.5f ) { 58 | v = 1.0f; 59 | } 60 | } 61 | 62 | if ( v > 1.0f ) { 63 | v = 1.0f; 64 | } else if ( v < 0.0f ) { 65 | v = 0.0f; 66 | } 67 | 68 | return v; 69 | } 70 | 71 | @Override 72 | public void parseString(String str, int t_idxTested) { 73 | String [] items = splitInputString(str); 74 | 75 | widthRelationType = WidthRelationType.valueOf(items[0]); 76 | 77 | idxTested = new int[1]; 78 | idxTested[0] = t_idxTested; 79 | 80 | if ( items[1] != null ) { 81 | idxInRel = new int[1]; 82 | idxInRel[0] = Integer.parseInt(items[1]); 83 | } 84 | 85 | } 86 | 87 | /* Factory method */ 88 | public static WidthRelation createFromString(String str, int t_idxTested) { 89 | WidthRelation r = new WidthRelation(); 90 | 91 | r.parseString(str, t_idxTested); 92 | return r; 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/me/scai/parsetree/scientific/ScientificConstants.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.scientific; 2 | 3 | import me.scai.parsetree.evaluation.PlatoVarMap; 4 | import me.scai.parsetree.evaluation.ValueUnion; 5 | import org.jscience.physics.amount.Constants; 6 | 7 | public class ScientificConstants { 8 | public static final void inject2VariableMap(PlatoVarMap varMap) { 9 | varMap.addVar("gr_pi", new ValueUnion(Math.PI, "ratio of a circle's circumference to its diameter")); // Pi 10 | varMap.addVar("e", new ValueUnion(Math.E, "base of natural log")); // e, base of natural logs 11 | 12 | varMap.addVar("c", new ValueUnion(Constants.c, "speed of light in vacuum")); // Speed of light 13 | varMap.addVar("h", new ValueUnion(Constants.ℎ, "Planck constant")); // Planck constant 14 | varMap.addVar("hbar", new ValueUnion(Constants.ℏ, "Planck constant divided by 2 * pi")); // Planck constant over 2*pi 15 | varMap.addVar("N_A", new ValueUnion(Constants.N, "Avogadro constant")); // Avogadro's number 16 | varMap.addVar("G", new ValueUnion(Constants.G, "gravitational constant")); // Newtonian constant of gravitatino 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/me/scai/plato/engine/HandwritingEngine.java: -------------------------------------------------------------------------------- 1 | package me.scai.plato.engine; 2 | 3 | import java.util.List; 4 | 5 | import com.google.gson.JsonArray; 6 | import com.google.gson.JsonObject; 7 | import me.scai.handwriting.*; 8 | import me.scai.parsetree.HandwritingEngineException; 9 | import me.scai.parsetree.TokenSetParserOutput; 10 | import me.scai.parsetree.evaluation.PlatoVarMap; 11 | import me.scai.parsetree.evaluation.ValueUnion; 12 | 13 | public interface HandwritingEngine { 14 | /* Add stroke to the token set */ 15 | void addStroke(CStroke stroke) 16 | throws HandwritingEngineException; 17 | 18 | /* Remove the last token */ 19 | void removeLastToken() 20 | throws HandwritingEngineException; 21 | 22 | /** 23 | * Remove i-th token 24 | * @param idxToken Index to the abstract token to be removed 25 | **/ 26 | void removeToken(int idxToken) 27 | throws HandwritingEngineException; 28 | 29 | /** 30 | * Remove an array of tokens by indices 31 | * @param tokenIndices Indices to the abstract token to be removed. Order doesn't matter. 32 | **/ 33 | void removeTokens(int[] tokenIndices) 34 | throws HandwritingEngineException; 35 | 36 | /* Move a token 37 | * @param tokenIdx Index to the abstract token (not written token) to be moved 38 | * @param newBounds Length-4 float array to describe the new bounds 39 | */ 40 | void moveToken(int tokenIdx, float [] newBounds) 41 | throws HandwritingEngineException; 42 | 43 | /* Move an array of tokens 44 | * @param tokenIndices Indices to the abstract token (not written token) to be moved 45 | * @param newBoundsArray An array of length-4 float array to describe the new bounds 46 | */ 47 | void moveTokens(int[] tokenIndices, float[][] newBoundsArray) 48 | throws HandwritingEngineException; 49 | 50 | /* Merge strokes with specified indices as a single token */ 51 | void mergeStrokesAsToken(int [] strokeInds) 52 | throws HandwritingEngineException; 53 | 54 | /* Force set the recognition winner of a given token */ 55 | void forceSetRecogWinner(int tokenIdx, String tokenName) 56 | throws HandwritingEngineException; 57 | 58 | /* Clear all strokes */ 59 | void clearStrokes() 60 | throws HandwritingEngineException; 61 | 62 | /* Get the abstract token set: Could contain node tokens */ 63 | CAbstractWrittenTokenSet getTokenSet(); 64 | 65 | /* Get the entire written token set.: Never contain node tokens. All are the base-level written tokens */ 66 | CWrittenTokenSet getWrittenTokenSet(); 67 | 68 | /* Get the constituent strokes of tokens, respectively */ 69 | List getTokenConstStrokeIndices(); 70 | 71 | /** 72 | * Disable graphical productions by grammar node names (e.g., "FRACTION") 73 | * @param grammarNodeNames Names of the grammar nodes (LHS / RHS) to disable 74 | * @return Number of grammar nodes disabled in this call. 75 | * @throws HandwritingEngineException If no production is dsiabled in this call (either because of invalid 76 | * grammar node names or because those productions are already disabled. */ 77 | int disableProductionsByGrammarNodeNames(String[] grammarNodeNames) 78 | throws HandwritingEngineException; 79 | 80 | /* Enable all graphical productions */ 81 | void enableAllProductions(); 82 | 83 | /* Perform parsing on the entire token set */ 84 | TokenSetParserOutput parseTokenSet() 85 | throws HandwritingEngineException; 86 | 87 | /* Perform parsing on selected tokens, causing the creation of a NodeToken (if successful) */ 88 | TokenSetParserOutput parseTokenSubset(int[] tokenIndices) 89 | throws HandwritingEngineException; 90 | 91 | /* Get the graphical production set */ 92 | JsonArray getGraphicalProductions(); 93 | 94 | /* Get the bounds of tokens: abstract tokens (There could be NodeTokens) */ 95 | float[] getTokenBounds(int tokenIdx) 96 | throws HandwritingEngineException; 97 | 98 | /* Get the bounds of tokens: basic written tokens (There will never be NodeTokens) */ 99 | float[] getWrittenTokenBounds(int tokenIdx) 100 | throws HandwritingEngineException; 101 | 102 | /* Get the currently defined items */ 103 | PlatoVarMap getVarMap() 104 | throws HandwritingEngineException; 105 | 106 | /* Get the currently defined item of the specified key */ 107 | ValueUnion getFromVarMap(String varName) 108 | throws HandwritingEngineException; 109 | 110 | /* Inject state data */ 111 | // void extractStateData() 112 | // throws 113 | 114 | void injectState(JsonObject stateData) 115 | throws HandwritingEngineException; 116 | 117 | /* Undo and redo */ 118 | HandwritingEngineUserAction getLastUserAction(); 119 | void undoUserAction(); 120 | void redoUserAction(); 121 | 122 | boolean canUndoUserAction(); 123 | boolean canRedoUserAction(); 124 | 125 | public void removeEngine() 126 | throws HandwritingEngineException; 127 | 128 | /** 129 | * @return All possible toke names (Not their display names) 130 | */ 131 | List getAllTokenNames() 132 | throws HandwritingEngineException; 133 | 134 | /** 135 | * Obtain the UUIDs of the written tokens in the stroke curator 136 | * @return UUIDs of the written tokens 137 | */ 138 | List getWrittenTokenUUIDs(); 139 | 140 | /** 141 | * Obtain the UUIDs of the tokens that comprise each abstract token 142 | * @return List of UUID lists 143 | */ 144 | List> getConstituentWrittenTokenUUIDs(); 145 | 146 | /* Injection of serialized state */ 147 | void injectSerializedState(JsonObject json); 148 | 149 | /* State and stack */ 150 | JsonObject getStateSerialization(); 151 | String getStateSerializationString(); 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/main/java/me/scai/plato/engine/HandwritingEngineState.java: -------------------------------------------------------------------------------- 1 | package me.scai.plato.engine; 2 | 3 | import com.google.gson.JsonObject; 4 | import me.scai.handwriting.HandwritingEngineUserAction; 5 | import me.scai.plato.state.PlatoState; 6 | 7 | public class HandwritingEngineState implements PlatoState { 8 | private HandwritingEngineUserAction userAction; 9 | private JsonObject state; 10 | 11 | /* Constructors */ 12 | public HandwritingEngineState(HandwritingEngineUserAction userAction, JsonObject state) { 13 | this.userAction = userAction; 14 | this.state = state; 15 | } 16 | 17 | /* Implementing interface methods */ 18 | @Override 19 | public String getUserAction() { 20 | return userAction.toString(); 21 | } 22 | 23 | @Override 24 | public JsonObject getState() { 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/me/scai/plato/helpers/CStrokeJsonHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by scai on 3/25/2015. 3 | */ 4 | 5 | package me.scai.plato.helpers; 6 | 7 | import com.google.gson.*; 8 | import me.scai.handwriting.CStroke; 9 | 10 | public class CStrokeJsonHelper { 11 | private final static Gson gson = new Gson(); 12 | 13 | /* Exception classes */ 14 | public static class CStrokeJsonConversionException extends Exception { 15 | /* Constructor */ 16 | public CStrokeJsonConversionException(String msg) { 17 | super(msg); 18 | } 19 | } 20 | 21 | private static final JsonParser jsonParser = new JsonParser(); 22 | 23 | public static JsonObject CStroke2JsonObject(CStroke stroke) { 24 | /* strokes */ 25 | JsonObject strokeObj = new JsonObject(); 26 | 27 | int numPoints = stroke.nPoints(); 28 | strokeObj.add("numPoints", new JsonPrimitive(numPoints)); 29 | 30 | JsonElement x = gson.toJsonTree(stroke.getXs()).getAsJsonArray(); 31 | JsonElement y = gson.toJsonTree(stroke.getYs()).getAsJsonArray(); 32 | 33 | strokeObj.add("x", x); 34 | strokeObj.add("y", y); 35 | 36 | return strokeObj; 37 | } 38 | 39 | public static CStroke json2CStroke(String json) 40 | throws CStrokeJsonConversionException { 41 | JsonObject jsonObj = null; 42 | try { 43 | jsonObj = jsonParser.parse(json).getAsJsonObject(); 44 | } 45 | catch (JsonParseException exc) { 46 | throw new CStrokeJsonConversionException("Failed to parse json for CStroke, due to " + exc.getMessage()); 47 | } 48 | 49 | int numPoints = jsonObj.get("numPoints").getAsInt(); 50 | if (numPoints <= 0) { 51 | return null; 52 | } 53 | 54 | JsonArray xs = jsonObj.get("x").getAsJsonArray(); 55 | JsonArray ys = jsonObj.get("y").getAsJsonArray(); 56 | 57 | if (xs.size() != numPoints) { 58 | throw new CStrokeJsonConversionException("Mismatch between the value of numPoints (" + numPoints + 59 | ") and the actual length of xs (" + xs.size() + ")"); 60 | } 61 | if (ys.size() != numPoints) { 62 | throw new CStrokeJsonConversionException("Mismatch between the value of numPoints (" + numPoints + 63 | ") and the actual length of xs (" + ys.size() + ")"); 64 | } 65 | 66 | CStroke stroke = new CStroke(); 67 | for (int i = 0; i < numPoints; ++i) { 68 | float x = xs.get(i).getAsFloat(); 69 | float y = ys.get(i).getAsFloat(); 70 | 71 | stroke.addPoint(x, y); 72 | } 73 | 74 | return stroke; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/me/scai/plato/helpers/CWrittenTokenJsonHelper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by scai on 3/25/2015. 3 | */ 4 | 5 | package me.scai.plato.helpers; 6 | 7 | import com.google.gson.*; 8 | import me.scai.handwriting.*; 9 | 10 | public class CWrittenTokenJsonHelper { 11 | private static final Gson gson = new Gson(); 12 | 13 | /** 14 | * Serialize CWrittenToken 15 | * @param wt 16 | * @return 17 | */ 18 | public static JsonObject CWrittenToken2JsonObject(CWrittenToken wt) { 19 | JsonObject wtObj = new JsonObject(); 20 | 21 | /* numStrokes */ 22 | final int numStrokes = wt.nStrokes(); 23 | wtObj.add("numStrokes", new JsonPrimitive(numStrokes)); 24 | 25 | /* strokes */ 26 | JsonObject strokes = new JsonObject(); 27 | for (int i = 0; i < numStrokes; ++i) { 28 | JsonObject stroke = CStrokeJsonHelper.CStroke2JsonObject(wt.getStroke(i)); 29 | 30 | strokes.add(Integer.toString(i), stroke); 31 | } 32 | 33 | wtObj.add("strokes", strokes); 34 | wtObj.add("bNormalized", new JsonPrimitive(wt.bNormalized)); 35 | 36 | return wtObj; 37 | } 38 | 39 | /** 40 | * Serialize NodeToken 41 | * @param token 42 | * @return 43 | */ 44 | public static JsonObject CWrittenToken2JsonObject(NodeToken token) { 45 | JsonObject wtObj = new JsonObject(); 46 | 47 | /* Node */ 48 | wtObj.add("node", gson.toJsonTree(token.getNode())); 49 | 50 | /* Written token set */ 51 | CAbstractWrittenTokenSet wtSet = token.getTokenSet(); 52 | assert(wtSet instanceof CWrittenTokenSetNoStroke); 53 | 54 | wtObj.add("wtSet", CWrittenTokenSetJsonHelper.CWrittenTokenSetNoStroke2JsonObj((CWrittenTokenSetNoStroke) wtSet)); // TODO 55 | wtObj.add("matchingGraphicalProductionIndices", gson.toJsonTree(token.getMatchingGraphicalProductionIndices())); 56 | 57 | return wtObj; 58 | } 59 | 60 | public static String CWrittenToken2JsonNoStroke(CWrittenToken wt) { 61 | return gson.toJson(CWrittenToken2JsonObjNoStroke(wt)); 62 | } 63 | 64 | public static JsonObject CWrittenToken2JsonObjNoStroke(CWrittenToken wt) { 65 | if ( !wt.bNormalized ) { 66 | throw new RuntimeException("Attempt to generate JSON string from un-normalized CWrittenToken object"); 67 | } 68 | 69 | JsonObject obj = new JsonObject(); 70 | 71 | /* Get the bounds: [min_x, min_y, max_x, max_y] */ 72 | float [] bounds = wt.getBounds(); 73 | JsonArray jsonBounds = new JsonArray(); 74 | for (int i = 0; i < bounds.length; ++i) { 75 | jsonBounds.add(new JsonPrimitive(bounds[i])); 76 | } 77 | 78 | obj.add("bounds", jsonBounds); 79 | 80 | /* Width and height */ 81 | obj.add("width", new JsonPrimitive(wt.width)); 82 | obj.add("height", new JsonPrimitive(wt.height)); 83 | 84 | /* Get the recognition winner (if exists) */ 85 | if (wt.getRecogResult() != null) { 86 | obj.add("recogWinner", new JsonPrimitive(wt.getRecogResult())); 87 | } 88 | 89 | /* Get the recognition p-values (if exists) */ 90 | if (wt.getRecogPs() != null) { 91 | double [] recogPs = wt.getRecogPs(); 92 | JsonArray jsonRecogPs = new JsonArray(); 93 | 94 | for (int i = 0; i < recogPs.length; ++i) { 95 | jsonRecogPs.add(new JsonPrimitive(recogPs[i])); 96 | } 97 | 98 | obj.add("recogPs", jsonRecogPs); 99 | } 100 | 101 | return obj; 102 | } 103 | 104 | public static CWrittenToken jsonObj2CWrittenTokenNoStroke(JsonObject jsonObj) { 105 | CWrittenToken wt = new CWrittenToken(); 106 | 107 | /* Width and height */ 108 | if (jsonObj.has("width")) { 109 | wt.width = jsonObj.get("width").getAsFloat(); 110 | } 111 | if (jsonObj.has("height")) { 112 | wt.height = jsonObj.get("height").getAsFloat(); 113 | } 114 | 115 | /* Bounds */ 116 | if (jsonObj.has("bounds")) { 117 | JsonArray jsBounds = jsonObj.get("bounds").getAsJsonArray(); 118 | 119 | float [] bounds = new float[jsBounds.size()]; 120 | for (int i = 0; i < jsBounds.size(); ++i) { 121 | bounds[i] = jsBounds.get(i).getAsFloat(); 122 | } 123 | 124 | wt.setBounds(bounds); 125 | } 126 | 127 | /* Recognition winner */ 128 | if (jsonObj.has("recogWinner")) { 129 | wt.setRecogResult(jsonObj.get("recogWinner").getAsString()); 130 | } 131 | 132 | /* Recognition p-values */ 133 | if (jsonObj.has("recogPs")) { 134 | JsonArray ps = jsonObj.get("recogPs").getAsJsonArray(); 135 | double [] recogPs = new double[ps.size()]; 136 | 137 | for (int i = 0; i < ps.size(); ++i) { 138 | recogPs[i] = ps.get(i).getAsDouble(); 139 | } 140 | wt.setRecogPs(recogPs); 141 | } 142 | 143 | // wt.bNormalized = jsonObj.get("bNormalized").getAsBoolean(); 144 | wt.normalizeAxes(); 145 | 146 | return wt; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/me/scai/plato/state/PlatoState.java: -------------------------------------------------------------------------------- 1 | package me.scai.plato.state; 2 | 3 | import com.google.gson.JsonObject; 4 | 5 | public interface PlatoState { 6 | String getUserAction(); 7 | JsonObject getState(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/config/stroke_curator_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "remoteTokenEngineUrl" : "http://127.0.0.1:11610/glyphoid/token-recog", 3 | "tokenPairRules": 4 | [ 5 | { 6 | "tokenA" : "-", 7 | "tokenB" : "-", 8 | "predicaments" : 9 | [ 10 | "relativeWidthDifference < 0.5", 11 | "relativeLeftXOffset < 0.4", 12 | "relativeRightXOffset < 0.4", 13 | "numTokensInBetweenY == 0" 14 | ], 15 | "recommendation" : "mergeAs: \"=\"" 16 | }, 17 | { 18 | "tokenA" : "tick", 19 | "tokenB" : "-", 20 | "predicaments" : 21 | [ 22 | "relativeHeightDifference > 0.3", 23 | "relativeTopYOffset < 0.3", 24 | "relativeBottomYOffset > 0.4", 25 | "relativeRightToLeftOffset < 0.25", 26 | "relativeRightToLeftOffset > -0.25", 27 | "numTokensInBetweenX == 0" 28 | ], 29 | "recommendation" : "mergeAs: \"root\"" 30 | }, 31 | { 32 | "tokenA" : "gr_io", 33 | "tokenB" : ".", 34 | "predicaments" : 35 | [ 36 | "isWidthAContainingWidthB == 1", 37 | "isHeightABelowHeightBNonOverlapping == 1", 38 | "numTokensInBetweenY == 0" 39 | ], 40 | "recommendation" : "mergeAs: \"i\"" 41 | }, 42 | { 43 | "tokenA" : "j", 44 | "tokenB" : ".", 45 | "predicaments" : 46 | [ 47 | "isWidthAContainingWidthB == 1", 48 | "isHeightABelowHeightBNonOverlapping == 1", 49 | "numTokensInBetweenY == 0" 50 | ], 51 | "recommendation" : "mergeAs: \"j\"" 52 | } 53 | ], 54 | "mergePartners": 55 | { 56 | "root": [] 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/resources/config/terminals.json: -------------------------------------------------------------------------------- 1 | { 2 | "terminals" : { 3 | "DIGIT" : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], 4 | "POINT" : ["."], 5 | "PLUS_OP" : ["+"], 6 | "MINUS_OP" : ["-"], 7 | "MULT_OP" : ["*", "X"], 8 | "ROOT_OP" : ["root"], 9 | "OBLIQUE_DIV_OP" : ["/"], 10 | "PARENTHESIS_L" : ["("], 11 | "PARENTHESIS_R" : [")"], 12 | "BRACKET_L" : ["["], 13 | "BRACKET_R" : ["]"], 14 | "CURLY_BRACKET_L" : ["cbl"], 15 | "CURLY_BRACKET_R" : ["cbr"], 16 | "COMPARATOR" : ["gt", "lt", "gte", "lte", "="], 17 | "VARIABLE_SYMBOL" : ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "Y", "Z", 18 | "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", 19 | "gr_Ga", "gr_De", "gr_Th", "gr_La", "gr_Xi", "gr_Pi", "gr_Si", "gr_Up", "gr_Ph", "gr_Ps", "gr_Om", 20 | "gr_al", "gr_be", "gr_ga", "gr_de", "gr_ep", "gr_ze", "gr_et", "gr_th", "gr_io", "gr_ka", "gr_la", "gr_mu", "gr_nu", "gr_xi", "gr_omicron", "gr_pi", "gr_rh", "gr_si", "gr_ta", "gr_up", "gr_ph", "gr_ch", "gr_omega"], 21 | "ASSIGN_OP" : ["="], 22 | "LOGICAL_AND_OP" : ["gr_La"], 23 | "LOGICAL_OR_OP" : ["V"], 24 | "INTEG_OP" : ["integ"], 25 | "COMMA" : [","] 26 | }, 27 | "texNotations" : { 28 | "lt" : "<", 29 | "gt" : ">", 30 | "lte" : "\\leq", 31 | "gte" : "\\geq", 32 | "gr_Ga" : "\\Gamma", 33 | "gr_De" : "\\Delta", 34 | "gr_Th" : "\\Theta", 35 | "gr_La" : "\\Lambda", 36 | "gr_Xi" : "\\Xi", 37 | "gr_Pi" : "\\Pi", 38 | "gr_Si" : "\\Sigma", 39 | "gr_Up" : "\\Upsilon", 40 | "gr_Ph" : "\\Phi", 41 | "gr_Ps" : "\\Psi", 42 | "gr_Om" : "\\Omega", 43 | "gr_al" : "\\alpha", 44 | "gr_be" : "\\beta", 45 | "gr_ga" : "\\gamma", 46 | "gr_de" : "\\delta", 47 | "gr_ep" : "\\epsilon", 48 | "gr_ze" : "\\zeta", 49 | "gr_et" : "\\eta", 50 | "gr_th" : "\\theta", 51 | "gr_io" : "\\iota", 52 | "gr_ka" : "\\kappa", 53 | "gr_la" : "\\lambda", 54 | "gr_mu" : "\\mu", 55 | "gr_nu" : "\\nu", 56 | "gr_xi" : "\\xi", 57 | "gr_pi" : "\\pi", 58 | "gr_rh" : "\\rho", 59 | "gr_si" : "\\sigma", 60 | "gr_ta" : "\\tau", 61 | "gr_up" : "\\upsilon", 62 | "gr_ph" : "\\phi", 63 | "gr_ch" : "\\chi", 64 | "gr_omega" : "\\omega" 65 | }, 66 | "tokenDegeneracy" : { 67 | "C" : "c", 68 | "O" : "0", 69 | "o" : "0", 70 | "S" : "s", 71 | "Z" : "z", 72 | "gr_ch" : "x", 73 | "gr_omicron" : "0", 74 | "gr_Up" : "Y", 75 | "gr_Ph" : "gr_ph", 76 | "gr_Th" : "gr_th", 77 | "gr_nu" : "v" 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/resources/token_engine/token_engine.sdv.sz0_whr1_ns1.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glyphoid/math-handwriting-lib/2049f321e4a654ad3eb69cfff5326d9761c8ccb0/src/main/resources/token_engine/token_engine.sdv.sz0_whr1_ns1.ser -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/Test_CStroke.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class Test_CStroke { 8 | private static final float floatTol = 1E-9f; 9 | 10 | @Test 11 | public void testMinMax() { 12 | final float x0 = -10.0f; 13 | final float y0 = -20.0f; 14 | 15 | CStroke stroke = new CStroke(x0, y0); 16 | 17 | assertEquals(stroke.min_x, x0, floatTol); 18 | assertEquals(stroke.min_y, y0, floatTol); 19 | assertEquals(stroke.max_x, x0, floatTol); 20 | assertEquals(stroke.max_y, y0, floatTol); 21 | 22 | final float x1 = -5.0f; 23 | final float y1 = -10.0f; 24 | 25 | stroke.addPoint(x1, y1); 26 | 27 | assertEquals(stroke.min_x, x0, floatTol); 28 | assertEquals(stroke.min_y, y0, floatTol); 29 | assertEquals(stroke.max_x, x1, floatTol); 30 | assertEquals(stroke.max_y, y1, floatTol); 31 | 32 | final float x2 = 10.0f; 33 | final float y2 = 20.0f; 34 | 35 | stroke.addPoint(x2, y2); 36 | 37 | assertEquals(stroke.min_x, x0, floatTol); 38 | assertEquals(stroke.min_y, y0, floatTol); 39 | assertEquals(stroke.max_x, x2, floatTol); 40 | assertEquals(stroke.max_y, y2, floatTol); 41 | 42 | final float x3 = -20.0f; 43 | final float y3 = -40.0f; 44 | 45 | stroke.addPoint(x3, y3); 46 | 47 | assertEquals(stroke.min_x, x3, floatTol); 48 | assertEquals(stroke.min_y, y3, floatTol); 49 | assertEquals(stroke.max_x, x2, floatTol); 50 | assertEquals(stroke.max_y, y2, floatTol); 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/Test_CWrittenTokenSet.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | * Created by scai on 5/23/2015. 9 | */ 10 | public class Test_CWrittenTokenSet { 11 | private static final float floatTol = 1e-6f; 12 | 13 | @Test 14 | public void TestWrittenTokenSet() { 15 | 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]}}}"; 16 | 17 | CWrittenToken wt = new CWrittenToken(testJSON); 18 | CWrittenTokenSet wtSet = new CWrittenTokenSet(); 19 | wtSet.addToken(wt); 20 | 21 | assertEquals(wtSet.getNumTokens(), 1); 22 | // assertEquals(wtSet.getNumStrokes(), 2); 23 | } 24 | 25 | @Test 26 | public void TestSetTokenBounds() { 27 | 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]}}}"; 28 | 29 | CWrittenToken wt = new CWrittenToken(testJSON); 30 | CWrittenTokenSet wtSet = new CWrittenTokenSet(); 31 | 32 | wtSet.addToken(wt); 33 | 34 | final float[] tokenBounds = wtSet.getTokenBounds(0); 35 | float[] knownBounds = new float[] {106.0f, 75.0f, 448.0f, 329.0f}; 36 | for (int k = 0; k < knownBounds.length; ++k) { 37 | assertEquals(knownBounds[k], tokenBounds[k], floatTol); 38 | } 39 | 40 | for (int i = 0; i < 2; ++i) { 41 | final float[] newBounds = new float[] {knownBounds[0] + 1.5f * i, 42 | knownBounds[1] + 1.5f * i, 43 | knownBounds[2] + 1.5f * i, 44 | knownBounds[3] + 1.5f * i}; 45 | 46 | wtSet.setTokenBounds(0, newBounds); 47 | final float[] newRetrievedBounds = wtSet.getTokenBounds(0); 48 | 49 | for (int k = 0; k < knownBounds.length; ++k) { 50 | assertEquals(newBounds[k], newRetrievedBounds[k], floatTol); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/Test_Rectangle.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | public class Test_Rectangle { 8 | private static final float floatTol = 1e-9f; 9 | 10 | @Test 11 | public void testRectangle1() { 12 | Rectangle rect = new Rectangle(new float[] {0f, 0f, 1f, 2f}); 13 | 14 | assertEquals(0.5f, rect.getCentralX(), floatTol); 15 | assertEquals(1f, rect.getCentralY(), floatTol); 16 | } 17 | 18 | @Test 19 | public void testRectangle2() { 20 | Rectangle rect = new Rectangle(0f, 0f, 1f, 2f); 21 | 22 | assertEquals(0.5f, rect.getCentralX(), floatTol); 23 | assertEquals(1f, rect.getCentralY(), floatTol); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/Test_StateStack.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import com.google.gson.JsonObject; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertNull; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | public class Test_StateStack { 11 | @Test 12 | public void testStateStack() { 13 | StateStack stack = new StateStack<>(3); 14 | 15 | assertEquals(3, stack.getCapacity()); 16 | assertTrue(stack.isEmpty()); 17 | 18 | // Push one action 19 | stack.push(new StrokeCuratorState(StrokeCuratorUserAction.AddStroke, new JsonObject())); 20 | assertEquals(StrokeCuratorUserAction.AddStroke.toString(), stack.getLastUserAction()); 21 | assertEquals(1, stack.getSize()); 22 | assertEquals(0, stack.getUndoneSize()); 23 | 24 | // Undo 25 | stack.undo(); 26 | assertEquals(1, stack.getSize()); 27 | assertEquals(1, stack.getUndoneSize()); 28 | assertNull(stack.getLastUserAction()); 29 | 30 | // Redo 31 | stack.redo(); 32 | assertEquals(1, stack.getSize()); 33 | assertEquals(0, stack.getUndoneSize()); 34 | assertEquals(StrokeCuratorUserAction.AddStroke.toString(), stack.getLastUserAction()); 35 | 36 | // Push two more actions 37 | stack.push(new StrokeCuratorState(StrokeCuratorUserAction.ForceSetTokenName, new JsonObject())); 38 | stack.push(new StrokeCuratorState(StrokeCuratorUserAction.RemoveLastToken, new JsonObject())); 39 | 40 | assertEquals(3, stack.getSize()); 41 | assertEquals(0, stack.getUndoneSize()); 42 | assertEquals(StrokeCuratorUserAction.RemoveLastToken.toString(), stack.getLastUserAction()); 43 | 44 | // Undo 45 | stack.undo(); 46 | assertEquals(3, stack.getSize()); 47 | assertEquals(1, stack.getUndoneSize()); 48 | assertEquals(StrokeCuratorUserAction.ForceSetTokenName.toString(), stack.getLastUserAction()); 49 | 50 | // Redo 51 | stack.redo(); 52 | assertEquals(3, stack.getSize()); 53 | assertEquals(0, stack.getUndoneSize()); 54 | assertEquals(StrokeCuratorUserAction.RemoveLastToken.toString(), stack.getLastUserAction()); 55 | 56 | // Redo twice 57 | for (int i = 0; i < 2; ++i) { 58 | stack.undo(); 59 | } 60 | 61 | assertEquals(3, stack.getCapacity()); 62 | assertEquals(3, stack.getSize()); 63 | assertEquals(2, stack.getUndoneSize()); 64 | 65 | // Push a new action, that should have reset the stack pointer to 0 66 | stack.push(new StrokeCuratorState(StrokeCuratorUserAction.ClearStrokes, new JsonObject())); 67 | 68 | assertEquals(3, stack.getCapacity()); 69 | assertEquals(2, stack.getSize()); 70 | assertEquals(0, stack.getUndoneSize()); 71 | assertEquals(StrokeCuratorUserAction.ClearStrokes.toString(), stack.getLastUserAction()); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/Test_StrokeCuratorConfig.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.io.BufferedInputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.ObjectInputStream; 11 | import java.net.URL; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | public class Test_StrokeCuratorConfig { 16 | private StrokeCuratorConfig config; 17 | 18 | private URL tokenEngineFileUrl; 19 | 20 | private static final String TOKEN_ENGINE_TYPE = "SDV"; 21 | 22 | private TokenRecogEngine tokEngine = null; 23 | 24 | @Before 25 | public void setUp() throws Exception { 26 | 27 | final String testResourcesPath = "test" + File.separator + "resources"; 28 | 29 | final URL strokeCuratorConfigUrl = this.getClass().getClassLoader().getResource(testResourcesPath + 30 | File.separator + "config" + File.separator + "stroke_curator_config_test_1.json"); 31 | config = StrokeCuratorConfig.fromJsonFileAtUrl(strokeCuratorConfigUrl); 32 | 33 | tokenEngineFileUrl = this.getClass().getClassLoader().getResource(testResourcesPath + 34 | File.separator + "token_engine" + File.separator + "token_engine.sdv.sz0_whr1_ns1.ser"); 35 | 36 | // URL tokenEngineFileUrl = new File(tokenEnginePath).toURI().toURL(); 37 | 38 | ObjectInputStream objInStream = null; 39 | boolean readSuccessful = false; 40 | try { 41 | // objInStream = new ObjectInputStream(new FileInputStream( 42 | // tokenEngineFN)); 43 | objInStream = new ObjectInputStream(new BufferedInputStream(tokenEngineFileUrl.openStream())); 44 | 45 | if (TOKEN_ENGINE_TYPE.equals("SDV")) { 46 | tokEngine = (TokenRecogEngineSDV) objInStream.readObject(); 47 | } else if (TOKEN_ENGINE_TYPE.equals("IMG")) { 48 | tokEngine = (TokenRecogEngineIMG) objInStream.readObject(); 49 | } else { 50 | throw new RuntimeException("Unrecognized token engine type: " + TOKEN_ENGINE_TYPE); 51 | } 52 | 53 | readSuccessful = true; 54 | // objInStream.close(); 55 | } catch (IOException e) { 56 | fail(); 57 | } catch (ClassNotFoundException e) { 58 | fail(); 59 | } finally { 60 | try { 61 | objInStream.close(); 62 | } catch (IOException e) { 63 | fail(); 64 | } 65 | } 66 | 67 | } 68 | 69 | @After 70 | public void tearDown() throws Exception { 71 | } 72 | 73 | @Test 74 | public void test() { 75 | // StrokeCuratorConfig config = StrokeCuratorConfig.fromJsonFile(configFilePath); 76 | 77 | assertEquals(config.tokenPairRules.size(), 2); 78 | 79 | assertEquals("http://127.0.0.1:11610/glyphoid/token-recog", config.getRemoteTokenEngineUrl()); 80 | 81 | assertEquals(config.tokenPairRules.get(0).tokenA, "-"); 82 | assertEquals(config.tokenPairRules.get(0).tokenB, "-"); 83 | assertEquals(config.tokenPairRules.get(0).predicaments.size(), 4); 84 | assertEquals(config.tokenPairRules.get(0).predicaments.get(0), "relativeLengthDifference < 0.5"); 85 | assertEquals(config.tokenPairRules.get(0).predicaments.get(1), "relativeLeftXOffset < 0.2"); 86 | assertEquals(config.tokenPairRules.get(0).predicaments.get(2), "relativeRightXOffset < 0.2"); 87 | assertEquals(config.tokenPairRules.get(0).predicaments.get(3), "numTokensInBetween == 0"); 88 | assertEquals(config.tokenPairRules.get(0).recommendation, "mergeAs: \"=\""); 89 | 90 | assertEquals(config.tokenPairRules.get(1).tokenA, "tick"); 91 | assertEquals(config.tokenPairRules.get(1).tokenB, "-"); 92 | assertEquals(config.tokenPairRules.get(1).predicaments.size(), 0); 93 | assertEquals(config.tokenPairRules.get(1).recommendation, "mergeAs: \"root\""); 94 | 95 | assertEquals(config.mergePartners.size(), 1); 96 | assertEquals(config.mergePartners.get("root").size(), 0); 97 | } 98 | 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/TokenSetJsonTestHelper.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import com.google.gson.JsonArray; 4 | import com.google.gson.JsonObject; 5 | 6 | import static org.junit.Assert.assertEquals; 7 | import static org.junit.Assert.assertFalse; 8 | import static org.junit.Assert.assertTrue; 9 | 10 | public class TokenSetJsonTestHelper { 11 | public static void verifyTokenSetJson(JsonObject tokenSetJson) { 12 | assertTrue(tokenSetJson.has("tokens")); 13 | assertTrue(tokenSetJson.get("tokens").isJsonArray()); 14 | JsonArray tokensJson = tokenSetJson.get("tokens").getAsJsonArray(); 15 | for (int i = 0; i < tokensJson.size(); ++i) { 16 | JsonObject tokenJson = tokensJson.get(i).getAsJsonObject(); 17 | 18 | if (tokenJson.has("bounds")) { // Normal token 19 | TokenSetJsonTestHelper.verifyWrittenTokenJson(tokenJson); 20 | } else if (tokenJson.has("node")) { 21 | TokenSetJsonTestHelper.verifyNodeTokenJson(tokenJson); 22 | } 23 | } 24 | } 25 | 26 | public static void verifyWrittenTokenJson(JsonObject tokenJson) { 27 | assertTrue(tokenJson.get("bounds").isJsonArray()); 28 | assertTrue(tokenJson.get("width").isJsonPrimitive()); 29 | assertTrue(tokenJson.get("height").isJsonPrimitive()); 30 | assertTrue(tokenJson.get("recogWinner").isJsonPrimitive()); 31 | assertFalse(tokenJson.has("node")); 32 | assertFalse(tokenJson.has("wtSet")); 33 | } 34 | 35 | public static void verifyNodeTokenJson(JsonObject tokenJson) { 36 | assertTrue(tokenJson.get("node").isJsonObject()); 37 | 38 | assertTrue(tokenJson.get("tokenBounds").isJsonArray()); 39 | assertTrue(tokenJson.get("width").isJsonPrimitive()); 40 | assertTrue(tokenJson.get("height").isJsonPrimitive()); 41 | assertFalse(tokenJson.get("parsingResult").getAsString().isEmpty()); 42 | 43 | assertTrue(tokenJson.get("wtSet").isJsonObject()); 44 | 45 | JsonObject tokenSetJson = tokenJson.get("wtSet").getAsJsonObject(); 46 | verifyTokenSetJson(tokenSetJson); 47 | 48 | JsonObject nodeJson = tokenJson.get("node").getAsJsonObject(); 49 | verifyNodeJson(nodeJson); 50 | } 51 | 52 | public static void verifyNodeJson(JsonObject nodeJson) { 53 | assertTrue(nodeJson.get("isTerminal").isJsonPrimitive()); 54 | assertFalse(nodeJson.get("lhs").getAsString().isEmpty()); 55 | assertFalse(nodeJson.get("prodSumString").getAsString().isEmpty()); 56 | 57 | if (nodeJson.has("bounds") && nodeJson.get("bounds").isJsonArray()) { 58 | JsonArray bounds = nodeJson.get("bounds").getAsJsonArray(); 59 | assertEquals(4, bounds.size()); 60 | } 61 | 62 | boolean isTerminal = nodeJson.get("isTerminal").getAsBoolean(); 63 | if (!isTerminal) { 64 | assertTrue(nodeJson.get("rhsTypes").isJsonArray()); 65 | assertTrue(nodeJson.get("ch").isJsonArray()); 66 | 67 | JsonArray children = nodeJson.get("ch").getAsJsonArray(); 68 | for (int i = 0; i < children.size(); ++i) { 69 | JsonObject childNode = children.get(i).getAsJsonObject(); 70 | 71 | verifyNodeJson(childNode); 72 | } 73 | } 74 | 75 | 76 | 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/utils/Test_CHandWritingTokenImageData.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.fail; 5 | 6 | import org.junit.Test; 7 | 8 | import org.apache.commons.io.FileUtils; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.net.URL; 13 | 14 | public class Test_CHandWritingTokenImageData { 15 | static final float floatTol = 1e-3F; 16 | 17 | @Test 18 | public void testReadImFileNoSourceEqualSeparator() { 19 | final String testResourcesPath = "test" + File.separator + "resources"; 20 | final URL imUrl = this.getClass().getClassLoader().getResource(testResourcesPath + 21 | File.separator + "tokens" + File.separator + 22 | "no_source_equal_separator.im"); 23 | 24 | File f = null; 25 | try { 26 | f = File.createTempFile("test", ".im"); 27 | FileUtils.copyURLToFile(imUrl, f); 28 | 29 | CHandWritingTokenImageData imData = CHandWritingTokenImageData.readImFile( 30 | f, false, true, true); 31 | 32 | assertEquals("a", imData.tokenName); 33 | assertEquals(16, imData.nw); 34 | assertEquals(16, imData.nh); 35 | assertEquals(122.77209, imData.w, floatTol); 36 | assertEquals(119.35361, imData.h, floatTol); 37 | assertEquals(1, imData.nStrokes); 38 | assertEquals(2 + 16 * 16, imData.imData.length); 39 | } catch (IOException e) { 40 | fail("Failed due to IOException: " + e.toString()); 41 | } finally { 42 | f.delete(); 43 | } 44 | } 45 | 46 | @Test 47 | public void testReadImFileWithSourceColonSeparator() { 48 | final String testResourcesPath = "test" + File.separator + "resources"; 49 | final URL imUrl = this.getClass().getClassLoader().getResource(testResourcesPath + 50 | File.separator + "tokens" + File.separator + 51 | "with_source_colon_separator.im"); 52 | 53 | File f = null; 54 | try { 55 | f = File.createTempFile("test2", ".im"); 56 | FileUtils.copyURLToFile(imUrl, f); 57 | 58 | CHandWritingTokenImageData imData = CHandWritingTokenImageData.readImFile( 59 | f, false, true, true); 60 | 61 | assertEquals("不", imData.tokenName); 62 | assertEquals(70, imData.nw); 63 | assertEquals(70, imData.nh); 64 | assertEquals(286.0, imData.w, floatTol); 65 | assertEquals(276.0, imData.h, floatTol); 66 | assertEquals(4, imData.nStrokes); 67 | assertEquals(2 + 70 * 70, imData.imData.length); 68 | } catch (IOException e) { 69 | fail("Failed due to IOException: " + e.toString()); 70 | } finally { 71 | f.delete(); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /src/test/java/me/scai/handwriting/utils/Test_LimitedStack.java: -------------------------------------------------------------------------------- 1 | package me.scai.handwriting.utils; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.NoSuchElementException; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | public class Test_LimitedStack { 10 | @Test 11 | public void testLimitedStack() { 12 | LimitedStack stack = new LimitedStack<>(3); 13 | 14 | assertEquals(3, stack.getCapacity()); 15 | assertEquals(0, stack.size()); 16 | assertTrue(stack.isEmpty()); 17 | 18 | stack.push(10); 19 | assertTrue(stack.peek() == 10); 20 | assertFalse(stack.isEmpty()); 21 | 22 | stack.push(20); 23 | assertTrue(stack.peek() == 20); 24 | assertTrue(stack.get(0) == 20); 25 | assertTrue(stack.get(1) == 10); 26 | 27 | assertTrue(stack.pop() == 20); 28 | assertTrue(stack.peek() == 10); 29 | 30 | stack.push(20); 31 | stack.push(30); 32 | assertEquals(3, stack.size()); 33 | 34 | stack.push(40); 35 | assertEquals(3, stack.size()); 36 | assertTrue(stack.pop() == 40); 37 | assertTrue(stack.pop() == 30); 38 | assertTrue(stack.pop() == 20); 39 | 40 | assertTrue(stack.isEmpty()); 41 | 42 | NoSuchElementException caughtException = null; 43 | try { 44 | stack.pop(); 45 | } catch (NoSuchElementException e) { 46 | caughtException = e; 47 | } 48 | assertNotNull(caughtException); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/Test_GraphicalProductionJsonConversion.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonArray; 5 | import com.google.gson.JsonObject; 6 | import me.scai.handwriting.TestHelper; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import static org.junit.Assert.assertFalse; 11 | import static org.junit.Assert.assertNotNull; 12 | import static org.junit.Assert.assertTrue; 13 | 14 | public class Test_GraphicalProductionJsonConversion { 15 | private static final Gson gson = new Gson(); 16 | 17 | GraphicalProductionSet gpSet; 18 | 19 | @Before 20 | public void setup() { 21 | TestHelper.WorkerTuple workerTuple = TestHelper.getTestWorkerTuple(); 22 | 23 | gpSet = workerTuple.tokenSetParser.getGraphicalProductionSet(); 24 | assertNotNull(gpSet); 25 | } 26 | 27 | @Test 28 | public void testConversionAll() { 29 | 30 | for (GraphicalProduction gp1 : gpSet.prods) { 31 | assertNotNull(gp1); 32 | JsonObject gpObj1 = gson.toJsonTree(gp1).getAsJsonObject(); 33 | 34 | assertNotNull(gpObj1.get("lhs").getAsString()); 35 | assertTrue(gpObj1.get("nrhs").getAsInt() > 0); 36 | 37 | assertNotNull(gpObj1.get("rhs").getAsJsonArray()); 38 | assertNotNull(gpObj1.get("rhsIsTerminal").getAsJsonArray()); 39 | 40 | assertNotNull(gpObj1.get("sumString").getAsString()); 41 | assertNotNull(gpObj1.get("geomShortcut").getAsJsonObject()); 42 | assertNotNull(gpObj1.get("geomShortcut").getAsJsonObject().get("shortcutType")); 43 | 44 | assertFalse(gpObj1.has("terminalSet")); 45 | } 46 | } 47 | 48 | @Test 49 | public void testConversionGraphicalProductions() { 50 | JsonArray gpSetArray = gson.toJsonTree(gpSet.prods).getAsJsonArray(); 51 | 52 | assertNotNull(gpSetArray); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/Test_NodeAnalyzer.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree; 2 | 3 | import me.scai.parsetree.evaluation.ParseTreeEvaluator; 4 | import org.junit.Test; 5 | 6 | import java.io.File; 7 | import java.io.FileNotFoundException; 8 | import java.io.IOException; 9 | import java.net.InetAddress; 10 | import java.net.URL; 11 | import java.util.List; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | public class Test_NodeAnalyzer { 16 | private static final float tol = 1e-9f; 17 | 18 | @Test 19 | public void testNodeHasTwoTerminalChildren() { 20 | Node n1 = new Node("ROOT", "ROOT->gpA", new String[] {"gpA"}); 21 | Node n2 = new Node("gpA", "gpA->gpB", new String[] {"gpB"}); 22 | Node n3 = new Node("gpB", "gpB->gpC", new String[] {"gpC"}); 23 | Node n4 = new Node("gpC", "gpC->terminalX terminalY", new String[] {"terminalX", "terminalY"}); 24 | Node nt1 = new Node(); 25 | Node nt2 = new Node(); 26 | 27 | assertTrue(nt1.isTerminal()); 28 | assertTrue(nt2.isTerminal()); 29 | 30 | n1.setChild(0, n2); 31 | n2.setChild(0, n3); 32 | n3.setChild(0, n4); 33 | n4.setChild(0, nt1); 34 | n4.setChild(1, nt2); 35 | 36 | List validLHS = NodeAnalyzer.getValidProductionLHS(n1); 37 | 38 | assertNotNull(validLHS); 39 | assertEquals(4, validLHS.size()); 40 | assertEquals("ROOT", validLHS.get(0)); 41 | assertEquals("gpA", validLHS.get(1)); 42 | assertEquals("gpB", validLHS.get(2)); 43 | assertEquals("gpC", validLHS.get(3)); 44 | } 45 | 46 | @Test 47 | public void testNodeHasOneChild() { 48 | Node n1 = new Node("ROOT", "ROOT->gpA", new String[] {"gpA"}); 49 | Node n2 = new Node("gpA", "gpA->gpB", new String[] {"gpB"}); 50 | Node n3 = new Node("gpB", "gpB->gpC", new String[] {"gpC"}); 51 | Node n4 = new Node("gpC", "gpC->terminalX", new String[] {"terminalX"}); 52 | Node nt1 = new Node(); 53 | 54 | assertTrue(nt1.isTerminal()); 55 | 56 | n1.setChild(0, n2); 57 | n2.setChild(0, n3); 58 | n3.setChild(0, n4); 59 | n4.setChild(0, nt1); 60 | 61 | List validLHS = NodeAnalyzer.getValidProductionLHS(n1); 62 | 63 | assertNotNull(validLHS); 64 | assertEquals(4, validLHS.size()); 65 | assertEquals("ROOT", validLHS.get(0)); 66 | assertEquals("gpA", validLHS.get(1)); 67 | assertEquals("gpB", validLHS.get(2)); 68 | assertEquals("gpC", validLHS.get(3)); 69 | 70 | } 71 | 72 | @Test 73 | public void testOneValidGraphicalProduction() { 74 | Node n1 = new Node("gpA", "gpA->terminalA", new String[] {"terminalA"}); 75 | Node nt1 = new Node(); 76 | 77 | assertFalse(n1.isTerminal()); 78 | assertTrue(nt1.isTerminal()); 79 | 80 | n1.setChild(0, nt1); 81 | 82 | List validLHS = NodeAnalyzer.getValidProductionLHS(n1); 83 | 84 | assertNotNull(validLHS); 85 | assertEquals(1, validLHS.size()); 86 | assertEquals("gpA", validLHS.get(0)); 87 | 88 | } 89 | 90 | @Test 91 | public void testCalcNodeBounds1() { 92 | Node n1 = new Node("gpA", "gpA->terminalA terminalB", new String[] {"terminalA", "terminalB"}); 93 | Node nt1 = new Node(); 94 | Node nt2 = new Node(); 95 | 96 | nt1.setBounds(new float[] {0f, 0f, 1f, 1f}); 97 | nt2.setBounds(new float[] {2f, 0f, 3f, 1f}); 98 | 99 | assertTrue(nt1.isTerminal()); 100 | assertTrue(nt2.isTerminal()); 101 | n1.setChild(0, nt1); 102 | n1.setChild(1, nt2); 103 | 104 | float[] nodeBounds = NodeAnalyzer.calcNodeBounds(n1); 105 | assertArrayEquals(new float[] {0f, 0f, 3f, 1f}, nodeBounds, tol); 106 | } 107 | 108 | @Test 109 | public void testCalcNodeBounds2() { 110 | 111 | Node n1 = new Node("gpA", "gpA->terminalA gpB", new String[] {"terminalA", "gpB"}); 112 | Node n2 = new Node("gpB", "gpB->gpC", new String[] {"grC"}); 113 | Node n3 = new Node("gpC", "gpC->terminalB", new String[] {"terminalB"}); 114 | 115 | Node ntA = new Node(); 116 | Node ntB = new Node(); 117 | 118 | ntA.setBounds(new float[] {0f, 0f, 1f, 1f}); 119 | ntB.setBounds(new float[] {0.5f, 0.25f, 1.5f, 0.75f}); 120 | 121 | assertFalse(n1.isTerminal()); 122 | assertFalse(n2.isTerminal()); 123 | assertFalse(n3.isTerminal()); 124 | 125 | assertTrue(ntA.isTerminal()); 126 | assertTrue(ntB.isTerminal()); 127 | 128 | n1.setChild(0, ntA); 129 | n1.setChild(1, n2); 130 | n2.setChild(0, n3); 131 | n3.setChild(0, ntB); 132 | 133 | float[] nodeBounds = NodeAnalyzer.calcNodeBounds(n1); 134 | assertArrayEquals(new float[] {0f, 0f, 1.5f, 1f}, nodeBounds, tol); 135 | } 136 | 137 | 138 | 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/Test_ParseTreeMathTexifier.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertArrayEquals; 5 | 6 | import org.junit.After; 7 | import org.junit.AfterClass; 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | 12 | public class Test_ParseTreeMathTexifier { 13 | 14 | @BeforeClass 15 | public static void setUpBeforeClass() throws Exception { 16 | } 17 | 18 | @AfterClass 19 | public static void tearDownAfterClass() throws Exception { 20 | } 21 | 22 | @Before 23 | public void setUp() throws Exception { 24 | } 25 | 26 | @After 27 | public void tearDown() throws Exception { 28 | } 29 | 30 | @Test 31 | public void testGetTexFunctionName() { 32 | String item = "GET(n0)"; 33 | 34 | String funcName = ParseTreeMathTexifier.getTexFunctionName(item); 35 | 36 | assertEquals(funcName, "GET"); 37 | } 38 | 39 | @Test 40 | public void testGetTexFunctionArgIndices0() { 41 | String item = "GETBAR(n1, n3)"; 42 | 43 | int [] argIndices = ParseTreeMathTexifier.getTexFunctionArgIndices(item); 44 | int [] trueArgIndices = {1, 3}; 45 | 46 | assertArrayEquals(argIndices, trueArgIndices); 47 | } 48 | 49 | @Test 50 | public void testGetTexFunctionArgIndices1() { 51 | String item = "GET_FOO(n88)"; 52 | 53 | int [] argIndices = ParseTreeMathTexifier.getTexFunctionArgIndices(item); 54 | int [] trueArgIndices = {88}; 55 | 56 | assertArrayEquals(argIndices, trueArgIndices); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/Test_SerializeNode.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import me.scai.handwriting.CWrittenTokenSetNoStroke; 6 | import me.scai.handwriting.NodeToken; 7 | import me.scai.handwriting.TestHelper; 8 | import me.scai.parsetree.evaluation.ParseTreeEvaluator; 9 | import me.scai.plato.helpers.CWrittenTokenJsonHelper; 10 | import me.scai.plato.helpers.CWrittenTokenSetJsonHelper; 11 | import org.junit.BeforeClass; 12 | import org.junit.Test; 13 | 14 | import static org.junit.Assert.*; 15 | 16 | public class Test_SerializeNode { 17 | private static final Gson gson = new Gson(); 18 | 19 | private static TokenSetParser tokenSetParser; 20 | private static ParseTreeStringizer stringizer; 21 | private static ParseTreeEvaluator evaluator; 22 | 23 | private static GraphicalProductionSet gpSet; 24 | private static TerminalSet termSet; 25 | 26 | 27 | @BeforeClass 28 | public static void beforeClass() { // TODO: Refactor and de-duplicate with Test_NodeToken 29 | TestHelper.WorkerTuple workerTuple = TestHelper.getTestWorkerTuple(); 30 | 31 | gpSet = workerTuple.gpSet; 32 | termSet = workerTuple.termSet; 33 | 34 | tokenSetParser = workerTuple.tokenSetParser; 35 | stringizer = workerTuple.stringizer; 36 | evaluator = workerTuple.evaluator; 37 | } 38 | 39 | private Node parseTokenSet(CWrittenTokenSetNoStroke wtSet) { 40 | Node node = null; 41 | try { 42 | node = tokenSetParser.parse(wtSet); 43 | } catch (TokenSetParserException e) { 44 | fail("Parsing failed due to TokenSetParserException: " + e.getMessage()); 45 | } catch (InterruptedException e) { 46 | fail("Parsing failed due to InterruptedException: " + e.getMessage()); 47 | } 48 | 49 | return node; 50 | } 51 | 52 | @Test 53 | public void test1() { 54 | Node node1 = new Node(); 55 | 56 | JsonObject jsonNode1 = gson.toJsonTree(node1).getAsJsonObject(); 57 | 58 | assertNotNull(jsonNode1); 59 | } 60 | 61 | @Test 62 | public void test2() { 63 | CWrittenTokenSetNoStroke wtSet = TestHelper.getMockTokenSet( 64 | new float[][] { 65 | {0f, 0f, 1f, 1f}, 66 | {2f, 0f, 3f, 1f}, 67 | {4f, 0f, 5f, 1f} 68 | }, 69 | new String[] {"1", "+", "2"} 70 | ); 71 | 72 | assertFalse(wtSet.hasNodeToken()); 73 | assertEquals(3, wtSet.getNumTokens()); 74 | 75 | Node node = parseTokenSet(wtSet); 76 | JsonObject jsonNode = gson.toJsonTree(node).getAsJsonObject(); 77 | 78 | NodeToken nodeToken = new NodeToken(node, wtSet); 79 | 80 | JsonObject jsonNodeToken = CWrittenTokenJsonHelper.CWrittenToken2JsonObject(nodeToken); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/Test_TerminalSet.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree; 2 | 3 | import static org.junit.Assert.*; 4 | 5 | import me.scai.handwriting.TestHelper; 6 | import org.junit.After; 7 | import org.junit.AfterClass; 8 | import org.junit.Before; 9 | import org.junit.BeforeClass; 10 | import org.junit.Test; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.net.URL; 15 | 16 | public class Test_TerminalSet { 17 | /* Constants */ 18 | private static final String TEST_ROOT_DIR = TestHelper.TEST_ROOT_DIR; 19 | private static final String RESOURCES_DIR = "resources"; 20 | private static final String RESOURCES_CONFIG_DIR = "config"; 21 | private static final String TERMINALS_JSON_FILE = "terminals.json"; 22 | 23 | /* Member variables */ 24 | TerminalSet ts; 25 | 26 | @BeforeClass 27 | public static void setUpBeforeClass() throws Exception { 28 | } 29 | 30 | @AfterClass 31 | public static void tearDownAfterClass() throws Exception { 32 | } 33 | 34 | @Before 35 | public void setUp() throws Exception { 36 | ts = new TerminalSet(); 37 | 38 | final URL tsJsonFileUrl = Test_TerminalSet.class.getClassLoader().getResource( 39 | TEST_ROOT_DIR + File.separator + RESOURCES_DIR + 40 | File.separator + RESOURCES_CONFIG_DIR + 41 | File.separator + TERMINALS_JSON_FILE); 42 | try { 43 | ts.readFromJsonAtUrl(tsJsonFileUrl); 44 | } 45 | catch (IOException exc) { 46 | fail("Failed due to IOException: " + exc.getMessage()); 47 | } 48 | } 49 | 50 | @After 51 | public void tearDown() throws Exception { 52 | } 53 | 54 | @Test 55 | public void test() { 56 | assertFalse(ts.token2TypesMap.isEmpty()); 57 | assertFalse(ts.type2TokenMap.isEmpty()); 58 | assertFalse(ts.token2TexNotationMap.isEmpty()); 59 | } 60 | 61 | @Test 62 | public void testFactoryMethod() { 63 | assertFalse(ts.token2TypesMap.isEmpty()); 64 | assertFalse(ts.type2TokenMap.isEmpty()); 65 | assertFalse(ts.token2TexNotationMap.isEmpty()); 66 | } 67 | 68 | @Test 69 | public void testTokenName2TokenTypeMatch() { 70 | // WARNING: These assertions are dependent on the content of the terminal set config file 71 | 72 | // Negative cases 73 | assertFalse(ts.match("1", "COMPARATOR")); 74 | assertFalse(ts.match("2", "COMPARATOR")); 75 | 76 | assertFalse(ts.match("k", "TERMINAL(j)")); 77 | 78 | // Positive cases 79 | assertTrue(ts.match("gt", "COMPARATOR")); 80 | assertTrue(ts.match("gte", "COMPARATOR")); 81 | 82 | // Overloaded operators 83 | assertTrue(ts.match("=", "COMPARATOR")); 84 | assertTrue(ts.match("=", "ASSIGN_OP")); 85 | 86 | assertTrue(ts.match("V", "VARIABLE_SYMBOL")); 87 | assertTrue(ts.match("V", "LOGICAL_OR_OP")); 88 | 89 | assertTrue(ts.match("i", "TERMINAL(i)")); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/geometry/Test_GeometricRelationJsonConversion.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class Test_GeometricRelationJsonConversion { 10 | private static final Gson gson = new Gson(); 11 | 12 | @Test 13 | public void testConvertAlignRelation() { 14 | AlignRelation ar1 = new AlignRelation(AlignRelation.AlignType.AlignBottom, 1, 0); 15 | 16 | JsonObject arObj1 = gson.toJsonTree(ar1).getAsJsonObject(); 17 | 18 | assertEquals("AlignBottom", arObj1.get("alignType").getAsString()); 19 | 20 | assertEquals(1, arObj1.get("idxTested").getAsJsonArray().size()); 21 | assertEquals(1, arObj1.get("idxTested").getAsJsonArray().get(0).getAsInt()); 22 | 23 | assertEquals(1, arObj1.get("idxInRel").getAsJsonArray().size()); 24 | assertEquals(0, arObj1.get("idxInRel").getAsJsonArray().get(0).getAsInt()); 25 | } 26 | 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/me/scai/parsetree/geometry/Test_GeometryHelper.java: -------------------------------------------------------------------------------- 1 | package me.scai.parsetree.geometry; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertArrayEquals; 6 | import static org.junit.Assert.assertEquals; 7 | 8 | public class Test_GeometryHelper { 9 | private static final float tol = 1e-9f; 10 | 11 | @Test 12 | public void testMergeBounds_bEnclosesA() { 13 | float[] boundsA = new float[] {10f, 10f, 20f, 20f}; 14 | float[] boundsB = new float[] {-20f, -10f, 30f, 40f}; 15 | 16 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 17 | assertEquals(4, mergedBounds.length); 18 | assertArrayEquals(new float[] {-20f, -10f, 30f, 40f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 19 | } 20 | 21 | @Test 22 | public void testMergeBounds_aEnclosesB() { 23 | float[] boundsA = new float[] {-20f, -10f, 30f, 40f}; 24 | float[] boundsB = new float[] {5f, 5f, 10f, 10f}; 25 | 26 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 27 | assertEquals(4, mergedBounds.length); 28 | assertArrayEquals(new float[] {-20f, -10f, 30f, 40f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 29 | } 30 | 31 | @Test 32 | public void testMergeBounds_nonEnclosingNonOverlap1() { 33 | float[] boundsA = new float[] {-20f, -10f, 30f, 40f}; 34 | float[] boundsB = new float[] {40f, -10f, 80f, 40f}; 35 | 36 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 37 | assertEquals(4, mergedBounds.length); 38 | assertArrayEquals(new float[] {-20f, -10f, 80f, 40f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 39 | } 40 | 41 | @Test 42 | public void testMergeBounds_nonEnclosingNonOverlap2() { 43 | float[] boundsA = new float[] {-20f, -10f, 30f, 40f}; 44 | float[] boundsB = new float[] {-20f, 50f, 30f, 80f}; 45 | 46 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 47 | assertEquals(4, mergedBounds.length); 48 | assertArrayEquals(new float[] {-20f, -10f, 30f, 80f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 49 | } 50 | 51 | @Test 52 | public void testMergeBounds_nonEnclosingNonOverlap3() { 53 | float[] boundsA = new float[] {-20f, -10f, 30f, 40f}; 54 | float[] boundsB = new float[] {30f, 40f, 80f, 90f}; 55 | 56 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 57 | assertEquals(4, mergedBounds.length); 58 | assertArrayEquals(new float[] {-20f, -10f, 80f, 90f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 59 | } 60 | 61 | @Test 62 | public void testMergeBounds_nonEnclosingOverlapping1() { 63 | float[] boundsA = new float[] {-20f, -10f, 30f, 40f}; 64 | float[] boundsB = new float[] {0f, 10f, 80f, 90f}; 65 | 66 | float[] mergedBounds = GeometryHelper.mergeBounds(boundsA, boundsB); 67 | assertEquals(4, mergedBounds.length); 68 | assertArrayEquals(new float[] {-20f, -10f, 80f, 90f}, GeometryHelper.mergeBounds(boundsA, boundsB), tol); 69 | } 70 | 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/resources/config/stroke_curator_config_test_1.json: -------------------------------------------------------------------------------- 1 | { 2 | "remoteTokenEngineUrl" : "http://127.0.0.1:11610/glyphoid/token-recog", 3 | "tokenPairRules" : 4 | [ 5 | { 6 | "tokenA" : "-", 7 | "tokenB" : "-", 8 | "predicaments" : 9 | [ 10 | "relativeLengthDifference < 0.5", 11 | "relativeLeftXOffset < 0.2", 12 | "relativeRightXOffset < 0.2", 13 | "numTokensInBetween == 0" 14 | ], 15 | "recommendation" : "mergeAs: \"=\"" 16 | }, 17 | { 18 | "tokenA" : "tick", 19 | "tokenB" : "-", 20 | "predicaments" : 21 | [ 22 | ], 23 | "recommendation" : "mergeAs: \"root\"" 24 | } 25 | ], 26 | "mergePartners" : 27 | { 28 | "root": [] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/token_engine/token_engine.sdv.sz0_whr1_ns1.ser: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Glyphoid/math-handwriting-lib/2049f321e4a654ad3eb69cfff5326d9761c8ccb0/src/test/resources/token_engine/token_engine.sdv.sz0_whr1_ns1.ser -------------------------------------------------------------------------------- /src/test/resources/tokens/no_source_equal_separator.im: -------------------------------------------------------------------------------- 1 | Token name: a 2 | n_w = 16 3 | n_h = 16 4 | ns = 1 5 | w = 122.77209 6 | h = 119.35361 7 | 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 8 | 0 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 9 | 0 0 0 0 1 0 0 0 0 1 0 1 0 0 0 0 10 | 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 11 | 0 0 1 0 0 0 0 0 1 0 0 0 1 0 0 0 12 | 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 13 | 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 14 | 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 15 | 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 16 | 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 17 | 1 0 0 0 0 1 1 0 0 0 0 0 0 0 0 1 18 | 1 0 0 0 0 1 1 0 0 0 0 0 0 0 1 0 19 | 1 0 0 0 1 0 0 1 0 0 0 0 0 1 0 0 20 | 1 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 21 | 1 1 1 0 0 0 0 1 0 0 0 0 1 0 0 0 22 | 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 23 | -------------------------------------------------------------------------------- /src/test/resources/tokens/token_no_source.wt: -------------------------------------------------------------------------------- 1 | Token name: f 2 | CWrittenToken (nStrokes=2): 3 | Stroke (np=28): 4 | xs=[1.000, 0.866, 0.808, 0.721, 0.642, 0.568, 0.493, 0.444, 0.405, 0.380, 0.362, 0.393, 0.407, 0.425, 0.441, 0.453, 0.440, 0.422, 0.411, 0.399, 0.362, 0.332, 0.279, 0.213, 0.142, 0.084, 0.027, 0.000] 5 | ys=[0.000, 0.044, 0.061, 0.088, 0.117, 0.161, 0.209, 0.251, 0.303, 0.359, 0.414, 0.528, 0.551, 0.594, 0.638, 0.681, 0.725, 0.767, 0.817, 0.855, 0.895, 0.929, 0.955, 0.976, 0.998, 1.000, 0.994, 0.983] 6 | Stroke (np=10): 7 | xs=[0.000, 0.124, 0.211, 0.332, 0.486, 0.627, 0.735, 0.815, 0.877, 0.916] 8 | ys=[0.328, 0.315, 0.314, 0.315, 0.324, 0.324, 0.324, 0.322, 0.324, 0.328] 9 | -------------------------------------------------------------------------------- /src/test/resources/tokens/token_with_source_and_unicode_token.wt: -------------------------------------------------------------------------------- 1 | Token name: 不 2 | source: ipad 3 | CWrittenToken (nStrokes=4): 4 | Stroke (np=10): 5 | xs=[0.187, 0.206, 0.257, 0.352, 0.463, 0.575, 0.692, 0.797, 0.88, 0.95] 6 | ys=[0.123, 0.126, 0.126, 0.126, 0.126, 0.117, 0.104, 0.088, 0.075, 0.066] 7 | Stroke (np=11): 8 | xs=[0.632, 0.61, 0.565, 0.508, 0.444, 0.378, 0.301, 0.228, 0.149, 0.095, 0.05] 9 | ys=[0.171, 0.187, 0.215, 0.269, 0.323, 0.381, 0.441, 0.495, 0.537, 0.575, 0.597] 10 | Stroke (np=14): 11 | xs=[0.533, 0.524, 0.521, 0.514, 0.508, 0.498, 0.492, 0.486, 0.483, 0.479, 0.473, 0.473, 0.473, 0.473] 12 | ys=[0.349, 0.368, 0.403, 0.463, 0.543, 0.632, 0.721, 0.797, 0.855, 0.896, 0.918, 0.931, 0.934, 0.928] 13 | Stroke (np=6): 14 | xs=[0.727, 0.753, 0.781, 0.813, 0.839, 0.87] 15 | ys=[0.47, 0.479, 0.489, 0.508, 0.524, 0.549] 16 | --------------------------------------------------------------------------------