package edu.ncsu.csc.itrust.action; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import edu.ncsu.csc.itrust.beans.DiagnosisBean; import edu.ncsu.csc.itrust.beans.DiagnosisStatisticsBean; import edu.ncsu.csc.itrust.dao.DAOFactory; import edu.ncsu.csc.itrust.dao.mysql.DiagnosesDAO; import edu.ncsu.csc.itrust.dao.mysql.ICDCodesDAO; import edu.ncsu.csc.itrust.exception.DBException; import edu.ncsu.csc.itrust.exception.FormValidationException; import edu.ncsu.csc.itrust.exception.ITrustException; /** * Used for the View Diagnosis Statistics page. Can return a list of all Diagnoses * and get diagnosis statistics for a specified Zip code, Diagnosis code, and date range. */ public class ViewDiagnosisStatisticsAction { /** Database access methods for ICD codes (diagnoses) */ private ICDCodesDAO icdDAO; /** Database access methods for diagnosis information */ private DiagnosesDAO diagnosesDAO; /** ICD Code for malaria */ private static final String ICD_MALARIA = "84.50"; /** ICD Code for Influenza */ private static final String ICD_INFLUENZA = "487.00"; /** * Parses a string date into a java.util.Date object. Also checks that the date * is in the format MM/dd/yyyy and ensures that it is not in the future. * @param dateString string representation of the date * @return parsed date as a java.util.Date object * @throws FormValidationException if the date is invalid */ private Date parseDate(String dateString) throws FormValidationException { Date next; try { DateFormat formatter = new SimpleDateFormat("MM/dd/yyyy"); formatter.setLenient(false); next = formatter.parse(dateString); } catch (ParseException e) { throw new FormValidationException("Enter dates in MM/dd/yyyy"); } if (next.after(new Date())) { throw new FormValidationException("Provided date is in the future."); } return next; } /** * Throws an exception if the string provided is not a valid zip code. * @param zip zip code as a string * @throws FormValidationException if the zip code is invalid */ private void validateZipCode(String zip) throws FormValidationException { if (!zip.matches("([0-9]{5})|([0-9]{5}-[0-9]{4})")) { throw new FormValidationException("Zip Code must be 5 digits!"); } } /** * Throws an exception if the ICDCode is not valid. * @param icdCode the ICD code * @throws ITrustException wrapper for a DB error * @throws FormValidationException if the ICD code is not valid */ private void validateIcdCode(String icdCode) throws ITrustException, FormValidationException { boolean validCode = false; for(DiagnosisBean diag : getDiagnosisCodes()) { if (diag.getICDCode().equals(icdCode)) { validCode = true; } } if (validCode == false) { throw new FormValidationException("ICDCode must be valid diagnosis!"); } } /** * Constructor for the action. Initializes DAO fields * @param factory The session's factory for DAOs */ public ViewDiagnosisStatisticsAction(DAOFactory factory) { this.icdDAO = factory.getICDCodesDAO(); this.diagnosesDAO = factory.getDiagnosesDAO(); } /** * Gets all the diagnosis codes in iTrust and returns them in a list of beans. * * @return List of DiagnosisBeans correlating to all ICDCodes * @throws ITrustException */ public List<DiagnosisBean> getDiagnosisCodes() throws ITrustException { return icdDAO.getAllICDCodes(); } /** * Gets the counts of local and regional diagnoses for the specified input * * @param lowerDate The beginning date for the time range * @param upperDate The ending date for the time range * @param icdCode The diagnosis code to examine * @param zip The zip code to examine * @return A bean containing the local and regional counts * @throws FormValidationException * @throws ITrustException */ public DiagnosisStatisticsBean getDiagnosisStatistics(String lowerDate, String upperDate, String icdCode, String zip) throws FormValidationException, ITrustException { DiagnosisStatisticsBean dsBean; if (lowerDate == null || upperDate == null || icdCode == null) { return null; } validateZipCode(zip); validateIcdCode(icdCode); Date lower = parseDate(lowerDate); Date upper = parseDate(upperDate); if (lower.after(upper)) { throw new FormValidationException("Start date must be before end date!"); } dsBean = diagnosesDAO.getDiagnosisCounts(icdCode, zip, lower, upper); return dsBean; } /** * Gets the counts of local and regional diagnoses for the specified input (lower date 8 weeks prior to upper date) * * @param upperDate The ending date for the time range * @param icdCode The diagnosis code to examine * @param zip The zip code to examine * @return A bean containing the local and regional counts * @throws FormValidationException * @throws ITrustException */ public DiagnosisStatisticsBean getDiagnosisStatistics(String upperDate, String icdCode, String zip) throws FormValidationException, ITrustException { DiagnosisStatisticsBean dsBean; try { if (upperDate == null || icdCode == null) return null; Date upper = new SimpleDateFormat("MM/dd/yyyy").parse(upperDate); //calculate lower date by finding the date that is 8 weeks prior to upperDate Calendar cal = Calendar.getInstance(); cal.setTime(upper); cal.add(Calendar.HOUR, -56*24); Date lower = cal.getTime(); if (!zip.matches("([0-9]{5})|([0-9]{5}-[0-9]{4})")) throw new FormValidationException("Zip Code must be 5 digits!"); boolean validCode = false; for(DiagnosisBean diag : getDiagnosisCodes()) { if (diag.getICDCode().equals(icdCode)) validCode = true; } if (validCode == false) { throw new FormValidationException("ICDCode must be valid diagnosis!"); } dsBean = diagnosesDAO.getDiagnosisCounts(icdCode, zip, lower, upper); } catch (ParseException e) { throw new FormValidationException("Enter date in MM/dd/yyyy"); } return dsBean; } public List<DiagnosisStatisticsBean> getDiagnosisTrends(String dateString, String icdCode, String zip) throws FormValidationException, ITrustException { ArrayList<DiagnosisStatisticsBean> dsBean = null; if (dateString == null || icdCode == null) { return null; } validateIcdCode(icdCode); validateZipCode(zip); Date next = parseDate(dateString); dsBean = new ArrayList<>(); Calendar cal = Calendar.getInstance(); cal.setTime(next); for (int i = 0; i < 8; i++) { Date weekDate = cal.getTime(); dsBean.add(diagnosesDAO.getCountForWeekBefore(icdCode, zip, weekDate)); cal.add(Calendar.HOUR, -24*7); } return dsBean; } /** * Gets the local and regional counts for the specified week and calculates the prior average. * * @param startDate a date in the week to analyze * @param icdCode the diagnosis to analyze * @param zip the area to analyze * @param threshold threshold * @return statistics for the week and previous averages * @throws FormValidationException * @throws DBException */ public ArrayList<DiagnosisStatisticsBean> getEpidemicStatistics(String startDate, String icdCode, String zip, String threshold) throws FormValidationException, DBException { if (startDate == null || icdCode == null) { return null; } if (!(icdCode.equals(ICD_MALARIA) || icdCode.equals(ICD_INFLUENZA)) ) { throw new FormValidationException("Invalid ICD code."); } if(ICD_MALARIA.equals(icdCode)){ try { Integer.parseInt(threshold); } catch(NumberFormatException e) { throw new FormValidationException("Threshold must be an integer."); } } validateZipCode(zip); Date lower = parseDate(startDate); DiagnosisStatisticsBean dbWeek = diagnosesDAO.getCountForWeekOf(icdCode, zip, lower); DiagnosisStatisticsBean dbAvg = new DiagnosisStatisticsBean(zip, 0, 0, lower, lower); Calendar cal = Calendar.getInstance(); Date start = diagnosesDAO.findEarliestIncident(icdCode); //start, which is set to earliest incident Calendar startCal = Calendar.getInstance(); if(start != null) startCal.setTime(start); ArrayList<DiagnosisStatisticsBean> ret = new ArrayList<DiagnosisStatisticsBean>(); if (start == null) { ret.add(dbWeek); ret.add(dbAvg); return ret; } cal.setTime(lower); //cal, which is set to lower Calendar lowerCal = Calendar.getInstance(); lowerCal.setTime(lower); int weekOfYr = cal.get(Calendar.WEEK_OF_YEAR); cal.set(Calendar.YEAR, startCal.get(Calendar.YEAR)); //cal's year then gets set to start's year ArrayList<DiagnosisStatisticsBean> dbList = new ArrayList<DiagnosisStatisticsBean>(); boolean differentYears = cal.get(Calendar.YEAR) != lowerCal.get(Calendar.YEAR); while( cal.getTime().before(lower) && differentYears) { dbList.add( diagnosesDAO.getCountForWeekOf(icdCode, zip, cal.getTime()) ); cal.add(Calendar.YEAR, 1); cal.set(Calendar.WEEK_OF_YEAR, weekOfYr); cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY); } long avg = 0; long avgRegion = 0; if (dbList.size() > 0) { for (DiagnosisStatisticsBean d : dbList) { avg += d.getZipStats(); avgRegion += d.getRegionStats(); } avg /= dbList.size(); avgRegion /= dbList.size(); } dbAvg.setRegionStats(avgRegion); dbAvg.setZipStats(avg); ret.add(dbWeek); ret.add(dbAvg); return ret; } /** * Determines if an Influenza Epidemic is happening * * @param weekDate a date in the currently evaluated week * @param zip the zip code to analyze * @return whether or not there is an epidemic * @throws ParseException * @throws DBException */ public boolean isFluEpidemic(String weekDate, String zip) throws ParseException, DBException { Date wkDate = new SimpleDateFormat("MM/dd/yyyy").parse(weekDate); Calendar cal = Calendar.getInstance(); cal.setTime(wkDate); int weekOfYr = cal.get(Calendar.WEEK_OF_YEAR); double threshold = calcInfluenzaThreshold(weekOfYr); DiagnosisStatisticsBean prev1 = diagnosesDAO.getCountForWeekBefore(ICD_INFLUENZA, zip, cal.getTime()); cal.add(Calendar.HOUR, -7*24); DiagnosisStatisticsBean prev2 = diagnosesDAO.getCountForWeekBefore(ICD_INFLUENZA, zip, cal.getTime()); long weekL1 = prev1.getRegionStats(); long weekL2 = prev2.getRegionStats(); boolean epidemicL1 = weekL1 > threshold; boolean epidemicL2 = weekL2 > threshold; return epidemicL1 && epidemicL2; } /** * Calculates the threshold of an influenza epidemic * * @param weekNumber the week of the year * @return the epidemic threshold for flu cases */ private double calcInfluenzaThreshold(double weekNumber) { return 5.34 + 0.271 * weekNumber + 3.45 * Math.sin(2 * Math.PI * weekNumber / 52.0) + 8.41 * Math.cos(2 * Math.PI * weekNumber / 52.0); } /** * Determines whether a Malaria epidemic is happening for two consecutive weeks * prior to the current date. * * @param weekDate a date in the currently evaluated week * @param zip the zip code to analyze * @param thresholdStr the threshold for an epidemic * @return whether or not there is an epidemic * @throws DBException * @throws ParseException */ public boolean isMalariaEpidemic(String weekDate, String zip, String thresholdStr) throws DBException, ParseException { Date wkDate = new SimpleDateFormat("MM/dd/yyyy").parse(weekDate); ArrayList<DiagnosisStatisticsBean> historyWkL1 = new ArrayList<DiagnosisStatisticsBean>(); ArrayList<DiagnosisStatisticsBean> historyWkL2 = new ArrayList<DiagnosisStatisticsBean>(); double threshold = Double.parseDouble(thresholdStr); Calendar cal = Calendar.getInstance(); cal.setTime(wkDate); DiagnosisStatisticsBean current = diagnosesDAO.getCountForWeekOf(ICD_MALARIA, zip, cal.getTime()); DiagnosisStatisticsBean prev1 = diagnosesDAO.getCountForWeekBefore(ICD_MALARIA, zip, cal.getTime()); long weekTotalL1 = prev1.getRegionStats(); cal.add(Calendar.HOUR, -7*24); DiagnosisStatisticsBean prev2 = diagnosesDAO.getCountForWeekBefore(ICD_MALARIA, zip, cal.getTime()); long weekTotalL2 = prev2.getRegionStats(); cal.setTime(wkDate); //Find earliest Malaria Case. Set calendar's year to that year Date startData = diagnosesDAO.findEarliestIncident(ICD_MALARIA); if (startData == null) { if (current.getRegionStats() > 0) { return true; } return false; } Calendar startDateCal = Calendar.getInstance(); startDateCal.setTime(startData); Calendar wkDateCal = Calendar.getInstance(); wkDateCal.setTime(wkDate); cal.set(Calendar.YEAR, startDateCal.get(Calendar.YEAR)); boolean differentYears = cal.get(Calendar.YEAR) != wkDateCal.get(Calendar.YEAR); while (cal.getTime().before(wkDate) && differentYears) { historyWkL1.add( diagnosesDAO.getCountForWeekBefore(ICD_MALARIA, zip, cal.getTime()) ); cal.add(Calendar.HOUR, -7*24); historyWkL2.add( diagnosesDAO.getCountForWeekBefore(ICD_MALARIA, zip, cal.getTime()) ); cal.add(Calendar.HOUR, 7*24); cal.add(Calendar.YEAR, 1); } long totalHistL1 = 0; long totalHistL2 = 0; long countHistL1 = historyWkL1.size(); long countHistL2 = historyWkL1.size(); for (DiagnosisStatisticsBean d : historyWkL1) { totalHistL1 += d.getRegionStats(); } for (DiagnosisStatisticsBean d : historyWkL2) { totalHistL2 += d.getRegionStats(); } double avgL1 = ((double)totalHistL1) / ((double)countHistL1); double avgL2 = ((double)totalHistL2) / ((double)countHistL2); boolean epidemicL1 = (weekTotalL1*100.0) / threshold > avgL1 || (countHistL1 == 0 && weekTotalL1 != 0); boolean epidemicL2 = (weekTotalL2*100.0) / threshold > avgL2 || (countHistL2 == 0 && weekTotalL2 != 0); return epidemicL1 && epidemicL2; } }