diff --git a/.gitignore b/.gitignore index 6245947..25eb8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ +# Ignored directories .gradle/ .nb-gradle/ + +# https://stackoverflow.com/questions/5533050/gitignore-exclude-folder-but-include-specific-subfolder +/build/* +!/build/libs/ + +# Ignored files .nb-gradle-properties -/build/classes/* -/build/tmp/* -/build/docs/* -/build/distributions/* -!/build/libs/* gradle.properties \ No newline at end of file diff --git a/README.md b/README.md index aacba11..853066e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # AutoCAD Drawing Checker -A simple, extendable Java program used to compare Excel exports from AutoCAD. +A simple, extendable Java program used to compare Excel and CSV files ## Project Summary Instructors and teachers in college classes are expected to teach their students by giving lectures and providing learning resources. @@ -10,6 +10,11 @@ Fortunately, computers are very good at doing tedious processes with objective s This program is meant to compare data exports from AutoCAD to each other, grading them based upon how similar they both are. These AutoCAD exports are in either XLS or XLSX format, which is easily parse-able by the Apache POI Library. +This application also supports a wider variety of files, including: +* any Excel file with headers in the first row +* CSV data with or without headers +* GPS survey data (see an example under src/main/resources/exports + ## Required Software * Users need only ensure they have [Java](https://java.com/en/) installed on their computer to run the application. * Developers wishing to change this application for their own use will need [Gradle](https://gradle.org/) installed to build the application. @@ -20,15 +25,19 @@ This file is a self-contained application, so you can move it wherever you want it doesn't rely on the other project files to run. Simply double-click the JAR file to run it. ### Steps -* Step 1: Choosing instructor and student files. The first page of the application has you choose at least 2 files: +* Step 1: choose the file format. This tells the program the format of the data you will provide, and determines which criteria it can grade. +* Step 2: Choosing instructor and student files. You will choose at least 2 files: the instructor file, and 1 or more student files. The instructor file is what the student files will be graded on: The more similar their file is to the instructor file, the higher their grade will be. When selecting student files, you have several options: - * Choose 1 or more Excel files - * Choose 1 or more folders. Note that the program locates all Excel files under this folder, so it's OK if it has other files in there, the program will just ignore them. + * Choose 1 or more Excel or CSV files + * Choose 1 or more folders. Note that the program locates all relevant files under this folder, so it's OK if it has other files in there, the program will just ignore them. * A combination of the above. Also of interest: you can drag and drop files into the two selectors instead of clicking the button. -* Step 2: Choosing Grading Criteria. You can choose what the program grades students on by toggling these check boxes on or off. Regardless of what you select, the program will still grade every column in the instructor file. -* Step 3: Running the Autograder. (Don't forget to click 'Run'!) Once the program is done grading, it will ask you where you want to save the grading report. Simply choose a folder, and the program will automatically name the file for you. +* Step 3: Choosing Grading Criteria. You can choose what the program grades students on by toggling these check boxes on or off. +* Step 4: Running the Autograder. The program will automatically run the grader when you get to the last page. +Once the program is done grading, it will ask you where you want to save the grading report. +Simply choose a folder, and the program will automatically name the file for you. +If you forget to save the file, just click "Run" to rerun the autograder. ### Troubleshooting If anything goes wrong, and you are unsure what to do about it, you'll want to click Log -> Save Log in the program menu bar along the top. @@ -37,13 +46,14 @@ You can contact Matt if you need help, and you provide him with the Log file you ## Matt's To Do List * see AutoCADElementMatcher * Should we match rows for each comparison, or for the export as a whole? (wait on this) -* Given double X and double Y, don't do "X == Y", instead do "Math.abs(X - Y) < threshold" * Look into Federal Section 508 * https://www.epa.gov/accessibility/what-section-508 * https://www.section508.gov/ * https://www.section508.gov/create * https://www.access-board.gov/guidelines-and-standards/communications-and-it/about-the-ict-refresh/final-rule/text-of-the-standards-and-guidelines - +* maybe just add a check for AbstractGradingCriteria.canGradeDataType(...) ## Helpful Links +* [Apache CLI](https://commons.apache.org/proper/commons-cli/javadocs/api-release/index.html) +* [CSV Library](https://javadoc.io/doc/org.apache.commons/commons-csv/latest/index.html) * [Excel Library](https://poi.apache.org/apidocs/4.1/) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7dcb025..fb7c36c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,15 @@ -apply plugin: 'java' -apply plugin: 'application' +plugins { // Double quotes evaluate some expressions a-la backticks in JS. Sounds like I should use single quotes by default + id 'java' + id 'application' +} + +/* + * Needs this specific name to work with the application + * plugin. Change this property to set the main class for + * running and Jar-ing +*/ +mainClassName = 'autocadDrawingChecker.start.Main' +version = '2.5' sourceCompatibility = '1.8' [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' @@ -12,16 +22,16 @@ sourceCompatibility = '1.8' // prefer. In this case NetBeans will not add these tasks but you may rely on // your own implementation. if (!hasProperty('mainClass')) { - ext.mainClass = 'autocadDrawingChecker.start.Main' + ext.mainClass = mainClassName } -mainClassName = 'autocadDrawingChecker.start.Main' // need this for the application plugin - jar { //need to manually set main class, otherwise, it just compiles the JAR as a library manifest { attributes( - 'Main-Class': 'autocadDrawingChecker.start.Main' + 'Main-Class': mainClassName, + 'Implementation-Title': project.name, + 'Implementation-Version': project.version ) } @@ -39,6 +49,10 @@ jar { exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' } +tasks.withType(JavaCompile) { + options.compilerArgs << '-Xlint:unchecked' +} + repositories { mavenCentral() // You may define additional repositories, or even remove "mavenCentral()". @@ -52,6 +66,11 @@ dependencies { // http://www.gradle.org/docs/current/userguide/dependency_management.html#sec:how_to_declare_your_dependencies testCompile group: 'junit', name: 'junit', version: '4.10' + compile group: 'commons-cli', name: 'commons-cli', version: '1.4' + + // https://mvnrepository.com/artifact/org.apache.commons/commons-csv + compile group: 'org.apache.commons', name: 'commons-csv', version: '1.8' + // https://mvnrepository.com/artifact/org.apache.poi/poi compile group: 'org.apache.poi', name: 'poi', version: '4.1.2' diff --git a/build/libs/AutoCADDrawingChecker.jar b/build/libs/AutoCADDrawingChecker-2.5.jar similarity index 94% rename from build/libs/AutoCADDrawingChecker.jar rename to build/libs/AutoCADDrawingChecker-2.5.jar index 9674d52..e29d855 100644 Binary files a/build/libs/AutoCADDrawingChecker.jar and b/build/libs/AutoCADDrawingChecker-2.5.jar differ diff --git a/build/scripts/AutoCADDrawingChecker b/build/scripts/AutoCADDrawingChecker deleted file mode 100644 index b381c22..0000000 --- a/build/scripts/AutoCADDrawingChecker +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## AutoCADDrawingChecker start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/.." >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="AutoCADDrawingChecker" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and AUTO_CAD_DRAWING_CHECKER_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/lib/AutoCADDrawingChecker.jar:$APP_HOME/lib/poi-ooxml-4.1.2.jar:$APP_HOME/lib/poi-4.1.2.jar:$APP_HOME/lib/commons-codec-1.13.jar:$APP_HOME/lib/commons-collections4-4.4.jar:$APP_HOME/lib/commons-math3-3.6.1.jar:$APP_HOME/lib/SparseBitSet-1.2.jar:$APP_HOME/lib/poi-ooxml-schemas-4.1.2.jar:$APP_HOME/lib/commons-compress-1.19.jar:$APP_HOME/lib/curvesapi-1.06.jar:$APP_HOME/lib/xmlbeans-3.1.0.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $AUTO_CAD_DRAWING_CHECKER_OPTS -classpath "\"$CLASSPATH\"" autocadDrawingChecker.start.Main "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/build/scripts/AutoCADDrawingChecker.bat b/build/scripts/AutoCADDrawingChecker.bat deleted file mode 100644 index 4eda231..0000000 --- a/build/scripts/AutoCADDrawingChecker.bat +++ /dev/null @@ -1,100 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem AutoCADDrawingChecker startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME%.. - -@rem Add default JVM options here. You can also use JAVA_OPTS and AUTO_CAD_DRAWING_CHECKER_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\lib\AutoCADDrawingChecker.jar;%APP_HOME%\lib\poi-ooxml-4.1.2.jar;%APP_HOME%\lib\poi-4.1.2.jar;%APP_HOME%\lib\commons-codec-1.13.jar;%APP_HOME%\lib\commons-collections4-4.4.jar;%APP_HOME%\lib\commons-math3-3.6.1.jar;%APP_HOME%\lib\SparseBitSet-1.2.jar;%APP_HOME%\lib\poi-ooxml-schemas-4.1.2.jar;%APP_HOME%\lib\commons-compress-1.19.jar;%APP_HOME%\lib\curvesapi-1.06.jar;%APP_HOME%\lib\xmlbeans-3.1.0.jar - -@rem Execute AutoCADDrawingChecker -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %AUTO_CAD_DRAWING_CHECKER_OPTS% -classpath "%CLASSPATH%" autocadDrawingChecker.start.Main %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable AUTO_CAD_DRAWING_CHECKER_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%AUTO_CAD_DRAWING_CHECKER_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/src/main/java/autocadDrawingChecker/data/AbstractGradableDataType.java b/src/main/java/autocadDrawingChecker/data/AbstractGradableDataType.java new file mode 100644 index 0000000..83a46c0 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/AbstractGradableDataType.java @@ -0,0 +1,40 @@ +package autocadDrawingChecker.data; + +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.util.FileType; + +/** + * AbstractGradableDataType represents a type of data the program can grade. + * This class serves as a bridge between the Application and data classes. + * The application will use the AbstractGradableDataType.parseFile method to + * read files the user provides, without needing to know exactly what subclass + * of DataSet the method returns. + * + * @author Matt Crow + */ +public interface AbstractGradableDataType { + /** + * @return a brief name for this data type, which will be presented to the user. + */ + public String getName(); + + /** + * + * @return a short description of how files for this data type are formatted. + */ + public String getDescription(); + + /** + * Use this method to read an Excel file, then return a subclass of ExcelParser + * + * @return the autocadDrawingChecker.data.core.AbstractTableParser + */ + public AbstractTableParser createParser(); + + /** + * + * @return the types of files this expects + * to parse. + */ + public FileType getRequiredFileType(); +} diff --git a/src/main/java/autocadDrawingChecker/data/AutoCADElementExtractor.java b/src/main/java/autocadDrawingChecker/data/AutoCADElementExtractor.java deleted file mode 100644 index d0c074f..0000000 --- a/src/main/java/autocadDrawingChecker/data/AutoCADElementExtractor.java +++ /dev/null @@ -1,88 +0,0 @@ -package autocadDrawingChecker.data; - -import autocadDrawingChecker.logging.Logger; -import java.util.HashMap; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.Row; - -/** - * @author Matt - */ -class AutoCADElementExtractor { - private HashMap currentCols; - private Row currentRow; - - private Object getCell(String col){ - Object ret = null; - int colIdx = currentCols.get(col); - if(colIdx == -1){ - throw new RuntimeException(String.format("Missing column: %s", col)); - } - Cell c = currentRow.getCell(colIdx); - switch(c.getCellType()){ - case BOOLEAN: - ret = c.getBooleanCellValue(); - break; - case NUMERIC: - ret = c.getNumericCellValue(); - break; - case STRING: - ret = c.getStringCellValue(); - break; - default: - Logger.logError(String.format("AutoCADElementExtractor encountered cell with type %s", c.getCellType().name())); - //ret = c.toString(); - break; - } - return ret; - } - - protected String getCellString(String col){ - String ret = null; - try { - ret = (String)getCell(col); - } catch(ClassCastException ex){ - Logger.logError(String.format("Column \"%s\" is not a string", col)); - throw ex; - } - return ret; - } - - protected boolean currentRowHasCell(String col){ - int colIdx = currentCols.get(col); - boolean hasCol = - colIdx != -1 && - currentRow.getCell(colIdx) != null && currentRow.getCell(colIdx).getCellType() != CellType.BLANK; - // getCell doesn't throw an exception if it doesn't have a cell for the given column: it just returns null - - return hasCol; - } - - /** - * Extracts data from the given row, and converts it to an AutoCAD element. - * @param columns the mapping of columns to the index of the column - * in the given row. - * @param currentRow the row to extract data from - * @return the extracted AutoCADElement. - */ - public synchronized final AutoCADElement extract(HashMap columns, Row currentRow){ - // temporarily set the columns and row. Note this method is synchronized to prevent multithreading issues - this.currentCols = columns; - this.currentRow = currentRow; - AutoCADElement ret = doExtract(); - this.currentCols = null; - this.currentRow = null; - return ret; - } - - private AutoCADElement doExtract(){ - AutoCADElement ret = new AutoCADElement(); - currentCols.keySet().forEach((header)->{ - if(this.currentRowHasCell(header)){ - ret.setAttribute(header, getCell(header)); - } - }); - return ret; - } -} diff --git a/src/main/java/autocadDrawingChecker/data/AutoCADExcelParser.java b/src/main/java/autocadDrawingChecker/data/AutoCADExcelParser.java deleted file mode 100644 index 5133eda..0000000 --- a/src/main/java/autocadDrawingChecker/data/AutoCADExcelParser.java +++ /dev/null @@ -1,150 +0,0 @@ -package autocadDrawingChecker.data; - -import autocadDrawingChecker.logging.Logger; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import org.apache.poi.hssf.usermodel.HSSFWorkbook; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellType; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; - -/** - * The AutoCADExcelParser is used to - * read an Excel spreadsheet, - * extracting the AutoCAD date within as an - * AutoCADExport object. - * - * @author Matt Crow - */ -public class AutoCADExcelParser { - private final String fileName; - private final HashMap headerToCol; - private Row currRow; - - /** - * @param fileToParse a path to the Excel file this should parse. - */ - public AutoCADExcelParser(String fileToParse){ - fileName = fileToParse; - headerToCol = new HashMap<>(); - currRow = null; - } - - /** - * Locates headers within the given - * row, and populates headerToCol appropriately. - * - * @param headerRow the first row of the spreadsheet, - * containing headers. - */ - private synchronized void locateColumns(Row headerRow){ - headerToCol.clear(); - ArrayList headers = new ArrayList<>(); - headerRow.cellIterator().forEachRemaining((Cell c)->{ - headers.add(c.toString().toUpperCase()); - }); - for(int i = 0; i < headers.size(); i++){ - headerToCol.put(headers.get(i), i); - } - } - - private boolean isRowEmpty(Row row){ - boolean couldBeEmpty = true; - Iterator cellIter = row.cellIterator(); - while(cellIter.hasNext() && couldBeEmpty){ - if(!cellIter.next().getCellType().equals(CellType.BLANK)){ - couldBeEmpty = false; - } - } - return couldBeEmpty; - } - private boolean isValidRow(Row row){ - return row != null && row.getLastCellNum() != -1 && !isRowEmpty(row); - } - - private String currRowToString(){ - StringBuilder b = new StringBuilder(); - b.append("["); - Iterator iter = currRow.cellIterator(); - while(iter.hasNext()){ - b.append(iter.next().toString()); - if(iter.hasNext()){ - b.append(", "); - } - } - b.append("]"); - return b.toString(); - } - - public final AutoCADExport parse() throws IOException { - InputStream in = new FileInputStream(fileName); - // new Excel format old Excel format - Workbook workbook = (fileName.endsWith("xlsx")) ? new XSSFWorkbook(in) : new HSSFWorkbook(in); - Sheet sheet = workbook.getSheetAt(0); - AutoCADExport containedTherein = new AutoCADExport(fileName); - locateColumns(sheet.getRow(0)); - - - AutoCADElementExtractor recExtr = new AutoCADElementExtractor(); - - int numRows = sheet.getLastRowNum() + 1; // need the + 1, otherwise it sometimes doesn't get the last row - /* - Note that numRows will be greater than or - equal to the last row with data, but not - every row is guaranteed to contain data. - - From the Apache POI javadoc: - """ - Gets the last row on the sheet - Note: rows which had content before and were set to empty later might still be counted as rows by Excel and Apache POI, - so the result of this method will include such rows and thus the returned value might be higher than expected! - """ - */ - - AutoCADElement rec = null; - - // skip headers - for(int rowNum = 1; rowNum < numRows; rowNum++){ - currRow = sheet.getRow(rowNum); - if(isValidRow(currRow)){ - try { - rec = recExtr.extract(headerToCol, currRow); - containedTherein.add(rec); - } catch(Exception ex){ - Logger.logError(String.format("Error while parsing row: %s", currRowToString())); - Logger.logError(ex); - } - } - } - //Logger.log("In AutoCADExcelParser.parse...\n" + containedTherein.toString()); - - workbook.close(); - return containedTherein; - } - - /** - * Reads the Excel file with the - * given complete file path, and - * returns its contents as an AutoCADExport. - * - * Later versions may extract multiple Exports based on column values, - * or this functionality may be deferred to AutoCADExport. - * - * @param fileName the complete path to an Excel file. - * @return the contents of the first sheet of the given Excel file , as - * an AutoCADExport. - * @throws IOException if the fileName given does not point to an Excel file - */ - public static AutoCADExport parse(String fileName) throws IOException{ - return new AutoCADExcelParser( - fileName - ).parse(); - } -} diff --git a/src/main/java/autocadDrawingChecker/data/AutoCADExport.java b/src/main/java/autocadDrawingChecker/data/AutoCADExport.java deleted file mode 100644 index 853512a..0000000 --- a/src/main/java/autocadDrawingChecker/data/AutoCADExport.java +++ /dev/null @@ -1,86 +0,0 @@ -package autocadDrawingChecker.data; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.Set; - -/** - * An AutoCADExport is used to store the data extracted by - * AutoCADExcelParser. One should use LinkedList methods, - * such as LinkedList::forEach and LinkedList::stream, to - * operate on the contents of the Export. - * - * @author Matt Crow - */ -public class AutoCADExport extends LinkedList { - private final String fileName; - - public AutoCADExport(String fileName){ - super(); - this.fileName = fileName; - } - - public final String getFileName(){ - return fileName; - } - - /** - * Computes how many lines are contained in each AutoCAD layer - * in the export. - * - * @return a HashMap, where the key is the name of an AutoCAD - * layer, and the value is the number of lines contained in this - * export that reside within that layer. Note: The value - * will always be greater than 0, so you needn't worry about - * checking for that - */ - public final HashMap getLayerLineCounts(){ - HashMap lineCounts = new HashMap<>(); - forEach((AutoCADElement row)->{ - String layer = row.getLayer(); - if(!lineCounts.containsKey(layer)){ - lineCounts.put(layer, 0); - } - lineCounts.put(layer, lineCounts.get(layer) + 1); - }); - return lineCounts; - } - - public final HashMap> sortRecordsByColumn(String column){ - HashMap> valueToRecords = new HashMap<>(); - forEach((record)->{ - Object value = record.getAttribute(column); - if(!valueToRecords.containsKey(value)){ - valueToRecords.put(value, new LinkedList<>()); - } - valueToRecords.get(value).add(record); - }); - return valueToRecords; - } - public final HashMap getCountsPerColumnValue(String column){ - HashMap> sorted = sortRecordsByColumn(column); - HashMap counts = new HashMap<>(); - sorted.forEach((key, list)->{ - counts.put(key, list.size()); - }); - return counts; - } - - public final Set getColumns(){ - Set cols = new HashSet<>(); - forEach((AutoCADElement e)->{ - cols.addAll(e.getAttributes()); - }); - return cols; - } - - @Override - public String toString(){ - StringBuilder sb = new StringBuilder(); - sb.append("AutoCAD Data Export:"); - forEach((AutoCADElement row)->sb.append("\n").append(row.toString())); - sb.append("\nEnd of AutoCAD Export Data"); - return sb.toString(); - } -} diff --git a/src/main/java/autocadDrawingChecker/data/GradableDataTypeLoader.java b/src/main/java/autocadDrawingChecker/data/GradableDataTypeLoader.java new file mode 100644 index 0000000..bb5ffd0 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/GradableDataTypeLoader.java @@ -0,0 +1,31 @@ +package autocadDrawingChecker.data; + +import autocadDrawingChecker.data.csv.GenericCsvDataType; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADDataType; +import autocadDrawingChecker.data.excel.GenericExcelDataType; +import autocadDrawingChecker.data.excel.surveyData.SurveyDataType; +import autocadDrawingChecker.util.AbstractLoader; +import java.util.ArrayList; +import java.util.List; + +/** + * This class is used to get the various data types defined in this package. + * When creating new data types, you will want to add them to the list returned + * by this class' getAll() method. + * + * @author Matt Crow + */ +public class GradableDataTypeLoader extends AbstractLoader { + + @Override + public List getAll() { + ArrayList ret = new ArrayList<>(); + ret.add(new GenericExcelDataType()); + ret.add(new GenericCsvDataType(true)); + ret.add(new GenericCsvDataType(false)); + ret.add(new AutoCADDataType()); + ret.add(new SurveyDataType()); + return ret; + } + +} diff --git a/src/main/java/autocadDrawingChecker/data/core/AbstractTableParser.java b/src/main/java/autocadDrawingChecker/data/core/AbstractTableParser.java new file mode 100644 index 0000000..11e3574 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/core/AbstractTableParser.java @@ -0,0 +1,285 @@ +package autocadDrawingChecker.data.core; + +import autocadDrawingChecker.logging.Logger; +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +/** + * The AbstractTableParser class is used to convert data from files into a more generic and useful format the + * program can use. In this way, where the data comes from is irrelevant to the grading process. + * + * This class uses the Algorithm design pattern, allowing developers to pick-and-choose what functionality they + * wish to override when subclassing. + * + * + * @author Matt Crow + * @param the class representing each individual sheet in the files this should parse. + * @param the class each row from the table this parses is stored as + */ +public abstract class AbstractTableParser { + /* + Part I: Public entry points + */ + + /** + * Reads the given file, and returns the contents of its first sheet as a DataSet. + * Use this method to get the instructor file contents. + * + * @param path the complete file path to the file to parse. + * @return the converted contents of the first sheet of the given file. + * @throws IOException if anything untoward happens when parsing the given file. + */ + public final DataSet parseFirstSheet(String path) throws IOException { + DataSet ret = null; + try { + List allSheets = this.extractSheets(path); + ret = parseSheet(path, allSheets.get(0)); + } catch(Exception ex){ + Logger.logError(ex); + throw ex; + } + return ret; + } + + /** + * Reads the given file, and returns the contents of each sheet contained therein as a DataSet. + * Use this method to get the student file contents. + * + * @param path the complete file path to the file to parse. + * @return the converted contents of the given file, with each sheet as a separate DataSet. + * @throws IOException if anything untoward happens when parsing the given file. + */ + public final List parseAllSheets(String path) throws IOException { + List allDataSets = new LinkedList<>(); + try { + List allSheets = this.extractSheets(path); + allSheets.stream().map((SheetType sheet)->{ + return this.parseSheet(String.format("%s - %s", path, getSheetName(sheet)), sheet); + }).filter((DataSet converted)->{ + return converted != null; + }).forEach(allDataSets::add); + } catch(Exception ex){ + Logger.logError(ex); + throw ex; + } + return allDataSets; + } + + + + /* + Part II: Private algorithm steps + */ + + /** + * Sanitizes the given header string to ensure all headers are in the same format. + * @param s the header to clean. + * @return the cleaned header. + */ + private String sanitize(String s){ + return s.trim().toUpperCase(); // not sure if I want to uppercase + } + + /** + * Checks if this can properly convert the given row. + * @param columns the set of columns for the sheet from whence this row came. + * @param row the row to check. + * @return whether or not the row can be converted. + */ + private boolean canConvert(Map columns, RowType row){ + boolean ret = true; + String[] reqCols = this.getRequiredColumns(); + for(int i = 0; i < reqCols.length && ret; i++){ + if(!this.doesRowHaveCell(row, columns.get(sanitize(reqCols[i])))){ + ret = false; + } + } + return ret; + } + + /** + * Extracts data from the given row, and converts it to a Record. + * @param columns the columns for the file this row comes from + * @param row the row to extract data from + * @return the extracted Record. + */ + private Record convertRecord(Map columns, RowType row){ + Record ret = createNew(); + columns.forEach((header, index)->{ + if(this.doesRowHaveCell(row, index)){ + ret.setAttribute(header, doGetCell(row, index)); + } + }); + + return ret; + } + + /** + * Locates headers in the given sheet, and converts them to the format + * this requires. + * + * @param sheet the sheet to locate headers in. + * @return a Map of sanitized headers to the column they map to in the given sheet. + */ + private Map getHeadersFrom(SheetType sheet){ + HashMap cleanedHeaders = new HashMap<>(); + Map raw = doGetHeadersFrom(sheet); + raw.forEach((k, v)->{ + cleanedHeaders.put(sanitize(k), v); + }); + for(String reqCol : this.getRequiredColumns()){ + for(String actualCol : raw.keySet()){ + if(!reqCol.equals(actualCol) && Pattern.matches(reqCol, actualCol)){ + cleanedHeaders.put(reqCol, raw.get(actualCol)); + } + } + } + return cleanedHeaders; + } + + /** + * Converts the given sheet into a DataSet usable by the program. + * + * @param dataSetName the name the program should give to the DataSet this returns. + * This will usually be in the format workbook_name - sheet_name. + * + * @param sheet the sheet to convert + * @return the converted sheet + */ + private DataSet parseSheet(String dataSetName, SheetType sheet){ + DataSet containedTherein = this.createExtractionHolder(dataSetName); + Map headers = this.getHeadersFrom(sheet); + forEachRowIn(sheet, (RowType row)->{ + if(isValidRow(row)){ + Record converted = null; + try { + if(canConvert(headers, row)){ + converted = this.convertRecord(headers, row); + } + } catch (Exception ex){ + Logger.logError(ex); + } + if(converted != null){ + containedTherein.add(converted); + } + } + }); + return containedTherein; + } + + + + + /* + Part III: Optionally overridable methods + */ + + /** + * Creates the DataSet which will hold the contents + * of the file this is parsing. Subclasses may want to + * override this method to return a DataSet for their + * own record type. + * + * @param name the name of the sheet this is parsing + * @return the DataSet this will store the parsed file contents in + */ + protected DataSet createExtractionHolder(String name){ + return new DataSet(name); + } + + /** + * Creates a new Record used to hold the converted contents of a spreadsheet row. + * Subclasses may want to override this method to return a subclass of Record. + * + * @return an empty Record + */ + protected Record createNew(){ + return new Record(); + } + + /** + * Only override this method if the parsed contents must have specific columns. + * Columns may include regular expressions if you wish. + * + * @return the list of columns each row converted by this must have. + */ + protected String[] getRequiredColumns(){ + return new String[0]; + } + + + + + /* + Part IV: Mandatory overridable methods + */ + + /** + * + * @param sheet the sheet to get the name for. + * @return the name of the given sheet. + */ + protected abstract String getSheetName(SheetType sheet); + + /** + * Subclasses should override this method to read the given file, run it through + * some file reader, and return each spreadsheet contained therein. + * + * @param path the complete file path to the file to parse. + * @return a list of sheets contained in the given file. Should never return null. + * @throws IOException if any errors occur when reading the file. + */ + protected abstract List extractSheets(String path) throws IOException; + + /** + * Use this method to get the headers contained in the given sheet. You needn't worry about + * whether or not the headers are formatted nicely, as the AbstractTableParser handles + * sanitization for you. + * + * @param sheet the sheet to get headers from. + * @return a mapping of column headers to their index. + */ + protected abstract Map doGetHeadersFrom(SheetType sheet); + + /** + * Checks to see if this should parse the given row. + * + * @param row the row to check. + * @return whether or not the given row is valid. + */ + protected abstract boolean isValidRow(RowType row); + + /** + * This method should be overridden to iterate over each row in the given sheet, + * and pass the row to the given Consumer. + * + * @param sheet the sheet to iterate over + * @param doThis the thing to do with each row in the given sheet + */ + protected abstract void forEachRowIn(SheetType sheet, Consumer doThis); + + /** + * Returns whether or not a row has a valid cell at the given index. + * You may want this method to return false if the given cell is empty or otherwise not useful. + * + * @param currRow the row to check for a cell. + * @param idx the index of the column the cell should be in. + * @return whether or not the given row has a valid cell at the given index. + */ + protected abstract boolean doesRowHaveCell(RowType currRow, int idx); + + /** + * Gets the value of a cell in the given row. Subclasses should + * try to convert the cell's value to a number whenever possible. + * + * @param currRow the row to get the cell from. + * @param idx the column index of the cell to get. + * @return the value of the cell this locates. + */ + protected abstract Object doGetCell(RowType currRow, int idx); +} diff --git a/src/main/java/autocadDrawingChecker/data/core/DataSet.java b/src/main/java/autocadDrawingChecker/data/core/DataSet.java new file mode 100644 index 0000000..b722e0e --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/core/DataSet.java @@ -0,0 +1,60 @@ +package autocadDrawingChecker.data.core; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +/** + * + * @author Matt + */ +public class DataSet extends LinkedList { + private final String fileName; + + public DataSet(String fileName){ + super(); + this.fileName = fileName; + } + + public final String getFileName(){ + return fileName; + } + + public final HashMap> sortRecordsByColumn(String column){ + HashMap> valueToRecords = new HashMap<>(); + forEach((record)->{ + Object value = record.getAttribute(column); + if(!valueToRecords.containsKey(value)){ + valueToRecords.put(value, new LinkedList<>()); + } + valueToRecords.get(value).add(record); + }); + return valueToRecords; + } + public final HashMap getCountsPerColumnValue(String column){ + HashMap> sorted = sortRecordsByColumn(column); + HashMap counts = new HashMap<>(); + sorted.forEach((key, list)->{ + counts.put(key, list.size()); + }); + return counts; + } + + public final Set getColumns(){ + Set cols = new HashSet<>(); + forEach((Record e)->{ + cols.addAll(e.getAttributes()); + }); + return cols; + } + + @Override + public String toString(){ + StringBuilder sb = new StringBuilder(); + sb.append(fileName).append(" Contents:"); + forEach((Record row)->sb.append("\n").append(row.toString())); + sb.append("\nEnd of Extracted Spreadsheet Contents"); + return sb.toString(); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/AutoCADElement.java b/src/main/java/autocadDrawingChecker/data/core/Record.java similarity index 77% rename from src/main/java/autocadDrawingChecker/data/AutoCADElement.java rename to src/main/java/autocadDrawingChecker/data/core/Record.java index f197ef3..56cf31f 100644 --- a/src/main/java/autocadDrawingChecker/data/AutoCADElement.java +++ b/src/main/java/autocadDrawingChecker/data/core/Record.java @@ -1,23 +1,20 @@ -package autocadDrawingChecker.data; +package autocadDrawingChecker.data.core; import autocadDrawingChecker.logging.Logger; import java.util.HashMap; import java.util.Set; /** - * An AutoCADElement represents - a single record in an AutoCADExport. + * Record represents a single row in a spreadsheet. + * This class can be extended to provide more specific behavior, + * or can be used as is: it is not abstract. * * @author Matt Crow */ -public class AutoCADElement { +public class Record { private final HashMap attributes; - public static final String NAME_COL = "Name"; - public static final String LAYER_COL = "Layer"; - - // no access modifier, so only classes in this package can invoke this - AutoCADElement(){ + protected Record(){ this.attributes = new HashMap<>(); } @@ -25,32 +22,12 @@ public final Set getAttributes(){ return attributes.keySet(); } - /** - * - * @return the value in this' "Name" column. - * This value is the type of AutoCAD element this is - * (line, circle, etc.) - */ - public final String getName(){ - return getAttributeString(NAME_COL); - } - - /** - * - * @return the AutoCAD layer this - * resides within - */ - public final String getLayer(){ - return (String)getAttribute(LAYER_COL); - } - private String sanitizeAttributeName(String name){ return name.trim().toUpperCase(); } - final AutoCADElement setAttribute(String attrName, Object value){ + public final void setAttribute(String attrName, Object value){ attributes.put(sanitizeAttributeName(attrName), value); - return this; } /** @@ -113,7 +90,7 @@ public final int getAttributeInt(String attributeName){ @Override public String toString(){ StringBuilder sb = new StringBuilder(); - sb.append("AutoCAD Element:"); + sb.append("Spreadsheet Record:"); attributes.forEach((k, v)->{ sb.append(String.format("\n- %s : %s", k, v.toString())); }); diff --git a/src/main/java/autocadDrawingChecker/data/core/package-info.java b/src/main/java/autocadDrawingChecker/data/core/package-info.java new file mode 100644 index 0000000..2a441be --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/core/package-info.java @@ -0,0 +1,11 @@ +/** + * The core package contains all the classes used to convert files into a format more usable by the program. + * + * + * + * + * + * + *
Class Purpose
AbstractTableParserReads a file, and converts it into a subclass of DataSet.
DataSet Stores a list of Records extracted from a file.
Record A single data entry in a file. This is usually rows in a spreadsheet. This is used by many of project's classes
+ */ +package autocadDrawingChecker.data.core; diff --git a/src/main/java/autocadDrawingChecker/data/csv/CsvParser.java b/src/main/java/autocadDrawingChecker/data/csv/CsvParser.java new file mode 100644 index 0000000..522b34a --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/csv/CsvParser.java @@ -0,0 +1,110 @@ +package autocadDrawingChecker.data.csv; + +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.logging.Logger; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; + +/** + * + * @author Matt Crow + */ +public class CsvParser extends AbstractTableParser, CSVRecord> { + private final boolean hasHeaders; + + public CsvParser(boolean hasHeaders){ + super(); + this.hasHeaders = hasHeaders; + } + + @Override + protected final String getSheetName(List sheet) { + return "data"; + } + + @Override + protected List> extractSheets(String path) throws IOException { + List> sheets = new LinkedList<>(); + CSVFormat format = (this.hasHeaders) ? CSVFormat.DEFAULT.withHeader() : CSVFormat.DEFAULT; + try (CSVParser parser = new CSVParser(new InputStreamReader(new FileInputStream(path)), format)) { + sheets.add(parser.getRecords()); + } catch (Exception ex){ + Logger.logError(ex); + } + return sheets; + } + + @Override + protected Map doGetHeadersFrom(List sheet) { + HashMap headerMap = new HashMap<>(); + if(sheet.isEmpty()){ + // cannot parse, just ignore + } else if(hasHeaders){ + sheet.get(0).getParser().getHeaderMap().forEach((k, v)->{ + headerMap.put(k, v); // copy headers from Apache parser + }); + } else { + // sheet isn't empty, and we don't have headers + int numCols = sheet.get(0).size(); + for(int i = 0; i < numCols; i++){ + headerMap.put(String.format("Column#%d", i + 1), i); + } // make my own headers + + } + return headerMap; + } + + @Override + protected final boolean isValidRow(CSVRecord row) { + return row.isConsistent(); + } + + @Override + protected final void forEachRowIn(List sheet, Consumer doThis) { + sheet.stream().forEach((CSVRecord apacheRecord)->{ + doThis.accept(apacheRecord); + }); + } + + @Override + protected final boolean doesRowHaveCell(CSVRecord currRow, int idx) { + return currRow.isSet(idx) && currRow.get(idx) != null; + } + + @Override + protected final Object doGetCell(CSVRecord currRow, int idx) { + Object ret = currRow.get(idx); + boolean foundType = false; + // see if it's numeric + try { + ret = Double.parseDouble(ret.toString()); + foundType = true; + } catch(NumberFormatException ex){ + // not a number + } + + if(!foundType){ + // first, convert to a string + ret = ret.toString(); + if(ret.toString().equalsIgnoreCase("true")){ + ret = Boolean.TRUE; + foundType = true; // it's a boolean + } else if(ret.toString().equalsIgnoreCase("false")){ + ret = Boolean.FALSE; + foundType = true; + } + // don't use Boolean.parseString methods, as they don't account for values other than true or false + // this way, if it is neither true nor false, this method returns it as a string + } + return ret; + } +} diff --git a/src/main/java/autocadDrawingChecker/data/csv/GenericCsvDataType.java b/src/main/java/autocadDrawingChecker/data/csv/GenericCsvDataType.java new file mode 100644 index 0000000..644a52f --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/csv/GenericCsvDataType.java @@ -0,0 +1,36 @@ +package autocadDrawingChecker.data.csv; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.util.FileType; +/** + * + * @author Matt + */ +public class GenericCsvDataType implements AbstractGradableDataType { + private final boolean hasHeaders; + + public GenericCsvDataType(boolean hasHeaders){ + this.hasHeaders = hasHeaders; + } + + @Override + public String getName() { + return "CSV"; + } + + @Override + public String getDescription() { + return String.format("A generic Comma Separated Values file, %s headers", (hasHeaders) ? "with" : "without"); + } + + @Override + public AbstractTableParser createParser() { + return new CsvParser(hasHeaders); + } + + @Override + public FileType getRequiredFileType() { + return FileType.CSV; + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/ExcelParser.java b/src/main/java/autocadDrawingChecker/data/excel/ExcelParser.java new file mode 100644 index 0000000..26f3222 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/ExcelParser.java @@ -0,0 +1,205 @@ +package autocadDrawingChecker.data.excel; + +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.logging.Logger; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; + +/** + * The ExcelParser is used to convert Excel files into DataSets usable by the program. + * Each GradableDataType should have an associated subclass of ExcelParser which accounts for any unique formatting the file may have. + * This class contains many methods which subclasses may wish to override to customize this class' behavior without having to rewrite + * an entirely new class: + *
    + *
  • createExtractionHolder will always be overridden to return a different subclass of DataSet
  • + *
  • createExtractor will always be overridden to return a different subclass of RecordExtractor
  • + *
  • locateHeaderRow should be overridden iff the Excel file has headers anywhere except the first row
  • + *
  • isValidRow should be overridden if you need to validate that the row contains specific formatting, such as requiring certain columns always contain a value
  • + *
+ * + * @author Matt Crow + */ +public class ExcelParser extends AbstractTableParser { + @Override + protected final String getSheetName(Sheet sheet) { + return sheet.getSheetName(); + } + + @Override + protected final List extractSheets(String path) throws IOException { + List sheets = new LinkedList<>(); + try (Workbook workbook = WorkbookFactory.create(new FileInputStream(path))) { + workbook.sheetIterator().forEachRemaining(sheets::add); + } catch(Exception ex){ + Logger.logError(ex); + } + return sheets; + } + + @Override + protected final Map doGetHeadersFrom(Sheet sheet) { + Row headerRow = locateHeaderRow(sheet); + return this.locateColumns(headerRow); + } + + /** + * Returns whether or not the given row is valid, and the program should + * attempt to convert it to a Record. + * + * @param row the row to validate. + * @return whether or not the row is valid. + */ + @Override + protected final boolean isValidRow(Row row){ + return row != null && row.getLastCellNum() != -1 && !isRowEmpty(row); + } + + @Override + protected final void forEachRowIn(Sheet sheet, Consumer doThis) { + Row headerRow = this.locateHeaderRow(sheet); + int numRows = sheet.getLastRowNum() + 1; // need the + 1, otherwise it sometimes doesn't get the last row + /* + Note that numRows will be greater than or + equal to the last row with data, but not + every row is guaranteed to contain data. + + From the Apache POI javadoc: + """ + Gets the last row on the sheet + Note: rows which had content before and were set to empty later might still be counted as rows by Excel and Apache POI, + so the result of this method will include such rows and thus the returned value might be higher than expected! + """ + */ + + Row currRow = null; + // skip headers + for(int rowNum = headerRow.getRowNum() + 1; rowNum < numRows; rowNum++){ + currRow = sheet.getRow(rowNum); + if(isValidRow(currRow)){// && recExtr.canExtractRow(currRow)){ + try { + doThis.accept(currRow); + } catch(Exception ex){ + Logger.logError(String.format("Error while parsing row: %s", rowToString(currRow))); + Logger.logError(ex); + } + } + } + } + + @Override + protected final boolean doesRowHaveCell(Row currRow, int idx) { + Cell c = currRow.getCell(idx); + return c != null && c.getCellType() != CellType.BLANK; + // getCell doesn't throw an exception if it doesn't have a cell for the given column: it just returns null + } + + @Override + protected final Object doGetCell(Row currRow, int idx){ + Object ret = null; + Cell c = currRow.getCell(idx); + switch(c.getCellType()){ + case BOOLEAN: + ret = c.getBooleanCellValue(); + break; + case NUMERIC: + ret = (Double)c.getNumericCellValue(); + break; + case STRING: + ret = c.getStringCellValue(); + break; + default: + Logger.logError(String.format("ExcelRecordExtractor encountered cell with type %s", c.getCellType().name())); + //ret = c.toString(); + break; + } + return ret; + } + + + + /* + New methods + */ + + /** + * Locates headers within the given row + * + * @param headerRow the first row of the spreadsheet, + * containing headers. + * @return a HashMap of header names (converted to upper case) to the index of their column. + */ + private HashMap locateColumns(Row headerRow){ + HashMap headerToCol = new HashMap<>(); + + ArrayList headers = new ArrayList<>(); + headerRow.cellIterator().forEachRemaining((Cell c)->{ + headers.add(c.toString()); + }); + for(int i = 0; i < headers.size(); i++){ + headerToCol.put(headers.get(i), i); + } + return headerToCol; + } + + /** + * + * @param row the row to check + * @return whether or not the given row is empty + */ + private boolean isRowEmpty(Row row){ + boolean couldBeEmpty = true; + Iterator cellIter = row.cellIterator(); + while(cellIter.hasNext() && couldBeEmpty){ + if(!cellIter.next().getCellType().equals(CellType.BLANK)){ + couldBeEmpty = false; + } + } + return couldBeEmpty; + } + + /** + * Converts the current row this is parsing to a string. + * This method is very helpful for debugging. + * + * @return the current row as a string, formatted to look like an array. + */ + private String rowToString(Row row){ + StringBuilder b = new StringBuilder(); + b.append("["); + Iterator iter = row.cellIterator(); + while(iter.hasNext()){ + b.append(iter.next().toString()); + if(iter.hasNext()){ + b.append(", "); + } + } + b.append("]"); + return b.toString(); + } + + /** + * Returns the row containing headers in the given sheet. + * By default, this method returns the first row of the + * given sheet. Subclasses should override this method iff + * their data is expected to have headers in another row. + * + * @param sheet the sheet which this is currently parsing. + * @return the row of the given sheet that contains headers. + */ + protected Row locateHeaderRow(Sheet sheet){ + return sheet.getRow(0); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/GenericExcelDataType.java b/src/main/java/autocadDrawingChecker/data/excel/GenericExcelDataType.java new file mode 100644 index 0000000..8e8f6bd --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/GenericExcelDataType.java @@ -0,0 +1,31 @@ +package autocadDrawingChecker.data.excel; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.util.FileType; + +/** + * + * @author Matt + */ +public class GenericExcelDataType implements AbstractGradableDataType { + @Override + public String getName() { + return "Basic Excel"; + } + + @Override + public String getDescription() { + return "Any Excel file with headers in the first row"; + } + + @Override + public AbstractTableParser createParser() { + return new ExcelParser(); + } + + @Override + public FileType getRequiredFileType() { + return FileType.EXCEL; + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADDataType.java b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADDataType.java new file mode 100644 index 0000000..e837122 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADDataType.java @@ -0,0 +1,31 @@ +package autocadDrawingChecker.data.excel.autoCADData; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.util.FileType; + +/** + * + * @author Matt + */ +public class AutoCADDataType implements AbstractGradableDataType { + @Override + public String getName() { + return "AutoCAD"; + } + + @Override + public String getDescription() { + return "Data extracted from an AutoCAD file into Excel"; + } + + @Override + public AbstractTableParser createParser() { + return new AutoCADExcelParser(); + } + + @Override + public FileType getRequiredFileType() { + return FileType.EXCEL; + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADElement.java b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADElement.java new file mode 100644 index 0000000..d42fd60 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADElement.java @@ -0,0 +1,44 @@ +package autocadDrawingChecker.data.excel.autoCADData; + +import autocadDrawingChecker.data.core.Record; + +/** + * An AutoCADElement represents + a single record in an AutoCADExport. + * + * @author Matt Crow + */ +public class AutoCADElement extends Record { + + public static final String NAME_COL = "Name"; + public static final String LAYER_COL = "Layer"; + + // no access modifier, so only classes in this package can invoke this + AutoCADElement(){ + super(); + } + + /** + * + * @return the value in this' "Name" column. + * This value is the type of AutoCAD element this is + * (line, circle, etc.) + */ + public final String getName(){ + return getAttributeString(NAME_COL); + } + + /** + * + * @return the AutoCAD layer this + * resides within + */ + public final String getLayer(){ + return (String)getAttribute(LAYER_COL); + } + + @Override + public String toString(){ + return String.format("AutoCAD Element: \n%s", super.toString()); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExcelParser.java b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExcelParser.java new file mode 100644 index 0000000..23984ca --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExcelParser.java @@ -0,0 +1,25 @@ +package autocadDrawingChecker.data.excel.autoCADData; + +import autocadDrawingChecker.data.excel.ExcelParser; +import autocadDrawingChecker.data.core.DataSet; + +/** + * The AutoCADExcelParser is used to + * read an Excel spreadsheet, + * extracting the AutoCAD date within as an + * AutoCADExport object. + * + * @author Matt Crow + */ +public class AutoCADExcelParser extends ExcelParser { + + @Override + protected DataSet createExtractionHolder(String name){ + return new AutoCADExport(name); + } + + @Override + protected AutoCADElement createNew(){ + return new AutoCADElement(); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExport.java b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExport.java new file mode 100644 index 0000000..8b88b70 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/AutoCADExport.java @@ -0,0 +1,49 @@ +package autocadDrawingChecker.data.excel.autoCADData; + +import autocadDrawingChecker.data.core.DataSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +/** + * An AutoCADExport is used to store the data extracted by + * AutoCADExcelParser. One should use LinkedList methods, + * such as LinkedList::forEach and LinkedList::stream, to + * operate on the contents of the Export. + * + * @author Matt Crow + */ +public class AutoCADExport extends DataSet { + + public AutoCADExport(String fileName){ + super(fileName); + } + + /** + * Computes how many lines are contained in each AutoCAD layer + * in the export. + * + * @return a HashMap, where the key is the name of an AutoCAD + * layer, and the value is the number of lines contained in this + * export that reside within that layer. Note: The value + * will always be greater than 0, so you needn't worry about + * checking for that + */ + public final HashMap getLayerLineCounts(){ + HashMap lineCounts = new HashMap<>(); + this.stream().map((r)->(AutoCADElement)r).forEach((AutoCADElement row)->{ + String layer = row.getLayer(); + if(!lineCounts.containsKey(layer)){ + lineCounts.put(layer, 0); + } + lineCounts.put(layer, lineCounts.get(layer) + 1); + }); + return lineCounts; + } + + @Override + public String toString(){ + return String.format("AutoCAD Data Export:\n%s", super.toString()); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/autoCADData/package-info.java b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/package-info.java new file mode 100644 index 0000000..ca2e1e6 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/autoCADData/package-info.java @@ -0,0 +1,13 @@ +/** + * The autocadData package contains classes + * used to convert AutoCAD Excel exports into + * a more useful format for the program. + * + * + * + * + * + * + *
Class Purpose
AutoCADElement Stores the data from one row in the AutoCAD Excel file.
AutoCADExcelParser Reads an Excel file, and extracts an AutoCADExport from it.
AutoCADExport Stores a list of all AutoCADElements stored in a file.
+ */ +package autocadDrawingChecker.data.excel.autoCADData; \ No newline at end of file diff --git a/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataParser.java b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataParser.java new file mode 100644 index 0000000..e804581 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataParser.java @@ -0,0 +1,44 @@ +package autocadDrawingChecker.data.excel.surveyData; + +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.data.excel.ExcelParser; +import java.util.Iterator; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; + +/** + * + * @author Matt + */ +public class SurveyDataParser extends ExcelParser { + + @Override + protected DataSet createExtractionHolder(String name){ + return new SurveyDataSet(name); + } + + @Override + protected Row locateHeaderRow(Sheet sheet){ + Iterator rows = sheet.rowIterator(); + Row headers = null; + Row currRow = null; + while(rows.hasNext() && headers == null){ + currRow = rows.next(); + if(currRow.getCell(0) != null && currRow.getCell(0).getStringCellValue().equalsIgnoreCase("STA")){ + headers = currRow; + } + } + return headers; + } + + @Override + protected String[] getRequiredColumns(){ + return SurveyDataRecord.REQ_COLS; + } + + @Override + protected Record createNew(){ + return new SurveyDataRecord(); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataRecord.java b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataRecord.java new file mode 100644 index 0000000..4d6ad00 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataRecord.java @@ -0,0 +1,45 @@ +package autocadDrawingChecker.data.excel.surveyData; + +import autocadDrawingChecker.data.core.Record; + +/** + * + * @author Matt + */ +public class SurveyDataRecord extends Record { + public static final String POINT_ID_HEADER = "POINT"; + public static final String X_HEADER = "X.*"; + public static final String Y_HEADER = "Y.*"; + public static final String Z_HEADER = "Z.*"; + + public static final String[] REQ_COLS = new String[]{ + POINT_ID_HEADER, + X_HEADER, + Y_HEADER, + Z_HEADER + }; + SurveyDataRecord(){ + super(); + } + + public final String getPointId(){ + return getAttributeString(POINT_ID_HEADER); + } + + public final Double getX(){ + return getAttributeDouble(X_HEADER); + } + + public final Double getY(){ + return getAttributeDouble(Y_HEADER); + } + + public final Double getZ(){ + return getAttributeDouble(Z_HEADER); + } + + @Override + public String toString(){ + return String.format("Survey Point %s (%f, %f, %f)", getPointId(), getX(), getY(), getZ()); + } +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataSet.java b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataSet.java new file mode 100644 index 0000000..f7646ca --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataSet.java @@ -0,0 +1,15 @@ +package autocadDrawingChecker.data.excel.surveyData; + +import autocadDrawingChecker.data.core.DataSet; + +/** + * + * @author Matt + */ +public class SurveyDataSet extends DataSet{ + + public SurveyDataSet(String fileName) { + super(fileName); + } + +} diff --git a/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataType.java b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataType.java new file mode 100644 index 0000000..7599bc0 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/data/excel/surveyData/SurveyDataType.java @@ -0,0 +1,31 @@ +package autocadDrawingChecker.data.excel.surveyData; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.AbstractTableParser; +import autocadDrawingChecker.util.FileType; + +/** + * + * @author Matt + */ +public class SurveyDataType implements AbstractGradableDataType { + @Override + public String getName() { + return "Survey Data"; + } + + @Override + public String getDescription() { + return "Survey data, with headers in the 8th row"; + } + + @Override + public AbstractTableParser createParser() { + return new SurveyDataParser(); + } + + @Override + public FileType getRequiredFileType() { + return FileType.EXCEL; + } +} diff --git a/src/main/java/autocadDrawingChecker/data/package-info.java b/src/main/java/autocadDrawingChecker/data/package-info.java index 69ad61f..52fe8a6 100644 --- a/src/main/java/autocadDrawingChecker/data/package-info.java +++ b/src/main/java/autocadDrawingChecker/data/package-info.java @@ -1,14 +1,20 @@ /** - * The autocadData package contains classes - * used to convert AutoCAD Excel exports into - * a more useful format for the program. + * The data package provides ways of converting Excel and CSV files into objects which the program can interact with. + * In this way, this package serves as the Model component of the standard Model-View-Controller archetype. * - * - * - * - * - * - * - *
Class Purpose
AutoCADElement Stores the data from one row in the AutoCAD Excel file.
AutoCADElementExtractorPackage-private. Used to convert rows in the Excel file to AutoCADElements.
AutoCADExcelParser Reads an Excel file, and extracts an AutoCADExport from it.
AutoCADExport Stores a list of all AutoCADElements stored in a file.
+ * Each sub-folder contained herein is a specific Gradable Data Type. + * A Gradable Data Type (or simply "data type") represents a type of Excel or CSV file that should be treated differently + * from other data types. For example, the AutoCAD data type is a subclass of the Generic Excel data type: the program + * can treat AutoCAD data as though it were just Generic Excel data, but AutoCAD data has special properties, such as + * "layer" and "name" columns, and should therefore be treated differently than Generic Excel data. + * + * To create a new data type: + *
    + *
  1. Create a new package in this folder
  2. + *
  3. Create a new subclass for each class in the "core" package
  4. + *
  5. Add your new data type to the list of data types returned by getAll() in GradableDataTypeLoader
  6. + *
+ * @see autocadDrawingChecker.data.core + * @see autocadDrawingChecker.data.GradableDataTypeLoader */ -package autocadDrawingChecker.data; \ No newline at end of file +package autocadDrawingChecker.data; diff --git a/src/main/java/autocadDrawingChecker/grading/AutoCADElementMatcher.java b/src/main/java/autocadDrawingChecker/grading/ElementMatcher.java similarity index 71% rename from src/main/java/autocadDrawingChecker/grading/AutoCADElementMatcher.java rename to src/main/java/autocadDrawingChecker/grading/ElementMatcher.java index dbd1472..b4cd278 100644 --- a/src/main/java/autocadDrawingChecker/grading/AutoCADElementMatcher.java +++ b/src/main/java/autocadDrawingChecker/grading/ElementMatcher.java @@ -1,16 +1,15 @@ package autocadDrawingChecker.grading; -import autocadDrawingChecker.data.AutoCADExport; -import autocadDrawingChecker.data.AutoCADElement; +import autocadDrawingChecker.data.core.Record; import autocadDrawingChecker.logging.Logger; import java.util.LinkedList; import java.util.List; import java.util.function.BiFunction; -import java.util.function.Predicate; +import java.util.function.Function; import java.util.stream.Collectors; /** - * The AutoCADElementMatcher class is + * The ElementMatcher class is used to pair elements in one AutoCADExport to elements in another. Essentially, this class compares two exports, and asks "which elements from the second export are supposed to match which elements in the first?" @@ -31,26 +30,27 @@ * * * @author Matt Crow + * @param the type of record this will match */ -public class AutoCADElementMatcher { - private final List exp1; - private final List exp2; - private final Predicate accepter; - private final BiFunction score; +public class ElementMatcher { + private final List exp1; + private final List exp2; + private final Function recordCaster; + private final BiFunction score; /** * * @param src the instructor AutoCADExport the student's file should conform to * @param cmp the student's file - * @param accepter a function which accepts an AutoCADElement, and returns true iff the element should be included in matching. + * @param recordCaster a function which accepts a Record, and attempts to cast it to T. Returns null if it fails * @param scoringFunction a function which returns a double between 0.0 and 1.0. When given an instructor and student file, it should return a number * within this range, with higher scores meaning the student's export is similar to the instructor export, and lower ones meaning the two exports are * different. Essentially, this acts as a grader assigning a score based on how well the student did by some metric. */ - public AutoCADElementMatcher(List src, List cmp, Predicate accepter, BiFunction scoringFunction){ + public ElementMatcher(List src, List cmp, Function recordCaster, BiFunction scoringFunction){ exp1 = src; exp2 = cmp; - this.accepter = accepter; + this.recordCaster = recordCaster; score = scoringFunction; } @@ -67,20 +67,20 @@ public AutoCADElementMatcher(List src, List cmp, * @return the list of matches between the two * given files. */ - public List findMatches(){ - List matches = new LinkedList<>(); + public List> findMatches(){ + List> matches = new LinkedList<>(); // pool of unmatched elements - List pool = exp2.stream().filter(accepter).collect(Collectors.toList()); + List pool = exp2.stream().map(recordCaster).filter((casted)->casted != null).collect(Collectors.toList()); - exp1.stream().filter(accepter).forEach((AutoCADElement srcRow)->{ + exp1.stream().map(recordCaster).filter((casted)->casted != null).forEach((T srcRow)->{ // find the closest match to srcRow double bestScore = 0.0; double currScore = 0.0; - AutoCADElement bestRow = null; + T bestRow = null; // find the highest score - for(AutoCADElement cmpRow : pool){ + for(T cmpRow : pool){ try { currScore = score.apply(srcRow, cmpRow); } catch (Exception ex){ @@ -95,8 +95,8 @@ public List findMatches(){ // some rows may not match at all if(bestRow != null){ - MatchingAutoCADElements m = new MatchingAutoCADElements(srcRow, bestRow); - //System.out.println("In AutoCADElementMatcher.findMatches: " + m); + MatchingElements m = new MatchingElements<>(srcRow, bestRow); + //System.out.println("In ElementMatcher.findMatches: " + m); matches.add(m); pool.remove(bestRow); } diff --git a/src/main/java/autocadDrawingChecker/grading/ExcelFileLocator.java b/src/main/java/autocadDrawingChecker/grading/FileLocator.java similarity index 64% rename from src/main/java/autocadDrawingChecker/grading/ExcelFileLocator.java rename to src/main/java/autocadDrawingChecker/grading/FileLocator.java index 8e2f33f..8df2b79 100644 --- a/src/main/java/autocadDrawingChecker/grading/ExcelFileLocator.java +++ b/src/main/java/autocadDrawingChecker/grading/FileLocator.java @@ -1,18 +1,19 @@ package autocadDrawingChecker.grading; +import autocadDrawingChecker.util.FileType; import java.io.File; import java.nio.file.Paths; import java.util.ArrayList; /** - * The ExcelFileLocator is a static helper class used to get all Excel files - * within a given directory. + * The FileLocator is a static helper class used to get all files + with a given extension within a given directory. * * @author Matt Crow */ -public class ExcelFileLocator { +public class FileLocator { /** - * Returns every Excel file under the given file or directory. + * Returns every file with the given extension under the given file or directory. * Not sure if I want this to return files instead of Strings, same with it's parameter *
    *
  • If the given path is an Excel file, returns that file
  • @@ -21,18 +22,19 @@ public class ExcelFileLocator { * or any folder contained within it recursively. *
* @param rootPath the path to the file or directory to scour for Excel files + * @param type the type of files to locate * @return a list of the complete path to each Excel file this locates */ - public static final ArrayList locateExcelFilesInDir(String rootPath){ + public static final ArrayList locateFilesInDir(String rootPath, FileType type){ ArrayList xlFiles = new ArrayList<>(); File root = Paths.get(rootPath).toFile(); if(root.isDirectory()){ for(String subFile : root.list()){ // root.list() does not give the full path, so need Paths.get() here - xlFiles.addAll(locateExcelFilesInDir(Paths.get(rootPath, subFile).toString())); + xlFiles.addAll(locateFilesInDir(Paths.get(rootPath, subFile).toString(), type)); } } else { - if(rootPath.endsWith("xlsx") || rootPath.endsWith("xls")){ + if(type.fileIsOfThisType(root)){ xlFiles.add(rootPath); } else { //Logger.log("Not an excel file: " + rootPath); @@ -42,6 +44,6 @@ public static final ArrayList locateExcelFilesInDir(String rootPath){ } public static void main(String[] args){ - ExcelFileLocator.locateExcelFilesInDir("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker").forEach(System.out::println); + FileLocator.locateFilesInDir("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker", FileType.CSV).forEach(System.out::println); } } diff --git a/src/main/java/autocadDrawingChecker/grading/GradedExport.java b/src/main/java/autocadDrawingChecker/grading/GradedExport.java index a5db41c..40965d9 100644 --- a/src/main/java/autocadDrawingChecker/grading/GradedExport.java +++ b/src/main/java/autocadDrawingChecker/grading/GradedExport.java @@ -1,11 +1,10 @@ package autocadDrawingChecker.grading; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import autocadDrawingChecker.data.AutoCADExport; +import autocadDrawingChecker.data.core.DataSet; import java.util.HashMap; -import java.util.HashSet; -import java.util.List; import java.util.Set; +import java.util.stream.Collectors; /** * The GradedExport class @@ -18,55 +17,39 @@ * @author Matt Crow. */ public class GradedExport { - private final AutoCADExport instructorExport; - private final AutoCADExport studentExport; - private final Set gradedCriteria; + private final DataSet instructorExport; + private final DataSet studentExport; private final HashMap grades; - private final double finalGrade; /** * Note that this constructor is is package-private, * so it can only be instantiated from within this package. * This is done by the Grader class. * - * @param instructorsExport the instructor's AutoCADExport - * @param studentsExport the student's AutoCADExport - * @param gradeOnThese the criteria to grade on. + * @param instructorsExport the instructor's export + * @param studentsExport the student's export */ - GradedExport(AutoCADExport instructorsExport, AutoCADExport studentsExport, List gradeOnThese){ + GradedExport(DataSet instructorsExport, DataSet studentsExport){ instructorExport = instructorsExport; studentExport = studentsExport; - gradedCriteria = new HashSet<>(gradeOnThese); grades = new HashMap<>(); - finalGrade = runComparison(); } /** - * Computes the grade for the student - * file. It grades the export on each - * provided criteria, and gives the final - * grade as the average of the grades for - * each criteria. + * Grades the student file on the given criteria, and adds it to the list of criteria + * this has graded. * - * @return the student's final grade. + * @param criteria the criteria to grade on. */ - private double runComparison(){ - double similarityScore = 0.0; - double newScore = 0.0; - for(AbstractGradingCriteria attr : gradedCriteria){ - newScore = attr.computeScore(instructorExport, studentExport); - grades.put(attr, newScore); - similarityScore += newScore; - } - - return similarityScore / (gradedCriteria.size()); // average similarity score + public final void addGradeFor(AbstractGradingCriteria criteria){ + grades.put(criteria, criteria.grade(instructorExport, studentExport)); } /** * * @return the instructor file this grades based on. */ - public final AutoCADExport getInstructorFile(){ + public final DataSet getInstructorFile(){ return instructorExport; } @@ -74,7 +57,7 @@ public final AutoCADExport getInstructorFile(){ * * @return the student file this grades. */ - public final AutoCADExport getStudentFile(){ + public final DataSet getStudentFile(){ return studentExport; } @@ -87,7 +70,7 @@ public final AutoCADExport getStudentFile(){ * @return the final grade for this export. */ public final double getFinalGrade(){ - return finalGrade; + return grades.values().stream().collect(Collectors.averagingDouble((d)->d)); } /** @@ -97,7 +80,15 @@ public final double getFinalGrade(){ * or null if this didn't grade on the given criteria. */ public final double getGradeFor(AbstractGradingCriteria criteria){ - return grades.get(criteria); + return (grades.containsKey(criteria)) ? grades.get(criteria) : 0.0; + } + + /** + * + * @return the criteria this has graded on + */ + public final Set getGradedCriteria(){ + return grades.keySet(); } /** @@ -117,7 +108,7 @@ public String toString(){ grades.forEach((attr, score)->{ sb.append(String.format("\n* %s: %d%%", attr.getName(), (int)(score * 100))); }); - sb.append(String.format("\nFinal Grade: %d%%", (int)(finalGrade * 100))); + sb.append(String.format("\nFinal Grade: %d%%", (int)(getFinalGrade() * 100))); return sb.toString(); } } diff --git a/src/main/java/autocadDrawingChecker/grading/Grader.java b/src/main/java/autocadDrawingChecker/grading/Grader.java index 8d67d16..87057c5 100644 --- a/src/main/java/autocadDrawingChecker/grading/Grader.java +++ b/src/main/java/autocadDrawingChecker/grading/Grader.java @@ -1,17 +1,15 @@ package autocadDrawingChecker.grading; +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.AbstractTableParser; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import autocadDrawingChecker.data.AutoCADExcelParser; -import autocadDrawingChecker.data.AutoCADExport; -import autocadDrawingChecker.grading.criteria.implementations.CompareColumn; +import autocadDrawingChecker.data.core.DataSet; import autocadDrawingChecker.logging.Logger; import java.io.IOException; -import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; /** @@ -24,42 +22,40 @@ * @author Matt Crow */ public class Grader { + private final AbstractGradableDataType dataType; private final String instrFilePath; private final String[] studentFilePaths; - private final List criteria; + private final HashSet criteria; /** * + * @param dataType the data type the given files are in * @param pathToInstructorFile the complete path to the instructor file to compare to. * @param pathsToStudentFiles a series of complete paths to student files, or folders containing them. * @param gradeThese the criteria to grade on. */ - public Grader(String pathToInstructorFile, String[] pathsToStudentFiles, List gradeThese){ + public Grader(AbstractGradableDataType dataType, String pathToInstructorFile, String[] pathsToStudentFiles, HashSet gradeThese){ + this.dataType = dataType; instrFilePath = pathToInstructorFile; studentFilePaths = pathsToStudentFiles; criteria = gradeThese; } - private AutoCADExport getInstructorFile() throws IOException{ - return AutoCADExcelParser.parse(instrFilePath); - } - - private List getStudentFiles(){ + private List getStudentFiles(AbstractTableParser parser){ return Arrays.stream(studentFilePaths).flatMap((cmpPath)->{ - // locate all Excel files in all given paths... - return ExcelFileLocator.locateExcelFilesInDir(cmpPath).stream(); - }).map((fileName)->{ - // try to convert them to AutoCADExports... - AutoCADExport r = null; + // locate all relevant files in all given paths... + return FileLocator.locateFilesInDir(cmpPath, dataType.getRequiredFileType()).stream(); + }).flatMap((fileName)->{ + List r = new LinkedList<>(); try { - r = AutoCADExcelParser.parse(fileName); + r = parser.parseAllSheets(fileName); } catch (IOException ex) { Logger.logError(ex); } - return r; - }).filter((e)->{ + return r.stream(); + }).filter((DataSet set)->{ // ignore any conversions that fail... - return e != null; + return set != null; }).collect(Collectors.toList()); // and return those successful conversions as a list } @@ -76,41 +72,30 @@ private List getStudentFiles(){ */ public final GradingReport grade(){ GradingReport report = new GradingReport(); - - AutoCADExport trySrc = null; - List cmp = null; + AbstractTableParser parser = dataType.createParser(); + DataSet trySrc = null; + List cmp = null; try { - trySrc = getInstructorFile(); + trySrc = parser.parseFirstSheet(this.instrFilePath); } catch (IOException ex) { Logger.logError(String.format("Failed to locate source file %s", instrFilePath)); Logger.logError(ex); } - AutoCADExport src = trySrc; // need this to be effectively final for lambda + DataSet src = trySrc; // need this to be effectively final for lambda - cmp = getStudentFiles(); + cmp = getStudentFiles(parser); - /* - see which columns exist in the instructor export, - and add those columns to the list of criteria this - should grade on. Don't directly add them to this.criteria - though, as that could cause problems - */ - Set colsToGrade = (src == null) ? new HashSet<>() : src.getColumns(); - LinkedList colCriteria = new LinkedList<>(); - for(String column : colsToGrade){ - colCriteria.add(new CompareColumn(column)); - } + criteria.forEach((c)->report.addCriteria(c)); - List finalGradedCriteria = new ArrayList<>(); - finalGradedCriteria.addAll(criteria); - finalGradedCriteria.addAll(colCriteria); - finalGradedCriteria.forEach((c)->report.addCriteria(c)); - - cmp.stream().forEach((exp)->{ - report.add(new GradedExport(src, exp, finalGradedCriteria)); - }); + for(DataSet studentData : cmp){ + GradedExport graded = new GradedExport(src, studentData); + for(AbstractGradingCriteria currCriteria : criteria){ + graded.addGradeFor(currCriteria); + } + report.add(graded); + } return report; } diff --git a/src/main/java/autocadDrawingChecker/grading/GradingReport.java b/src/main/java/autocadDrawingChecker/grading/GradingReport.java index 6a780e0..3d004cb 100644 --- a/src/main/java/autocadDrawingChecker/grading/GradingReport.java +++ b/src/main/java/autocadDrawingChecker/grading/GradingReport.java @@ -1,5 +1,6 @@ package autocadDrawingChecker.grading; +import autocadDrawingChecker.data.core.DataSet; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; import java.util.ArrayList; import java.util.HashMap; diff --git a/src/main/java/autocadDrawingChecker/grading/MatchingAutoCADElements.java b/src/main/java/autocadDrawingChecker/grading/MatchingElements.java similarity index 54% rename from src/main/java/autocadDrawingChecker/grading/MatchingAutoCADElements.java rename to src/main/java/autocadDrawingChecker/grading/MatchingElements.java index 14e9679..765dbbb 100644 --- a/src/main/java/autocadDrawingChecker/grading/MatchingAutoCADElements.java +++ b/src/main/java/autocadDrawingChecker/grading/MatchingElements.java @@ -1,11 +1,11 @@ package autocadDrawingChecker.grading; -import autocadDrawingChecker.data.AutoCADElement; +import autocadDrawingChecker.data.core.Record; /** - * The MatchingAutoCADElements class is - * used to connect AutoCADElements from one - * file to another. Essentially, a match + * The MatchingElements class is + used to connect SpreadsheetRecords from one + file to another. Essentially, a match * between element a in drawing 1 and * element b in drawing 2 means that, * when grading, the program assumes @@ -14,21 +14,22 @@ * other. * * @author Matt Crow + * @param the subclass of Record this should hold */ -public class MatchingAutoCADElements { - private final AutoCADElement element1; - private final AutoCADElement element2; +public class MatchingElements { + private final T element1; + private final T element2; - public MatchingAutoCADElements(AutoCADElement a, AutoCADElement b){ + public MatchingElements(T a, T b){ element1 = a; element2 = b; } - public final AutoCADElement getElement1(){ + public final T getElement1(){ return element1; } - public final AutoCADElement getElement2(){ + public final T getElement2(){ return element2; } diff --git a/src/main/java/autocadDrawingChecker/grading/MathUtil.java b/src/main/java/autocadDrawingChecker/grading/MathUtil.java index 27268df..875a531 100644 --- a/src/main/java/autocadDrawingChecker/grading/MathUtil.java +++ b/src/main/java/autocadDrawingChecker/grading/MathUtil.java @@ -1,6 +1,7 @@ package autocadDrawingChecker.grading; -import autocadDrawingChecker.logging.Logger; +import autocadDrawingChecker.start.Application; + /** * MathUtil is a static class used to hold helpful math-related functions @@ -32,6 +33,22 @@ public static double percentError(double whatIExpected, double whatIGot){ } return ret; } + private static double gradeDouble(double d1, double d2){ + return (Math.abs(d2 - d1) <= Application.getInstance().getData().getCriteriaThreshold()) ? 1.0 : 0.0; + } + private static double gradeInt(int i1, int i2){ + return (Math.abs(i2 - i1) <= Application.getInstance().getData().getCriteriaThreshold()) ? 1.0 : 0.0; + } + + public static double gradeSimilarity(Object obj1, Object obj2){ + double score = (obj1.equals(obj2)) ? 1.0 : 0.0; + if(obj1 instanceof Double && obj2 instanceof Double){ + score = gradeDouble(((Double)obj1), ((Double)obj2)); + } else if(obj1 instanceof Integer && obj2 instanceof Integer){ + score = gradeInt((Integer)obj1, (Integer)obj2); + } + return score; + } public static int rotate180(int theta){ return (180 + theta) % 360; diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/AbstractElementCriteria.java b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractElementCriteria.java index 652b507..48d1cf8 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/AbstractElementCriteria.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractElementCriteria.java @@ -1,11 +1,11 @@ package autocadDrawingChecker.grading.criteria; -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.data.AutoCADExport; -import autocadDrawingChecker.grading.AutoCADElementMatcher; -import autocadDrawingChecker.grading.MatchingAutoCADElements; -import java.util.Arrays; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.grading.ElementMatcher; +import autocadDrawingChecker.grading.MatchingElements; import java.util.List; +import java.util.stream.Collectors; /** * AbstractElementCriteria adds behavior to AbstractGradingCriteria to add @@ -13,11 +13,21 @@ * is graded based on how well its individual elements score. * * @author Matt Crow + * @param the type of DataSet this expects to grade + * @param the type of Record this expects to grade */ -public interface AbstractElementCriteria extends AbstractGradingCriteria { - public static final String[] ANY_TYPE = new String[0]; - - public abstract double getMatchScore(AutoCADElement e1, AutoCADElement e2); +public interface AbstractElementCriteria extends AbstractGradingCriteria { + /** + * Calculates the grade for the two given elements. + * Returns a value between 0.0 and 1.0, with 1.0 meaning + * they match perfectly, and 0.0 meaning they don't match + * at all, or are ungradable by this criteria. + * + * @param e1 the instructor element + * @param e2 the student element + * @return the student's score for the given element + */ + public abstract double getMatchScore(RecordType e1, RecordType e2); /** * Computes the average match score of elements in the given exports. @@ -26,44 +36,29 @@ public interface AbstractElementCriteria extends AbstractGradingCriteria { * @return the student's net score for this criteria. Ranges from 0.0 to 1.0 */ @Override - public default double computeScore(AutoCADExport exp1, AutoCADExport exp2){ - List matches = new AutoCADElementMatcher(exp1, exp2, this::canAccept, this::getMatchScore).findMatches(); - double netScore = matches.stream().map((MatchingAutoCADElements match)->{ - return getMatchScore(match.getElement1(), match.getElement2()); + public default double doGrade(DataSetType exp1, DataSetType exp2){ + List gradableElements = exp1.stream().map(this::tryCastRecord).filter((RecordType converted)->{ + return converted != null; + }).filter((RecordType nonNullConv)->{ + return getMatchScore(nonNullConv, nonNullConv) != 0; // filter out non-gradable rows + }).collect(Collectors.toList()); + List> matches = new ElementMatcher<>(gradableElements, exp2, this::tryCastRecord, this::getMatchScore).findMatches(); + double netScore = matches.stream().map((MatchingElements match)->{ + return getMatchScore(tryCastRecord(match.getElement1()), tryCastRecord(match.getElement2())); }).reduce(0.0, Double::sum); if(!matches.isEmpty()){ - netScore /= matches.size(); + netScore /= gradableElements.size(); } return netScore; } /** - * Checks to see if the given AutoCADElement can -or should- - * be graded by this criteria. This is used by the AutoCADElementMatcher - * to decide if the given element should be graded. - * @see AutoCADElementMatcher - * @param e the AutoCADElement to check - * @return whether or not this criteria can grade e - */ - public default boolean canAccept(AutoCADElement e){ - String[] types = getAllowedTypes(); - String eType = e.getName(); - boolean acceptable = Arrays.equals(types, ANY_TYPE); - for(int i = 0; i < types.length && !acceptable; i++){ - acceptable = eType.equalsIgnoreCase(types[i]); - } - return acceptable; - } - - /** - * - * @return a list of AutoCAD Name column values. - * This is meant to filter out unwanted rows. - * May remove later. + * Attempts to cast the given Record to the RecordType + * this expects. * - * You can make this return AbstractElementCriteria.ANY_TYPE - * to allow all record types. + * @param rec the Record to cast + * @return the casted Record, or null if the conversion is impossible */ - public abstract String[] getAllowedTypes(); + public abstract RecordType tryCastRecord(Record rec); } diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/AbstractGradingCriteria.java b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractGradingCriteria.java index a1e4bb8..8261968 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/AbstractGradingCriteria.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractGradingCriteria.java @@ -1,6 +1,6 @@ package autocadDrawingChecker.grading.criteria; -import autocadDrawingChecker.data.AutoCADExport; +import autocadDrawingChecker.data.core.DataSet; /** * The AbstractGradingCriteria interface is used to @@ -9,17 +9,18 @@ * exports. * * @author Matt Crow + * @param the type of data export this can grade */ -public interface AbstractGradingCriteria { +public interface AbstractGradingCriteria { /** - * This method should compare the given AutoCADExports to each other, and - * return a double between 0.0 and 1.0. A value of 1.0 denotes the student's + * This method should compare the given DataSet to each other, and + return a double between 0.0 and 1.0. A value of 1.0 denotes the student's * file meets this criteria perfectly, while a 0.0 means it fails entirely. * @param exp1 The instructor file to compare to. * @param exp2 The student file to grade. * @return a double from 0.0 to 1.0. */ - public abstract double computeScore(AutoCADExport exp1, AutoCADExport exp2); + public abstract double doGrade(DataSetType exp1, DataSetType exp2); /** * @@ -39,4 +40,33 @@ public interface AbstractGradingCriteria { * grading criteria. */ public abstract String getDescription(); + + /** + * Attempts to cast the given data set to the DataSetType this + * expects. Java does not allow notation of casting to a generic + * type, so this handles the casting instead. + * + * @param contents the DataSet to attempt to cast + * @return the given DataSet, upcasted to the data type this + * expects, or null if the conversion is not possible. + */ + public abstract DataSetType tryCastDataSet(DataSet contents); + + /** + * Grades the two given data sets, and returns their score. + * Implementations should rarely need to override this method. + * + * @param s1 the instructor data set + * @param s2 the student data set + * @return the student's grade + */ + public default double grade(DataSet s1, DataSet s2){ + double ret = 0.0; + DataSetType casted1 = tryCastDataSet(s1); + DataSetType casted2 = tryCastDataSet(s2); + if(casted1 != null && casted2 != null){ + ret = this.doGrade(casted1, casted2); + } + return ret; + } } diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/AbstractVectorCriteria.java b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractVectorCriteria.java similarity index 53% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/AbstractVectorCriteria.java rename to src/main/java/autocadDrawingChecker/grading/criteria/AbstractVectorCriteria.java index 4cee1c5..da5347d 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/AbstractVectorCriteria.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/AbstractVectorCriteria.java @@ -1,7 +1,8 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria; -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.grading.MathUtil; /** * AbstractVectorCriteria is used grading based on some multi-element grading @@ -9,17 +10,19 @@ * but I may change to normalized dot product later. * * @author Matt Crow + * @param the type of DataSet this expects to grade + * @param the type of Record this expects to grade */ -public interface AbstractVectorCriteria extends AbstractElementCriteria { +public interface AbstractVectorCriteria extends AbstractElementCriteria { @Override - public default double getMatchScore(AutoCADElement e1, AutoCADElement e2){ + public default double getMatchScore(RecordType e1, RecordType e2){ double score = 0.0; double[] v1 = extractVector(e1); double[] v2 = extractVector(e2); int numComponents = Math.min(v1.length, v2.length); for(int i = 0; i < numComponents; i++){ - score += (v1[i] == v2[i]) ? 1.0 : 0.0;//1.0 - MathUtil.percentError(v1[i], v2[i]); + score += MathUtil.gradeSimilarity(v1[i], v2[i]); } score /= numComponents; // average percent correct @@ -32,5 +35,5 @@ public default double getMatchScore(AutoCADElement e1, AutoCADElement e2){ * @return the vector interpretation of the given element, * which this will grade on */ - public abstract double[] extractVector(AutoCADElement e); + public abstract double[] extractVector(RecordType e); } diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/CompareColumn.java b/src/main/java/autocadDrawingChecker/grading/criteria/CompareColumn.java new file mode 100644 index 0000000..580ff77 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/grading/criteria/CompareColumn.java @@ -0,0 +1,64 @@ +package autocadDrawingChecker.grading.criteria; + +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.grading.MathUtil; +import java.util.Objects; + +/** + * + * @author Matt + */ +public class CompareColumn implements AbstractElementCriteria { + private final String column; + + /** + * + * @param column the column to compare + */ + public CompareColumn(String column){ + this.column = column; + } + + @Override + public boolean equals(Object obj){ + return obj != null && obj instanceof CompareColumn && ((CompareColumn)obj).column.equals(this.column); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 67 * hash + Objects.hashCode(this.column); + return hash; + } + + @Override + public double getMatchScore(Record e1, Record e2) { + double ret = 0.0; + if(e1.hasAttribute(column) && e2.hasAttribute(column)){ + ret = MathUtil.gradeSimilarity(e1.getAttribute(column), e2.getAttribute(column)); + } + return ret; + } + + @Override + public String getName() { + // Easier to read when it simply starts with the column name + return String.format("%s column", column); + } + + @Override + public String getDescription() { + return String.format("Grades the student file based on how closely its %s column matches with that of the instructor file", column); + } + + @Override + public Record tryCastRecord(Record rec) { + return (rec != null) ? rec : null; + } + + @Override + public DataSet tryCastDataSet(DataSet contents) { + return (contents != null) ? contents : null; + } +} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/GradingCriteriaLoader.java b/src/main/java/autocadDrawingChecker/grading/criteria/GradingCriteriaLoader.java index 6c18df2..6c2dc27 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/GradingCriteriaLoader.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/GradingCriteriaLoader.java @@ -1,13 +1,16 @@ package autocadDrawingChecker.grading.criteria; -import autocadDrawingChecker.grading.criteria.implementations.LinesPerLayer; -import autocadDrawingChecker.grading.criteria.implementations.LineStart; -import autocadDrawingChecker.grading.criteria.implementations.LineLength; -import autocadDrawingChecker.grading.criteria.implementations.LineEnd; -import autocadDrawingChecker.grading.criteria.implementations.LineCount; -import autocadDrawingChecker.grading.criteria.implementations.LineAngle; -import autocadDrawingChecker.grading.criteria.implementations.TextMatches; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.LinesPerLayer; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.LineStart; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.LineLength; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.LineEnd; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.LineAngle; +import autocadDrawingChecker.grading.criteria.autoCADCriteria.TextMatches; +import autocadDrawingChecker.grading.criteria.surveyCriteria.GradeCoords; import autocadDrawingChecker.util.AbstractLoader; +import java.util.ArrayList; +import java.util.List; /** * The GradingCriteriaLoader is used @@ -17,15 +20,16 @@ */ public class GradingCriteriaLoader extends AbstractLoader{ @Override - public final AbstractGradingCriteria[] getAll(){ - return new AbstractGradingCriteria[]{ - new LineCount(), - new LinesPerLayer(), - new LineLength(), - new LineAngle(), - new LineStart(), - new LineEnd(), - new TextMatches() - }; + public final List getAll(){ + ArrayList ret = new ArrayList<>(); + ret.add(new LineCount()); + ret.add(new LinesPerLayer()); + ret.add(new LineLength()); + ret.add(new LineAngle()); + ret.add(new LineStart()); + ret.add(new LineEnd()); + ret.add(new TextMatches()); + ret.add(new GradeCoords()); + return ret; } } diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/LineCount.java b/src/main/java/autocadDrawingChecker/grading/criteria/LineCount.java new file mode 100644 index 0000000..f4007d6 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/grading/criteria/LineCount.java @@ -0,0 +1,30 @@ +package autocadDrawingChecker.grading.criteria; + +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.grading.MathUtil; + +/** + * @author Matt Crow + */ +public class LineCount implements AbstractGradingCriteria { + + @Override + public double doGrade(DataSet exp1, DataSet exp2) { + return MathUtil.gradeSimilarity(exp1.size(), exp2.size()); + } + + @Override + public String getDescription() { + return "Grades the student on how close their number of lines match with that of the comparison file."; + } + + @Override + public String getName() { + return "Line count"; + } + + @Override + public DataSet tryCastDataSet(DataSet contents) { + return (contents != null) ? contents : null; + } +} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/AbstractAutoCADElementCriteria.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/AbstractAutoCADElementCriteria.java new file mode 100644 index 0000000..5cb230f --- /dev/null +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/AbstractAutoCADElementCriteria.java @@ -0,0 +1,64 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package autocadDrawingChecker.grading.criteria.autoCADCriteria; + +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADExport; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; +import java.util.Arrays; + +/** + * + * @author Matt + */ +public interface AbstractAutoCADElementCriteria extends AbstractElementCriteria { + public static final String[] ANY_TYPE = new String[0]; + /** + * Checks to see if the given AutoCADElement can -or should- + * be graded by this criteria. This is used by tryCastRecord + to decide if the given element should be graded. + * @param e the AutoCADElement to check + * @return whether or not this criteria can grade e + */ + public default boolean canAccept(AutoCADElement e){ + String[] types = getAllowedTypes(); + String eType = e.getName(); + boolean acceptable = Arrays.equals(types, ANY_TYPE); + for(int i = 0; i < types.length && !acceptable; i++){ + acceptable = eType.equalsIgnoreCase(types[i]); + } + return acceptable; + } + + /** + * + * @return a list of AutoCAD Name column values. + * This is meant to filter out unwanted rows. + * May remove later. + * + * You can make this return AbstractAutoCADElementCriteria.ANY_TYPE + * to allow all record types. + */ + public abstract String[] getAllowedTypes(); + + @Override + public default AutoCADElement tryCastRecord(Record rec){ + AutoCADElement ret = null; + // only cast elements this can accept + if(rec != null && rec instanceof AutoCADElement && canAccept((AutoCADElement)rec)){ + ret = (AutoCADElement)rec; + } + return ret; + } + + @Override + public default AutoCADExport tryCastDataSet(DataSet contents){ + return (contents != null && contents instanceof AutoCADExport) ? (AutoCADExport)contents : null; + } + +} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineAngle.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineAngle.java similarity index 64% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineAngle.java rename to src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineAngle.java index 872a663..9143d9f 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineAngle.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineAngle.java @@ -1,13 +1,12 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria.autoCADCriteria; -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; import autocadDrawingChecker.grading.MathUtil; /** * @author Matt Crow */ -public class LineAngle implements AbstractElementCriteria { +public class LineAngle implements AbstractAutoCADElementCriteria { /** * * @param r1 a line in the instructor's file @@ -19,8 +18,9 @@ public double getMatchScore(AutoCADElement r1, AutoCADElement r2){ int r1Angle = r1.getAttributeInt("angle"); int r2Angle = r2.getAttributeInt("angle"); - return (r1Angle == r2Angle || r1Angle == MathUtil.rotate180(r2Angle)) ? 1.0 : 0.0; - // check if they got a line reversed + double ret = Math.max(MathUtil.gradeSimilarity(r1Angle, r2Angle), MathUtil.gradeSimilarity(r1Angle, MathUtil.rotate180(r2Angle))); + // check if they got a line reversed + return ret; } @Override diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineEnd.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineEnd.java similarity index 62% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineEnd.java rename to src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineEnd.java index 46cb244..f378d27 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineEnd.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineEnd.java @@ -1,12 +1,14 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria.autoCADCriteria; -import autocadDrawingChecker.data.AutoCADElement; +import autocadDrawingChecker.grading.criteria.AbstractVectorCriteria; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADExport; /** * * @author Matt */ -public class LineEnd implements AbstractVectorCriteria { +public class LineEnd implements AbstractVectorCriteria, AbstractAutoCADElementCriteria { @Override public String getDescription() { return "Grades based on how closesly the student's line end points match up with those of the instructor's"; diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineLength.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineLength.java new file mode 100644 index 0000000..6f46c70 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineLength.java @@ -0,0 +1,52 @@ +package autocadDrawingChecker.grading.criteria.autoCADCriteria; + +import autocadDrawingChecker.data.excel.autoCADData.AutoCADExport; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.grading.MathUtil; + +/** + * + * @author Matt Crow + */ +public class LineLength implements AbstractAutoCADElementCriteria { + + @Override + public double getMatchScore(AutoCADElement r1, AutoCADElement r2){ + return MathUtil.gradeSimilarity(r1.getAttributeDouble("length"), r2.getAttributeDouble("length")); + } + + private double getTotalLineLength(AutoCADExport exp){ + return exp.stream().filter((Record e)->{ + return e instanceof AutoCADElement; + }).map((rec)->{ + return (AutoCADElement)rec; + }).filter(this::canAccept).map((AutoCADElement imReallyALine)->{ + return imReallyALine.getAttributeDouble("length"); + }).reduce(0.0, Double::sum); + } + + @Override + public double doGrade(AutoCADExport exp1, AutoCADExport exp2) { + double srcTotalLength = getTotalLineLength(exp1); + double cmpTotalLength = getTotalLineLength(exp2); + double avgLenScore = AbstractAutoCADElementCriteria.super.doGrade(exp1, exp2); + double totalLengthScore = MathUtil.gradeSimilarity(srcTotalLength, cmpTotalLength); + return (avgLenScore + totalLengthScore) / 2; + } + + @Override + public String getDescription() { + return "Grades the drawing based on how closely the length of lines match the original drawing"; + } + + @Override + public String getName() { + return "Line Length"; + } + + @Override + public String[] getAllowedTypes() { + return new String[]{"Line"}; + } +} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineStart.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineStart.java similarity index 62% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineStart.java rename to src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineStart.java index 5b7d905..781a5b5 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineStart.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LineStart.java @@ -1,12 +1,14 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria.autoCADCriteria; -import autocadDrawingChecker.data.AutoCADElement; +import autocadDrawingChecker.grading.criteria.AbstractVectorCriteria; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADExport; /** * * @author Matt Crow */ -public class LineStart implements AbstractVectorCriteria { +public class LineStart implements AbstractVectorCriteria, AbstractAutoCADElementCriteria { @Override public String getDescription() { return "Grades based on how closesly the student's line start points match up with those of the instructor's"; diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LinesPerLayer.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LinesPerLayer.java similarity index 51% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/LinesPerLayer.java rename to src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LinesPerLayer.java index 2547aa1..028b667 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LinesPerLayer.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/LinesPerLayer.java @@ -1,6 +1,8 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria.autoCADCriteria; -import autocadDrawingChecker.data.AutoCADExport; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADExport; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.grading.MathUtil; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; import java.util.HashMap; import java.util.Objects; @@ -9,26 +11,20 @@ * * @author Matt Crow */ -public class LinesPerLayer implements AbstractGradingCriteria { +public class LinesPerLayer implements AbstractGradingCriteria { @Override - public double computeScore(AutoCADExport exp1, AutoCADExport exp2) { + public double doGrade(AutoCADExport exp1, AutoCADExport exp2) { double score = 0.0; - HashMap srcLines = exp1.getCountsPerColumnValue("Layer");//exp1.getLayerLineCounts(); - HashMap cmpLines = exp2.getCountsPerColumnValue("Layer");//exp2.getLayerLineCounts(); + HashMap srcLines = exp1.getCountsPerColumnValue("Layer"); + HashMap cmpLines = exp2.getCountsPerColumnValue("Layer"); for(Object layer : srcLines.keySet()){ - if(cmpLines.containsKey(layer) && Objects.equals(cmpLines.get(layer), srcLines.get(layer))){ - score++; // only gives points on layers which contain the exact same number of lines - } - /* if(cmpLines.containsKey(layer)){ - score += MathUtil.percentError(srcLines.get(layer), cmpLines.get(layer)); + score += MathUtil.gradeSimilarity(cmpLines.get(layer), srcLines.get(layer)); } - */ } return score / srcLines.size(); - //return 1.0 - (score / srcLines.size()); } @Override @@ -41,4 +37,8 @@ public String getName() { return "Lines per layer"; } + @Override + public AutoCADExport tryCastDataSet(DataSet contents) { + return (contents != null && contents instanceof AutoCADExport) ? (AutoCADExport)contents : null; + } } diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/TextMatches.java b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/TextMatches.java similarity index 76% rename from src/main/java/autocadDrawingChecker/grading/criteria/implementations/TextMatches.java rename to src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/TextMatches.java index 29401d2..5374fbb 100644 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/TextMatches.java +++ b/src/main/java/autocadDrawingChecker/grading/criteria/autoCADCriteria/TextMatches.java @@ -1,13 +1,13 @@ -package autocadDrawingChecker.grading.criteria.implementations; +package autocadDrawingChecker.grading.criteria.autoCADCriteria; -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; +import autocadDrawingChecker.data.excel.autoCADData.AutoCADElement; +import autocadDrawingChecker.grading.MathUtil; /** * * @author Matt */ -public class TextMatches implements AbstractElementCriteria { +public class TextMatches implements AbstractAutoCADElementCriteria { @Override public double getMatchScore(AutoCADElement row1, AutoCADElement row2){ @@ -25,7 +25,7 @@ public double getMatchScore(AutoCADElement row1, AutoCADElement row2){ text2 = row2.getAttributeString("value"); } - return (text1.equals(text2)) ? 1.0 : 0.0; + return MathUtil.gradeSimilarity(text1, text2); } @Override diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/CompareColumn.java b/src/main/java/autocadDrawingChecker/grading/criteria/implementations/CompareColumn.java deleted file mode 100644 index 2bbdcc4..0000000 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/CompareColumn.java +++ /dev/null @@ -1,46 +0,0 @@ -package autocadDrawingChecker.grading.criteria.implementations; - -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; - -/** - * - * @author Matt - */ -public class CompareColumn implements AbstractElementCriteria { - private final String column; - - /** - * - * @param column the column to compare - */ - public CompareColumn(String column){ - this.column = column; - } - - @Override - public double getMatchScore(AutoCADElement e1, AutoCADElement e2) { - double ret = 0.0; - if(e1.hasAttribute(column) && e2.hasAttribute(column)){ - ret = (e1.getAttribute(column).equals(e2.getAttribute(column))) ? 1.0 : 0.0; - } - return ret; - } - - @Override - public String[] getAllowedTypes() { - return AbstractElementCriteria.ANY_TYPE; - } - - @Override - public String getName() { - // Easier to read when it simply starts with the column name - return String.format("%s column", column); - } - - @Override - public String getDescription() { - return String.format("Grades the student file based on how closely its %s column matches with that of the instructor file", column); - } - -} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineCount.java b/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineCount.java deleted file mode 100644 index 2f9879b..0000000 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineCount.java +++ /dev/null @@ -1,26 +0,0 @@ -package autocadDrawingChecker.grading.criteria.implementations; - -import autocadDrawingChecker.data.AutoCADExport; -import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; - -/** - * @author Matt Crow - */ -public class LineCount implements AbstractGradingCriteria { - - @Override - public double computeScore(AutoCADExport exp1, AutoCADExport exp2) { - return (exp1.size() == exp2.size()) ? 1.0 : 0.0;//1.0 - MathUtil.percentError(exp1.size(), exp2.size()); - } - - @Override - public String getDescription() { - return "Grades the student on how close their number of lines match with that of the comparison file."; - } - - @Override - public String getName() { - return "Line count"; - } - -} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineLength.java b/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineLength.java deleted file mode 100644 index 2291fdd..0000000 --- a/src/main/java/autocadDrawingChecker/grading/criteria/implementations/LineLength.java +++ /dev/null @@ -1,50 +0,0 @@ -package autocadDrawingChecker.grading.criteria.implementations; - -import autocadDrawingChecker.data.AutoCADExport; -import autocadDrawingChecker.data.AutoCADElement; -import autocadDrawingChecker.grading.criteria.AbstractElementCriteria; - -/** - * - * @author Matt Crow - */ -public class LineLength implements AbstractElementCriteria { - - @Override - public double getMatchScore(AutoCADElement r1, AutoCADElement r2){ - return (r1.getAttributeDouble("length") == r2.getAttributeDouble("length")) ? 1.0 : 0.0; - } - - private double getTotalLineLength(AutoCADExport exp){ - return exp.stream().filter((AutoCADElement e)->{ - return this.canAccept(e); - }).map((AutoCADElement imReallyALine)->{ - return imReallyALine.getAttributeDouble("length"); - }).reduce(0.0, Double::sum); - } - - @Override - public double computeScore(AutoCADExport exp1, AutoCADExport exp2) { - double srcTotalLength = getTotalLineLength(exp1); - double cmpTotalLength = getTotalLineLength(exp2); - double avgLenScore = AbstractElementCriteria.super.computeScore(exp1, exp2); - double totalLengthScore = (srcTotalLength == cmpTotalLength) ? 1.0 : 0.0; - return (avgLenScore + totalLengthScore) / 2;//* (1.0 - MathUtil.percentError(srcTotalLength, cmpTotalLength)); - } - - @Override - public String getDescription() { - return "Grades the drawing based on how closely the length of lines match the original drawing"; - } - - @Override - public String getName() { - return "Line Length"; - } - - @Override - public String[] getAllowedTypes() { - return new String[]{"Line"}; - } - -} diff --git a/src/main/java/autocadDrawingChecker/grading/criteria/surveyCriteria/GradeCoords.java b/src/main/java/autocadDrawingChecker/grading/criteria/surveyCriteria/GradeCoords.java new file mode 100644 index 0000000..74f7c9c --- /dev/null +++ b/src/main/java/autocadDrawingChecker/grading/criteria/surveyCriteria/GradeCoords.java @@ -0,0 +1,43 @@ +package autocadDrawingChecker.grading.criteria.surveyCriteria; + +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.data.core.Record; +import autocadDrawingChecker.data.excel.surveyData.SurveyDataRecord; +import autocadDrawingChecker.data.excel.surveyData.SurveyDataSet; +import autocadDrawingChecker.grading.criteria.AbstractVectorCriteria; + +/** + * + * @author Matt + */ +public class GradeCoords implements AbstractVectorCriteria{ + @Override + public double[] extractVector(SurveyDataRecord e) { + return new double[]{ + e.getX(), + e.getY(), + e.getZ() + }; + } + + @Override + public SurveyDataRecord tryCastRecord(Record rec) { + return (rec instanceof SurveyDataRecord) ? (SurveyDataRecord)rec : null; + } + + @Override + public String getName() { + return "Grade Survey Data Coordinates"; + } + + @Override + public String getDescription() { + return "Grades Survey Data based on how closely its coordinates match that of the instructor file"; + } + + @Override + public SurveyDataSet tryCastDataSet(DataSet contents) { + return (contents instanceof SurveyDataSet) ? (SurveyDataSet)contents : null; + } + +} diff --git a/src/main/java/autocadDrawingChecker/grading/package-info.java b/src/main/java/autocadDrawingChecker/grading/package-info.java index cbb94ca..29eddb5 100644 --- a/src/main/java/autocadDrawingChecker/grading/package-info.java +++ b/src/main/java/autocadDrawingChecker/grading/package-info.java @@ -1,6 +1,6 @@ /** * The grading package contains classes - * relevant to comparing two AutoCADExports, + * relevant to comparing two DataSets, * and assigning a grade to them. The primary * classes of interest are the AbstractGradingCriteria * class, which serves as the base for diff --git a/src/main/java/autocadDrawingChecker/gui/AbstractPage.java b/src/main/java/autocadDrawingChecker/gui/AbstractPage.java index b43acaf..310dcdc 100644 --- a/src/main/java/autocadDrawingChecker/gui/AbstractPage.java +++ b/src/main/java/autocadDrawingChecker/gui/AbstractPage.java @@ -1,5 +1,6 @@ package autocadDrawingChecker.gui; +import autocadDrawingChecker.start.DrawingCheckerData; import javax.swing.JPanel; /** @@ -19,4 +20,12 @@ public final String getTitle(){ } protected abstract boolean checkIfReadyForNextPage(); + + /** + * This method is invoked when switching to this page. Updates + * the GUI of this page based on the data contained in newData. + * + * @param newData the current set of DrawingCheckerData. + */ + protected abstract void dataUpdated(DrawingCheckerData newData); } diff --git a/src/main/java/autocadDrawingChecker/gui/PageRenderer.java b/src/main/java/autocadDrawingChecker/gui/PageRenderer.java index 7e2bd5f..6e12aca 100644 --- a/src/main/java/autocadDrawingChecker/gui/PageRenderer.java +++ b/src/main/java/autocadDrawingChecker/gui/PageRenderer.java @@ -2,8 +2,10 @@ import autocadDrawingChecker.gui.runPage.OutputPage; import autocadDrawingChecker.gui.chooseCriteria.ChooseCriteriaPage; +import autocadDrawingChecker.gui.chooseDataType.ChooseDataTypePage; import autocadDrawingChecker.gui.chooseFiles.ChooseFilesPage; import autocadDrawingChecker.logging.Logger; +import autocadDrawingChecker.start.Application; import java.awt.BorderLayout; import java.awt.CardLayout; import java.awt.Color; @@ -32,6 +34,7 @@ public class PageRenderer extends JPanel { private final HashMap pages; // need this to map names to pages private int currPageIdx; + public static final String CHOOSE_DATA_TYPE = "choose data type"; public static final String CHOOSE_FILES = "choose files"; public static final String CHOOSE_CRITERIA = "choose criteria"; public static final String OUTPUT = "output"; @@ -66,6 +69,7 @@ public PageRenderer(){ pages = new HashMap<>(); // populated by addPage pageNames = new ArrayList<>(); currPageIdx = 0; + addPage(CHOOSE_DATA_TYPE, new ChooseDataTypePage()); addPage(CHOOSE_FILES, new ChooseFilesPage()); addPage(CHOOSE_CRITERIA, new ChooseCriteriaPage()); addPage(OUTPUT, new OutputPage()); @@ -89,6 +93,7 @@ private void updateRenderedPage(){ pageTitle.setText(pages.get(pageName).getTitle()); prevButton.setVisible(currPageIdx > 0); nextButton.setVisible(currPageIdx < pageNames.size() - 1); + pages.get(pageName).dataUpdated(Application.getInstance().getData()); revalidate(); repaint(); } else { @@ -99,13 +104,13 @@ private void updateRenderedPage(){ private void tryPrevPage(){ if(currPageIdx > 0){ currPageIdx--; - updateRenderedPage();//switchToPage(pageNames.get(currPageIdx)); + updateRenderedPage(); } } private void tryNextPage(){ if(currPageIdx < pageNames.size() && pages.get(pageNames.get(currPageIdx)).checkIfReadyForNextPage()){ currPageIdx = (currPageIdx + 1) % pageNames.size(); // loop around to first page if we click next on the last one - updateRenderedPage();//switchToPage(pageNames.get(currPageIdx)); + updateRenderedPage(); } } } diff --git a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/ChooseCriteriaPage.java b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/ChooseCriteriaPage.java index d9dfa73..4ce2b92 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/ChooseCriteriaPage.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/ChooseCriteriaPage.java @@ -1,10 +1,15 @@ package autocadDrawingChecker.gui.chooseCriteria; +import autocadDrawingChecker.data.core.DataSet; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; import autocadDrawingChecker.gui.AbstractPage; -import java.awt.GridLayout; +import autocadDrawingChecker.start.DrawingCheckerData; +import java.awt.BorderLayout; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import javax.swing.JOptionPane; +import javax.swing.JPanel; /** * @@ -15,10 +20,16 @@ public class ChooseCriteriaPage extends AbstractPage { public ChooseCriteriaPage() { super("Choose criteria to grade on"); - setLayout(new GridLayout(1, 1)); + setLayout(new BorderLayout()); critList = new CriteriaSelectionList(); - add(critList); + add(critList, BorderLayout.CENTER); + + JPanel bottom = new JPanel(); + + bottom.add(new CriteriaThresholdInput()); + + add(bottom, BorderLayout.PAGE_END); } public final void setCriteriaSelected(AbstractGradingCriteria crit, boolean isSelected){ @@ -31,10 +42,19 @@ public final List getSelectedCriteria(){ @Override protected boolean checkIfReadyForNextPage() { - boolean ready = !critList.getSelectedCriteria().isEmpty(); + boolean ready = true || !critList.getSelectedCriteria().isEmpty(); if(!ready){ JOptionPane.showMessageDialog(this, "Please select at least 1 criteria to grade on"); } return ready; } + + @Override + protected void dataUpdated(DrawingCheckerData newData) { + HashMap critToIsSel = newData.getGradableCriteriaToIsSelected(); + critList.setCriteriaOptions(new LinkedList<>(critToIsSel.keySet())); + critToIsSel.entrySet().forEach((entry)->{ + critList.setCriteriaSelected(entry.getKey(), entry.getValue()); + }); + } } diff --git a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaSelectionList.java b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaSelectionList.java index 5490354..7481042 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaSelectionList.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaSelectionList.java @@ -1,11 +1,10 @@ package autocadDrawingChecker.gui.chooseCriteria; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import autocadDrawingChecker.start.Application; import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; @@ -20,15 +19,17 @@ */ public class CriteriaSelectionList extends JComponent { private final HashMap criteriaList; // name to criteria toggle + private final JPanel content; + private final GridBagConstraints gbc; public CriteriaSelectionList(){ super(); criteriaList = new HashMap<>(); setLayout(new BorderLayout()); - JPanel content = new JPanel(); + content = new JPanel(); content.setLayout(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); + gbc = new GridBagConstraints(); gbc.gridx = 0; gbc.gridy = GridBagConstraints.RELATIVE; gbc.gridheight = 1; @@ -37,19 +38,29 @@ public CriteriaSelectionList(){ gbc.weighty = 0.0; // this make things "stick" together gbc.fill = GridBagConstraints.HORIZONTAL; gbc.anchor = GridBagConstraints.PAGE_START; - Arrays.stream(Application.getInstance().getGradedCriteria()).map((AbstractGradingCriteria criteria)->{ - return new CriteriaToggle(criteria); - }).forEach((CriteriaToggle component)->{ - criteriaList.put(component.getCriteria().getName(), component); - content.add(component, gbc.clone()); - }); JScrollPane scroll = new JScrollPane(content); scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scroll.getVerticalScrollBar().setUnitIncrement(10); add(scroll, BorderLayout.CENTER); } + final void setCriteriaOptions(List criteria){ + criteriaList.clear(); + content.removeAll(); + + CriteriaToggle toggle = null; + // sort criteria by name + criteria = criteria.subList(0, criteria.size()); // copy to avoid changing the original + Collections.sort(criteria, (crit1, crit2)->crit1.getName().compareTo(crit2.getName())); + for(AbstractGradingCriteria crit : criteria){ + toggle = new CriteriaToggle(crit); + criteriaList.put(toggle.getCriteria().getName(), toggle); + content.add(toggle, gbc.clone()); + } + } + public final void setCriteriaSelected(AbstractGradingCriteria crit, boolean isSelected){ criteriaList.get(crit.getName()).setSelected(isSelected); } diff --git a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaThresholdInput.java b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaThresholdInput.java new file mode 100644 index 0000000..3260e0e --- /dev/null +++ b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaThresholdInput.java @@ -0,0 +1,84 @@ +package autocadDrawingChecker.gui.chooseCriteria; + +import autocadDrawingChecker.start.Application; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import javax.swing.JComponent; +import javax.swing.JFormattedTextField; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.text.NumberFormatter; + +/** + * This is a widget allowing the user to select the criteria threshold as defined in DrawingCheckerData + * + * @author Matt Crow + */ +public class CriteriaThresholdInput extends JComponent { + private final JFormattedTextField threshInput; + private final JPanel body; + + public CriteriaThresholdInput(){ + super(); + setLayout(new BorderLayout()); + + JLabel title = new JLabel("Criteria Threshold"); + title.setToolTipText("How much student values can differ yet still be considered correct"); + add(title, BorderLayout.PAGE_START); + + body = new JPanel(); + // https://stackoverflow.com/questions/11093326/restricting-jtextfield-input-to-integers + NumberFormat format = DecimalFormat.getInstance(); + NumberFormatter formatter = new NumberFormatter(format); + formatter.setAllowsInvalid(true); + formatter.setValueClass(Double.class); + formatter.setMinimum(0.0); + formatter.setMaximum(Double.MAX_VALUE); + formatter.setCommitsOnValidEdit(true); + threshInput = new JFormattedTextField(formatter); + threshInput.setToolTipText("Enter a non-negative number (it can be a decimal number)"); + threshInput.setValue(Application.getInstance().getData().getCriteriaThreshold()); + threshInput.setColumns(20); + threshInput.addFocusListener(new FocusListener(){ + @Override + public void focusGained(FocusEvent e) {} + + @Override + public void focusLost(FocusEvent e) { + checkIfValid(); + } + }); + threshInput.addKeyListener(new KeyListener(){ + @Override + public void keyTyped(KeyEvent e) {} + + @Override + public void keyPressed(KeyEvent e) {} + + @Override + public void keyReleased(KeyEvent e) { + if(e.getKeyCode() == KeyEvent.VK_ENTER){ + checkIfValid(); + } + } + }); + body.add(threshInput); + add(body, BorderLayout.CENTER); + } + + private void checkIfValid(){ + if(threshInput.isEditValid()){ + Application.getInstance().getData().setCriteriaThreshold((Double)threshInput.getValue()); + body.setBackground(Color.green); + } else { + // not valid + body.setBackground(Color.red); + } + } +} diff --git a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaToggle.java b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaToggle.java index 3ed5853..a6fba79 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaToggle.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseCriteria/CriteriaToggle.java @@ -1,5 +1,6 @@ package autocadDrawingChecker.gui.chooseCriteria; +import autocadDrawingChecker.data.core.DataSet; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; import autocadDrawingChecker.start.Application; import java.awt.BorderLayout; @@ -10,8 +11,8 @@ /** * A CriteriaToggle allow the user to choose whether - * or not they want to grade a student's submission - * based on a given criteria. + or not they want to grade a student's submission + based on a given criteria. * * @author Matt Crow */ diff --git a/src/main/java/autocadDrawingChecker/gui/chooseDataType/ChooseDataTypePage.java b/src/main/java/autocadDrawingChecker/gui/chooseDataType/ChooseDataTypePage.java new file mode 100644 index 0000000..b1c8aed --- /dev/null +++ b/src/main/java/autocadDrawingChecker/gui/chooseDataType/ChooseDataTypePage.java @@ -0,0 +1,43 @@ +package autocadDrawingChecker.gui.chooseDataType; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.gui.AbstractPage; +import autocadDrawingChecker.start.DrawingCheckerData; +import java.awt.GridLayout; +import javax.swing.JOptionPane; + +/** + * + * @author Matt + */ +public class ChooseDataTypePage extends AbstractPage { + private final DataTypeList typeList; + public ChooseDataTypePage() { + super("Choose the type of data to grade"); + setLayout(new GridLayout(1, 1)); + typeList = new DataTypeList(); + add(typeList); + } + + public final void setSelectedDataType(AbstractGradableDataType type){ + typeList.setDataTypeSelected(type); + } + + @Override + protected boolean checkIfReadyForNextPage() { + boolean ready = typeList.isAnySelected(); + if(!ready){ + JOptionPane.showMessageDialog(this, "Please choose a data type to grade"); + } + return ready; + } + + @Override + protected void dataUpdated(DrawingCheckerData newData) { + typeList.setDataTypeOptions(newData.getGradableDataTypes()); + if(newData.isDataTypeSelected()){ + typeList.setDataTypeSelected(newData.getSelectedDataType()); + } + } + +} diff --git a/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeList.java b/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeList.java new file mode 100644 index 0000000..d4e5339 --- /dev/null +++ b/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeList.java @@ -0,0 +1,74 @@ +package autocadDrawingChecker.gui.chooseDataType; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import java.awt.BorderLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.HashMap; +import java.util.List; +import javax.swing.ButtonGroup; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; + +/** + * + * @author Matt + */ +public class DataTypeList extends JComponent { + private final JPanel content; + private final ButtonGroup buttons; + private final GridBagConstraints gbc; + private final HashMap typeToSel; + + public DataTypeList(){ + super(); + buttons = new ButtonGroup(); + typeToSel = new HashMap<>(); + setLayout(new BorderLayout()); + + // copy-pasted from CriteriaSelectionList + content = new JPanel(); + content.setLayout(new GridBagLayout()); + gbc = new GridBagConstraints(); + gbc.gridx = 0; + gbc.gridy = GridBagConstraints.RELATIVE; + gbc.gridheight = 1; + gbc.gridwidth = 1; + gbc.weightx = 1.0; + gbc.weighty = 0.0; // this make things "stick" together + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.anchor = GridBagConstraints.PAGE_START; + + JScrollPane scroll = new JScrollPane(content); + scroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); + scroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + add(scroll, BorderLayout.CENTER); + } + + final void setDataTypeOptions(List dataTypes){ + typeToSel.values().stream().map((sel)->sel.getButton()).forEach(buttons::remove); + typeToSel.clear(); + content.removeAll(); + + dataTypes.stream().map((type)->{ + typeToSel.put(type, new DataTypeSelector(type)); + return typeToSel.get(type); + }).forEach((sel)->{ + content.add(sel, gbc.clone()); + buttons.add(sel.getButton()); + }); + setDataTypeSelected(dataTypes.get(0)); + } + + public final void setDataTypeSelected(AbstractGradableDataType type){ + typeToSel.get(type).getButton().setSelected(true); + } + + public final boolean isAnySelected(){ + return typeToSel.values().stream().anyMatch((sel)->{ + return sel.isSelected(); + }); + } +} diff --git a/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeSelector.java b/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeSelector.java new file mode 100644 index 0000000..2ad4c8b --- /dev/null +++ b/src/main/java/autocadDrawingChecker/gui/chooseDataType/DataTypeSelector.java @@ -0,0 +1,73 @@ +package autocadDrawingChecker.gui.chooseDataType; + +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.start.Application; +import java.awt.BorderLayout; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JRadioButton; +import javax.swing.JTextArea; + +/** + * + * @author Matt + */ +public class DataTypeSelector extends JComponent implements MouseListener { + private final JRadioButton button; + private final AbstractGradableDataType dataType; + + DataTypeSelector(AbstractGradableDataType dataType){ + super(); + this.dataType = dataType; + setLayout(new BorderLayout()); + add(new JLabel(dataType.getName()), BorderLayout.PAGE_START); + button = new JRadioButton(); + button.addActionListener((e)->{ + setSelected(true); + }); + + add(button, BorderLayout.LINE_START); + JTextArea desc = new JTextArea(dataType.getDescription()); + desc.setLineWrap(true); + desc.setWrapStyleWord(true); + desc.setEditable(false); + add(desc, BorderLayout.CENTER); + desc.addMouseListener(this); + + this.addMouseListener(this); + } + + final JRadioButton getButton(){ + return button; + } + + final boolean isSelected(){ + return button.isSelected(); + } + + final void setSelected(boolean b){ + button.setSelected(b); + if(b){ + Application.getInstance().getData().setSelectedDataType(dataType); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + setSelected(true); + } + + @Override + public void mousePressed(MouseEvent e) {} + + @Override + public void mouseReleased(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseExited(MouseEvent e) {} +} diff --git a/src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractExcelFileChooser.java b/src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractFileChooser.java similarity index 93% rename from src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractExcelFileChooser.java rename to src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractFileChooser.java index 136a062..6a484b7 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractExcelFileChooser.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseFiles/AbstractFileChooser.java @@ -24,14 +24,14 @@ /** * @author Matt - * @param the type returned by AbstractExcelFileChooser::getSelected() + * @param the type returned by AbstractFileChooser::getSelected() */ -public abstract class AbstractExcelFileChooser extends JComponent implements ActionListener, DropTargetListener { +public abstract class AbstractFileChooser extends JComponent implements ActionListener, DropTargetListener { private T selected; private final JTextArea text; private final String popupTitle; - public AbstractExcelFileChooser(String title, String popupText){ + public AbstractFileChooser(String title, String popupText){ super(); setLayout(new BorderLayout()); diff --git a/src/main/java/autocadDrawingChecker/gui/chooseFiles/ChooseFilesPage.java b/src/main/java/autocadDrawingChecker/gui/chooseFiles/ChooseFilesPage.java index 592d106..3f809c1 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseFiles/ChooseFilesPage.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseFiles/ChooseFilesPage.java @@ -1,10 +1,12 @@ package autocadDrawingChecker.gui.chooseFiles; import autocadDrawingChecker.gui.AbstractPage; +import autocadDrawingChecker.start.DrawingCheckerData; import java.awt.GridLayout; import java.io.File; import java.awt.Color; import java.awt.Dimension; +import java.util.Arrays; import javax.swing.JOptionPane; import javax.swing.JSplitPane; @@ -57,4 +59,15 @@ protected boolean checkIfReadyForNextPage() { } return ready; } + + @Override + protected void dataUpdated(DrawingCheckerData newData) { + if(newData.isInstructorFilePathSet()){ + srcChooser.setSelected(new File(newData.getInstructorFilePath())); + } + if(newData.isStudentFilePathsSet()){ + File[] fs = Arrays.stream(newData.getStudentFilePaths()).map((p)->new File(p)).toArray((s)->new File[s]); + cmpChooser.setSelected(fs); + } + } } diff --git a/src/main/java/autocadDrawingChecker/gui/chooseFiles/CompareExcelFileChooser.java b/src/main/java/autocadDrawingChecker/gui/chooseFiles/CompareExcelFileChooser.java index fc9732c..edf5344 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseFiles/CompareExcelFileChooser.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseFiles/CompareExcelFileChooser.java @@ -11,11 +11,19 @@ * * @author Matt */ -public class CompareExcelFileChooser extends AbstractExcelFileChooser { +public class CompareExcelFileChooser extends AbstractFileChooser { public CompareExcelFileChooser(String title, String popupText) { super(title, popupText); } + + private FileType getRequiredFileType(){ + FileType reqType = (Application.getInstance().getData().isDataTypeSelected()) + ? Application.getInstance().getData().getSelectedDataType().getRequiredFileType() + : FileType.ANYTHING; + return reqType; + } + @Override public boolean isFileSelected() { @@ -31,7 +39,7 @@ protected final void setSelected(File[] fs){ @Override protected void selectButtonPressed() { - FileChooserUtil.askChooseFiles(getPopupTitle(), FileType.EXCEL_OR_FOLDER, (File[] fs)->{ + FileChooserUtil.askChooseFiles(getPopupTitle(), getRequiredFileType(), (File[] fs)->{ userSelectedFile(fs); }); } @@ -50,6 +58,6 @@ protected void userSelectedFile(File[] sel) { @Override protected boolean canAccept(File f) { - return f.isDirectory() || FileType.EXCEL.fileIsOfThisType(f); + return f.isDirectory() || getRequiredFileType().fileIsOfThisType(f); } } diff --git a/src/main/java/autocadDrawingChecker/gui/chooseFiles/SourceExcelFileChooser.java b/src/main/java/autocadDrawingChecker/gui/chooseFiles/SourceExcelFileChooser.java index 3a1eb51..ae7b28a 100644 --- a/src/main/java/autocadDrawingChecker/gui/chooseFiles/SourceExcelFileChooser.java +++ b/src/main/java/autocadDrawingChecker/gui/chooseFiles/SourceExcelFileChooser.java @@ -10,10 +10,17 @@ * * @author Matt */ -public class SourceExcelFileChooser extends AbstractExcelFileChooser{ +public class SourceExcelFileChooser extends AbstractFileChooser{ public SourceExcelFileChooser(String title, String popupText) { super(title, popupText); } + + private FileType getRequiredFileType(){ + FileType reqType = (Application.getInstance().getData().isDataTypeSelected()) + ? Application.getInstance().getData().getSelectedDataType().getRequiredFileType() + : FileType.NON_FOLDER; + return reqType; + } @Override public boolean isFileSelected() { @@ -28,7 +35,8 @@ protected final void setSelected(File f){ @Override protected void selectButtonPressed() { - FileChooserUtil.askChooseFile(getPopupTitle(), FileType.EXCEL, (File f)->{ + + FileChooserUtil.askChooseFile(getPopupTitle(), getRequiredFileType(), (File f)->{ userSelectedFile(f); }); } @@ -48,6 +56,6 @@ protected void userSelectedFile(File sel) { @Override protected boolean canAccept(File f) { - return FileType.EXCEL.fileIsOfThisType(f); + return getRequiredFileType().fileIsOfThisType(f); } } diff --git a/src/main/java/autocadDrawingChecker/gui/runPage/OutputPage.java b/src/main/java/autocadDrawingChecker/gui/runPage/OutputPage.java index 9d5a0ab..4a6c508 100644 --- a/src/main/java/autocadDrawingChecker/gui/runPage/OutputPage.java +++ b/src/main/java/autocadDrawingChecker/gui/runPage/OutputPage.java @@ -4,6 +4,7 @@ import autocadDrawingChecker.gui.AbstractPage; import autocadDrawingChecker.logging.Logger; import autocadDrawingChecker.start.Application; +import autocadDrawingChecker.start.DrawingCheckerData; import java.awt.BorderLayout; import javax.swing.JButton; import javax.swing.JOptionPane; @@ -42,7 +43,7 @@ private void runAsync(){ new Thread(){ @Override public void run(){ - GradingReport report = Application.getInstance().grade(); + GradingReport report = Application.getInstance().getData().grade(); Logger.log(report.toString()); GradingReportSaver.saveReport(report, (savedTo)->{ @@ -63,4 +64,9 @@ protected boolean checkIfReadyForNextPage() { } return ready; } + + @Override + protected void dataUpdated(DrawingCheckerData newData) { + runAsync(); + } } diff --git a/src/main/java/autocadDrawingChecker/start/Application.java b/src/main/java/autocadDrawingChecker/start/Application.java index 3459ab5..fed6fbb 100644 --- a/src/main/java/autocadDrawingChecker/start/Application.java +++ b/src/main/java/autocadDrawingChecker/start/Application.java @@ -1,14 +1,6 @@ package autocadDrawingChecker.start; -import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import autocadDrawingChecker.grading.Grader; -import autocadDrawingChecker.grading.GradingReport; -import autocadDrawingChecker.gui.PageRenderer; import autocadDrawingChecker.gui.ViewController; -import autocadDrawingChecker.gui.chooseCriteria.ChooseCriteriaPage; -import autocadDrawingChecker.gui.chooseFiles.ChooseFilesPage; -import java.io.File; -import java.util.Arrays; /** * @@ -18,8 +10,6 @@ public class Application { private final DrawingCheckerData data; private ViewController window; - private AbstractGradingCriteria[] criteria; - private static Application instance; public static final String APP_NAME = "AutoCAD Drawing Checker"; @@ -28,7 +18,6 @@ private Application(){ throw new ExceptionInInitializerError("Application is supposed to be a singleton: No more than one instance!"); } data = new DrawingCheckerData(); - criteria = new AbstractGradingCriteria[0]; } public static final Application getInstance(){ @@ -38,62 +27,16 @@ public static final Application getInstance(){ return instance; } - public final Application setLoadedCriteria(AbstractGradingCriteria[] criteria){ - this.criteria = criteria; - data.clearCriteria(); - for(AbstractGradingCriteria crit : criteria){ - data.addCriteria(crit); - } - return this; - } - - public final AbstractGradingCriteria[] getGradedCriteria(){ - return criteria; - } - public final Application createGui(){ if(window != null){ window.dispose(); } window = new ViewController(); - PageRenderer pane = window.getAppPane(); - - ChooseFilesPage chooseFiles = (ChooseFilesPage)pane.getPage(PageRenderer.CHOOSE_FILES); - if(data.isInstructorFilePathSet()){ - chooseFiles.setSrcFile(new File(data.getInstructorFilePath())); - } - if(data.isStudentFilePathsSet()){ - chooseFiles.setCmpFiles(Arrays.stream(data.getStudentFilePaths()).map((path)->{ - return new File(path); - }).toArray((size)->new File[size])); - } - - ChooseCriteriaPage chooseCriteria = (ChooseCriteriaPage)pane.getPage(PageRenderer.CHOOSE_CRITERIA); - data.getGradingCriteria().forEach((criteria, isSel)->{ - chooseCriteria.setCriteriaSelected(criteria, isSel); - }); return this; } public final DrawingCheckerData getData(){ return data; - } - - public final boolean isReadyToGrade(){ - return - data.isInstructorFilePathSet() && - data.isStudentFilePathsSet() && - data.isAnyCriteriaSelected(); - } - - public final GradingReport grade(){ - Grader g = new Grader( - data.getInstructorFilePath(), - data.getStudentFilePaths(), - data.getSelectedCriteria() - ); - - return g.grade(); } } diff --git a/src/main/java/autocadDrawingChecker/start/DrawingCheckerData.java b/src/main/java/autocadDrawingChecker/start/DrawingCheckerData.java index ed99cea..6bd5a4f 100644 --- a/src/main/java/autocadDrawingChecker/start/DrawingCheckerData.java +++ b/src/main/java/autocadDrawingChecker/start/DrawingCheckerData.java @@ -1,87 +1,358 @@ package autocadDrawingChecker.start; +import autocadDrawingChecker.data.AbstractGradableDataType; +import autocadDrawingChecker.data.core.DataSet; +import autocadDrawingChecker.grading.Grader; +import autocadDrawingChecker.grading.GradingReport; import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import java.util.Arrays; +import autocadDrawingChecker.grading.criteria.CompareColumn; +import autocadDrawingChecker.logging.Logger; +import java.io.IOException; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; -import java.util.Map.Entry; -import java.util.stream.Collectors; - /** - * - * @author Matt + * DrawingCheckerData stores all the parameters used by the program + * when grading. These parameters include: + *
    + *
  • The subclasses of AbstractGradableDataType the program can grade
  • + *
  • The specific AbstractGradableDataType the user selected from the above list
  • + *
  • The complete path to the instructor file the user selected
  • + *
  • The complete file paths to the student files the user selected
  • + *
  • The AbstractGradingCriteria the program can grade on
  • + *
  • The AbstractGradingCriteria the user has chosen from the above list
  • + *
  • How accurate student values must be to instructor values to be considered a match
  • + *
+ * An instance of this class can be obtained from the singleton Application class, so you should + * work with those values if you want to change the program data. + * + * @author Matt Crow */ public class DrawingCheckerData { + private final List gradableDataTypes; + private AbstractGradableDataType selectedDataType; + private String instructorFilePath; private String[] studentFilePaths; - private final HashMap selectedCriteria; - private final HashMap nameToCriteria; - public DrawingCheckerData(){ + private final HashSet availableCriteria; + private final HashMap selectedCriteria; + + /** + * The maximum difference between + * student and instructor values + * that is considered correct. + */ + private double criteriaThreshold; + + DrawingCheckerData(){ + gradableDataTypes = new LinkedList<>(); + selectedDataType = null; instructorFilePath = null; studentFilePaths = new String[0]; + availableCriteria = new HashSet<>(); selectedCriteria = new HashMap<>(); - nameToCriteria = new HashMap<>(); + criteriaThreshold = 0.0; } - public final boolean isInstructorFilePathSet(){ - return instructorFilePath != null; + + + /* + GradableDataType methods + */ + + /** + * + * @param type the type to add to the list of data types this can grade + */ + final void addGradableDataType(AbstractGradableDataType type){ + gradableDataTypes.add(type); + if(gradableDataTypes.size() == 1){ + setSelectedDataType(type); + } } - public final boolean isStudentFilePathsSet(){ - return studentFilePaths != null && studentFilePaths.length > 0; + + /** + * + * @param type the data type this should read user files as + */ + public final void setSelectedDataType(AbstractGradableDataType type){ + selectedDataType = type; } - public final boolean isAnyCriteriaSelected(){ - return selectedCriteria.values().contains(Boolean.TRUE); + + /** + * + * @return whether or not the user has chosen + * a data type to grade. + */ + public final boolean isDataTypeSelected(){ + return selectedDataType != null; } - public final boolean isCriteriaSelected(AbstractGradingCriteria criteria){ - // has this loaded the criteria? is it toggled to true? - return selectedCriteria.containsKey(criteria.getName()) && selectedCriteria.get(criteria.getName()); + + /** + * + * @return the list of all data types this can grade + */ + public final List getGradableDataTypes(){ + return gradableDataTypes; + } + + /** + * + * @return the data type the user has selected to grade + */ + public final AbstractGradableDataType getSelectedDataType(){ + return selectedDataType; + } + + + + /* + Instructor file methods + */ + + /** + * + * @param path the complete file path to the instructor file to grade on. + * @return this, for chaining purposes + */ + public final DrawingCheckerData setInstructorFilePath(String path){ + instructorFilePath = path; + return this; + } + + /** + * + * @return whether or not the user has selected + * an instructor file. + */ + public final boolean isInstructorFilePathSet(){ + return instructorFilePath != null; } + /** + * + * @return the complete file path to the instructor + * file this will grade based on. + */ public final String getInstructorFilePath(){ return instructorFilePath; } + + /** + * + * @return the contents of the selected + * instructor file + */ + private DataSet parseInstructorFile(){ + DataSet ret = null; + if(isInstructorFilePathSet() && isDataTypeSelected()){ + try { + ret = this.selectedDataType.createParser().parseFirstSheet(instructorFilePath); + } catch (IOException ex) { + Logger.logError(ex); + } + } else { + Logger.logError("Instructor file path or data type is not selected, so I can't parse the instructor file"); + } + return ret; + } + + + + + /* + Student file methods + */ + + /** + * + * @param paths a series of complete file paths to either student files + * or folders containing them. + * + * @return this, for chaining purposes + */ + public final DrawingCheckerData setStudentFilePaths(String... paths){ + studentFilePaths = paths; + return this; + } + + /** + * + * @return whether or not at least one student file or folder is selected. + */ + public final boolean isStudentFilePathsSet(){ + return studentFilePaths != null && studentFilePaths.length > 0; + } + + /** + * + * @return an array of complete file paths to + * files this should grade. + */ public final String[] getStudentFilePaths(){ return studentFilePaths; } - public final void clearCriteria(){ - selectedCriteria.clear(); - nameToCriteria.clear(); + + + /* + Grading criteria methods + */ + + /** + * Adds a new criteria to the list of criteria this can grade on. + * + * @param crit the criteria to add + */ + final void addCriteria(AbstractGradingCriteria crit){ + selectedCriteria.put(crit, Boolean.TRUE); + availableCriteria.add(crit); + } + + /** + * Sets whether or not to grade based on the given criteria. + * + * @param criteria the criteria to set + * @param isSelected whether or not the given criteria should be used when grading student files + * @return this, for chaining purposes + */ + public final DrawingCheckerData setCriteriaSelected(AbstractGradingCriteria criteria, boolean isSelected){ + selectedCriteria.put(criteria, isSelected); + return this; + } + + /** + * + * @return whether or not at least one grading criteria is selected + */ + public final boolean isAnyCriteriaSelected(){ + return selectedCriteria.values().contains(Boolean.TRUE); } - public final void addCriteria(AbstractGradingCriteria crit){ - selectedCriteria.put(crit.getName(), Boolean.TRUE); - nameToCriteria.put(crit.getName(), crit); + /** + * + * @param criteria the criteria to check if it is selected + * @return whether or not the given criteria is selected + */ + public final boolean isCriteriaSelected(AbstractGradingCriteria criteria){ + // has this loaded the criteria? is it toggled to true? + return selectedCriteria.containsKey(criteria) && selectedCriteria.get(criteria); } - public final List getSelectedCriteria(){ - return selectedCriteria.entrySet().stream().filter((Entry nameToIsSelected)->{ - return nameToIsSelected.getValue(); // the "isSelected" part of the entry - }).map((Entry nameToIsSelected)->{ - return nameToCriteria.get(nameToIsSelected.getKey()); - }).filter((AbstractGradingCriteria criteria)->{ - return criteria != null; - }).collect(Collectors.toList()); + /** + * If their is not instructor file selected, or the data type is not set, + * returns all criteria this can grade on. + * @return the list of criteria this has which can grade the current data type + */ + private List getGradableCriteria(){ + LinkedList crit = new LinkedList<>(); + DataSet instructorFile = parseInstructorFile(); + this.availableCriteria.forEach((availableCrit)->{ + // maybe just add a check for AbstractGradingCriteria.canGradeDataType(...) + if(instructorFile == null || availableCrit.tryCastDataSet(instructorFile) != null){ + crit.add(availableCrit); + } + }); + if(instructorFile != null){ + instructorFile.getColumns().forEach((String colName)->{ + CompareColumn compCol = new CompareColumn(colName); + crit.add(compCol); + this.setCriteriaSelected(compCol, true); + }); + } + return crit; } - public final HashMap getGradingCriteria(){ + + /** + * + * @return a mapping of criteria to whether or not they are selected and gradable for the currently selected + * instructor file + */ + public final HashMap getGradableCriteriaToIsSelected(){ HashMap critToIsSel = new HashMap<>(); - selectedCriteria.entrySet().forEach((entry)->{ - critToIsSel.put(nameToCriteria.get(entry.getKey()), entry.getValue()); + // filters based on if the current set is gradable by the criteria + getGradableCriteria().forEach((crit)->{ + critToIsSel.put(crit, this.isCriteriaSelected(crit)); }); return critToIsSel; } - public final DrawingCheckerData setInstructorFilePath(String path){ - instructorFilePath = path; - return this; + /** + * This method determines which criteria the user has chosen to grade on, and the + * instructor file provided is gradable on. + * + * @return the criteria this can and should grade based on + */ + private HashSet getCriteriaToGradeOn(){ + HashSet criteriaToGradeOn = new HashSet<>(); + getGradableCriteriaToIsSelected().entrySet().stream().filter((entry)->{ + return entry.getValue(); + }).map((entry)->entry.getKey()).forEach(criteriaToGradeOn::add); + return criteriaToGradeOn; } - public final DrawingCheckerData setStudentFilePaths(String... paths){ - studentFilePaths = paths; - return this; + + + + /* + Criteria threshold methods + */ + + /** + * Sets the maximum difference between student and instructor + * files which is considered a match. + * + * @param newValue the new maximum difference. Must be non-negative. + */ + public final void setCriteriaThreshold(double newValue){ + if(newValue < 0){ + throw new IllegalArgumentException("criteria threshold must be non-negative"); + } + this.criteriaThreshold = newValue; } - public final DrawingCheckerData setCriteriaSelected(AbstractGradingCriteria criteria, boolean isSelected){ - selectedCriteria.put(criteria.getName(), isSelected); - return this; + + /** + * + * @return the maximum difference between + * numerical values in a student export and + * instructor export which the program should + * consider a match. + */ + public final double getCriteriaThreshold(){ + return this.criteriaThreshold; + } + + + + /** + * + * @return whether or not this is ready + * to grade the students' files. + */ + public final boolean isReadyToGrade(){ + return + isDataTypeSelected() && + isInstructorFilePathSet() && + isStudentFilePathsSet(); + } + + /** + * Grades the student files the user has selected, + * comparing them to the instructor file the user selected, + * grading each criteria the user has selected, + * and returning a comprehensive grading report. + * + * @return a report of all the students' grades. + */ + public final GradingReport grade(){ + Grader g = new Grader( + getSelectedDataType(), + getInstructorFilePath(), + getStudentFilePaths(), + getCriteriaToGradeOn() + ); + + return g.grade(); } } diff --git a/src/main/java/autocadDrawingChecker/start/DrawingCheckerProperties.java b/src/main/java/autocadDrawingChecker/start/DrawingCheckerProperties.java deleted file mode 100644 index 083982f..0000000 --- a/src/main/java/autocadDrawingChecker/start/DrawingCheckerProperties.java +++ /dev/null @@ -1,98 +0,0 @@ -package autocadDrawingChecker.start; - -import autocadDrawingChecker.grading.criteria.AbstractGradingCriteria; -import java.util.List; -import java.util.Properties; - -/** - * Currently unused, as I'm not sure how close this - * project is to being done. - * - * DrawingCheckerProperties is used to store application data. - * This will allow the program to load data in a variety of ways: - *
    - *
  • Through the GUI
  • - *
  • From a property file (not implemented yet)
  • - *
  • From the command line (not implemented yet)
  • - *
- * - * Not sure if I need to save this as a Properties subclass. - * - * @author Matt Crow - */ -public class DrawingCheckerProperties extends Properties { - private static final String SRC_KEY = "srcPath"; - private static final String CMP_KEY = "cmpPath"; - private static final String CRIT_KEY = "critKey"; - - public static final String DEFAULT_SRC = "SRC_PATH_NOT_SET"; - public static final String DEFAULT_CMP = "CMP_PATH_NOT_SET"; - public static final String DEFAULT_CRITERIA = "CRIT_NOT_SET"; - - - public DrawingCheckerProperties(){ - super(); - setProperty(SRC_KEY, DEFAULT_SRC); - setProperty(CMP_KEY, DEFAULT_CMP); - setProperty(CRIT_KEY, DEFAULT_CRITERIA); - } - - /** - * - * @return whether or not an instructor - * file has been selected. - */ - public final boolean isSrcSet(){ - return !DEFAULT_SRC.equals(getProperty(SRC_KEY)); - } - - /** - * - * @return whether or not student - * files to grade have been selected. - */ - public final boolean isCmpSet(){ - return !DEFAULT_CMP.equals(getProperty(CMP_KEY)); - } - - /** - * - * @return whether or not any grading - * criteria have been selected. - */ - public final boolean isCritSet(){ - return !DEFAULT_CRITERIA.equals(getProperty(CRIT_KEY)); - } - - public final String getSrcPath(){ - return (isSrcSet()) ? getProperty(SRC_KEY) : null; - } - - public final String[] getCmpPaths(){ - return (isCmpSet()) ? getProperty(CMP_KEY).split(",") : null; - } - - public final List getCriteria(){ - List ret = null; - if(isCritSet()){ - String[] critNames = getProperty(CRIT_KEY).split(","); - throw new UnsupportedOperationException("Can I store a list of strings as properties?"); - } - return ret; - } - - public final DrawingCheckerProperties setSrcPath(String path){ - setProperty(SRC_KEY, path); - return this; - } - - public final DrawingCheckerProperties setCmpPaths(String[] paths){ - setProperty(CMP_KEY, String.join(",", paths)); - return this; - } - - public final DrawingCheckerProperties setCriteria(String[] critNames){ - throw new UnsupportedOperationException(); - //return this; - } -} diff --git a/src/main/java/autocadDrawingChecker/start/Main.java b/src/main/java/autocadDrawingChecker/start/Main.java index d7c7362..303bf2d 100644 --- a/src/main/java/autocadDrawingChecker/start/Main.java +++ b/src/main/java/autocadDrawingChecker/start/Main.java @@ -1,6 +1,10 @@ package autocadDrawingChecker.start; +import autocadDrawingChecker.data.GradableDataTypeLoader; +import autocadDrawingChecker.data.csv.CsvParser; +import autocadDrawingChecker.data.excel.surveyData.SurveyDataParser; import autocadDrawingChecker.grading.criteria.GradingCriteriaLoader; +import java.io.IOException; import java.util.Arrays; import java.util.HashSet; @@ -28,9 +32,9 @@ private Main(){ */ public static void main(String[] args) { Main main = new Main(); - main.app.setLoadedCriteria(new GradingCriteriaLoader().getAll()); - - + DrawingCheckerData data = main.app.getData(); + new GradingCriteriaLoader().getAll().forEach(data::addCriteria); + new GradableDataTypeLoader().getAll().forEach(data::addGradableDataType); System.out.println("Args are " + Arrays.toString(args)); @@ -39,13 +43,29 @@ public static void main(String[] args) { argSet.add(arg.toLowerCase()); } boolean debug = argSet.contains("--debug"); + boolean noGui = argSet.contains("--no-gui"); if(debug){ - main.app.getData() - .setInstructorFilePath("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\sample\\Check Sample - Master File.xls.xlsx") - .setStudentFilePaths("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\sample"); - System.out.println(main.app.grade().toString()); - } else { + try { + new SurveyDataParser().parseAllSheets("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\Survey Data\\template\\GPS Style - Survey Field Notes - Template.xlsx").forEach(System.out::println); + new CsvParser(true).parseFirstSheet("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\Csv Data\\nodeCoords.csv").forEach(System.out::println); + } catch (IOException ex) { + ex.printStackTrace(); + } + /* + data + .setInstructorFilePath("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\Survey Data\\civil67\\67Civi-Student 1 - GPS Style Survey Simulation - Survey Field Notes.xlsx") + .setStudentFilePaths("C:\\Users\\Matt\\Desktop\\AutoCAD Drawing Checker\\sample files to work with\\Survey Data\\civil67"); + for(double d = 0.0; d < 1000.0; d += 100.0){ + data.setCriteriaThreshold(d); + System.out.printf("\nWhen threshold is %f...\n", d); + data.grade().forEach((gradedExport)->{ + System.out.printf("%5.3f ", gradedExport.getFinalGrade()); + }); + }*/ + } + + if(!noGui){ main.app.createGui(); } } diff --git a/src/main/java/autocadDrawingChecker/util/AbstractLoader.java b/src/main/java/autocadDrawingChecker/util/AbstractLoader.java index d143451..95e0aac 100644 --- a/src/main/java/autocadDrawingChecker/util/AbstractLoader.java +++ b/src/main/java/autocadDrawingChecker/util/AbstractLoader.java @@ -24,5 +24,5 @@ public abstract class AbstractLoader { * * @return a list of objects of T. */ - public abstract T[] getAll(); + public abstract List getAll(); } diff --git a/src/main/java/autocadDrawingChecker/util/FileType.java b/src/main/java/autocadDrawingChecker/util/FileType.java index 9c3fb57..3f2bad5 100644 --- a/src/main/java/autocadDrawingChecker/util/FileType.java +++ b/src/main/java/autocadDrawingChecker/util/FileType.java @@ -7,7 +7,10 @@ */ public enum FileType { EXCEL("Excel Workbook", new String[]{"xlsx", "xls"}), - EXCEL_OR_FOLDER("Excel Workbook or a folder", new String[]{"xlsx", "xls"}); + CSV("Comma Separated Values", new String[]{"csv"}), + NON_FOLDER("Excel or CSV", new String[]{"xlsx", "xls", "csv"}), + EXCEL_OR_FOLDER("Excel Workbook or a folder", new String[]{"xlsx", "xls"}), + ANYTHING("Excel Workbook, CSV file, or a folder", new String[]{"xlsx", "xls", "csv"}); private final String name; private final String[] extensions; diff --git a/src/main/resources/exports/GPS Data Example.xlsx b/src/main/resources/exports/GPS Data Example.xlsx new file mode 100644 index 0000000..589fff6 Binary files /dev/null and b/src/main/resources/exports/GPS Data Example.xlsx differ