/* * This file is part of ADDIS (Aggregate Data Drug Information System). * ADDIS is distributed from http://drugis.org/. * Copyright © 2009 Gert van Valkenhoef, Tommi Tervonen. * Copyright © 2010 Gert van Valkenhoef, Tommi Tervonen, Tijs Zwinkels, * Maarten Jacobs, Hanno Koeslag, Florin Schimbinschi, Ahmad Kamal, Daniel * Reid. * Copyright © 2011 Gert van Valkenhoef, Ahmad Kamal, Daniel Reid, Florin * Schimbinschi. * Copyright © 2012 Gert van Valkenhoef, Daniel Reid, Joël Kuiper, Wouter * Reckman. * Copyright © 2013 Gert van Valkenhoef, Joël Kuiper. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.drugis.addis.presentation.wizard; import static org.apache.commons.collections15.CollectionUtils.find; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.Collection; import java.util.HashMap; import org.apache.commons.collections15.Predicate; import org.drugis.addis.entities.AbstractDose; import org.drugis.addis.entities.Domain; import org.drugis.addis.entities.DoseUnit; import org.drugis.addis.entities.Drug; import org.drugis.addis.entities.FixedDose; import org.drugis.addis.entities.FlexibleDose; import org.drugis.addis.entities.UnknownDose; import org.drugis.addis.entities.treatment.Category; import org.drugis.addis.entities.treatment.ChoiceNode; import org.drugis.addis.entities.treatment.DecisionTree; import org.drugis.addis.entities.treatment.DecisionTreeEdge; import org.drugis.addis.entities.treatment.DecisionTreeNode; import org.drugis.addis.entities.treatment.DoseQuantityChoiceNode; import org.drugis.addis.entities.treatment.LeafNode; import org.drugis.addis.entities.treatment.RangeEdge; import org.drugis.addis.entities.treatment.TreatmentCategorization; import org.drugis.addis.presentation.DecisionTreeChildModel; import org.drugis.addis.presentation.DecisionTreeOutEdgesModel; import org.drugis.addis.presentation.DoseUnitPresentation; import org.drugis.addis.presentation.ModifiableHolder; import org.drugis.addis.presentation.ValueHolder; import org.drugis.addis.presentation.ValueModelWrapper; import org.drugis.common.EqualsUtil; import org.drugis.common.beans.AffixedObservableList; import org.drugis.common.beans.ContentAwareListModel; import org.drugis.common.beans.FilteredObservableList; import org.drugis.common.beans.TransformOnceObservableList; import org.drugis.common.beans.TransformedObservableList.Transform; import org.drugis.common.beans.ValueEqualsModel; import org.drugis.common.validation.BooleanNotModel; import org.drugis.common.validation.PropertyUniqueModel; import com.jgoodies.binding.PresentationModel; import com.jgoodies.binding.list.ObservableList; import com.jgoodies.binding.value.AbstractValueModel; import com.jgoodies.binding.value.ValueModel; import edu.uci.ics.jung.graph.event.GraphEvent; import edu.uci.ics.jung.graph.event.GraphEventListener; import edu.uci.ics.jung.graph.util.Pair; @SuppressWarnings("serial") public class TreatmentCategorizationWizardPresentation extends PresentationModel<TreatmentCategorization> { private final class CategoryUsedModel extends AbstractValueModel { private final Category d_category; private boolean d_value; private CategoryUsedModel(Category category) { d_category = category; getBean().getDecisionTree().getObservableGraph().addGraphEventListener( new GraphEventListener<DecisionTreeNode, DecisionTreeEdge>() { public void handleGraphEvent(GraphEvent<DecisionTreeNode, DecisionTreeEdge> evt) { update(); } }); d_knownDoseChoice.addValueChangeListener( new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { update(); } }); d_value = calculate(); } protected void update() { boolean oldValue = d_value; d_value = calculate(); fireValueChange(oldValue, d_value); } private boolean calculate() { DecisionTreeNode knownDose = d_knownDoseChoice.getValue(); if (knownDose instanceof LeafNode) { LeafNode leafNode = (LeafNode) knownDose; return EqualsUtil.equal(leafNode.getCategory(), d_category) || isLeafNodeReachable(getEdgeTarget(findTypeEdge(UnknownDose.class)), d_category); } if (knownDose.equals(CategorySpecifiers.DO_NOT_CONSIDER)) { return isLeafNodeReachable(getEdgeTarget(findTypeEdge(FixedDose.class)), d_category) || isLeafNodeReachable(getEdgeTarget(findTypeEdge(UnknownDose.class)), d_category); } return isLeafNodeReachable(getBean().getDecisionTree().getRoot(), d_category); // CONSIDER } @Override public Object getValue() { return d_value; } @Override public void setValue(Object newValue) { throw new UnsupportedOperationException(); } } public static enum CategorySpecifiers implements DecisionTreeNode { CONSIDER("Consider dose type"), DO_NOT_CONSIDER("Do not consider dose type"); private final String d_title; private CategorySpecifiers(final String title) { d_title = title; } @Override public String getName() { return d_title; } @Override public boolean equivalent(DecisionTreeNode o) { if (!(o instanceof CategorySpecifiers)) { return false; } else { return equals(o); } } @Override public String toString() { return getName(); } } private final Domain d_domain; private final ObservableList<Category> d_contentAwareCategories; private final ValueHolder<DecisionTreeNode> d_knownDoseChoice; private final ObservableList<DecisionTreeNode> d_knownDoseOptions; private final ValueModel d_considerDoseType; private final ValueModel d_ignoreDoseType; private final DoseQuantityChoiceNode d_fixedRangeNode; private final DoseQuantityChoiceNode d_flexibleLowerNode; private final DoseQuantityChoiceNode d_flexibleUpperNode; private final ValueModel d_considerFixed; private final ValueModel d_considerFlexibleLower; private final ValueModel d_considerFlexibleUpper; private final HashMap<DecisionTreeEdge, ValueModel> d_choiceForEdge = new HashMap<DecisionTreeEdge, ValueModel>(); private final HashMap<DecisionTreeEdge, ObservableList<DecisionTreeNode>> d_optionsForEdge = new HashMap<DecisionTreeEdge, ObservableList<DecisionTreeNode>>(); private final HashMap<Category, ValueHolder<Boolean>> d_categoryUsed = new HashMap<Category, ValueHolder<Boolean>>(); private ValueModel d_nameAvailableModel; public TreatmentCategorizationWizardPresentation(final TreatmentCategorization bean, final Domain domain) { super(bean); d_domain = domain; d_contentAwareCategories = new ContentAwareListModel<Category>(bean.getCategories()); d_nameAvailableModel = createNameAvailableModel(); // Magic nodes d_fixedRangeNode = new DoseQuantityChoiceNode(FixedDose.class, FixedDose.PROPERTY_QUANTITY, getBean().getDoseUnit()); d_flexibleLowerNode = createMinDoseNode(); d_flexibleUpperNode = createMaxDoseNode(); d_knownDoseChoice = new ModifiableHolder<DecisionTreeNode>(new LeafNode()); d_knownDoseOptions = createOptions( CategorySpecifiers.CONSIDER, CategorySpecifiers.DO_NOT_CONSIDER, d_knownDoseChoice.getValue()); d_considerDoseType = new ValueEqualsModel(d_knownDoseChoice, CategorySpecifiers.CONSIDER); d_ignoreDoseType = new ValueEqualsModel(d_knownDoseChoice, CategorySpecifiers.DO_NOT_CONSIDER); final DecisionTreeEdge unknownDoseEdge = findTypeEdge(UnknownDose.class); d_choiceForEdge.put(unknownDoseEdge, createModelForEdge(unknownDoseEdge)); d_optionsForEdge.put(unknownDoseEdge, createOptions(getEdgeTarget(unknownDoseEdge))); final DecisionTreeEdge fixedDoseEdge = findTypeEdge(FixedDose.class); d_choiceForEdge.put(fixedDoseEdge, createModelForEdge(fixedDoseEdge)); d_optionsForEdge.put(fixedDoseEdge, createOptions(d_fixedRangeNode, getEdgeTarget(fixedDoseEdge))); final DecisionTreeEdge flexibleDoseEdge = findTypeEdge(FlexibleDose.class); d_choiceForEdge.put(flexibleDoseEdge, createModelForEdge(flexibleDoseEdge)); d_optionsForEdge.put(flexibleDoseEdge, createOptions(d_flexibleLowerNode, d_flexibleUpperNode, getEdgeTarget(flexibleDoseEdge))); d_considerFixed = new ValueEqualsModel(getModelForFixedDose(), d_fixedRangeNode); d_considerFlexibleLower = new ValueEqualsModel(getModelForFlexibleDose(), d_flexibleLowerNode); d_considerFlexibleUpper = new ValueEqualsModel(getModelForFlexibleDose(), d_flexibleUpperNode); } public boolean isLeafNodeReachable(DecisionTreeNode node, Category category) { if (node == null) { return false; } if (node instanceof LeafNode) { LeafNode leafNode = (LeafNode) node; return EqualsUtil.equal(leafNode.getCategory(), category); } if (node instanceof ChoiceNode) { for (DecisionTreeEdge edge : getBean().getDecisionTree().getOutEdges(node)) { if (isLeafNodeReachable(getEdgeTarget(edge), category)) { return true; } } } return false; } private ValueHolder<Boolean> createCategoryNotUsedModel(final Category category) { return new ValueModelWrapper<Boolean>(new BooleanNotModel(new CategoryUsedModel(category))); } private DecisionTreeNode getEdgeTarget(final DecisionTreeEdge edge) { return getBean().getDecisionTree().getEdgeTarget(edge); } public ValueModel getDrug() { return getModel(TreatmentCategorization.PROPERTY_DRUG); } public ValueModel getName() { return getModel(TreatmentCategorization.PROPERTY_NAME); } public ObservableList<Category> getCategories() { return getBean().getCategories(); } public DoseUnit getDoseUnit() { return getBean().getDoseUnit(); } public DoseUnitPresentation getDoseUnitPresentation() { return new DoseUnitPresentation(getDoseUnit()); } /** * Add the DosedDrugTreatment to the domain. Throws an exception if the treatment is already in the domain. * Note that domain can be null in which case a null-pointer exception will occur. * @return The DosedDrugTreatment that was added. */ public TreatmentCategorization commit() { if (d_domain.getTreatmentCategorizations().contains(getBean())) { throw new IllegalStateException("Treatment already exists in domain"); } d_domain.getTreatmentCategorizations().add(getBean()); return getBean(); } public ObservableList<DecisionTreeEdge> getOutEdges(final DecisionTreeNode node) { return new DecisionTreeOutEdgesModel(getBean().getDecisionTree(), node); } public String getCategory(final AbstractDose dose) { return getBean().getCategory(dose).toString(); } /** * ValueModel that holds the decision (DecisionTreeNode) for the given edge. */ private ValueModel createModelForEdge(final DecisionTreeEdge edge) { final DecisionTree tree = getBean().getDecisionTree(); final DecisionTreeChildModel model = new DecisionTreeChildModel(tree, edge); return model; } public DecisionTreeEdge findTypeEdge(final Class<?> type) { final DecisionTree tree = getBean().getDecisionTree(); return getBean().getDecisionTree().findMatchingEdge(tree.getRoot(), type); } public ValueModel getModelForEdge(final DecisionTreeEdge edge) { if (d_choiceForEdge.get(edge) == null) { d_choiceForEdge.put(edge, createModelForEdge(edge)); } return d_choiceForEdge.get(edge); } /** * Selection holder for action on unknown doses. */ public ValueModel getModelForUnknownDose() { return getModelForEdge(findTypeEdge(UnknownDose.class)); } /** * Selection holder for action on "known" doses (fixed or flexible). */ public ValueModel getModelForKnownDose() { return d_knownDoseChoice; } /** * Selection holder for action on fixed doses. */ public ValueModel getModelForFixedDose() { return getModelForEdge(findTypeEdge(FixedDose.class)); } /** * Selection holder for action on flexible doses. */ public ValueModel getModelForFlexibleDose() { return getModelForEdge(findTypeEdge(FlexibleDose.class)); } /** * ValueModel (Boolean) that indicates whether fixed and flexible doses should be treated separately. */ public ValueModel getConsiderDoseType() { return d_considerDoseType; } /** * ValueModel (Boolean) that indicates whether fixed and flexible doses should be treated identically. */ public ValueModel getIgnoreDoseType() { return d_ignoreDoseType; } /** * ValueModel (Boolean) that indicates whether quantity should be considered for fixed doses. */ public ValueModel getConsiderFixed() { return d_considerFixed; } /** * ValueModel (Boolean) that indicates whether the MIN_DOSE should be considered first for flexible doses. */ public ValueModel getConsiderFlexibleLowerFirst() { return d_considerFlexibleLower; } /** * ValueModel (Boolean) that indicates whether the MAX_DOSE should be considered first for flexible doses. */ public ValueModel getConsiderFlexibleUpperFirst() { return d_considerFlexibleUpper; } public ObservableList<DecisionTreeEdge> getFlexibleLowerRanges() { return new DecisionTreeOutEdgesModel(getBean().getDecisionTree(), d_flexibleLowerNode); } public ObservableList<DecisionTreeEdge> getFlexibleUpperRanges() { return new DecisionTreeOutEdgesModel(getBean().getDecisionTree(), d_flexibleUpperNode); } public ChoiceNode getFlexibleLowerRangeNode() { return d_flexibleLowerNode; } public ChoiceNode getFlexibleUpperRangeNode() { return d_flexibleUpperNode; } public ChoiceNode getFixedRangeNode() { return d_fixedRangeNode; } /** * A utility for checking if a category is used in the tree. * @param category * @return a ValueModel which is true when the category is used in the tree, false otherwise */ public ValueModel getCategoryUsed(Category category) { if(d_categoryUsed.get(category) == null) { d_categoryUsed.put(category, createCategoryNotUsedModel(category)); } return d_categoryUsed.get(category); } private ObservableList<DecisionTreeNode> createOptions(final DecisionTreeNode ... extraOptions) { final TransformOnceObservableList<Category, DecisionTreeNode> transformedCategories = new TransformOnceObservableList<Category, DecisionTreeNode>( d_contentAwareCategories, new Transform<Category, DecisionTreeNode>() { public DecisionTreeNode transform(final Category a) { return new LeafNode(a); } }); return AffixedObservableList.createSuffixed(transformedCategories, extraOptions); } public ObservableList<DecisionTreeNode> getOptionsForEdge(final DecisionTreeEdge edge) { if (d_optionsForEdge.get(edge) == null) { d_optionsForEdge.put(edge, createOptions(new LeafNode())); } return d_optionsForEdge.get(edge); } public ObservableList<DecisionTreeNode> getOptionsForKnownDose() { return d_knownDoseOptions; } public ObservableList<DecisionTreeNode> getOptionsForUnknownDose() { return getOptionsForEdge(findTypeEdge(UnknownDose.class)); } public ObservableList<DecisionTreeNode> getOptionsForFixedDose() { return getOptionsForEdge(findTypeEdge(FixedDose.class)); } public ObservableList<DecisionTreeNode> getOptionsForFlexibleDose() { return getOptionsForEdge(findTypeEdge(FlexibleDose.class)); } /** * Transform the tree according to the option chosen for "known dose". */ public void transformTree() { Object knownDose = getModelForKnownDose().getValue(); if (knownDose instanceof LeafNode) { transformLeaf(); } else if (knownDose.equals(CategorySpecifiers.DO_NOT_CONSIDER)) { assert(d_fixedRangeNode == getModelForFixedDose().getValue()); transformSubtree(); } } private void transformSubtree() { DecisionTree tree = getBean().getDecisionTree(); getModelForFlexibleDose().setValue(d_flexibleLowerNode); for (DecisionTreeEdge edge : getOutEdges(d_fixedRangeNode)) { DoseQuantityChoiceNode node = new DoseQuantityChoiceNode(FlexibleDose.class, FlexibleDose.PROPERTY_MAX_DOSE, getDoseUnit()); RangeEdge rangeEdge = (RangeEdge) edge; tree.addChild(RangeEdge.copy(rangeEdge), d_flexibleLowerNode, node); LeafNode leafChild = new LeafNode(((LeafNode)tree.getEdgeTarget(edge)).getCategory()); if (leafChild.getCategory() == null) { RangeEdge upperEdge = new RangeEdge(rangeEdge.getLowerBound(), rangeEdge.isLowerBoundOpen(), Double.POSITIVE_INFINITY, true); tree.addChild(upperEdge, node, new LeafNode()); } else { tree.addChild(RangeEdge.copy(rangeEdge), node, leafChild); if (!Double.isInfinite(rangeEdge.getUpperBound())) { RangeEdge upperEdge = new RangeEdge(rangeEdge.getUpperBound(), !rangeEdge.isUpperBoundOpen(), Double.POSITIVE_INFINITY, true); tree.addChild(upperEdge, node, new LeafNode()); } } } } private void transformLeaf() { final Category category = ((LeafNode) getModelForKnownDose().getValue()).getCategory(); getModelForFlexibleDose().setValue(findLeafNode(getOptionsForFlexibleDose(), category)); getModelForFixedDose().setValue(findLeafNode(getOptionsForFixedDose(), category)); } /** * {@link TreatmentCategorization#splitRange(RangeEdge, double, boolean)} */ public Pair<RangeEdge> splitRange(final RangeEdge range, final double value, final boolean lowerRangeOpen) { ObservableList<DecisionTreeNode> options1 = getOptionsForEdge(range); Pair<RangeEdge> ranges = getBean().splitRange(range, value, lowerRangeOpen); // Ensure options list is preserved for first range d_optionsForEdge.remove(range); d_optionsForEdge.put(ranges.getFirst(), options1); // Ensure exclude-node for second range is in the options list DecisionTree tree = getBean().getDecisionTree(); ObservableList<DecisionTreeNode> options2 = createOptionsForChildrenOfNode((ChoiceNode) tree.getEdgeSource(ranges.getSecond())); d_optionsForEdge.put(ranges.getSecond(), options2); getModelForEdge(ranges.getSecond()).setValue(findLeafNode(options2, null)); return ranges; } /** * In the haystack, find a leaf node with the needle as its category. */ public static LeafNode findLeafNode(final Collection<DecisionTreeNode> haystack, final Category needle) { return (LeafNode)find(haystack, new Predicate<DecisionTreeNode>() { public boolean evaluate(DecisionTreeNode object) { if (object instanceof LeafNode) { LeafNode node = (LeafNode) object; return EqualsUtil.equal(node.getCategory(), needle); } return false; } }); } public RangeEdge addDefaultRangeEdge(ChoiceNode node) { RangeEdge rangeEdge = createRangeEdge(node); ObservableList<DecisionTreeNode> options = createOptionsForChildrenOfNode(node); d_optionsForEdge.put(rangeEdge, options); getBean().getDecisionTree().addChild(rangeEdge, node, findLeafNode(options, null)); return rangeEdge; } private RangeEdge createRangeEdge(ChoiceNode node) { DecisionTree tree = getBean().getDecisionTree(); String nodeProperty = node.getPropertyName(); ChoiceNode parentNode = (ChoiceNode)tree.getParent(node); if (EqualsUtil.equal(nodeProperty, FlexibleDose.PROPERTY_MIN_DOSE) && EqualsUtil.equal(parentNode.getPropertyName(), FlexibleDose.PROPERTY_MAX_DOSE)) { RangeEdge range = (RangeEdge) tree.getParentEdge(node); return new RangeEdge(0.0, false, range.getUpperBound(), range.isUpperBoundOpen()); } else if (EqualsUtil.equal(nodeProperty, FlexibleDose.PROPERTY_MAX_DOSE) && EqualsUtil.equal(parentNode.getPropertyName(), FlexibleDose.PROPERTY_MIN_DOSE)) { RangeEdge range = (RangeEdge) tree.getParentEdge(node); return new RangeEdge(range.getLowerBound(), range.isLowerBoundOpen(), Double.POSITIVE_INFINITY, true); } else { return RangeEdge.createDefault(); } } private ObservableList<DecisionTreeNode> createOptionsForChildrenOfNode(ChoiceNode node) { DecisionTree tree = getBean().getDecisionTree(); String nodeProperty = node.getPropertyName(); String parentProperty = ((ChoiceNode)tree.getParent(node)).getPropertyName(); if (nodeProperty.equals(FlexibleDose.PROPERTY_MIN_DOSE) && !parentProperty.equals(FlexibleDose.PROPERTY_MAX_DOSE)) { return createOptions(createMaxDoseNode(), new LeafNode()); } else if (nodeProperty.equals(FlexibleDose.PROPERTY_MAX_DOSE) && !parentProperty.equals(FlexibleDose.PROPERTY_MIN_DOSE)) { return createOptions(createMinDoseNode(), new LeafNode()); } else { return createOptions(new LeafNode()); } } private DoseQuantityChoiceNode createMinDoseNode() { return new DoseQuantityChoiceNode(FlexibleDose.class, FlexibleDose.PROPERTY_MIN_DOSE, getBean().getDoseUnit()); } private DoseQuantityChoiceNode createMaxDoseNode() { return new DoseQuantityChoiceNode(FlexibleDose.class, FlexibleDose.PROPERTY_MAX_DOSE, getBean().getDoseUnit()); } public ValueModel getNameAvailableModel() { return d_nameAvailableModel ; } private ValueModel createNameAvailableModel() { final FilteredObservableList<TreatmentCategorization> categorizations = new FilteredObservableList<TreatmentCategorization>( d_domain.getTreatmentCategorizations(), createDrugFilter((Drug)getDrug().getValue())); getDrug().addValueChangeListener(new PropertyChangeListener() { public void propertyChange(PropertyChangeEvent evt) { categorizations.setFilter(createDrugFilter((Drug)getDrug().getValue())); } }); return new PropertyUniqueModel<TreatmentCategorization>(categorizations, getBean(), TreatmentCategorization.PROPERTY_NAME); } private Predicate<TreatmentCategorization> createDrugFilter(final Drug drug) { return new Predicate<TreatmentCategorization>() { public boolean evaluate(TreatmentCategorization obj) { return EqualsUtil.equal(obj.getDrug(), drug); } }; } }