// 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.text.DecimalFormat; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.commons.math3.stat.inference.ChiSquareTest; import de.ugoe.cs.autoquest.eventcore.Event; import de.ugoe.cs.autoquest.eventcore.IEventTarget; import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonDown; import de.ugoe.cs.autoquest.eventcore.gui.MouseButtonInteraction; import de.ugoe.cs.autoquest.eventcore.gui.MouseClick; import de.ugoe.cs.autoquest.eventcore.gui.Scroll; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement; import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIView; import de.ugoe.cs.autoquest.eventcore.guimodel.ITextArea; import de.ugoe.cs.autoquest.eventcore.guimodel.ITextField; 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.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 DefaultCursorPositioningRule implements UsabilityEvaluationRule { /* * (non-Javadoc) * * @see de.ugoe.cs.usability.UsabilityEvaluationRule#evaluate(TaskTree) */ @Override public UsabilityEvaluationResult evaluate(ITaskModel taskModel) { FirstViewActionStatistics statistics = new FirstViewActionStatistics(); calculateStatistics(taskModel.getUserSessions(), statistics); UsabilityEvaluationResult results = new UsabilityEvaluationResult(taskModel); analyzeStatistics(statistics, results); return results; } /** * */ private void analyzeStatistics(FirstViewActionStatistics statistics, UsabilityEvaluationResult results) { for (Map.Entry> firstActions : statistics.getFirstViewActions().entrySet()) { long[] observed = new long[firstActions.getValue().size()]; long allObservedInView = 0; long maxObserved = 0; int i = 0; IEventTask mostOftenDoneFirst = null; for (Map.Entry firstAction : firstActions.getValue().entrySet()) { observed[i++] = firstAction.getValue(); allObservedInView += firstAction.getValue(); if (maxObserved < firstAction.getValue()) { maxObserved = firstAction.getValue(); mostOftenDoneFirst = firstAction.getKey(); } } double[] expected = new double[firstActions.getValue().size()]; double expectedFrequency = ((double) statistics.getViewOpenedCount(firstActions.getKey())) / expected.length; for (i = 0; i < expected.length; i++) { expected[i] = expectedFrequency; } if ((allObservedInView > 1) && ((expected.length == 1) || (new ChiSquareTest().chiSquareTest(expected, observed, 0.05)))) { // values are not equally distributed. UsabilitySmellIntensity intensity = UsabilitySmellIntensity.getIntensity ((int) (1000 * maxObserved / allObservedInView), (int) allObservedInView, -1); if ((intensity != null) && isCursorPositioningAction(mostOftenDoneFirst)) { Map parameters = new HashMap(); parameters.put("view", firstActions.getKey()); parameters.put("textfield", getTarget(mostOftenDoneFirst)); parameters.put("ratio", new DecimalFormat("#.##").format (100.0 * maxObserved / allObservedInView)); results.addSmell(intensity, UsabilitySmellDescription.MOST_OFTEN_DONE_FIRST, parameters); } } } } /** * */ private boolean isCursorPositioningAction(IEventTask task) { Event event = ((IEventTaskInstance) task.getInstances().iterator().next()).getEvent(); return ((event.getType() instanceof MouseClick) || (event.getType() instanceof MouseButtonDown)) && (((MouseButtonInteraction) event.getType()).getButton() == MouseButtonInteraction.Button.LEFT) && ((event.getTarget() instanceof ITextField) || (event.getTarget() instanceof ITextArea)); } /** * */ private IEventTarget getTarget(IEventTask task) { return ((IEventTaskInstance) task.getInstances().iterator().next()).getEvent().getTarget(); } /** * */ private void calculateStatistics(List sessions, final FirstViewActionStatistics statistics) { final IGUIView[] currentView = new IGUIView[1]; for (IUserSession session : sessions) { for (final ITaskInstance currentRoot : session) { currentRoot.accept(new DefaultTaskInstanceTraversingVisitor() { @Override public void visit(IEventTaskInstance eventTaskInstance) { // ignore scrolling to get something semantically helpful if ((eventTaskInstance.getEvent().getTarget() instanceof IGUIElement) && (!(eventTaskInstance.getEvent().getType() instanceof Scroll))) { IGUIView view = ((IGUIElement) eventTaskInstance.getEvent().getTarget()).getView(); if (((currentView[0] == null) && (view != null)) || ((currentView[0] != null) && (!currentView[0].equals(view)))) { currentView[0] = view; statistics.addFirstViewAction (view, eventTaskInstance.getEventTask()); } } } }); } } } /** * */ private class FirstViewActionStatistics { /** */ private Map> firstViewActions = new HashMap<>(); /** * */ private void addFirstViewAction(IGUIView view, IEventTask action) { Map counterMap = firstViewActions.get(view); if (counterMap == null) { counterMap = new HashMap<>(); firstViewActions.put(view, counterMap); } Integer counter = counterMap.get(action); if (counter == null) { counterMap.put(action, 1); } else { counterMap.put(action, counter + 1); } } /** * */ private Map> getFirstViewActions() { return firstViewActions; } /** * */ private int getViewOpenedCount(IGUIView view) { Map counterMap = firstViewActions.get(view); int count = 0; for (Integer counter : counterMap.values()) { count += counter; } return count; } } }