/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.ohd.pophealth.evaluator; import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.logging.Level; import java.util.logging.Logger; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.map.JsonMappingException; import org.ohd.pophealth.json.clinicalmodel.Allergy; import org.ohd.pophealth.json.measuremodel.CodedValue; import org.ohd.pophealth.json.measuremodel.Measure; import org.ohd.pophealth.json.measuremodel.PopHealthPatientRecord; import org.ohd.pophealth.json.measuremodel.QualityMeasure; import org.ohd.pophealth.json.clinicalmodel.Record; import org.ohd.pophealth.json.clinicalmodel.Condition; import org.ohd.pophealth.json.clinicalmodel.Encounter; import org.ohd.pophealth.json.clinicalmodel.Goal; import org.ohd.pophealth.json.clinicalmodel.Medication; import org.ohd.pophealth.json.clinicalmodel.Order; import org.ohd.pophealth.json.clinicalmodel.Procedure; import org.ohd.pophealth.json.clinicalmodel.Result; import org.ohd.pophealth.json.clinicalmodel.Test; import org.ohd.pophealth.json.measuremodel.BooleanItem; import org.ohd.pophealth.json.measuremodel.DateItem; import org.ohd.pophealth.json.measuremodel.DateRangeItem; import org.ohd.pophealth.json.measuremodel.Item; import org.ohd.pophealth.json.measuremodel.ValueDateItem; /** * This class handles the evaluation of a <code>Record</code> against a set of * quality measures. * * @author ohdohd */ public class QualityMeasureEvaluator { private final static Logger LOG = Logger.getLogger(QualityMeasureEvaluator.class.getName()); private Record r; //current working Record private PopHealthPatientRecord pop; // current working popHealth result record /** * Evaluate a record against a set of quality measures * * @param record The extracted patient data * @param qList The set of <code>QualityMeasure</code> items to evaluate against * @return a JSON string representing the result of the evaluation as defined * by the <code>QualityMeasure</code> items used. */ public String evaluate(Record record, ArrayList<QualityMeasure> qList) { LOG.log(Level.FINEST, "Evaluating {0} measures", qList.size()); // Set the current working record this.r = record; // Create a new result object which represent the JSON result pop = new PopHealthPatientRecord(); // Set the information about the patient pop.setPatient(r.getPatient()); // Iterate through each quality measure and evaluate against it for (QualityMeasure q : qList) { evaluate(pop, q); } // Reset the working record r = null; try { // TODO set to false for production String jsonResult = pop.toJson(true); pop = null; return jsonResult; } catch (JsonMappingException ex) { Logger.getLogger(QualityMeasureEvaluator.class.getName()).log(Level.SEVERE, null, ex); } catch (JsonGenerationException ex) { Logger.getLogger(QualityMeasureEvaluator.class.getName()).log(Level.SEVERE, null, ex); } catch (IOException ex) { Logger.getLogger(QualityMeasureEvaluator.class.getName()).log(Level.SEVERE, null, ex); } return null; } /* * This method evaluates a single quality measure */ private void evaluate(PopHealthPatientRecord pop, QualityMeasure q) { // Create a map to house the result items LinkedHashMap<String, Item> items = new LinkedHashMap<String, Item>(); // Work through each measure in the quality measure for (Measure m : q.getMeasures()) { // Each type of item might (most likely) needs to be handled differently // Uses the enum Measure.CAT boolean match = false; switch (m.getCategory()) { case Condition: evaluateCondition(m, items); break; case Characteristic: evaluateCondition(m, items); break; case Encounter: evaluateEncounter(m, items); break; case Result: evaluateResult(m, items); break; case VitalSign: // VitalSigns are the same as results evaluateResult(m, items); break; case Medication: evaluateMedication(m, items); break; case Immunization: // Immunizations are the same as Medications evaluateMedication(m, items); break; case PhysicalExam: // Physical Exam items could be either a procedure or result match = evaluateProcedure(m, items); if (!match){ items.remove(m.getName()); evaluateResult(m, items); } break; case Communication: // Communication maybe an encounter or an order match = evaluateEncounter(m, items); if (!match){ items.remove(m.getName()); evaluateOrder(m, items); } break; case Allergy: evaluateAllergy(m, items); break; case Procedure: evaluateProcedure(m, items); break; case Order: evaluateOrder(m, items); break; case Goal: evaluateGoal(m, items); break; default: LOG.log(Level.WARNING, "Found Unknown or Unsupported Category Type [{0}]", m.getCategory()); } LOG.log(Level.FINER, "Adding Quality Measure {0} to popHealth record", q.getId()); } // Add the results of the quality measure evaluation to the result object pop.addMeasureResult(q.getId(), items); } // TODO Pull the category specific evaluations out into another class to allow for // multiple implementations in the future. private boolean evaluateCondition(Measure m, LinkedHashMap<String, Item> items) { LOG.log(Level.FINEST, "Evaluating Measure {0} against conditions", m.getName()); boolean match = false; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Condition c : r.getConditions()) { // TODO Do we need to handle Active vs. Resolved conditions if (codeMatch(m.getCodes(), c.getDescription())) { dL.add(new Long(c.getOnset())); match = true; LOG.log(Level.FINEST, "Match Found for {0} in condition {1}", new Object[]{m.getDescription(), c.getId()}); } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: DateRangeItem dri = new DateRangeItem(); for (Condition c : r.getConditions()) { if (codeMatch(m.getCodes(), c.getDescription())) { dri.addRange(c.getOnset(), c.getResolution()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in condition {1}", new Object[]{m.getDescription(), c.getId()}); } } items.put(m.getName(), dri); break; case ValueDateItem: LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Conditions", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Condition c : r.getConditions()) { if (codeMatch(m.getCodes(), c.getDescription())) { bi.setValue(true); match = true; } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateEncounter(Measure m, LinkedHashMap<String, Item> items) { LOG.log(Level.FINEST, "Evaluating Measure {0} against encounters", m.getName()); boolean match = false; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Encounter e : r.getEncounters()) { if (codeMatch(m.getCodes(), e.getDescription())) { dL.add(new Long(e.getOccured())); match = true; } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: DateRangeItem dri = new DateRangeItem(); for (Encounter e : r.getEncounters()) { if (codeMatch(m.getCodes(), e.getDescription())) { dri.addRange(e.getOccured(), e.getEnded()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in encounter {1}", new Object[]{m.getDescription(), e.getId()}); } } items.put(m.getName(), dri); break; case ValueDateItem: LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Encounters", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Encounter e : r.getEncounters()) { if (codeMatch(m.getCodes(), e.getDescription())) { bi.setValue(true); match = true; } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateProcedure(Measure m, LinkedHashMap<String, Item> items) { // Currently just handles a procedure like an Encounter boolean match = false; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Procedure p : r.getProcedures()) { if (codeMatch(m.getCodes(), p.getDescription())) { dL.add(new Long(p.getOccured())); match = true; } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: DateRangeItem dri = new DateRangeItem(); for (Procedure p : r.getProcedures()) { if (codeMatch(m.getCodes(), p.getDescription())) { dri.addRange(p.getOccured(), p.getEnded()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in encounter {1}", new Object[]{m.getDescription(), p.getId()}); } } items.put(m.getName(), dri); break; case ValueDateItem: LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Procedure", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Procedure p : r.getProcedures()) { if (codeMatch(m.getCodes(), p.getDescription())) { bi.setValue(true); match = true; } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateResult(Measure m, LinkedHashMap<String, Item> items) { boolean match = false; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Result e : r.getResults()) { if (codeMatch(m.getCodes(), e.getDescription())) { dL.add(new Long(e.getCollectionTime())); match = true; } else { for (Test t : e.getTests()) { if (codeMatch(m.getCodes(), t.getDescription())) { dL.add(new Long(t.getCollectionTime())); match = true; } } } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: LOG.log(Level.WARNING, "Processing measure [{0}] and DateRangeItem is not valid for Results", m.getName()); break; case ValueDateItem: ValueDateItem vdi = new ValueDateItem(); for (Result e : r.getResults()) { if (codeMatch(m.getCodes(), e.getDescription())) { // Assume only one test and it contains the value if (e.getTests().size() == 1) { vdi.addValueDate(e.getCollectionTime(), e.getTests().get(0).getValueString()); } match = true; } else { for (Test t : e.getTests()) { if (codeMatch(m.getCodes(), t.getDescription())) { vdi.addValueDate(t.getCollectionTime(), t.getValueString()); match = true; } } } } items.put(m.getName(), vdi); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Result e : r.getResults()) { if (codeMatch(m.getCodes(), e.getDescription())) { bi.setValue(true); match = true; } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateMedication(Measure m, LinkedHashMap<String, Item> items) { boolean match = false; switch (m.getItemType()) { case DateItem: // Assumption: DateItem is always the start date of the medication DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Medication med : r.getMedications()) { if (codeMatch(m.getCodes(), med.getDescription())) { // Assumption: A medication may have been stopped or not dL.add(new Long(med.getStarted())); match = true; LOG.log(Level.FINEST, "Match Found for {0} in medication {1}", new Object[]{m.getDescription(), med.getId()}); } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: DateRangeItem dri = new DateRangeItem(); for (Medication med : r.getMedications()) { if (codeMatch(m.getCodes(), med.getDescription())) { dri.addRange(med.getStarted(), med.getStopped()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in medication {1}", new Object[]{m.getDescription(), med.getId()}); } } items.put(m.getName(), dri); break; case ValueDateItem: // TODO Will there be any valuedate items for medications? LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Medications", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Medication med : r.getMedications()) { if (codeMatch(m.getCodes(), med.getDescription())) { bi.setValue(true); match = true; LOG.log(Level.FINEST, "Match Found for {0} in medication {1}", new Object[]{m.getDescription(), med.getId()}); } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateAllergy(Measure m, LinkedHashMap<String, Item> items) { boolean match = false; switch (m.getItemType()) { case DateItem: // Assumption: DateItem is always the start date of the medication DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Allergy alg : r.getAllergies()) { if (codeMatch(m.getCodes(), alg.getDescription())) { // Assumption: A medication may have been stopped or not dL.add(new Long(alg.getOnset())); match = true; LOG.log(Level.FINEST, "Match Found for {0} in allergy {1}", new Object[]{m.getDescription(), alg.getId()}); } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: DateRangeItem dri = new DateRangeItem(); for (Allergy alg : r.getAllergies()) { if (codeMatch(m.getCodes(), alg.getDescription())) { dri.addRange(alg.getOnset(), alg.getResolution()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in allergy {1}", new Object[]{m.getDescription(), alg.getId()}); } } items.put(m.getName(), dri); break; case ValueDateItem: // TODO Will there be any valuedate items for medications? LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Allergies", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Allergy alg : r.getAllergies()) { if (codeMatch(m.getCodes(), alg.getDescription())) { bi.setValue(true); match = true; LOG.log(Level.FINEST, "Match Found for {0} in allergy {1}", new Object[]{m.getDescription(), alg.getId()}); } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateOrder(Measure m, LinkedHashMap<String, Item> items) { // TODO Finish method - need to check for match in Order.orderrequests boolean match = true; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Order ord : r.getOrders()) { if (codeMatch(m.getCodes(), ord.getDescription())) { // Assumption: A medication may have been stopped or not dL.add(new Long(ord.getOrderDate())); match = true; LOG.log(Level.FINEST, "Match Found for {0} in order {1}", new Object[]{m.getDescription(), ord.getId()}); } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: LOG.log(Level.WARNING, "Processing measure [{0}] and DateRangeItem is not valid for Orders", m.getName()); break; case ValueDateItem: LOG.log(Level.WARNING, "Processing measure [{0}] and ValueDateItem is not valid for Orders", m.getName()); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Order ord : r.getOrders()) { if (codeMatch(m.getCodes(), ord.getDescription())) { bi.setValue(true); match = true; LOG.log(Level.FINEST, "Match Found for {0} in order {1}", new Object[]{m.getDescription(), ord.getId()}); } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } private boolean evaluateGoal(Measure m, LinkedHashMap<String, Item> items) { boolean match = false; switch (m.getItemType()) { case DateItem: DateItem di = new DateItem(); LinkedList<Long> dL = new LinkedList<Long>(); for (Order ord : r.getOrders()) { for (Goal gol : ord.getGoals()) { if (codeMatch(m.getCodes(), gol.getDescription())) { // Assumption: A medication may have been stopped or not dL.add(new Long(gol.getGoalDate())); match = true; LOG.log(Level.FINEST, "Match Found for {0} in goal {1}", new Object[]{m.getDescription(), gol.getId()}); } } } if (!dL.isEmpty()) { di.setDate((Long[]) dL.toArray(new Long[0])); } items.put(m.getName(), di); break; case DateRangeItem: LOG.log(Level.WARNING, "Processing measure [{0}] and DateRangeItem is not valid for Goal", m.getName()); break; case ValueDateItem: ValueDateItem vdi = new ValueDateItem(); for (Order ord : r.getOrders()) { for (Goal gol : ord.getGoals()) { if (codeMatch(m.getCodes(), gol.getDescription())) { // Assumption: A medication may have been stopped or not vdi.addValueDate(gol.getGoalDate(), gol.getValueString()); match = true; LOG.log(Level.FINEST, "Match Found for {0} in goal {1}", new Object[]{m.getDescription(), gol.getId()}); } // TODO Add check for match for order description } } items.put(m.getName(), vdi); break; case BooleanItem: BooleanItem bi = new BooleanItem(); for (Order ord : r.getOrders()) { for (Goal gol : ord.getGoals()) { if (codeMatch(m.getCodes(), gol.getDescription())) { bi.setValue(true); match = true; LOG.log(Level.FINEST, "Match Found for {0} in goal {1}", new Object[]{m.getDescription(), gol.getId()}); } } } // If no condition found BooleanItem.isValue defaults to false items.put(m.getName(), bi); break; default: LOG.log(Level.WARNING, "Non supported ITEM TYPE [{0}]", m.getItemType()); } return match; } /* * Utility method to compare to lists of coded values. Returns true if any code * in one list matches any code in another list */ private boolean codeMatch(ArrayList<CodedValue> mCodes, ArrayList<CodedValue> cCodes) { // TODO Check for perfomance improvement // Bad Big-O notation algorithm, but currently expecting short lists // TODO Need to check for coding system. // Do not now becuase no overlap in SNOMED, ICD9, ICD10 for (CodedValue cm : mCodes) { for (String cmv : cm.getValues()) { for (CodedValue cc : cCodes) { for (String ccv : cc.getValues()) { if (cmv.equalsIgnoreCase(ccv)) { return true; } } } } } // No code match found return false; } }