// Copyright 2012 Georg-August-Universität Göttingen, Germany // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package de.ugoe.cs.autoquest.usability; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import org.apache.commons.math3.stat.inference.ChiSquareTest; import de.ugoe.cs.autoquest.eventcore.Event; import de.ugoe.cs.autoquest.eventcore.gui.TextInput; import de.ugoe.cs.autoquest.eventcore.gui.ValueSelection; import de.ugoe.cs.autoquest.eventcore.guimodel.GUIModel; import de.ugoe.cs.autoquest.eventcore.guimodel.ICheckBox; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElementSpec; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIView; import de.ugoe.cs.autoquest.eventcore.guimodel.IRadioButton; import de.ugoe.cs.autoquest.tasktrees.treeifc.DefaultTaskInstanceTraversingVisitor; import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask; import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTaskInstance; import de.ugoe.cs.autoquest.tasktrees.treeifc.ITask; import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskInstance; import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskModel; import de.ugoe.cs.autoquest.tasktrees.treeifc.IUserSession; /** * TODO comment * * @version $Revision: $ $Date: 16.07.2012$ * @author 2012, last modified by $Author: pharms$ */ public class DefaultValueRule implements UsabilityEvaluationRule { /** */ private static String DEFAULT_VALUE = "DEFAULT_VALUE"; /* * (non-Javadoc) * * @see de.ugoe.cs.usability.UsabilityEvaluationRule#evaluate(TaskTree) */ @Override public UsabilityEvaluationResult evaluate(ITaskModel taskModel) { System.out.println("determining value selection targets"); Set targets = getValueSelectionTargets(taskModel.getTasks()); System.out.println("found " + targets.size() + " targets"); System.out.println("grouping radio buttons and check boxes targets"); condenseRadioButtonAndCheckBoxGroups(targets); System.out.println(targets.size() + " remaining"); System.out.println("calculating statistics"); ValueChangeStatistics statistics = new ValueChangeStatistics(targets); calculateStatistics(taskModel.getUserSessions(), statistics); System.out.println("analyzing statistics"); UsabilityEvaluationResult results = new UsabilityEvaluationResult(taskModel); analyzeStatistics(statistics, results); return results; } /** * */ private void analyzeStatistics(ValueChangeStatistics statistics, UsabilityEvaluationResult results) { System.out.println ("determined " + statistics.getViewsWithValueSelections().size() + " views"); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> int valueSelectionTargetCounter = 0; for (IGUIView view : statistics.getViewsWithValueSelections()) { valueSelectionTargetCounter += statistics.getValueSelectionTargetsInView(view).size(); } if (valueSelectionTargetCounter != statistics.selectedValues.size()) { throw new IllegalStateException("this should not happen"); } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< for (IGUIView view : statistics.getViewsWithValueSelections()) { //System.out.println("view " + view + " has " + // statistics.getValueSelectionTargetsInView(view) + // " value selection targets"); for (ValueSelectionTarget target : statistics.getValueSelectionTargetsInView(view)) { analyzeStatistics (view, target, statistics.getStatisticsForValueSelectionTarget(target), results); } } } /** * */ private void analyzeStatistics(IGUIView view, ValueSelectionTarget target, Map selectedValues, UsabilityEvaluationResult results) { //System.out.println(" analyzing selected values for " + target); long[] observed = new long[selectedValues.size()]; long allObserved = 0; long maxObserved = 0; LinkedList mostOftenSelected = new LinkedList<>(); int i = 0; for (Map.Entry selectedValue : selectedValues.entrySet()) { //System.out.println(" " + selectedValue.getValue() + " \t " + selectedValue.getKey()); observed[i++] = selectedValue.getValue(); allObserved += selectedValue.getValue(); maxObserved = Math.max(maxObserved, selectedValue.getValue()); if (mostOftenSelected.size() > 0) { ListIterator iterator = mostOftenSelected.listIterator(); while (iterator.hasNext()) { if (selectedValues.get(iterator.next()) < selectedValue.getValue()) { iterator.previous(); iterator.add(selectedValue.getKey()); while (mostOftenSelected.size() > 5) { mostOftenSelected.removeLast(); } break; } } } else { mostOftenSelected.add(selectedValue.getKey()); } } double[] expected = new double[observed.length]; double expectedFrequency = ((double) allObserved) / expected.length; for (i = 0; i < expected.length; i++) { expected[i] = expectedFrequency; } if ((expected.length == 1) || (new ChiSquareTest().chiSquareTest(expected, observed, 0.05))) { // values are not equally distributed. // if the default value is most often selected, everything is fine. If not, smell is // detected if (!DEFAULT_VALUE.equals(mostOftenSelected.get(0))) { UsabilitySmellIntensity intensity = UsabilitySmellIntensity.getIntensity ((int) (1000 * maxObserved / allObserved), (int) allObserved, -1); if (intensity != null) { List mostOftenSelectedValues = new ArrayList<>(mostOftenSelected.size()); for (Object oftenSelected : mostOftenSelected) { mostOftenSelectedValues.add (oftenSelected + " (" + (100.0 * selectedValues.get(oftenSelected) / allObserved) + "%)"); } Map parameters = new HashMap(); parameters.put("view", view); parameters.put("guiElement", target); parameters.put("selectedValues", mostOftenSelectedValues); results.addSmell(intensity, UsabilitySmellDescription.GOOD_DEFAULTS, parameters); } } } } /** * */ private Set getValueSelectionTargets(Collection tasks) { Set result = new HashSet<>(); for (ITask task : tasks) { if (task instanceof IEventTask) { for (ITaskInstance instance : task.getInstances()) { if (isValueSelection(instance)) { result.add(newValueSelectionTarget(instance)); } } } } return result; } /** * */ private boolean isValueSelection(ITaskInstance instance) { return (instance instanceof IEventTaskInstance) && ((((IEventTaskInstance) instance).getEvent().getType() instanceof TextInput) || (((IEventTaskInstance) instance).getEvent().getType() instanceof ValueSelection)); } /** * */ private static ValueSelectionTarget newValueSelectionTarget(ITaskInstance instance) { Event event = ((IEventTaskInstance) instance).getEvent(); return new ValueSelectionTarget((IGUIElement) event.getTarget()); } /** * */ private void condenseRadioButtonAndCheckBoxGroups(Set targets) { // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Set viewsBefore = new HashSet(); for (ValueSelectionTarget target : targets) { viewsBefore.add(target.getView()); } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< condenseGuiElementGroups(targets, IRadioButton.class); condenseGuiElementGroups(targets, ICheckBox.class); // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>> TEST IMPLEMENTATION >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Set viewsAfter = new HashSet(); for (ValueSelectionTarget target : targets) { viewsAfter.add(target.getView()); } if (viewsBefore.size() != viewsAfter.size()) { throw new IllegalStateException("this should not happen"); } for (IGUIView view : viewsBefore) { if (!viewsAfter.contains(view)) { throw new IllegalStateException("this should not happen too"); } } // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< TEST IMPLEMENTATION <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< } /** * */ private void condenseGuiElementGroups(Set targets, Class type) { Map> guiElementsInViews = new HashMap<>(); // determine the targets under consideration for (ValueSelectionTarget target : targets) { if (type.isInstance(target.getTarget())) { List guiElementsInView = guiElementsInViews.get(target.getView()); if (guiElementsInView == null) { guiElementsInView = new LinkedList(); guiElementsInViews.put(target.getView(), guiElementsInView); } guiElementsInView.add(target); } } for (Map.Entry> guiElements : guiElementsInViews.entrySet()) { Map> groups = RuleUtils.getGroups(guiElements.getValue(), 3); for (Map.Entry> group : groups.entrySet()) { //System.out.println("replacing"); // it is important to provide the correct view here, as the view of group.getKey() // for HTML stuff may not be correct due to GUI model condensing ValueSelectionTargetGroup valueSelectionTargetGroup = new ValueSelectionTargetGroup(group.getKey(), guiElements.getKey()); for (IGUIElement toBeMerged : group.getValue()) { //System.out.println(targets.size() + " " + toBeMerged.getView() + " " + // toBeMerged); valueSelectionTargetGroup.addTargetToGroup(toBeMerged); targets.remove(toBeMerged); } //System.out.println("with " + valueSelectionTargetGroup.getView() + " " + // valueSelectionTargetGroup); targets.add(valueSelectionTargetGroup); } } } /** * */ private void calculateStatistics(List sessions, final ValueChangeStatistics statistics) { final IGUIView[] currentView = new IGUIView[1]; final List valueChangesInViewDisplay = new ArrayList<>(); for (IUserSession session : sessions) { currentView[0] = null; valueChangesInViewDisplay.clear(); for (final ITaskInstance currentRoot : session) { currentRoot.accept(new DefaultTaskInstanceTraversingVisitor() { @Override public void visit(IEventTaskInstance eventTaskInstance) { if (eventTaskInstance.getEvent().getTarget() instanceof IGUIElement) { IGUIView view = ((IGUIElement) eventTaskInstance.getEvent().getTarget()).getView(); if ((currentView[0] == null) && (view != null)) { currentView[0] = view; valueChangesInViewDisplay.clear(); } else if ((currentView[0] != null) && (!currentView[0].equals(view))) { statistics.addValueChangesInViewDisplay (currentView[0], valueChangesInViewDisplay); currentView[0] = view; valueChangesInViewDisplay.clear(); } } if (isValueSelection(eventTaskInstance)) { valueChangesInViewDisplay.add(eventTaskInstance); } } }); } // add the selected values of the last shown view in the session if (currentView[0] != null) { statistics.addValueChangesInViewDisplay(currentView[0], valueChangesInViewDisplay); } } } /** * */ private static class ValueChangeStatistics { /** */ private Map targetReplacements = new HashMap<>(); /** */ private Map> valueSelectionTargetsInView = new HashMap<>(); /** */ private Map> selectedValues = new HashMap<>(); /** * */ public ValueChangeStatistics(Set targets) { for (ValueSelectionTarget target : targets) { if (target instanceof ValueSelectionTargetGroup) { for (IGUIElement groupElement : (ValueSelectionTargetGroup) target) { targetReplacements.put((ValueSelectionTarget) groupElement, target); } } else { targetReplacements.put((ValueSelectionTarget) target, target); } List targetsInView = valueSelectionTargetsInView.get(target.getView()); if (targetsInView == null) { targetsInView = new LinkedList(); valueSelectionTargetsInView.put(target.getView(), targetsInView); } targetsInView.add((ValueSelectionTarget) target); } } /** * */ private void addValueChangesInViewDisplay(IGUIView view, List valueChanges) { if (!valueSelectionTargetsInView.containsKey(view)) { if (!valueChanges.isEmpty()) { throw new IllegalStateException("this should not happen, as we already " + "recorded the views with possible value " + "changes at this point"); } // view does not contain value changes --> return return; } //System.out.println // ("handling " + valueChanges.size() + " value changes on view " + view); Map lastSelectedValues = new HashMap<>(); // determine the last selected value for each of the value selection targets that were // actively selected by the user for (IEventTaskInstance valueChange : valueChanges) { ValueSelectionTarget target = newValueSelectionTarget(valueChange); Object selectedValue; if (valueChange.getEvent().getType() instanceof TextInput) { selectedValue = ((TextInput) valueChange.getEvent().getType()).getEnteredText(); } else if (valueChange.getEvent().getType() instanceof ValueSelection) { selectedValue = ((ValueSelection) valueChange.getEvent().getType()).getSelectedValue(); if ((target instanceof IRadioButton) || (target instanceof ICheckBox)) { selectedValue = selectedValue + " (" + target + ")"; } } else { throw new IllegalStateException("the implementation needs to be extended to " + "handle further value change types"); } target = targetReplacements.get(target); lastSelectedValues.put(target, selectedValue); } // add the default value selection for the unchanged value selection targets for (ValueSelectionTarget target : valueSelectionTargetsInView.get(view)) { if (!lastSelectedValues.containsKey(target)) { lastSelectedValues.put(target, DEFAULT_VALUE); } } // and now store the statistics for (Map.Entry selectedValue : lastSelectedValues.entrySet()) { Map statistics = selectedValues.get(selectedValue.getKey()); if (statistics == null) { statistics = new HashMap<>(); selectedValues.put(selectedValue.getKey(), statistics); } Integer counter = statistics.get(selectedValue.getValue()); if (counter == null) { statistics.put(selectedValue.getValue(), 1); } else { statistics.put(selectedValue.getValue(), counter + 1); } } } /** * */ private Collection getViewsWithValueSelections() { return valueSelectionTargetsInView.keySet(); } /** * */ private Collection getValueSelectionTargetsInView(IGUIView view) { return valueSelectionTargetsInView.get(view); } /** * */ private Map getStatisticsForValueSelectionTarget (ValueSelectionTarget target) { return selectedValues.get(target); } } /** * */ private static class ValueSelectionTarget implements IGUIElement { /** */ private static final long serialVersionUID = 1L; /** */ private IGUIView view; /** */ private IGUIElement target; /** * */ private ValueSelectionTarget(IGUIElement target) { this.view = target.getView(); this.target = target; } /** * */ private ValueSelectionTarget(IGUIElement target, IGUIView view) { this.view = view; this.target = target; } /** * */ public IGUIElementSpec getSpecification() { return target.getSpecification(); } /** * */ public String getPlatform() { return target.getPlatform(); } /** * */ public IGUIElement getParent() { return target.getParent(); } /** * */ public String getStringIdentifier() { return target.getStringIdentifier(); } /** * */ public GUIModel getGUIModel() { return target.getGUIModel(); } /** * */ public boolean isUsed() { return target.isUsed(); } /** * */ public void markUsed() { target.markUsed(); } /** * */ public boolean equals(Object obj) { if (this == obj) { return true; } else if (obj instanceof ValueSelectionTarget) { ValueSelectionTarget other = (ValueSelectionTarget) obj; return ((this.view == null) ? other.view == null : other.view.equals(this.view) && other.target.equals(this.target)); } return false; } /** * */ public int hashCode() { if (view != null) { return view.hashCode(); } else { return 0; } } /** * */ public void updateSpecification(IGUIElementSpec furtherSpec) { target.updateSpecification(furtherSpec); } /** * */ public void addEqualGUIElement(IGUIElement equalElement) { target.addEqualGUIElement(equalElement); } /** * */ public double getDistanceTo(IGUIElement otherElement) { return target.getDistanceTo(otherElement); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return target.toString(); } /** * */ public IGUIView getView() { return view; } /** * */ private IGUIElement getTarget() { return target; } } /** * */ private static class ValueSelectionTargetGroup extends ValueSelectionTarget implements Iterable { /** */ private static final long serialVersionUID = 1L; /** */ private List groupedTargets = new LinkedList<>(); /** * */ private ValueSelectionTargetGroup(IGUIElement target, IGUIView view) { super(target, view); } /** * */ private void addTargetToGroup(IGUIElement target) { groupedTargets.add(target); } /* (non-Javadoc) * @see java.lang.Iterable#iterator() */ @Override public Iterator iterator() { return groupedTargets.iterator(); } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "group(" + groupedTargets.size() + " targets, view " + super.getView() + ")"; } } }