diff --git a/.gitignore b/.gitignore index db0b6ea..4f1662f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ out/ .gradle/ -.idea/ \ No newline at end of file +build/ +run/ +.idea/libraries/ +.idea/modules/ +.idea/.name +.idea/*.xml \ No newline at end of file diff --git a/.idea/runConfigurations/AttendanceManager.xml b/.idea/runConfigurations/AttendanceManager.xml new file mode 100644 index 0000000..1773df4 --- /dev/null +++ b/.idea/runConfigurations/AttendanceManager.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 193db76..a88540e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,23 +1,43 @@ -group 'com.westisliprobotics' -version '1.0-SNAPSHOT' +allprojects { + group 'com.westisliprobotics' -apply plugin: 'java' + repositories { + mavenCentral() + maven { + url "$rootDir/libs" + } + } -sourceCompatibility = 1.8 - -repositories { - mavenCentral() + apply plugin: 'idea' + apply plugin: 'java' } dependencies { - compile 'org.apache.poi:poi:4.0.0', + compile 'com.intellij:annotations:12.0', + 'org.apache.poi:poi:4.0.0', 'org.apache.poi:poi-ooxml:4.0.0', - 'info.picocli:picocli:3.7.0', 'org.slf4j:slf4j-api:1.7.25', 'ch.qos.logback:logback-classic:1.2.3', - 'org.apache.commons:commons-math3:3.6.1', - 'org.json:json:20180813', 'com.google.zxing:core:3.3.3', - 'com.google.zxing:javase:3.3.3' - testCompile group: 'junit', name: 'junit', version: '4.12' + 'com.google.zxing:javase:3.3.3', + 'org.javapos:javapos:1.14.0', + 'net.sourceforge.barbecue:barbecue:1.5-beta1', + 'xerces:xercesImpl:2.8.0', + 'org.apache.commons:commons-math3:3.6.1', + 'jpos:jposService:1.0', + 'jpos:jposServiceIni:1.0', + 'jpos:jposServiceScale:1.0', + 'jpos:jposServiceScanner:1.0' } + +jar { + from(configurations.compile.collect{it.isDirectory() ? it : zipTree(it)}){ + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + + manifest { + attributes('Main-Class': 'com.team871.ui.AttendanceManager') + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 99bba94..7a47d64 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 063a6d5..f28f5ee 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 25 23:43:10 EDT 2018 +#Mon Oct 14 22:30:05 EDT 2019 +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/libs/jpos/jposService/1.0/jposService-1.0.jar b/libs/jpos/jposService/1.0/jposService-1.0.jar new file mode 100644 index 0000000..72f5202 Binary files /dev/null and b/libs/jpos/jposService/1.0/jposService-1.0.jar differ diff --git a/libs/jpos/jposServiceScale/1.0/jposServiceScale-1.0.jar b/libs/jpos/jposServiceScale/1.0/jposServiceScale-1.0.jar new file mode 100644 index 0000000..469ab80 Binary files /dev/null and b/libs/jpos/jposServiceScale/1.0/jposServiceScale-1.0.jar differ diff --git a/libs/jpos/jposServiceScanner/1.0/jposServiceScanner-1.0.jar b/libs/jpos/jposServiceScanner/1.0/jposServiceScanner-1.0.jar new file mode 100644 index 0000000..1320511 Binary files /dev/null and b/libs/jpos/jposServiceScanner/1.0/jposServiceScanner-1.0.jar differ diff --git a/libs/jpos/jposServiceini/1.0/jposServiceIni-1.0.jar b/libs/jpos/jposServiceini/1.0/jposServiceIni-1.0.jar new file mode 100644 index 0000000..76c9a3e Binary files /dev/null and b/libs/jpos/jposServiceini/1.0/jposServiceIni-1.0.jar differ diff --git a/libs/native/CSJPOSScale.dll b/libs/native/CSJPOSScale.dll new file mode 100644 index 0000000..ad17d84 Binary files /dev/null and b/libs/native/CSJPOSScale.dll differ diff --git a/libs/native/CSJPOSScale64.dll b/libs/native/CSJPOSScale64.dll new file mode 100644 index 0000000..2912694 Binary files /dev/null and b/libs/native/CSJPOSScale64.dll differ diff --git a/libs/native/CSJPOSScanner.dll b/libs/native/CSJPOSScanner.dll new file mode 100644 index 0000000..b1be2f6 Binary files /dev/null and b/libs/native/CSJPOSScanner.dll differ diff --git a/libs/native/CSJPOSScanner64.dll b/libs/native/CSJPOSScanner64.dll new file mode 100644 index 0000000..2dfa40f Binary files /dev/null and b/libs/native/CSJPOSScanner64.dll differ diff --git a/settings.gradle b/settings.gradle index 5cac421..cec7e3e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1 @@ -rootProject.name = 'excelparser' - +rootProject.name = 'AttendenceManager' diff --git a/src/main/java/BarcodeReader.java b/src/main/java/BarcodeReader.java deleted file mode 100644 index 2bbd9c6..0000000 --- a/src/main/java/BarcodeReader.java +++ /dev/null @@ -1,29 +0,0 @@ -import com.google.zxing.BinaryBitmap; -import com.google.zxing.FormatException; -import com.google.zxing.NotFoundException; -import com.google.zxing.Result; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.common.HybridBinarizer; -import com.google.zxing.oned.MultiFormatOneDReader; - -import javax.imageio.ImageIO; -import java.io.File; -import java.io.IOException; - -public class BarcodeReader { - private static MultiFormatOneDReader reader; - - public static void main(String[] args) { - reader = new MultiFormatOneDReader(null); - try { - Result r = reader.decode(new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(ImageIO.read(new File(args[0])))))); - System.out.println(r.toString()); - } catch (NotFoundException e) { - e.printStackTrace(); - } catch (FormatException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - } -} diff --git a/src/main/java/Excel2Json.java b/src/main/java/Excel2Json.java deleted file mode 100644 index c90f359..0000000 --- a/src/main/java/Excel2Json.java +++ /dev/null @@ -1,166 +0,0 @@ -import org.apache.poi.ss.usermodel.*; -import org.json.JSONWriter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import picocli.CommandLine; -import picocli.CommandLine.Option; -import picocli.CommandLine.Parameters; - -import java.io.*; -import java.util.*; -import java.util.concurrent.Callable; - -import static picocli.CommandLine.Command; - -@Command(description="Convert an excel worksheet into a set of JSON objects", name="x2j", mixinStandardHelpOptions = true, version = "0.1") -public class Excel2Json implements Callable { - @Parameters(index = "0", description = "The excel file to load") - private File file; - - @Option(names = {"-w", "--worksheet"}, description = "The worksheet to convert") - private String worksheet; - - @Option(names = {"-o", "--output"}, required = true, description = "The output file") - private File outputFile; - - @Option(names = {"-c", "--columns"}, split = ",") - private Set columns; - - @Option(names = {"-e", "--exclude"}, description = "Exclude the columns denoted by -c", defaultValue = "false") - private boolean isExclude = false; - - @Option(names = {"-r", "--headerRow"}, description = "The row index to expect a header row", defaultValue = "-1") - private int headerRowIndex; - - private final Logger log = LoggerFactory.getLogger("Excel2Json"); - - private Workbook workbook; - private FormulaEvaluator eval; - private DataFormatter formatter; - private JSONWriter jsonWriter; - - public static void main(String[] args) { - CommandLine.call(new Excel2Json(), args); - } - - @Override - public Void call() throws Exception { - log.info("Excel2Json Starting"); - - if(!file.exists()) { - log.error("File "+file.getName()+" does not exist!"); - return null; - } - - if(file.isDirectory()) { - log.error(file.getName()+" is a directory!"); - return null; - } - - if(columns != null) { - if(headerRowIndex < 0) { - log.error("A header row must be specified when using --columns"); - return null; - } - - if(worksheet == null || worksheet.isEmpty()) { - log.error("A worksheet must be specified when using --columns"); - return null; - } - } - - if(outputFile.isDirectory()) { - log.error("The output file " + outputFile + " is a directory"); - return null; - } - - try(final FileWriter writer = new FileWriter(outputFile)) { - jsonWriter = new JSONWriter(writer); - log.info("Loading workbook "+file.getName()); - try(FileInputStream fis = new FileInputStream(file)) { - workbook = WorkbookFactory.create(fis); - eval = workbook.getCreationHelper().createFormulaEvaluator(); - formatter = new DataFormatter(); - } - - if(worksheet != null && !worksheet.isEmpty()) { - final Sheet s = workbook.getSheet(worksheet); - if(s == null) { - log.error("Workbook " + file.getName() + " has no worksheet " + worksheet); - return null; - } - - log.info("Processing worksheet " + worksheet); - convertToJson(s); - } else { - for(int i = 0; i < workbook.getNumberOfSheets(); i++) { - log.info("Processing worksheet "+workbook.getSheetName(i)); - convertToJson(workbook.getSheetAt(i)); - } - } - } - - log.info("Done!"); - return null; - } - - private void convertToJson(Sheet s) { - final int rowCnt = s.getPhysicalNumberOfRows(); - - final Map columnMap; - if(headerRowIndex < 0) { - columnMap = Collections.emptyMap(); - } else { - columnMap = new HashMap<>(); - final Row headerRow = s.getRow(headerRowIndex); - for(int i = headerRow.getFirstCellNum(); i <= headerRow.getLastCellNum(); i++) { - final String headerVal = formatCell(headerRow.getCell(i)); - if(headerVal != null && !headerVal.isEmpty()) { - columnMap.put(i, headerVal); - } - } - } - - jsonWriter.array(); - for(int i = headerRowIndex < 0 ? 0 : headerRowIndex+1; i columnMap) { - final StringBuilder sb = new StringBuilder(); - jsonWriter.object(); - for(int i = row.getFirstCellNum(); i >= 0 && i <= row.getLastCellNum(); i++) { - final Cell cell = row.getCell(i); - - if(shouldInclude(i, columnMap)) { - final String cellValueString = formatCell(cell); - sb.append(sb.length() > 0 ? ", " : "").append(cellValueString); - jsonWriter.key(columnMap.get(i)); - jsonWriter.value(cellValueString); - } - } - jsonWriter.endObject(); - - log.info(sb.toString()); - } - - private String formatCell(Cell c) { - return formatter.formatCellValue(c, eval); - } - - private boolean shouldInclude(int cellIndex, Map columnMap) { - final String columnName = columnMap.get(cellIndex); - if(columnName == null || columnName.isEmpty()) { - return false; - } - - final boolean isRequested = columns.contains(columnName); - return isExclude != isRequested; - } -} diff --git a/src/main/java/com/team871/data/AttendanceItem.java b/src/main/java/com/team871/data/AttendanceItem.java new file mode 100644 index 0000000..f40aeda --- /dev/null +++ b/src/main/java/com/team871/data/AttendanceItem.java @@ -0,0 +1,35 @@ +package com.team871.data; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; + +public class AttendanceItem { + private LocalDateTime inTime = null; + private LocalDateTime outTime = null; + + public AttendanceItem() { + inTime = LocalDateTime.now(); + } + + public AttendanceItem(LocalDate date) { + inTime = date.atTime(LocalTime.now()); + } + + public AttendanceItem(LocalDate date, LocalTime intime, LocalTime outTime) { + this.inTime = intime.atDate(date); + this.outTime = outTime.atDate(date); + } + + public LocalDateTime getInTime() { + return inTime; + } + + public LocalDateTime getOutTime() { + return outTime; + } + + public void signOut() { + outTime = LocalDateTime.now(); + } +} diff --git a/src/main/java/com/team871/data/FirstRegistration.java b/src/main/java/com/team871/data/FirstRegistration.java new file mode 100644 index 0000000..d297009 --- /dev/null +++ b/src/main/java/com/team871/data/FirstRegistration.java @@ -0,0 +1,27 @@ +package com.team871.data; + +public enum FirstRegistration { + None("Not Signed"), + MissingWaiver("Missing Waiver"), + Complete("Done"); + + final String key; + + FirstRegistration(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public static FirstRegistration getByKey(String key) { + for(FirstRegistration r : values()) { + if(r.key.equals(key)) { + return r; + } + } + + throw new IllegalArgumentException("No such key " + key); + } +} diff --git a/src/main/java/com/team871/data/Member.java b/src/main/java/com/team871/data/Member.java new file mode 100644 index 0000000..666aa46 --- /dev/null +++ b/src/main/java/com/team871/data/Member.java @@ -0,0 +1,239 @@ +package com.team871.data; + +import com.team871.util.Settings; +import com.team871.util.ThrowingRunnable; +import com.team871.util.Utils; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Member implements Comparable { + private static final Logger log = LoggerFactory.getLogger("Member"); + + private String firstName; + private String lastName; + private final Map attendance = new HashMap<>(); + private final List listeners = new ArrayList<>(); + + private String id = null; + private int grade = -1; + private Subteam subteam = null; + private SafeteyFormState safeteyFormState = SafeteyFormState.None; + private FirstRegistration registration = FirstRegistration.None; + + private int rosterRow; + private int attendanceRow; + + private final SheetConfig rosterSheet; + private final SheetConfig attendanceSheet; + + public interface Listener { + void onLogin(Member member); + void onLogout(Member member); + void onNameChanged(Member member, String oldLastName, String oldFirstName); + void onIdChanged(Member member, String oldSid); + } + + public Member(int row, SheetConfig roster, SheetConfig attendanceSheet) { + this.rosterRow = row; + this.lastName = roster.getValue(row, Utils.LAST_NAME_COL); + this.firstName = roster.getValue(row, Utils.FIRST_NAME_COL); + + this.id = roster.getValue(row, "SID"); + + Integer val = roster.getIntValue(row, "Grade"); + this.grade = val == null ? -1 : val; + + checkAndTry(roster.getValue(row, "Safety"), v -> safeteyFormState = SafeteyFormState.valueOf(v)); + checkAndTry(roster.getValue(row, "First Reg."), v -> registration = FirstRegistration.getByKey(v)); + checkAndTry(roster.getValue(row, "Team"), v -> subteam = Subteam.valueOf(v)); + rosterSheet = roster; + this.attendanceSheet = attendanceSheet; + } + + public Member(String firstName, String lastName, SheetConfig rosterSheet, SheetConfig attendanceSheet) { + this.firstName = firstName; + this.lastName = lastName; + + // Do something smart + this.rosterSheet = rosterSheet; + this.attendanceSheet = attendanceSheet; + + rosterRow = rosterSheet.addRow(); + attendanceRow = attendanceSheet.addRow(); + + rosterSheet.setCell(rosterRow, Utils.LAST_NAME_COL, true, lastName); + rosterSheet.setCell(rosterRow, Utils.FIRST_NAME_COL, true, firstName); + rosterSheet.setCell(rosterRow, Utils.SAFETY_COL, true, SafeteyFormState.None.name()); + rosterSheet.setCell(rosterRow, Utils.FIRST_REG_COL, true, FirstRegistration.None.getKey()); + + attendanceSheet.setCell(rosterRow, Utils.LAST_NAME_COL, true, lastName); + attendanceSheet.setCell(rosterRow, Utils.FIRST_NAME_COL, true, firstName); + } + + public void addListener(Listener l) { + listeners.add(l); + } + + public void removeListener(Listener l) { + listeners.remove(l); + } + + public SafeteyFormState getSafeteyFormState() { + return safeteyFormState; + } + + public FirstRegistration getRegistration() { + return registration; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public void processAttendance(int row) { + attendanceRow = row; + final int firstDataColumn = Settings.getInstance().getAttendanceFirstDataColumn(); + + // This is actually pretty terrible. + for(int i = firstDataColumn; i < attendanceSheet.getColumnCount(); i++) { + final String dateString = attendanceSheet.getHeaderValue(i); + if(Settings.isNullOrEmpty(dateString)) { + continue; + } + if(Utils.TOTAL_COL.equals(dateString)) { + break; + } + + final String[] dateParts = dateString.split("/"); + if(dateParts.length < 2 ) { + continue; + } + final LocalDate date = LocalDate.of(LocalDate.now().getYear(), + Integer.parseInt(dateParts[0]), + Integer.parseInt(dateParts[1])); + + String cellValue = attendanceSheet.getValue(row, dateString); + if(Settings.isNullOrEmpty(cellValue)) { + continue; + } + + attendance.put(date, new AttendanceItem(date)); + } + } + + private void checkAndTry(String value, ThrowingRunnable action) { + if(!Settings.isNullOrEmpty(value)) { + try { + action.run(value); + } catch (Exception ignored) {} + } + } + + @Override + public int compareTo(@NotNull Member o) { + int result = lastName.compareTo(o.lastName); + if(result == 0) { + result = firstName.compareTo(o.firstName); + } + + return result; + } + + public String getId() { + return id; + } + + public boolean isSignedIn(LocalDate date) { + return attendance.get(date) != null; + } + + public boolean isSignedOut(LocalDate date) { + final AttendanceItem item = attendance.get(date); + if(item == null) { + return false; + } + + return item.getOutTime() != null; + } + + public void signIn(LocalDate date) { + final AttendanceItem item = attendance.computeIfAbsent(date, d -> new AttendanceItem(date)); + + updateAttendanceCell(date, item); + listeners.forEach(l -> l.onLogin(this)); + } + + public void signOut(LocalDate date) { + final AttendanceItem item = attendance.get(date); + if(item == null) { + return; + } + + item.signOut(); + updateAttendanceCell(date, item); + listeners.forEach(l -> l.onLogout(this)); + } + + public void setId(String sid) { + final String oldId = id; + this.id = sid; + rosterSheet.setCell(rosterRow, "SID", true, sid); + + listeners.forEach(l -> l.onIdChanged(this, oldId)); + } + + public void setName(String first, String last) { + final String oldLast = lastName; + final String oldFirst = firstName; + + this.firstName = first; + this.lastName = last; + + rosterSheet.setCell(rosterRow, Utils.LAST_NAME_COL, true, last); + rosterSheet.setCell(rosterRow, Utils.FIRST_NAME_COL, true, first); + attendanceSheet.setCell(rosterRow, Utils.LAST_NAME_COL, true, last); + attendanceSheet.setCell(rosterRow, Utils.FIRST_NAME_COL, true, first); + listeners.forEach(l -> l.onNameChanged(this, oldLast, oldFirst)); + } + + public LocalTime getSignInTime(LocalDate date) { + final AttendanceItem item = attendance.get(date); + if(item == null) { + return null; + } + + return item.getInTime().toLocalTime(); + } + + public LocalTime getSignOutTime(LocalDate date) { + final AttendanceItem item = attendance.get(date); + if(item == null) { + return null; + } + + return item.getOutTime().toLocalTime(); + } + + private void updateAttendanceCell(LocalDate date, AttendanceItem item) { + final String columnName = Utils.DATE_FORMATTER.format(date); + if(!attendanceSheet.columnExists(columnName)) { + attendanceSheet.addColumn(columnName); + } + + attendanceSheet.setCell(attendanceRow, columnName, true, "(" + + Utils.TIME_FORMATTER.format(item.getInTime()) + "," + + (item.getOutTime() == null ? "" : Utils.TIME_FORMATTER.format(item.getOutTime())) + ")"); + } +} diff --git a/src/main/java/com/team871/data/SafeteyFormState.java b/src/main/java/com/team871/data/SafeteyFormState.java new file mode 100644 index 0000000..bbff214 --- /dev/null +++ b/src/main/java/com/team871/data/SafeteyFormState.java @@ -0,0 +1,8 @@ +package com.team871.data; + +public enum SafeteyFormState { + None, + Printed, + Given, + Signed +} diff --git a/src/main/java/com/team871/data/SheetConfig.java b/src/main/java/com/team871/data/SheetConfig.java new file mode 100644 index 0000000..626789c --- /dev/null +++ b/src/main/java/com/team871/data/SheetConfig.java @@ -0,0 +1,107 @@ +package com.team871.data; + +import org.apache.poi.ss.usermodel.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class SheetConfig { + private static final DataFormatter FORMATTER = new DataFormatter(); + + private final Sheet sheet; + private final int headerRow; + private int columnCount; + private final Map columnMap; + + public SheetConfig(Sheet sheet, int headerRow) { + this.sheet = sheet; + this.headerRow = headerRow; + columnMap = new LinkedHashMap<>(); + + final Row header = sheet.getRow(headerRow); + columnCount = header.getLastCellNum(); + for(int i = 0; i < header.getLastCellNum(); i++) { + final Cell cell = header.getCell(i); + if(cell == null || cell.getCellType() == CellType.FORMULA) { + continue; + } + + columnMap.put(FORMATTER.formatCellValue(cell), i); + } + } + + public String getValue(int row, String column) { + final Integer cellIndex = columnMap.get(column); + if(cellIndex == null) { + return null; + } + + return FORMATTER.formatCellValue(sheet.getRow(row + headerRow + 1).getCell(cellIndex)); + } + + public Integer getIntValue(int row, String column) { + final Integer cellIndex = columnMap.get(column); + if(cellIndex == null) { + return null; + } + + return (int)sheet.getRow(row + headerRow + 1).getCell(cellIndex).getNumericCellValue(); + } + + public String getHeaderValue(int cell) { + return sheet.getRow(headerRow).getCell(cell).getStringCellValue(); + } + + public int getDataRowCount() { + return sheet.getLastRowNum() - headerRow; + } + + public int getColumnCount() { + return columnCount; + } + + public boolean rowExists(int row) { + return sheet.getRow(row + headerRow + 1) != null; + } + + public Row getRow(int row) { + return sheet.getRow(row + headerRow + 1); + } + + public int addRow() { + return sheet.createRow(sheet.getLastRowNum() + 1).getRowNum() - headerRow - 1; + } + + public void setCell(int rowNum, String column, boolean create, String value) { + final Integer cellIndex = columnMap.get(column); + if(cellIndex == null ) { + throw new IllegalArgumentException("Column " + column + " does not exist"); + } + + final Row row = sheet.getRow(rowNum + headerRow + 1); + if(row == null) { + throw new IllegalArgumentException("Row " + rowNum + " does not exist"); + } + + Cell cell = row.getCell(cellIndex); + if(cell == null) { + if(create) { + cell = row.createCell(cellIndex); + } else { + throw new IllegalArgumentException(("Cell does not exist and create not set")); + } + } + + cell.setCellValue(value); + } + + public boolean columnExists(String columnName) { + return columnMap.get(columnName) != null; + } + + public void addColumn(String columnName) { + columnMap.put(columnName, columnCount); + columnCount++; + setCell(-1, columnName, true, columnName); + } +} diff --git a/src/main/java/com/team871/data/Subteam.java b/src/main/java/com/team871/data/Subteam.java new file mode 100644 index 0000000..1c3ca6b --- /dev/null +++ b/src/main/java/com/team871/data/Subteam.java @@ -0,0 +1,10 @@ +package com.team871.data; + +public enum Subteam { + Software, + Mechanical, + Electrical, + CAD, + Business, + Undecided +} diff --git a/src/main/java/com/team871/exception/RobotechException.java b/src/main/java/com/team871/exception/RobotechException.java new file mode 100644 index 0000000..941dd0a --- /dev/null +++ b/src/main/java/com/team871/exception/RobotechException.java @@ -0,0 +1,14 @@ +package com.team871.exception; + +/** + * The root of all Exceptions generated by US! + */ +public class RobotechException extends Exception { + public RobotechException(String message) { + super(message); + } + + public RobotechException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/team871/provider/BufferedImageProvider.java b/src/main/java/com/team871/provider/BufferedImageProvider.java new file mode 100644 index 0000000..e62e84d --- /dev/null +++ b/src/main/java/com/team871/provider/BufferedImageProvider.java @@ -0,0 +1,28 @@ +package com.team871.provider; + +import java.awt.image.BufferedImage; + +public class BufferedImageProvider implements ImageProvider{ + + private final BufferedImage img; + + public BufferedImageProvider(BufferedImage img) { + this.img = img; + } + + @Override + public BufferedImage getImage() { + return img; + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getInfo() { + return img.toString(); + } + +} diff --git a/src/main/java/com/team871/provider/FileImageProvider.java b/src/main/java/com/team871/provider/FileImageProvider.java new file mode 100644 index 0000000..681282d --- /dev/null +++ b/src/main/java/com/team871/provider/FileImageProvider.java @@ -0,0 +1,22 @@ +package com.team871.provider; + +import javax.imageio.ImageIO; +import java.io.File; +import java.io.IOException; + +public class FileImageProvider extends BufferedImageProvider { + + private final File file; + + public FileImageProvider(File file) throws IOException { + super(ImageIO.read(file)); + this.file = file; + } + + @Override + public String getInfo(){ + return file.getName(); + } + + +} diff --git a/src/main/java/com/team871/provider/ImageProvider.java b/src/main/java/com/team871/provider/ImageProvider.java new file mode 100644 index 0000000..66e73dd --- /dev/null +++ b/src/main/java/com/team871/provider/ImageProvider.java @@ -0,0 +1,11 @@ +package com.team871.provider; + +import java.awt.image.BufferedImage; + +public interface ImageProvider { + + BufferedImage getImage(); + boolean isAvailable(); + String getInfo(); + +} diff --git a/src/main/java/com/team871/sensing/AbstractBarcodeReader.java b/src/main/java/com/team871/sensing/AbstractBarcodeReader.java new file mode 100644 index 0000000..e00a5b8 --- /dev/null +++ b/src/main/java/com/team871/sensing/AbstractBarcodeReader.java @@ -0,0 +1,41 @@ +package com.team871.sensing; + +import com.team871.ui.TickListener; + +import java.awt.*; +import java.util.*; +import java.util.List; + +public abstract class AbstractBarcodeReader implements TickListener { + private BarcodeResult cachedResult; + private final List listeners = new ArrayList<>(); + private final Deque dataQueue = new ArrayDeque<>(); + + protected void enqueueResult(String data) { + dataQueue.offer(new BarcodeResult(data, System.currentTimeMillis())); + } + + protected void fireScannedEvent(BarcodeResult event, boolean changed) { + listeners.forEach(l -> l.onScanned(event, changed)); + } + + public BarcodeResult getLastResult() { + return cachedResult; + } + + protected BarcodeResult getNextResult() { + return dataQueue.poll(); + } + + public void addListener(ScannerListener listener) { + listeners.add(listener); + } + + public void resetLast() { + cachedResult = null; + } + + public abstract Collection getDebugInfo(); + public abstract void render(Graphics2D g, int width, int height); + public abstract void shutdown(); +} diff --git a/src/main/java/com/team871/sensing/BarcodeResult.java b/src/main/java/com/team871/sensing/BarcodeResult.java new file mode 100644 index 0000000..cd3219f --- /dev/null +++ b/src/main/java/com/team871/sensing/BarcodeResult.java @@ -0,0 +1,21 @@ +package com.team871.sensing; + +public class BarcodeResult { + + private final String text; + private final long time; + + public BarcodeResult(String text, long time){ + this.text = text; + this.time = time; + } + + public String getText(){ + return text; + } + + public long getTime(){ + return time; + } + +} diff --git a/src/main/java/com/team871/sensing/JPOSSense.java b/src/main/java/com/team871/sensing/JPOSSense.java new file mode 100644 index 0000000..242eeeb --- /dev/null +++ b/src/main/java/com/team871/sensing/JPOSSense.java @@ -0,0 +1,237 @@ +package com.team871.sensing; + +import com.team871.exception.RobotechException; +import com.team871.ui.SettingsMenu; +import com.team871.util.BarcodeUtils; +import com.team871.util.Settings; +import jpos.JposException; +import jpos.Scanner; +import jpos.util.JposPropertiesConst; +import net.sourceforge.barbecue.Barcode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import java.awt.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +public class JPOSSense extends AbstractBarcodeReader { + private static final Logger logger = LoggerFactory.getLogger(JPOSSense.class); + private static final Font FONT = new Font(Font.MONOSPACED, Font.BOLD, 32); + private final Scanner scanner; + + private BarcodeResult lastResult = null; + + Image scannerImage; + Image timImage; + + int danceTimer = 0; + Clip coolSound; + + private boolean running = false; + private boolean ready = false; + private Thread connectThread; + + public JPOSSense() throws RobotechException { + String jposXmlPath = Settings.getInstance().getJposXmlPath(); + logger.info("YeahTim! -> " + getClass().getClassLoader().getResource("audio/tim.wav")); + logger.info("Looking for jpos.xml at " + jposXmlPath); + System.setProperty(JposPropertiesConst.JPOS_POPULATOR_FILE_PROP_NAME, jposXmlPath); + + try { + scanner = new Scanner(); + scanner.addErrorListener(e -> logger.warn("Scanner error: " + e.toString())); + scanner.addStatusUpdateListener(e -> logger.info("Scanner status change: " + e.toString())); + + scanner.addDataListener(e -> { + try { + final Scanner scn = (Scanner) e.getSource(); + final String data = new String(scn.getScanData()); + + enqueueResult(data); + scn.setDataEventEnabled(true); + } catch (Exception ex) { + logger.error("Error reading data from scanner.", ex); + } + }); + + scanner.open("ZebraAllScanners"); + } catch (JposException e) { + logger.error("Failed to create scanner!", e); + throw new RobotechException("Failed to create and open scanner", e); + } + + try { + scannerImage = ImageIO.read(getClass().getResource("img/cool image.png")); + timImage = ImageIO.read(getClass().getResource("img/tim.jpg")); + } catch (Exception e) { + logger.error("Failed to load exciting extra images", e); + } + + beginConnect(); + } + + private void beginConnect() { + if(ready) { + return; + } + + running = true; + connectThread = new Thread(() -> { + while (running) { + logger.info("Connecting to scanner..."); + try { + scanner.claim(1000); + break; + } catch (JposException e) { + logger.warn("Could not connect (" + e.getMessage() + "). Retrying in 2s..."); + } + } + + if(!running) { + return; + } + + logger.info("Connected to scanner!"); + try { + scanner.setDeviceEnabled(true); + scanner.setDataEventEnabled(true); + scanner.checkHealth(1); + } catch (JposException e) { + logger.info("Error initializing device after connection.", e); + ready = false; + return; + } + + ready = true; + }); + + connectThread.setDaemon(true); + connectThread.start(); + } + + @Override + public Collection getDebugInfo() { + final List ret = new ArrayList<>(); + ret.add("Scanner = " + scanner); + if (scanner != null) { + ret.add("Scanner State = " + scanner.getState()); + try { + if (scanner.getClaimed()) { + ret.add("Scanner Name = " + scanner.getPhysicalDeviceName()); + ret.add("Scanner Health = " + scanner.getCheckHealthText()); + } else { + ret.add("Scanner Not Claimed"); + } + } catch (JposException e) { + logger.error("Failed top get scanner debug info", e); + } + } + + return ret; + } + + @Override + public void tick(long time) { + if (danceTimer > 0) { + danceTimer--; + } + + final BarcodeResult newResult = getNextResult(); + if (newResult == null) { + return; + } + + final BarcodeResult oldResult = lastResult; + lastResult = newResult; + + fireScannedEvent(newResult, (oldResult == null || !newResult.getText().equals(oldResult.getText()))); + } + + @Override + public void shutdown() { + running = false; + try { + connectThread.join(); + } catch (InterruptedException e) { + logger.error("Failed to wait for connect thread to end", e); + } + } + + @Override + public void render(Graphics2D g, int width, int height) { + g.setColor(Color.LIGHT_GRAY); + g.fillRect(0, 0, width, height); + g.setColor(Color.GREEN); + + g.setColor(Color.BLUE); + String s = "Scan a Barcode!"; + g.setFont(FONT); + + g.setColor(Color.WHITE); + int sWidth = g.getFontMetrics().stringWidth(s); + int pos = 0; + for (int i = 0; i < s.length(); i++) { + String ch = s.substring(i, i + 1); + if (Settings.getInstance().getFun()) { + g.setColor(rainbowColor(0.005, i * -50)); + } + + int xOfs = !Settings.getInstance().getFun() ? 0 : (int) (Math.cos((System.currentTimeMillis() + 50 * i) / 200.0) * 5); + int yOfs = !Settings.getInstance().getFun() ? 0 : (int) (Math.sin((System.currentTimeMillis() + 50 * i) / 200.0) * 5); + g.drawString(ch, (width / 2) - (sWidth / 2) + xOfs + pos, 50 + yOfs); + pos += g.getFontMetrics().stringWidth(ch); + } + + Barcode b = BarcodeUtils.getBarcodeByName("Sign In/Out by Name"); + BarcodeUtils.drawBarcode(b, g, (width / 2) - (b.getWidth() / 2), 100); + + b = BarcodeUtils.getBarcodeByName("Correct Name"); + BarcodeUtils.drawBarcode(b, g, (width / 4) - (b.getWidth() / 2), 300); + + b = BarcodeUtils.getBarcodeByName("Add Student"); + BarcodeUtils.drawBarcode(b, g, (3*width / 4) - (b.getWidth() / 2), 300); + } + + private static Color rainbowColor(double frequency, int timeOffset) { + long i = System.currentTimeMillis() + timeOffset; + + float red = (float) (Math.sin(frequency * i + 0) * 127 + 128); + float green = (float) (Math.sin(frequency * i + 2) * 127 + 128); + float blue = (float) (Math.sin(frequency * i + 4) * 127 + 128); + + return new Color(red / 255f, green / 255f, blue / 255f); + } + + public void dance() { + if (!Settings.getInstance().getFun()) { + return; + } + + danceTimer = 240; + playGoodAudio(); + } + + private void playGoodAudio() { + try { + if (coolSound == null) { + coolSound = AudioSystem.getClip(); + } else { + coolSound.close(); + } + + AudioInputStream inputStream = AudioSystem.getAudioInputStream(SettingsMenu.class.getClassLoader().getResource("dance" + new Random().nextInt(3) + ".wav")); + coolSound.open(inputStream); + coolSound.setFramePosition(0); + coolSound.start(); + } catch (Exception e) { + System.err.println(e.getMessage()); + } + } +} diff --git a/src/main/java/com/team871/sensing/KeyboardSense.java b/src/main/java/com/team871/sensing/KeyboardSense.java new file mode 100644 index 0000000..83c9589 --- /dev/null +++ b/src/main/java/com/team871/sensing/KeyboardSense.java @@ -0,0 +1,56 @@ +package com.team871.sensing; + +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.util.*; +import java.util.List; + +import static java.awt.event.KeyEvent.VK_ENTER; + +public class KeyboardSense extends AbstractBarcodeReader implements KeyListener { + private StringBuilder buffer = new StringBuilder(); + private BarcodeResult lastResult = null; + + @Override + public void render(Graphics2D g, int width, int height) { + g.setColor(Color.BLUE); + g.fillRect(0,0, width, height); + g.drawString("Buffer: " + buffer, 10, 10); + } + + @Override + public void shutdown() { + + } + + @Override + public void keyTyped(KeyEvent e) { + if((int)e.getKeyChar() == VK_ENTER) { + enqueueResult(buffer.toString()); + buffer = new StringBuilder(); + } else { + buffer.append(e.getKeyChar()); + } + } + + @Override + public void keyPressed(KeyEvent e) { + + } + + @Override + public void keyReleased(KeyEvent e) { + + } + + @Override + public Collection getDebugInfo() { + return Collections.singletonList("Buffer = \"" + buffer + "\""); + } + + @Override + public void tick(long time) { + + } +} diff --git a/src/main/java/com/team871/sensing/ScannerListener.java b/src/main/java/com/team871/sensing/ScannerListener.java new file mode 100644 index 0000000..85fb92f --- /dev/null +++ b/src/main/java/com/team871/sensing/ScannerListener.java @@ -0,0 +1,5 @@ +package com.team871.sensing; + +public interface ScannerListener { + void onScanned(BarcodeResult result, boolean changed); +} diff --git a/src/main/java/com/team871/ui/AttendanceManager.java b/src/main/java/com/team871/ui/AttendanceManager.java new file mode 100644 index 0000000..21056ab --- /dev/null +++ b/src/main/java/com/team871/ui/AttendanceManager.java @@ -0,0 +1,588 @@ +package com.team871.ui; + +import com.team871.data.Member; +import com.team871.exception.RobotechException; +import com.team871.sensing.AbstractBarcodeReader; +import com.team871.sensing.BarcodeResult; +import com.team871.sensing.JPOSSense; +import com.team871.util.BarcodeUtils; +import com.team871.util.Settings; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.geom.AffineTransform; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * @author Dave + * i know this code is terrible + * once i get a prototype working ill make it good + */ +public class AttendanceManager { + private static final Logger logger = LoggerFactory.getLogger(AttendanceManager.class); + private static final int TARGET_FRAMERATE = 60; + private static final long MILLIS_PER_FRAME = 1000 / TARGET_FRAMERATE; + + private final Frame frame; + private int time = 0; + + AbstractBarcodeReader barcodeSensor; + + private int flashTimer = 0; + private int flashTimerMax = 30; + + private BarcodeResult lastResult; + private String lastSID = "???"; + private Member member = null; + + TableRenderer tableRenderer; + AttendanceTable table; + + TableRenderer mentorRenderer; + AttendanceTable mentorTable; + + DisplayTable displayTable = DisplayTable.Students; + + private boolean enteringNewSID = false; + + private SettingsMenu settingsMenu; + + private int clearTimerMax = 60 * 4; + private int clearTimer = clearTimerMax; + + private State currentState = State.Normal; + private Workbook workbook; + + private enum State { + Settings, + Normal, + Shutdown + } + + private enum DisplayTable { + Students, Mentors + } + + public static void main(String[] args) { + try { + if (args.length > 0) { + Settings.getInstance().init(args[0]); + } else { + throw new RobotechException("No prefs file provided"); + } + + final AttendanceManager manager = new AttendanceManager(); + manager.init(); + manager.run(); + } catch (RobotechException e) { + logger.error("Failed to initialize: ", e); + } + } + + private AttendanceManager() { + logger.info("Initializing AttendenceManager"); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) { + logger.error("Error setting LAF: " + e.toString()); + } + frame = new Frame(); + } + + private void init() throws RobotechException { + barcodeSensor = new JPOSSense(); + settingsMenu = new SettingsMenu(this, barcodeSensor); + barcodeSensor.addListener( (code, changed) -> { + if (BarcodeUtils.isSettingsCommand(code)) { + settingsMenu.handleResult(code); + } else if(BarcodeUtils.isAdminCommand(code)) { + handleAdmin(code); + } else { + handleBarcode(code); + } + }); + final Settings settings = Settings.getInstance(); + + try(final FileInputStream stream = new FileInputStream(settings.getSheetPath().toFile())) { + workbook = WorkbookFactory.create(stream); + } catch (IOException e) { + throw new RobotechException("Failed to load attendance file", e); + } + + table = new AttendanceTable(workbook, + settings.getRosterSheet(), settings.getAttendanceSheet(), + settings.getAttendanceHeaderRow(), settings.getRosterHeaderRow(), + settings.getAttendanceFirstDataRow(), settings.getRosterFirstDataRow()); + tableRenderer = new TableRenderer(table); + + mentorTable = new AttendanceTable(workbook, + settings.getMentorRosterSheet(), settings.getMentorAttendanceSheet(), + settings.getMentorAttendanceHeaderRow(), settings.getMentorRosterHeaderRow(), + settings.getMentorAttendanceFirstDataRow(), settings.getMentorRosterFirstDataRow()); + + mentorRenderer = new TableRenderer(mentorTable); + + frame.addMouseWheelListener(tableRenderer); + frame.addMouseWheelListener(mentorRenderer); + frame.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_S && e.isControlDown()) { + showSaveDialog(); + } else if(e.getKeyCode() == KeyEvent.VK_Q && e.isControlDown()) { + if (table.hasUnsaved() || mentorTable.hasUnsaved()) { + int result = JOptionPane.showConfirmDialog(frame.getCanvas(), "You have unsaved changes!\nDo you want to save?", frame.getTitle(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + + switch (result) { + case JOptionPane.YES_OPTION: + showSaveDialog(); + int result2 = JOptionPane.showConfirmDialog(frame.getCanvas(), "Exit?", frame.getTitle(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); + if (result2 == JOptionPane.YES_OPTION) { + currentState = State.Shutdown; + } + break; + case JOptionPane.NO_OPTION: + currentState = State.Shutdown; + break; + default: // cancel or x-out + // don't do anything + break; + } + } else { + currentState = State.Shutdown; + } + } else if (e.getKeyCode() == KeyEvent.VK_F5) { + currentState = currentState == State.Settings ? State.Normal : State.Settings; + } else if (e.getKeyCode() == KeyEvent.VK_ENTER && e.isControlDown() && e.isAltDown()) { + playYeaTim(); + } else if(e.getKeyCode() == KeyEvent.VK_L) { + settingsMenu.doManualLogin(); + } else if(e.getKeyCode() == KeyEvent.VK_N) { + switch (displayTable) { + case Students: + displayTable = DisplayTable.Mentors; + break; + case Mentors: + displayTable = DisplayTable.Students; + break; + } + } + } + }); + + frame.setVisible(true); + } + + private void run() { + long timer = System.currentTimeMillis(); + long preRenderTime; + + int frames = 0; + int ticks = 0; + + long totalSleepTime = 0; + long totalTickTime = 0; + long totalRenderTime = 0; + + long last = System.currentTimeMillis(); + while (currentState != State.Shutdown) { + long now = System.currentTimeMillis(); + long diff = now - last; + + for (long tickDelta = diff / MILLIS_PER_FRAME; tickDelta >= 1; tickDelta--) { + tick(); + ticks++; + } + + preRenderTime = System.currentTimeMillis(); + totalTickTime += (preRenderTime - now); + + render(); + totalRenderTime += System.currentTimeMillis() - preRenderTime; + frames++; + last = now; + + try { + final long sleepTime = Math.max(1, MILLIS_PER_FRAME - (System.currentTimeMillis() - now)); + totalSleepTime += sleepTime; + Thread.sleep(sleepTime); + } catch (InterruptedException ignored) { + } + + if (System.currentTimeMillis() - timer >= 1000) { + timer = System.currentTimeMillis(); + frame.setTitle("Attendance UI | " + frames + " FPS " + ticks + " TPS " + ((float) totalSleepTime / frames) + " " + ((float) totalTickTime / frames) + " " + (totalRenderTime / frames)); + totalSleepTime = 0; + totalRenderTime = 0; + totalTickTime = 0; + frames = 0; + ticks = 0; + } + } + + barcodeSensor.shutdown(); + System.exit(0); + } + + private void tick() { + if (flashTimer > 0) { + flashTimer--; + } + + barcodeSensor.tick(time); + switch (displayTable) { + case Students: + tableRenderer.tick(time); + break; + case Mentors: + mentorRenderer.tick(time); + break; + } + settingsMenu.tick(time); + + if (clearTimer > 0) { + clearTimer--; + if (clearTimer == 0) { + clearTimer = -1; + lastResult = null; + member = null; + lastSID = "???"; + barcodeSensor.resetLast(); + } + } + + time++; + } + + private void render() { + final Graphics2D g = frame.getCanvas().getRenderGraphics(); + final Canvas canvas = frame.getCanvas(); + switch (currentState) { + case Settings: + settingsMenu.render(g, canvas.getWidth(), canvas.getHeight()); + break; + case Normal: + renderNormal(g, canvas.getWidth(), canvas.getHeight()); + break; + } + + frame.paint(); + } + + private void renderBackground(Graphics2D g, int width, int height) { + if (frame.hasFocus()) { + g.setColor(Color.RED); + } else if (flashTimer > 0) { + Color c1 = Color.DARK_GRAY; + Color c2 = Color.GREEN; + float thru = flashTimer / (float) flashTimerMax; + int r = (int) (c1.getRed() + thru * (c2.getRed() - c1.getRed())); + int gr = (int) (c1.getGreen() + thru * (c2.getGreen() - c1.getGreen())); + int b = (int) (c1.getBlue() + thru * (c2.getBlue() - c1.getBlue())); + Color lerp = new Color(r, gr, b); + g.setColor(lerp); + } else { + g.setColor(Color.DARK_GRAY); + } + + g.fillRect(0, 0, width, height); + } + + private void renderSensorPanel(Graphics2D g, Rectangle sensorPanel) { + // "Camera" rectangle. In reality, this is where the scanner will render state. + g.setColor(Color.LIGHT_GRAY); + g.setStroke(new BasicStroke(8f)); + g.drawRect(sensorPanel.x, sensorPanel.y, sensorPanel.width, sensorPanel.height); + + g.setColor(Color.BLUE); + g.drawRect(100 + (int) (50 * Math.sin(time / 10f)), 100 + (int) (50 * Math.cos(time / 10f)), 20, 20); + + AffineTransform tr = g.getTransform(); + g.setClip(sensorPanel); + g.translate(sensorPanel.x, sensorPanel.y); + barcodeSensor.render(g, sensorPanel.width, sensorPanel.height); + g.setTransform(tr); + g.setClip(null); + } + + private void renderInfoRectangle(Graphics2D g, Rectangle infoRect) { + g.setColor(Color.LIGHT_GRAY); + g.setStroke(new BasicStroke(8f)); + g.drawRect(infoRect.x, infoRect.y, infoRect.width, infoRect.height); + g.setColor(Color.WHITE); + g.fillRect(infoRect.x, infoRect.y, infoRect.width, infoRect.height); + + g.setColor(Color.BLACK); + g.setFont(new Font(Font.MONOSPACED, Font.BOLD, 32)); + + final List lines = new ArrayList<>(); + lines.add("Scanned: " + (lastResult == null ? "???" : lastResult.getText())); + lines.add("SID (hash): " + lastSID); + if (!lastSID.equalsIgnoreCase("???") && member != null) { + lines.add("Name: " + member.getLastName()); + } + + for (int i = 0; i < lines.size(); i++) { + g.drawString(lines.get(i), infoRect.x + 10, infoRect.y + 32 + 32 * i); + } + } + + private void renderTable(Graphics2D g, Rectangle tableRect) { + g.setColor(Color.LIGHT_GRAY); + g.setStroke(new BasicStroke(8f)); + g.drawRect(tableRect.x, tableRect.y, tableRect.width, tableRect.height); + g.setColor(Color.WHITE); + g.fillRect(tableRect.x, tableRect.y, tableRect.width, tableRect.height); + + g.setStroke(new BasicStroke(1f)); + + final AffineTransform tr = g.getTransform(); + g.setClip(tableRect); + g.translate(tableRect.x, tableRect.y); + + switch (displayTable) { + case Students: + tableRenderer.setDimension(tableRect); + tableRenderer.drawTable(g); + break; + case Mentors: + mentorRenderer.setDimension(tableRect); + mentorRenderer.drawTable(g); + break; + } + + g.setTransform(tr); + } + + private void renderNormal(Graphics2D g, int width, int height) { + int padding = 16; + + renderBackground(g, width, height); + renderSensorPanel(g, new Rectangle(padding, padding, (int) (width * 0.4), (int) (height * 0.4))); + renderInfoRectangle(g, new Rectangle((int) (width - width * 0.5 - padding), padding, (int) (width * 0.5), (int) (height * 0.4))); + renderTable(g, new Rectangle(padding, (int) (height * 0.4 + padding + padding), width - padding * 2, (int) ((height) - (height * 0.4 + padding + padding) - padding))); + } + + private void handleBarcode(BarcodeResult result) { + if (enteringNewSID || + currentState == State.Settings || + BarcodeUtils.isSettingsCommand(result) || + BarcodeUtils.isAdminCommand(result)) { + return; //don't accept new scans if setting up new SID + } + + logger.info("Scanned: " + result.getText()); + clearTimer = clearTimerMax; + this.member = null; + final String newSID = result.getText().replaceFirst("^0+(?!$)", ""); // remove leading zeros; + + if(!isValidSID(newSID)) { + lastResult = result; + lastSID = "INVALID"; + if (lastResult.getText().equals("871")) { + lastSID = "YEA TIM"; + this.member = null; + playYeaTim(); + } + return; + } + + lastResult = new BarcodeResult("(hidden student id)", result.getTime()); + try { + final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + messageDigest.update(newSID.getBytes()); + lastSID = Integer.toHexString(Arrays.hashCode(messageDigest.digest())); + logger.info("hash = " + lastSID); + } catch (Exception e) { + logger.error("Error validating MD5sum", e); + } + + final Member member = findMember(newSID); + if (member != null) { + handleLogin(member); + } else if (!enteringNewSID) { + enteringNewSID = true; + SwingUtilities.invokeLater(() -> handleNewSID(newSID)); + } + } + + private Member findMember(String newSID) { + Member member = table.getStudentById(newSID); + if(member == null) { + member = mentorTable.getStudentById(newSID); + } + + return member; + } + + private void handleNewSID(String sid) { + int response = JOptionPane.showConfirmDialog(frame.getCanvas(), "The scanned ID is not present in the system.\nAdd it?", frame.getTitle(), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE); + + cancel: + if (response == JOptionPane.YES_OPTION) { + Map students; + String name = null; + do { + name = JOptionPane.showInputDialog((name != null ? "That name is not present.\n" : "") + "Enter the last name for the member associated with this ID:"); + if (name == null) { + break cancel; + } + } while ((students = getStudentsWithLastName(name)).isEmpty()); + + Member member; + if (students.size() > 1) { + String firstName = null; + do { + firstName = JOptionPane.showInputDialog((firstName != null ? "That name is not present.\n" : "") + "There are multiple people with that last name!\nEnter the first name for the member associated with this ID:"); + if (firstName == null) { + break cancel; + } + } while ((member = students.get(firstName)) == null); + } else { + member = students.values().iterator().next(); + } + + member.setId(sid); + handleLogin(member); + } + + enteringNewSID = false; + } + + Map getStudentsWithLastName(String name) { + Map members = table.getStudentsWithLastName(name); + + if(members == null || members.isEmpty()) { + members = mentorTable.getStudentsWithLastName(name); + } + + return members; + } + + private void handleLogin(Member member) { + if (barcodeSensor instanceof JPOSSense) { + ((JPOSSense) barcodeSensor).dance(); + } + + final LocalDate today = Settings.getInstance().getDate(); + if (!member.isSignedIn(today)) { + member.signIn(today); + flashTimer = flashTimerMax; + } else if (member.isSignedIn(today)) { + member.signOut(today); + flashTimer = flashTimerMax; + } + + this.member = member; + } + + private void handleAdmin(BarcodeResult result) { + final String cmd = BarcodeUtils.getAdminCommand(result); + if ("SETTINGS".equals(cmd)) { + currentState = currentState == State.Settings ? State.Normal : State.Settings; + } + } + + private boolean isValidSID(String test) { + if (!test.matches("^F?\\d+(\\d+)?$")) { + return false; //is numeric + } + + return test.length() >= 5 && test.length() <= 7; // must be 5 or 6 digits + } + + private void showSaveDialog() { + if(!table.areAllSignedOut() || !mentorTable.areAllSignedOut()) { + int result = JOptionPane.showConfirmDialog(null, "There are people that haven't signed out.\nDo you want to sign them out?\n(If not, sign in time will be saved)", "Attendance Manager", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE); + if (result == JOptionPane.YES_OPTION) { + table.forceSignOut(); + mentorTable.forceSignOut(); + } + } + + final JFileChooser chooser = new JFileChooser(); + chooser.setSelectedFile(Settings.getInstance().getSheetPath().toFile()); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setMultiSelectionEnabled(false); + chooser.showOpenDialog(frame.getCanvas()); + + final File f = chooser.getSelectedFile(); + + boolean success = true; + System.out.println("Saving attendance to " + f.getAbsolutePath()); + try { + f.createNewFile(); //create the file if it doesn't exist + FileOutputStream out = new FileOutputStream(f); + workbook.write(out); // write the workbook to the file + out.close(); + } catch(Exception e) { + logger.warn("Error writing spreadsheet: ", e); + success = false; + } + + table.setSaved(); + mentorTable.setSaved(); + + if (success) { + JOptionPane.showMessageDialog(frame.getCanvas(), "Attendance saved.", frame.getTitle(), JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(frame.getCanvas(), "Failed to save attendance (see console).", frame.getTitle(), JOptionPane.WARNING_MESSAGE); + } + } + + private void playYeaTim() { + try { + final Clip yeatim = AudioSystem.getClip(); + + final AudioInputStream inputStream = AudioSystem.getAudioInputStream(SettingsMenu.class.getClassLoader().getResource("audio/tim.wav")); + yeatim.open(inputStream); + yeatim.setFramePosition(0); + yeatim.start(); + } catch (Exception e) { + logger.error("Failed to YeahTim", e); + } + } + + public void setFullscreen(boolean fullscreen) { + frame.setFullscreen(fullscreen); + } + + public boolean isFullscreen() { + return frame.isFullscreen(); + } + + public Canvas getCanvas() { + return frame.getCanvas(); + } + + //// incubation + + public void createStudent(String first, String last) { + switch (displayTable) { + case Students: + table.createStudent(first, last); + break; + case Mentors: + mentorTable.createStudent(first, last); + break; + } + } +} diff --git a/src/main/java/com/team871/ui/AttendanceTable.java b/src/main/java/com/team871/ui/AttendanceTable.java new file mode 100644 index 0000000..af909b7 --- /dev/null +++ b/src/main/java/com/team871/ui/AttendanceTable.java @@ -0,0 +1,284 @@ +package com.team871.ui; + +import com.team871.data.SheetConfig; +import com.team871.data.Member; +import com.team871.exception.RobotechException; +import com.team871.util.Settings; +import com.team871.util.Utils; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.time.LocalDate; +import java.util.*; + +public class AttendanceTable { + private static final Logger logger = LoggerFactory.getLogger(AttendanceTable.class); + + private final Map> studentsByName = new TreeMap<>(); + private final Map studentsById = new HashMap<>(); + private final List allMembers = new ArrayList<>(); + private final List listeners = new ArrayList<>(); + private final Member.Listener studentListener = new Member.Listener() { + @Override + public void onLogin(Member member) { + listeners.forEach(l -> l.onSignIn(member)); + } + + @Override + public void onLogout(Member member) { + listeners.forEach(l -> l.onSignOut(member)); + } + + @Override + public void onNameChanged(Member member, String oldLastName, String oldFirstName) { + Map byFirstName = studentsByName.get(oldLastName); + if(byFirstName == null) { + throw new IllegalStateException("Student never existed"); + } + + byFirstName.remove(oldFirstName); + byFirstName = studentsByName.computeIfAbsent(member.getLastName(), n -> new HashMap<>()); + byFirstName.put(member.getFirstName(), member); + + sortStudents(); + listeners.forEach(l -> l.nameChanged(member)); + } + + @Override + public void onIdChanged(Member member, String oldSid) { + if(oldSid != null) { + studentsById.remove(oldSid); + } + + studentsById.put(member.getId(), member); + } + }; + + private void sortStudents() { + allMembers.sort(Comparator.comparing(Member::getLastName).thenComparing(Member::getFirstName)); + } + + private Workbook workbook; + private SheetConfig roster; + private SheetConfig attendance; + private final String rosterSheetName; + private final String attendanceSheetName; + private final int attHeaderRow; + private final int rosterHeaderRow; + private final int attFirstRow; + private final int rosterFirstRow; + + private final List attendanceDates = new ArrayList<>(); + private boolean unsaved = false; + + public interface Listener { + void onSignIn(Member member); + void onSignOut(Member member); + void nameChanged(Member member); + void onStudentAdded(Member member); + } + + public AttendanceTable(Workbook workbook, String rosterSheet, String attendanceSheet, + int attHeaderRow, int rosterHeaderRow, + int attFirstRow, int rosterFirstRow) throws RobotechException { + this.workbook = workbook; + this.rosterSheetName = rosterSheet; + this.attendanceSheetName = attendanceSheet; + this.attHeaderRow = attHeaderRow; + this.rosterHeaderRow = rosterHeaderRow; + this.attFirstRow = attFirstRow; + this.rosterFirstRow = rosterFirstRow; + + loadAttendance(); + Settings.getInstance().addListener(this::updateDate); + } + + public Map getStudentsWithLastName(String lastName) { + return studentsByName.getOrDefault(lastName, Collections.emptyMap()); + } + + public int getStudentIndex(Member highlightMember) { + return allMembers.indexOf(highlightMember); + } + + public void createStudent(String first, String last) { + final Map byFirstName = studentsByName.computeIfAbsent(last, n -> new HashMap<>()); + if(byFirstName.containsKey(first)) { + throw new IllegalStateException("Name already exists!"); + } + + final Member member = new Member(first, last, roster, attendance); + member.addListener(studentListener); + + byFirstName.put(member.getFirstName(), member); + allMembers.add(member); + sortStudents(); + + listeners.forEach(l -> l.onStudentAdded(member)); + } + + public void addListener(Listener l) { + listeners.add(l); + } + + public void removeListener(Listener l) { + listeners.remove(l); + } + + private void loadAttendance() throws RobotechException { + logger.info("Loading attendance sheet"); + final Sheet attendanceSheet = workbook.getSheet(attendanceSheetName); + if(attendanceSheet == null) { + throw new RuntimeException("Attendance worksheet " + attendanceSheetName + " does not exist"); + } + attendance = new SheetConfig(attendanceSheet, attHeaderRow); + + final Sheet rosterSheet = workbook.getSheet(rosterSheetName); + if(rosterSheet == null) { + throw new RuntimeException("Roster worksheet " + rosterSheetName + " does not exist"); + } + roster = new SheetConfig(rosterSheet,rosterHeaderRow); + + // Now process the sheets into a set of students and their attendances. + // Start with the roster + for(int i = 0; i < roster.getDataRowCount(); i++) { + if(!roster.rowExists(i)) { + continue; + } + + final String lastName = roster.getValue(i, Utils.LAST_NAME_COL); + final String firstName = roster.getValue(i, Utils.FIRST_NAME_COL); + + if(Utils.isNullOrEmpty(lastName) || Utils.isNullOrEmpty(firstName)) { + continue; + } + + final Map byFirstName = studentsByName.computeIfAbsent(lastName, k -> new TreeMap<>()); + if(byFirstName.containsKey(firstName)) { + logger.error("Duplicate name! " + firstName + " " + lastName); + continue; + } + + final Member member = new Member(i, roster, attendance); + byFirstName.put(firstName, member); + allMembers.add(member); + + // If an ID existed, add to the mapping + if(member.getId() != null) { + studentsById.put(member.getId(), member); + } + + member.addListener(studentListener); + } + sortStudents(); + + // Then process the attendance + for(int i = attFirstRow; i < attendance.getColumnCount(); i++) { + final String headerVal = attendance.getHeaderValue(i); + if("Total".equals(headerVal)) { + break; + } + switch(headerVal) { + case Utils.ID_COL: + case Utils.FIRST_NAME_COL: + case Utils.LAST_NAME_COL: + break; + default: + attendanceDates.add(Utils.getLocalDate(headerVal)); + } + } + attendanceDates.sort(Comparator.comparing(LocalDate::toEpochDay)); + + for(int i = 0; i byFirstName = studentsByName.get(lastName); + if(byFirstName == null || byFirstName.isEmpty()) { + logger.warn("No student `" + firstName + " " + lastName + "` exists."); + continue; + } + + final Member member = byFirstName.get(firstName); + if(member == null) { + logger.warn("No student `" + firstName + " " + lastName + "` exists."); + continue; + } + + member.processAttendance(i); + } + + updateDate(); + } + + public Member getStudent(int index) { + return allMembers.get(index); + } + + public int getStudentCount() { + return allMembers.size(); + } + + public Member getStudentById(String id) { + return studentsById.get(id); + } + + public Map getStudentsByLastName(String lastName) { + final Map byFirstName = studentsByName.get(lastName); + if(byFirstName == null || byFirstName.isEmpty()) { + return Collections.emptyMap(); + } + + return Collections.unmodifiableMap(byFirstName); + } + + // TODO: This doesn't work anymore. + public boolean hasUnsaved() { + return unsaved; + } + + public void setSaved() { + unsaved = false; + } + + List getAttendanceDates() { + return attendanceDates; + } + + public boolean areAllSignedOut() { + if(Settings.getInstance().getLoginType() == LoginType.IN_ONLY) { + return false; + } + + return allMembers.stream() + .filter(s -> s.isSignedIn(Settings.getInstance().getDate())) + .allMatch(s -> s.isSignedOut(Settings.getInstance().getDate())); + } + + public void forceSignOut() { + final LocalDate date = Settings.getInstance().getDate(); + allMembers.stream() + .filter(s -> s.isSignedIn(date)) + .forEach(s -> s.signOut(date)); + } + + private void updateDate() { + final LocalDate date = Settings.getInstance().getDate(); + if(attendanceDates.stream().noneMatch(d -> d.isEqual(date))) { + attendanceDates.add(date); + attendanceDates.sort(Comparator.comparing(LocalDate::toEpochDay)); + } + } +} diff --git a/src/main/java/com/team871/ui/Frame.java b/src/main/java/com/team871/ui/Frame.java new file mode 100644 index 0000000..36a8df7 --- /dev/null +++ b/src/main/java/com/team871/ui/Frame.java @@ -0,0 +1,126 @@ +package com.team871.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; + +public class Frame { + private final JFrame frame; + private final JPanel panel; + private final UICanvas canvas; + + private boolean hasFocus = false; + private boolean fullscreen = false; + + private Dimension minimizedSize; + + public Frame() { + frame = new JFrame("Attendance UI"); + panel = new JPanel(); + canvas = new UICanvas(1200, 800); + + setFullscreen(false); + minimizedSize = frame.getSize(); + setFullscreen(true); + } + + public void setFullscreen(boolean fullscreen) { + frame.setVisible(false); + frame.dispose(); + + if (fullscreen) { + Dimension dim = Toolkit.getDefaultToolkit().getScreenSize(); + panel.setPreferredSize(dim); + canvas.resizeCanvas(dim.width, dim.height); + + frame.getContentPane().add(canvas); + frame.setExtendedState(JFrame.MAXIMIZED_BOTH); + frame.setUndecorated(true); + frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.setResizable(false); + } else { + Dimension dim = new Dimension(1200, 800); + panel.setPreferredSize(minimizedSize != null ? minimizedSize : dim); + panel.setSize(minimizedSize != null ? minimizedSize : dim); + canvas.resizeCanvas(dim.width, dim.height); + frame.setUndecorated(false); + + if (minimizedSize != null) { + frame.setState(JFrame.NORMAL); + frame.add(panel); + frame.pack(); + frame.remove(panel); + frame.setSize(minimizedSize.width - 10, minimizedSize.height - 10); + } else { + frame.add(panel); + frame.pack(); + frame.remove(panel); + } + + frame.getContentPane().add(canvas); + frame.setResizable(false); + + frame.setState(JFrame.NORMAL); + + frame.setLocationRelativeTo(null); + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + } + + frame.addWindowFocusListener(new WindowFocusListener() { + @Override + public void windowGainedFocus(WindowEvent e) { + if (e.getNewState() == 0) { + hasFocus = false; + } + } + + @Override + public void windowLostFocus(WindowEvent e) { + hasFocus = true; + } + }); + frame.setVisible(true); + this.fullscreen = fullscreen; + } + + public UICanvas getCanvas() { + return canvas; + } + + public void paint() { + canvas.paint(canvas.getGraphics()); + } + + public void addKeyListener(KeyListener l) { + canvas.addKeyListener(l); + } + + public void addMouseWheelListener(MouseWheelListener l) { + canvas.addMouseWheelListener(l); + } + + public void addWindowListener(WindowListener l) { + frame.addWindowListener(l); + } + + public void setTitle(String title) { + frame.setTitle(title); + } + + public boolean hasFocus() { + return hasFocus; + } + + public void setVisible(boolean visible) { + frame.setVisible(visible); + } + + public String getTitle() { + return frame.getTitle(); + } + + public boolean isFullscreen() { + return fullscreen; + } +} diff --git a/src/main/java/com/team871/ui/LoginType.java b/src/main/java/com/team871/ui/LoginType.java new file mode 100644 index 0000000..e1eb132 --- /dev/null +++ b/src/main/java/com/team871/ui/LoginType.java @@ -0,0 +1,5 @@ +package com.team871.ui; + +public enum LoginType { + IN_ONLY, IN_OUT +} \ No newline at end of file diff --git a/src/main/java/com/team871/ui/SettingsMenu.java b/src/main/java/com/team871/ui/SettingsMenu.java new file mode 100644 index 0000000..bba0080 --- /dev/null +++ b/src/main/java/com/team871/ui/SettingsMenu.java @@ -0,0 +1,260 @@ +package com.team871.ui; + +import com.team871.data.Member; +import com.team871.sensing.BarcodeResult; +import com.team871.sensing.AbstractBarcodeReader; +import com.team871.util.BarcodeUtils; +import com.team871.util.Settings; +import com.team871.util.Utils; +import net.sourceforge.barbecue.Barcode; + +import javax.sound.sampled.AudioInputStream; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.Clip; +import javax.swing.*; +import java.awt.*; +import java.time.LocalDate; +import java.util.*; +import java.util.List; + +public class SettingsMenu implements TickListener { + private static final Font HEADER_FONT = new Font("Arial", Font.PLAIN, 32); + private static final Font SETTINGS_FONT = new Font("Arial", Font.BOLD, 12); + + private Map actions; + private Clip testSound; + private AttendanceManager attendanceManager; + private AbstractBarcodeReader barcodeSensor; + + private boolean lock = false; + + public SettingsMenu(AttendanceManager attendanceManager, AbstractBarcodeReader barcodeSensor) { + this.attendanceManager = attendanceManager; + this.barcodeSensor = barcodeSensor; + registerActions(); + } + + private void registerActions() { + actions = new LinkedHashMap<>(); + actions.put(BarcodeUtils.getBarcodeByName("Test Scanner"), () -> { + System.out.println("test 1!"); + playTestSound(); + }); + + actions.put(BarcodeUtils.getBarcodeByName("Set Date"), () -> new Thread(() -> { + lock = true; + + String res = null; + while (true) { + res = JOptionPane.showInputDialog(attendanceManager.getCanvas(), (res != null) ? "\"" + res + "\" is not a valid date.\n" : "" + "Enter a new date:"); + if (res != null) { + try { + Utils.getLocalDate(res); + Settings.getInstance().setDate(res); + break; + } catch(Exception ignored) {} + } else { + break; + } + } + lock = false; + }).start()); + + actions.put(BarcodeUtils.getBarcodeByName("Sign In/Out by Name"), this::doManualLogin); + + actions.put(BarcodeUtils.getBarcodeByName("Toggle Fullscreen"), () -> attendanceManager.setFullscreen(!attendanceManager.isFullscreen())); + + actions.put(BarcodeUtils.getBarcodeByName("Correct Name"), () -> new Thread(()-> { + final Member member = getStudent(); + if(member == null) { + return; + } + String res = null; + while (true) { + res = JOptionPane.showInputDialog(attendanceManager.getCanvas(), (res != null) ? "\"" + res + "\" is not a valid name.\n" : "" + "Enter a new Name:"); + if (res != null) { + final String[] parts = res.split("\\s+"); + if(parts.length != 2) { + continue; + } + + member.setName(parts[0], parts[1]); + return; + } else { + break; + } + } + }).start()); + + actions.put(BarcodeUtils.getBarcodeByName("Add Student"), () -> new Thread(()-> { + String res = null; + while (true) { + res = JOptionPane.showInputDialog(attendanceManager.getCanvas(), (res != null) ? "\"" + res + "\" is not a valid name.\n" : "" + "Enter a new Name:"); + if (res != null) { + final String[] parts = res.split("\\s+"); + if(parts.length != 2) { + continue; + } + + Map byFirstName = attendanceManager.getStudentsWithLastName(parts[1]); + if(byFirstName != null) { + if(byFirstName.get(parts[0]) != null) { + break; + } + } + + attendanceManager.createStudent(parts[0], parts[1]); + return; + } else { + break; + } + } + }).start()); + } + + public void doManualLogin() { + new Thread(() -> { + lock = true; + final Member member = getStudent(); + if(member == null) { + lock = false; + return; + } + + final LocalDate date = Settings.getInstance().getDate(); + if (!member.isSignedIn(date)) { + member.signIn(date); + JOptionPane.showMessageDialog(attendanceManager.getCanvas(), "Signed in."); + } else { + member.signOut(date); + JOptionPane.showMessageDialog(attendanceManager.getCanvas(), "Signed out."); + } + + lock = false; + }).start(); + } + + private Member getStudent() { + Map students; + String name = null; + do { + name = JOptionPane.showInputDialog((name != null ? "That name is not present.\n" : "") + "Enter the last name of the member:"); + if (name == null) { + return null; + } + } while ((students = attendanceManager.getStudentsWithLastName(name)).isEmpty()); + + Member member; + if (students.size() > 1) { + String firstName = null; + do { + firstName = JOptionPane.showInputDialog((firstName != null ? "That name is not present.\n" : "") + "There are multiple people with that last name!\nEnter the first name of the member:"); + if (firstName == null) { + return null; + } + } while ((member = students.get(firstName)) == null); + } else { + member = students.values().stream().findFirst().get(); + } + + return member; + } + + public void tick(long time) { + for (Barcode b : actions.keySet()) { + Color foreground = b.getForeground(); + if (!foreground.equals(Color.BLACK)) { + foreground = new Color(0, Math.max(foreground.getGreen() - 8, 0), 0); + b.setForeground(foreground); + } + } + } + + public void render(Graphics2D g, int width, int height) { + g.setColor(Color.LIGHT_GRAY); + g.fillRect(0, 0, width, height); + + g.setColor(Color.BLACK); + g.setFont(HEADER_FONT); + String header = "Debug Menu"; + g.drawString(header, width / 2 - g.getFontMetrics().stringWidth(header) / 2, 40); + + // TODO: re-implement this + final List params = Collections.emptyList(); + + g.setFont(SETTINGS_FONT); + int colCt = 5; + for (int i = 0; i < params.size(); i++) { + int col = i / colCt; + + int bx = 20 + 400 * (col); + int by = 80 + (i % colCt) * 20; + + g.setClip(bx - 4, by - 14, 400 + 4 - 10, 12 + 4); + g.setColor(Color.BLACK); + + int w = g.getFontMetrics().stringWidth(params.get(i)) + 6; + if (w > 400 + 4 - 10) { + bx += (Math.sin(System.currentTimeMillis() / 5000.0) + 1) / 2f * ((400 + 4 - 10) - w); + } + + g.drawString(params.get(i), bx, by); + } + + g.setClip(null); + + int rowCt = 3; + int spacingX = 400; + int spacingY = 300; + int startY = 200; + int i = 0; + + for (Barcode b : actions.keySet()) { + int ix = i % 3; + int iy = i / 3; + BarcodeUtils.drawBarcode(b, g, width / 2 - b.getWidth() / 2 + (int) (ix * spacingX - (rowCt - 1) / 2f * spacingX), iy * spacingY + startY); + i++; + } + } + + public void handleResult(BarcodeResult result) { + if (isLocked()) { + return; + } + + final Barcode b = BarcodeUtils.getBarcode(result); + if (b == null) { + return; + } + + final Runnable action = actions.get(b); + + if (action == null) { + return; + } + + action.run(); + b.setForeground(Color.GREEN); + } + + private void playTestSound() { + try { + if (testSound == null) { + testSound = AudioSystem.getClip(); + } else { + testSound.close(); + } + + AudioInputStream inputStream = AudioSystem.getAudioInputStream(SettingsMenu.class.getClassLoader().getResource("audio/test.wav")); + testSound.open(inputStream); + testSound.setFramePosition(0); + testSound.start(); + } catch (Exception e) { + System.err.println(e.getMessage()); + } + } + + public boolean isLocked() { + return lock; + } +} diff --git a/src/main/java/com/team871/ui/TableRenderer.java b/src/main/java/com/team871/ui/TableRenderer.java new file mode 100644 index 0000000..9120b15 --- /dev/null +++ b/src/main/java/com/team871/ui/TableRenderer.java @@ -0,0 +1,302 @@ +package com.team871.ui; + +import com.team871.data.FirstRegistration; +import com.team871.data.SafeteyFormState; +import com.team871.data.Member; +import com.team871.util.Settings; +import com.team871.util.Utils; + +import java.awt.*; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.geom.AffineTransform; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +public class TableRenderer implements MouseWheelListener { + private static final int NAME_COL_WIDTH = 100; + + private static final Color CURRENT_DATE_COL = new Color(225, 210, 110); + private static final Color ABSENT_EVEN = new Color(0.7f, 0.35f, 0.35f); + private static final Color ABSENT_ODD = new Color(1f, 0.4f, 0.4f); + private static final Color PRESENT_EVEN = new Color(0.3f, 0.65f, 0.3f); + private static final Color PRESENT_ODD = new Color(0.4f, 1f, 0.4f); + + private final AttendanceTable table; + + private Font tableFont; + + private float destScroll = 0f; + private float currScroll = 0f; + private int cellHeight = 25; + private int indexColumnWidth = 0; + private int attendanceColumnWidth = 0; + + private Member highlightMember; + private int highlightTimer; + private int highlightTimerMax = 120; + + private int scrollTimer = 0; + private int scrollAcc = 0; + + private Rectangle dimension; + private int maxScroll; + + public TableRenderer(AttendanceTable table) { + this.table = table; + tableFont = new Font("Arial", Font.BOLD, 12); + + table.addListener(new AttendanceTable.Listener() { + @Override + public void onSignIn(Member member) { + highlightStudent(member); + } + + @Override + public void onSignOut(Member member) { + highlightStudent(member); + } + + @Override + public void nameChanged(Member member) { + highlightStudent(member); + } + + @Override + public void onStudentAdded(Member member) { + highlightStudent(member); + } + }); + } + + public void tick(int time) { + if(highlightTimer == 0) { + highlightMember = null; + highlightTimer = -1; + } + + float speed; + + if(scrollTimer > 0) { + destScroll += scrollAcc * -cellHeight; + destScroll = cellHeight*(Math.round(destScroll/cellHeight)) - 1; + scrollAcc = 0; + speed = 5f; + scrollTimer--; + } else { + if (highlightMember != null) { + destScroll = -(cellHeight * (table.getStudentIndex(highlightMember) - 2)); + speed = 10f; + } else { + destScroll = -(int) (((Math.sin(time / (60f * 4)) + 1) / 2f) * maxScroll); + speed = 20f; + } + } + + if(destScroll > 0) { + destScroll = 0; + } + + if(-destScroll > maxScroll) { + destScroll = -maxScroll; + } + + currScroll += (destScroll - currScroll) / speed; + + if(highlightTimer > 0) { + highlightTimer--; + } + } + + public void drawTable(Graphics2D g) { + if(indexColumnWidth <= 0) { + indexColumnWidth = g.getFontMetrics().stringWidth(Integer.toString(table.getStudentCount())) + 10; + } + + attendanceColumnWidth = (int)((dimension.width - (NAME_COL_WIDTH * 2f) - indexColumnWidth) / table.getAttendanceDates().size()); + + g.setColor(Color.BLACK); + g.setFont(tableFont); + + int cy = cellHeight; + + final AffineTransform oTrans = g.getTransform(); + g.translate(0, currScroll); + for (int r = 0; r < table.getStudentCount(); r++) { + drawRow(g, cy, 0, r); + cy += cellHeight; + } + g.setTransform(oTrans); + + // Draw header row + int xPos = 0; + drawCell(g, xPos, 0, indexColumnWidth, cellHeight, Color.LIGHT_GRAY, Utils.ID_COL); xPos += indexColumnWidth; + drawCell(g, xPos, 0, NAME_COL_WIDTH, cellHeight, Color.LIGHT_GRAY, Utils.LAST_NAME_COL); xPos += NAME_COL_WIDTH; + drawCell(g, xPos, 0, NAME_COL_WIDTH, cellHeight, Color.LIGHT_GRAY, Utils.FIRST_NAME_COL); xPos += NAME_COL_WIDTH; + for(LocalDate date : table.getAttendanceDates()) { + drawCell(g, xPos, 0, attendanceColumnWidth, cellHeight, Color.LIGHT_GRAY, Utils.DATE_FORMATTER.format(date)); + xPos += attendanceColumnWidth; + } + } + + private void drawRow(Graphics g, int cy, int cx, int r) { + final Member member = table.getStudent(r); + + // Start with the default background + Color cellColor; + + // Draw column headers ( ID, First/ Last Name ) + drawCell(g, cx, cy, indexColumnWidth/2, cellHeight, getSafetyFormColor(member), null); + drawCell(g, cx + (indexColumnWidth/2), cy, indexColumnWidth/2, cellHeight, getRegistrationColor(member), null); + cellColor = new Color(0,0,0, 0); + drawCell(g, cx, cy, indexColumnWidth, cellHeight, cellColor, Integer.toString(r + 1)); + cx += indexColumnWidth; + + drawCell(g, cx, cy, NAME_COL_WIDTH, cellHeight, cellColor, member.getLastName()); + cx += NAME_COL_WIDTH; + + drawCell(g, cx, cy, NAME_COL_WIDTH, cellHeight, cellColor, member.getFirstName()); + cx += NAME_COL_WIDTH; + + boolean foundCurrentDate = false; + + // Draw attendance columns + final List attendanceDates = table.getAttendanceDates(); + for (int i = 0; i < attendanceDates.size(); i++) { + final LocalDate date = attendanceDates.get(i); + final boolean isCurrentDateColumn = Objects.equals(date, Settings.getInstance().getDate()); + cellColor = r % 2 == 0 ? Color.LIGHT_GRAY : Color.WHITE; + if (isCurrentDateColumn) { + foundCurrentDate = true; + if (member.isSignedIn(date)) { + if (Settings.getInstance().getLoginType() == LoginType.IN_ONLY || + member.isSignedOut(date)) { + cellColor = Color.GREEN; + } else { + cellColor = Color.ORANGE; + } + } else { + cellColor = CURRENT_DATE_COL; + } + } else if (!foundCurrentDate) { + if (member.isSignedIn(date)) { + cellColor = r % 2 == 0 ? PRESENT_EVEN : PRESENT_ODD; + } else { + cellColor = r % 2 == 0 ? ABSENT_EVEN : ABSENT_ODD; + } + } else { + g.setColor(Color.GRAY); + } + + String cellText = null; + if (Settings.getInstance().getLoginType() == LoginType.IN_OUT && + foundCurrentDate) { + if(member.isSignedOut(date)) { + cellText = "Out: " + Utils.TIME_FORMATTER.format(member.getSignOutTime(date)); + } else if(member.isSignedIn(date)) { + cellText = "In: " + Utils.TIME_FORMATTER.format(member.getSignInTime(date)); + } + } + + drawCell(g, cx, cy, attendanceColumnWidth, cellHeight, cellColor, cellText); + + // Handle Highlighting now. + if (member == highlightMember) { + int localMax = highlightTimerMax / 2; + if (isCurrentDateColumn) { + int timer = highlightTimerMax - highlightTimer - highlightTimerMax / 4; + if (timer < 0) timer = 0; + if (timer > localMax) timer = localMax; + + float th = (timer) / (float) localMax * (float) Math.PI * 2f; + float a = (float) (-Math.cos(th) + 1) / 2f; + + cellColor = new Color(0f, 1f, 0f, a); + drawCell(g, cx, cy, attendanceColumnWidth, cellHeight, cellColor, cellText); + } else if (!foundCurrentDate) { + float thruBar = (float)i / attendanceDates.size(); + + int timer = highlightTimerMax - highlightTimer - (int) (thruBar * highlightTimerMax / 4); + if (timer < 0) timer = 0; + if (timer > localMax) timer = localMax; + + float th = (timer) / (float) localMax * (float) Math.PI * 2f; + float a = (float) (-Math.cos(th) + 1) / 2f; + + cellColor = new Color(0f, 1f, 0f, a / 2f); + drawCell(g, cx, cy, attendanceColumnWidth, cellHeight, cellColor, cellText); + } + } + cx += attendanceColumnWidth; + } + + // Draw column footer (Totals) + } + + private Color getSafetyFormColor(Member member) { + final SafeteyFormState state = member.getSafeteyFormState(); + if(state == null) { + return Color.GRAY; + } + + switch (state) { + default: + case None: + return Color.GRAY; + case Printed: + return Color.CYAN; + case Given: + return Color.RED; + case Signed: + return Color.GREEN; + } + } + + private Color getRegistrationColor(Member member) { + final FirstRegistration registration = member.getRegistration(); + if(registration == null) { + return Color.GRAY; + } + + switch(registration) { + default: + return Color.GRAY; + case None: + return Color.RED; + case MissingWaiver: + return Color.ORANGE; + case Complete: + return Color.GREEN; + } + } + + private void drawCell(Graphics g, int left, int top, int width, int height, Color background, String text) { + g.setColor(background); + g.fillRect(left, top, width, height); + g.setColor(Color.BLACK); + g.drawRect(left, top, width, height); + + if(!Utils.isNullOrEmpty(text)) { + g.drawString(text, left + 4, top + height/2 + g.getFont().getSize()/2); + } + } + + private void highlightStudent(Member member) { + highlightMember = member; + highlightTimer = highlightTimerMax; + scrollTimer = 0; + scrollAcc = 0; + } + + @Override + public void mouseWheelMoved(MouseWheelEvent e) { + scrollAcc = e.getWheelRotation(); + scrollTimer = 120; + } + + public void setDimension(Rectangle dimension) { + this.dimension = dimension; + this.maxScroll = Math.max(0, (cellHeight * (table.getStudentCount() + 1)) - dimension.height + 2); + } +} diff --git a/src/main/java/com/team871/ui/TickListener.java b/src/main/java/com/team871/ui/TickListener.java new file mode 100644 index 0000000..e2a3558 --- /dev/null +++ b/src/main/java/com/team871/ui/TickListener.java @@ -0,0 +1,8 @@ +package com.team871.ui; + +/** + * A listener for tick events from the main program loop; + */ +public interface TickListener { + void tick(long time); +} diff --git a/src/main/java/com/team871/ui/UICanvas.java b/src/main/java/com/team871/ui/UICanvas.java new file mode 100644 index 0000000..b83d284 --- /dev/null +++ b/src/main/java/com/team871/ui/UICanvas.java @@ -0,0 +1,41 @@ +package com.team871.ui; + +import java.awt.*; +import java.awt.image.BufferedImage; + +public class UICanvas extends Canvas { + + BufferedImage img; + + public UICanvas(int w, int h){ + img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + } + + @Override + public void paint(Graphics g) { + paintComponent(g); + } + + public void paintComponent(Graphics g) { + try{ + g.drawImage(img, 0, 0, this); + }catch(NullPointerException e){} + } + + @Override + public void update(Graphics g) { + paintComponent(g); + } + + public Graphics2D getRenderGraphics(){ + return (Graphics2D) img.getGraphics(); + } + + public Dimension getDimensions(){ + return new Dimension(img.getWidth(), img.getHeight()); + } + + public void resizeCanvas(int w, int h){ + img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + } +} diff --git a/src/main/java/com/team871/util/BarcodeUtils.java b/src/main/java/com/team871/util/BarcodeUtils.java new file mode 100644 index 0000000..dcc7663 --- /dev/null +++ b/src/main/java/com/team871/util/BarcodeUtils.java @@ -0,0 +1,99 @@ +package com.team871.util; + +import com.team871.sensing.BarcodeResult; +import net.sourceforge.barbecue.Barcode; +import net.sourceforge.barbecue.BarcodeException; +import net.sourceforge.barbecue.output.OutputException; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +public class BarcodeUtils { + private static final Logger log = LoggerFactory.getLogger(BarcodeUtils.class); + private static final String SETTINGS_CODE_PREFIX = "SET-"; + private static final String ADMIN_PREFIX = "A871L%4$9Z-"; + private static final Map barcodesByCommand = new HashMap<>(); + private static final Map barcodesByName = new HashMap<>(); + + private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("MM/DD"); + + static { + try { + addBarcode("Test Scanner", "T1"); + addBarcode("Set Date", "D8"); + addBarcode("Sign In/Out by Name", "SI"); + addBarcode("Toggle Fullscreen", "FS"); + addBarcode("Correct Name", "CN"); + addBarcode("Add Student", "AS"); + } catch (BarcodeException ex) { + log.error("Failed to add barcode:", ex); + } + } + + private static void addBarcode(String name, String command) throws BarcodeException { + Barcode b = makeBarcode(command, name); + barcodesByCommand.put(command, b); + barcodesByName.put(name, b); + } + + private static Barcode makeBarcode(String command, String label) throws BarcodeException { + Barcode barcode = new Code39Barcode(SETTINGS_CODE_PREFIX + command, false); + barcode.setBackground(Color.LIGHT_GRAY); + barcode.setForeground(Color.BLACK); + barcode.setDrawingText(true); + barcode.setLabel(label); + + return barcode; + } + + public static void drawBarcode(Barcode bar, Graphics2D g, int x, int y) { + try { + bar.draw(g, x, y); + } catch (OutputException e) { + log.error("Failed to draw barcode", e); + } + } + + public static Barcode getBarcode(BarcodeResult br) { + return getBarcode(getSettingsCommand(br)); + } + + public static Barcode getBarcode(String code) { + return barcodesByCommand.get(code); + } + + public static Barcode getBarcodeByName(String name) { + return barcodesByName.get(name); + } + + public static boolean isSettingsCommand(@NotNull BarcodeResult br) { + final String text = br.getText(); + return !Utils.isNullOrEmpty(text) && text.startsWith(SETTINGS_CODE_PREFIX); + } + + public static boolean isAdminCommand(@NotNull BarcodeResult result) { + final String text = result.getText(); + return !Utils.isNullOrEmpty(text) && text.startsWith(ADMIN_PREFIX); + } + + public static String getSettingsCommand(BarcodeResult br) { + return getSuffix(br.getText(), SETTINGS_CODE_PREFIX); + } + + public static String getAdminCommand(BarcodeResult result) { + return getSuffix(result.getText(), ADMIN_PREFIX); + } + + private static String getSuffix(String text, @NotNull String prefix) { + if (Utils.isNullOrEmpty(text) || !text.startsWith(prefix)) { + return null; + } + + return text.substring(prefix.length()); + } +} diff --git a/src/main/java/com/team871/util/Code39Barcode.java b/src/main/java/com/team871/util/Code39Barcode.java new file mode 100644 index 0000000..b564c81 --- /dev/null +++ b/src/main/java/com/team871/util/Code39Barcode.java @@ -0,0 +1,147 @@ +package com.team871.util; + +import net.sourceforge.barbecue.BarcodeException; +import net.sourceforge.barbecue.CompositeModule; +import net.sourceforge.barbecue.Module; +import net.sourceforge.barbecue.SeparatorModule; +import net.sourceforge.barbecue.linear.LinearBarcode; +import net.sourceforge.barbecue.linear.code39.ModuleFactory; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.List; + +/** + * This is a concrete implementation of the Code 39 barcode, AKA 3of9, + * USD-3. + * + * @author Ian Bourke + */ +public class Code39Barcode extends LinearBarcode { + /** + * A list of type identifiers for the Code39 barcode format + */ + public static final String[] TYPES = new String[]{ + "Code39", "USD3", "3of9" + }; + private final boolean requiresChecksum; + + /** + * Constructs a basic mode Code 39 barcode with the specified data and an optional + * checksum. + * + * @param data The data to encode + * @param requiresChecksum A flag indicating whether a checksum is required or not + * @throws BarcodeException If the data to be encoded is invalid + */ + public Code39Barcode(String data, boolean requiresChecksum) throws BarcodeException { + this(data, requiresChecksum, false); + } + + /** + * Constructs an extended mode Code 39 barcode with the specified data and an optional + * checksum. The extended mode encodes all 128 ASCII characters using two character pairs + * from the basic Code 39 character set. Note that most barcode scanners will need to + * be configured to accept extended Code 39. + * + * @param data The data to encode + * @param requiresChecksum A flag indicating whether a checksum is required or not + * @param extendedMode Puts the barcode into extended mode, where all 128 ASCII characters can be encoded + * @throws BarcodeException If the data to be encoded is invalid + */ + public Code39Barcode(String data, boolean requiresChecksum, boolean extendedMode) throws BarcodeException { + super(extendedMode ? encodeExtendedChars(data) : validateBasicChars(data)); + this.requiresChecksum = requiresChecksum; + } + + /** + * Returns the encoded data for the barcode. + * + * @return An array of modules that represent the data as a barcode + */ + protected Module[] encodeData() { + List modules = new ArrayList(); + for (int i = 0; i < data.length(); i++) { + char c = data.charAt(i); + modules.add(new SeparatorModule(1)); + Module module = ModuleFactory.getModule(String.valueOf(c)); + modules.add(module); + } + modules.add(new SeparatorModule(1)); + return (Module[]) modules.toArray(new Module[0]); + } + + /** + * Returns the checksum for the barcode, pre-encoded as a Module. + * + * @return Null if no checksum is required, a Mod-43 calculated checksum otherwise + */ + protected Module calculateChecksum() { + if (requiresChecksum) { + int checkIndex = calculateMod43(data); + CompositeModule compositeModule = new CompositeModule(); + compositeModule.add(ModuleFactory.getModuleForIndex(checkIndex)); + compositeModule.add(new SeparatorModule(1)); + return compositeModule; + } + return null; + } + + /** + * Returns the for the Mod-43 checkIndex for the barcode as an int + * + * @return Mod-43 checkIndex for the given data String + */ + public static int calculateMod43(final String givenData) { + int sum = 0; + StringCharacterIterator iter = new StringCharacterIterator(givenData); + for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { + sum += ModuleFactory.getIndex(String.valueOf(c)); + } + int checkIndex = sum % 43; + return checkIndex; + } + + /** + * Returns the pre-amble for the barcode. + * + * @return ModuleFactory.START_STOP + */ + protected Module getPreAmble() { + return ModuleFactory.START_STOP; + } + + /** + * Returns the post-amble for the barcode. + * + * @return ModuleFactory.START_STOP + */ + protected Module getPostAmble() { + return ModuleFactory.START_STOP; + } + + private static String validateBasicChars(String data) throws BarcodeException { + StringCharacterIterator iter = new StringCharacterIterator(data); + for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { + if (!ModuleFactory.hasModule(String.valueOf(c), false)) { + throw new BarcodeException("Illegal character - try using extended mode if you need " + + "to encode the full ASCII character set"); + } + } + return data; + } + + private static String encodeExtendedChars(String data) { + StringBuffer buf = new StringBuffer(); + StringCharacterIterator iter = new StringCharacterIterator(data); + for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { + if (!ModuleFactory.hasModule(String.valueOf(c), true)) { + buf.append(ModuleFactory.getExtendedCharacter(c)); + } else { + buf.append(c); + } + } + return buf.toString(); + } +} diff --git a/src/main/java/com/team871/util/Settings.java b/src/main/java/com/team871/util/Settings.java new file mode 100644 index 0000000..ed58757 --- /dev/null +++ b/src/main/java/com/team871/util/Settings.java @@ -0,0 +1,151 @@ +package com.team871.util; + +import com.team871.exception.RobotechException; +import com.team871.ui.LoginType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class Settings { + private static final Logger logger = LoggerFactory.getLogger(Settings.class); + private static final Settings INSTANCE = new Settings(); + + private final Properties props; + private final List listeners = new ArrayList<>(); + + public interface Listener { + void onDateChanged(); + } + + public static Settings getInstance() { + return INSTANCE; + } + + private Settings() { + props = new Properties(); + } + + public void addListener(Listener l) { + listeners.add(l); + } + + public void removeListener(Listener l) { + listeners.remove(l); + } + + public void setDate(String date) { + props.setProperty("Date", date); + listeners.forEach(Listener::onDateChanged); + } + + public LocalDate getDate() { + return Utils.getLocalDate(props.getProperty("Date")); + } + + public String getJposXmlPath() { + return props.getProperty("jposPath"); + } + + public Path getSheetPath() { + return Paths.get(props.getProperty("worksheet")); + } + + public boolean getFun() { + return Boolean.getBoolean(props.getProperty("fun")); + } + + public LoginType getLoginType(){ + return LoginType.valueOf(props.getProperty("loginType")); + } + + public String getAttendanceSheet() { + return props.getProperty("attendanceSheet"); + } + + public String getRosterSheet() { + return props.getProperty("rosterSheet"); + } + + public int getRosterHeaderRow() { + return Integer.parseInt(props.getProperty("roster.headerRow")); + } + + public int getRosterFirstDataRow() { + return Integer.parseInt(props.getProperty("roster.firstRow")); + } + + public int getAttendanceHeaderRow() { + return Integer.parseInt(props.getProperty("attendance.headerRow")); + } + + public int getAttendanceFirstDataRow() { + return Integer.parseInt(props.getProperty("attendance.firstRow")); + } + + public String getMentorAttendanceSheet() { + return props.getProperty("mentorSheet"); + } + + public String getMentorRosterSheet() { + return props.getProperty("mentorRSheet"); + } + + public int getMentorRosterHeaderRow() { + return Integer.parseInt(props.getProperty("mentorRos.headerRow")); + } + + public int getMentorRosterFirstDataRow() { + return Integer.parseInt(props.getProperty("mentorRos.firstRow")); + } + + public int getMentorAttendanceHeaderRow() { + return Integer.parseInt(props.getProperty("mentorAtt.headerRow")); + } + + public int getMentorAttendanceFirstDataRow() { + return Integer.parseInt(props.getProperty("mentorAtt.firstRow")); + } + + public void init(String prefsFile) throws RobotechException { + final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("M/d"); + + props.setProperty("Date", LocalDate.now().format(fmt)); + props.setProperty("Fun", "true"); + + final File file = new File(prefsFile); + + if(file.isDirectory()) { + throw new RobotechException("File must not be a directory"); + } + + logger.info("Looking for prefs file at: " + file.getAbsolutePath()); + if (!file.exists()) { + logger.error("Prefs file does not exist"); + throw new RobotechException("Preference file does not exist"); + } + + try(final FileInputStream fis = new FileInputStream(file)) { + props.load(fis); + } catch (IOException e) { + throw new RobotechException("Unable to load preferences.", e); + } + } + + public static boolean isNullOrEmpty(String value) { + return value == null || value.isEmpty(); + } + + public int getAttendanceFirstDataColumn() { + return Integer.parseInt(props.getProperty("attendance.firstColumn")); + } +} diff --git a/src/main/java/com/team871/util/ThrowingRunnable.java b/src/main/java/com/team871/util/ThrowingRunnable.java new file mode 100644 index 0000000..444bede --- /dev/null +++ b/src/main/java/com/team871/util/ThrowingRunnable.java @@ -0,0 +1,6 @@ +package com.team871.util; + +@FunctionalInterface +public interface ThrowingRunnable { + void run(P param) throws E; +} diff --git a/src/main/java/com/team871/util/Utils.java b/src/main/java/com/team871/util/Utils.java new file mode 100644 index 0000000..2f9e9b9 --- /dev/null +++ b/src/main/java/com/team871/util/Utils.java @@ -0,0 +1,25 @@ +package com.team871.util; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +public class Utils { + public static final String FIRST_NAME_COL = "First"; + public static final String LAST_NAME_COL = "Last"; + public static final String ID_COL = "ID"; + public static final String TOTAL_COL = "Total"; + public static final String SAFETY_COL = "Safety"; + public static final String FIRST_REG_COL = "First Reg."; + + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("M/d"); + + public static boolean isNullOrEmpty(String val) { + return val == null || val.isEmpty(); + } + + public static LocalDate getLocalDate(String date) { + final String[] dateParts = date.split("/"); + return LocalDate.of(LocalDate.now().getYear(), Integer.parseInt(dateParts[0]), Integer.parseInt(dateParts[1])); + } +} diff --git a/src/main/resources/README.txt b/src/main/resources/README.txt new file mode 100644 index 0000000..dd8bbf9 --- /dev/null +++ b/src/main/resources/README.txt @@ -0,0 +1,10 @@ +A copy of jpos.xml must be located in the user.home directory + +jpos/res/jpos.properties needs to stay where it is because of the way the JPOS library works. + +The .scncfg files are scanner configurations for 123Scan. + +scan.pxt is the source for the scan sound. + edit with SeaTone(https://www.cavestory.org/downloads/tone_pusher_rev1.zip) or the original PixTone +scan.wav is the exported audio of scan.pxt. +scan_compress.wav is scan.wav in the correct format for 123Scan. \ No newline at end of file diff --git a/src/main/resources/audio/dance0.wav b/src/main/resources/audio/dance0.wav new file mode 100644 index 0000000..597ce49 Binary files /dev/null and b/src/main/resources/audio/dance0.wav differ diff --git a/src/main/resources/audio/dance1.wav b/src/main/resources/audio/dance1.wav new file mode 100644 index 0000000..9354c80 Binary files /dev/null and b/src/main/resources/audio/dance1.wav differ diff --git a/src/main/resources/audio/dance2.wav b/src/main/resources/audio/dance2.wav new file mode 100644 index 0000000..0665af3 Binary files /dev/null and b/src/main/resources/audio/dance2.wav differ diff --git a/src/main/resources/audio/scan.wav b/src/main/resources/audio/scan.wav new file mode 100644 index 0000000..64ba180 Binary files /dev/null and b/src/main/resources/audio/scan.wav differ diff --git a/src/main/resources/audio/scan_compress.wav b/src/main/resources/audio/scan_compress.wav new file mode 100644 index 0000000..dadfb55 Binary files /dev/null and b/src/main/resources/audio/scan_compress.wav differ diff --git a/src/main/resources/audio/test.wav b/src/main/resources/audio/test.wav new file mode 100644 index 0000000..ab8191f Binary files /dev/null and b/src/main/resources/audio/test.wav differ diff --git a/src/main/resources/audio/tim.wav b/src/main/resources/audio/tim.wav new file mode 100644 index 0000000..af9579f Binary files /dev/null and b/src/main/resources/audio/tim.wav differ diff --git a/src/main/resources/audio/tim2.wav b/src/main/resources/audio/tim2.wav new file mode 100644 index 0000000..ada63c7 Binary files /dev/null and b/src/main/resources/audio/tim2.wav differ diff --git a/src/main/resources/config/Config File_DS9908_Config.scncfg b/src/main/resources/config/Config File_DS9908_Config.scncfg new file mode 100644 index 0000000..1874948 Binary files /dev/null and b/src/main/resources/config/Config File_DS9908_Config.scncfg differ diff --git a/src/main/resources/config/Config File_DS9908_Config_KEYBOARD.scncfg b/src/main/resources/config/Config File_DS9908_Config_KEYBOARD.scncfg new file mode 100644 index 0000000..a69df99 Binary files /dev/null and b/src/main/resources/config/Config File_DS9908_Config_KEYBOARD.scncfg differ diff --git a/src/main/resources/config/Config File_DS9908_Config_OPOS.scncfg b/src/main/resources/config/Config File_DS9908_Config_OPOS.scncfg new file mode 100644 index 0000000..4f95951 Binary files /dev/null and b/src/main/resources/config/Config File_DS9908_Config_OPOS.scncfg differ diff --git a/src/main/resources/config/jpos.xml b/src/main/resources/config/jpos.xml new file mode 100644 index 0000000..66a97d0 --- /dev/null +++ b/src/main/resources/config/jpos.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/config/scan.pxt b/src/main/resources/config/scan.pxt new file mode 100644 index 0000000..9bf0f83 --- /dev/null +++ b/src/main/resources/config/scan.pxt @@ -0,0 +1,124 @@ +use :1 +size :2000 +main_model :4 +main_freq :40.00 +main_top :32 +main_offset :0 +pitch_model :4 +pitch_freq :1.00 +pitch_top :31 +pitch_offset :128 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +{1,2000,4,40.00,32,0,4,1.00,31,128,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, + +-> {0,0.00,0,0,0,0.00,32,0,0,0.00,32,0,0,0.00,32,0}, +-> {255,0,255,0,255,0,255,0}, + +pitch2_model :4 +pitch2_freq :1.00 +pitch2_top :31 +pitch2_offset:128 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + diff --git a/src/main/resources/config/test.pxt b/src/main/resources/config/test.pxt new file mode 100644 index 0000000..9080802 --- /dev/null +++ b/src/main/resources/config/test.pxt @@ -0,0 +1,124 @@ +use :1 +size :4000 +main_model :0 +main_freq :110.00 +main_top :32 +main_offset :0 +pitch_model :4 +pitch_freq :1.00 +pitch_top :12 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +use :0 +size :22050 +main_model :0 +main_freq :440.00 +main_top :32 +main_offset :0 +pitch_model :0 +pitch_freq :0.00 +pitch_top :32 +pitch_offset :0 +volume_model :0 +volume_freq :0.00 +volume_top :32 +volume_offset:0 +initialY:63 +ax :64 +ay :63 +bx :128 +by :63 +cx :255 +cy :63 + +{1,4000,0,110.00,32,0,4,1.00,12,0,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, +{0,22050,0,440.00,32,0,0,0.00,32,0,0,0.00,32,0,63,64,63,128,63,255,63}, + +-> {0,0.00,32,0,0,0.00,32,0,0,0.00,32,0,0,0.00,32,0}, +-> {255,0,255,0,255,0,255,0}, + +pitch2_model :4 +pitch2_freq :1.00 +pitch2_top :12 +pitch2_offset:0 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + +pitch2_model :0 +pitch2_freq :0.00 +pitch2_top :32 +pitch2_offset:0 +dx :255 +dy :0 + diff --git a/src/main/resources/img/cool image.png b/src/main/resources/img/cool image.png new file mode 100644 index 0000000..f251e0a Binary files /dev/null and b/src/main/resources/img/cool image.png differ diff --git a/src/main/resources/img/tim.jpg b/src/main/resources/img/tim.jpg new file mode 100644 index 0000000..b87fd5f Binary files /dev/null and b/src/main/resources/img/tim.jpg differ diff --git a/src/main/resources/jpos/res/jpos.properties b/src/main/resources/jpos/res/jpos.properties new file mode 100644 index 0000000..a2ce2ac --- /dev/null +++ b/src/main/resources/jpos/res/jpos.properties @@ -0,0 +1,95 @@ +#------------------------------------------------------------------------------ +# JposTestCase.createPropFile() --> ./jpos/res/jpos.properties file +# Thu Jul 05 11:37:25 EDT 2001 +#------------------------------------------------------------------------------ + +#------------------------------------------------------------------------------ +# Required properties +# ------------------- +# 1) jpos.loader.serviceManagerClass +# +# This property specifies the manager bootstrap class for the whole JCL. Use +# this property to replace the default "simple" JCL implementation with your +# own. The value must be a fully qualified class name that implements the +# interface jpos.loader.JposServiceManager +# +# 2) jpos.config.regPopulatorClass +# +# This property specifies the registry populator class---that populates the +# entry registry. It must be a fully qualified class name that implements +# the jpos.config.JposRegPopulator interface. If you want to specify +# multiple populators then you should instead use the: +# jpos.config.populator.class. +# property---defined below---that allows you to specify many populators +# There are 3 populators that are provided with the JCL by default: +# a) jpos.config.simple.SimpleRegPopulator +# This populator loads/saves JposEntry objects as serialized objects in +# a Java serialized file, typically named: jpos.cfg +# b) jpos.config.simple.xml.XercesRegPopulator +# This populator uses Xerces and implements an XML parser according to +# the jpos/res/jcl.dtd. By default, the XML file must be named jpos.xml +# However, this named can be changed using the property (see below): +# jpos.config.populator.file. +# c) jpos.config.simple.xml.Xerces2RegPopulator +# This populator uses Xerces2 and implements an XML parser according to +# the jpos/res/jcl.xsd XML schema. The file name is same as above; however, +# since this parser expects an XML schema, the XML file header is different. +# See the jpos-schema.xml file. +# +# NOTE: Xerces and Xerces2 are XML parsers from the http://www.apache.org +# Jakarta projects. +#------------------------------------------------------------------------------ + +jpos.loader.serviceManagerClass=jpos.loader.simple.SimpleServiceManager + +#jpos.config.regPopulatorClass=jpos.config.simple.SimpleRegPopulator +jpos.config.regPopulatorClass=jpos.config.simple.xml.SimpleXmlRegPopulator +#jpos.config.regPopulatorClass=jpos.config.simple.xml.Xerces2RegPopulator + +#------------------------------------------------------------------------------ +# Use this property to for the JCL to load a specific file (cfg or XML) +# when not using multiple populators via the jpos.config.populator.class. +# multi-property +#------------------------------------------------------------------------------ + +#jpos.config.populatorFile=jpos1.cfg + +#------------------------------------------------------------------------------ +# To define multiple populator then comment the property +# "jpos.config.regPopulatorClass" +# and use the following multiproperty properties (defines 2 populators one +# XML and one serialized) +#------------------------------------------------------------------------------ + +#jpos.config.populator.class.0=jpos.config.simple.xml.SimpleXmlRegPopulator +#jpos.config.populator.class.1=jpos.config.simple.SimpleRegPopulator + +#------------------------------------------------------------------------------ +# You can also define populator files for each populator as follows +#------------------------------------------------------------------------------ + +#jpos.config.populator.file.0=jpos0.xml +#jpos.config.populator.file.1=jpos1.cfg + +#------------------------------------------------------------------------------ +# Tracing properties +# ------------------ +# All tracing properties that are boolean attributes can be turned on of +# off using ON/on/TRUE/true or OFF/false for any other value. +# The available properties are (see commented properties below): +# 1) jpos.tracing +# This is a legacy property and will turn the global tracer on. You should +# note that it is preferable to use the named tracing property instead +# 2) jpos.com.team871.util.tracing.TurnOnAllNamedTracers +# This property when turned on will enable all named tracers. A named tracer +# will print out a message prepended by [] where is the name of +# the tracer in question. +# 3) jpos.com.team871.util.tracing.TurnOnNamedTracers = name1, name2, ... +# This will turn on the named tracers listed as name1, name2, ... this is +# useful if when all named tracers are on you want to filter the output +#------------------------------------------------------------------------------ + +#jpos.tracing=ON +#jpos.com.team871.util.tracing.TurnOnNamedTracers=XercesRegPopulator, AbstractRegPopulator, MainFrame +#jpos.com.team871.util.tracing.TurnOnNamedTracers=JposServiceLoader,SimpleEntryRegistry,SimpleRegPopulator,XercesRegPopulator +#jpos.com.team871.util.tracing.TurnOnAllNamedTracers=OFF \ No newline at end of file