/** * Squidy Interaction Library is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation, either version 3 of the License, * or (at your option) any later version. * * Squidy Interaction Library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Squidy Interaction Library. If not, see * <http://www.gnu.org/licenses/>. * * 2009 Human-Computer Interaction Group, University of Konstanz. * <http://hci.uni-konstanz.de> * * Please contact info@squidy-lib.de or visit our website * <http://www.squidy-lib.de> for further information. */ package org.squidy.nodes; import javax.xml.bind.annotation.XmlAttribute; import javax.xml.bind.annotation.XmlType; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.squidy.manager.controls.CheckBox; import org.squidy.manager.controls.TextField; import org.squidy.manager.data.DataConstant; import org.squidy.manager.data.IData; import org.squidy.manager.data.Processor; import org.squidy.manager.data.Property; import org.squidy.manager.data.Processor.Status; import org.squidy.manager.data.impl.DataButton; import org.squidy.manager.data.impl.DataPosition2D; import org.squidy.manager.model.AbstractNode; import org.squidy.nodes.easyclick.DataHistory; /** * TODO: needs to be refreshed (check if wait/notify is possible instead of sleep) * * <code>EasyClick</code>. * * <pre> * Date: Feb 12, 2008 * Time: 1:39:46 AM * </pre> * * @author Werner Koenig, werner.koenig@uni-konstanz.de, University of Konstanz * @author Roman Rädle, <a * href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>, University * of Konstanz * @version $Id: EasyClick.java 772 2011-09-16 15:39:44Z raedle $ * @since 1.0.0 */ @XmlType(name = "EasyClick") @Processor( name = "Easy Click", icon = "/org/squidy/nodes/image/48x48/tactilefinger.png", description = "/org/squidy/nodes/html/EasyClick.html", types = { Processor.Type.FILTER }, tags = {"click", "drift", "shift" }, status = Status.UNSTABLE ) public class EasyClick extends AbstractNode { // Logger to log info, error, debug,... messages. private static final Log LOG = LogFactory.getLog(EasyClick.class); // Defined data constants. public static final DataConstant EASY_CLICKED = DataConstant.get(Boolean.class, "EASY_CLICKED"); // ################################################################################ // BEGIN OF ADJUSTABLES // ################################################################################ @XmlAttribute(name = "history-size") @Property( name = "History size", description = "The size of the position history." ) @TextField private int historySize = 1000; /** * @return the historySize */ public final int getHistorySize() { return historySize; } /** * @param historySize * the historySize to set */ public final void setHistorySize(int historySize) { this.historySize = historySize; } @XmlAttribute(name = "history-time-difference-x") @Property( name = "History time difference X", description = "The history time difference in x-coordinate direction." ) @TextField private long historyTimeDifferenceX = 20; /** * @return the historyTimeDifferenceX */ public final long getHistoryTimeDifferenceX() { return historyTimeDifferenceX; } /** * @param historyTimeDifferenceX * the historyTimeDifferenceX to set */ public final void setHistoryTimeDifferenceX(long historyTimeDifferenceX) { this.historyTimeDifferenceX = historyTimeDifferenceX; } @XmlAttribute(name = "history-time-difference-y") @Property( name = "History time difference Y", description = "The history time difference in y-coordinate direction." ) @TextField private long historyTimeDifferenceY = 150; /** * @return the historyTimeDifferenceY */ public final long getHistoryTimeDifferenceY() { return historyTimeDifferenceY; } /** * @param historyTimeDifferenceY * the historyTimeDifferenceY to set */ public final void setHistoryTimeDifferenceY(long historyTimeDifferenceY) { this.historyTimeDifferenceY = historyTimeDifferenceY; } @XmlAttribute(name = "history-time-threshold-x") @Property( name = "History time threshold X", description = "The history time threshold in x-coordinate direction." ) @TextField private int histTimeThresholdX = 20; /** * @return the histTimeThresholdX */ public final int getHistTimeThresholdX() { return histTimeThresholdX; } /** * @param histTimeThresholdX * the histTimeThresholdX to set */ public final void setHistTimeThresholdX(int histTimeThresholdX) { this.histTimeThresholdX = histTimeThresholdX; } @XmlAttribute(name = "history-time-threshold-y") @Property( name = "History time threshold Y", description = "The history time threshold in y-coordinate direction." ) @TextField private int histTimeThresholdY = 20; /** * @return the histTimeThresholdY */ public final int getHistTimeThresholdY() { return histTimeThresholdY; } /** * @param histTimeThresholdY * the histTimeThresholdY to set */ public final void setHistTimeThresholdY(int histTimeThresholdY) { this.histTimeThresholdY = histTimeThresholdY; } @XmlAttribute(name = "max-distance-x") @Property( name = "Maximum distance X", description = "The maximum distance in x-direction." ) @TextField private double maxDistanceX = 0.01; /** * @return the maxDistanceX */ public final double getMaxDistanceX() { return maxDistanceX; } /** * @param maxDistanceX * the maxDistanceX to set */ public final void setMaxDistanceX(double maxDistanceX) { this.maxDistanceX = maxDistanceX; } @XmlAttribute(name = "max-distance-y") @Property( name = "Maximum distance Y", description = "The maximum distance in y-direction." ) @TextField private double maxDistanceY = 0.01; /** * @return the maxDistanceY */ public final double getMaxDistanceY() { return maxDistanceY; } /** * @param maxDistanceY * the maxDistanceY to set */ public final void setMaxDistanceY(double maxDistanceY) { this.maxDistanceY = maxDistanceY; } @XmlAttribute(name = "timer") @Property( name = "Timer", description = "The time the position sending will be locked." ) @TextField private int timer = 150; /** * @return the timer */ public int getTimer() { return timer; } /** * @param timer * the timer to set */ public void setTimer(int timer) { this.timer = timer; } @XmlAttribute(name = "if-instant-drag") @Property( name = "If instant drag", description = "True if mode changes immidiately to dragging (vs. keeping position)" ) @CheckBox private boolean ifInstantDrag = true; public final boolean getIfInstantDrag() { return ifInstantDrag; } public final void setIfInstantDrag(boolean ifInstantDrag) { this.ifInstantDrag = ifInstantDrag; } // ################################################################################ // END OF ADJUSTABLES // ################################################################################ private DataHistory history; private HistoryTracking historyTracking; private boolean keeping; private boolean waiting; private DataPosition2D clickPos = null; private DataPosition2D lastPos; /* (non-Javadoc) * @see org.squidy.manager.ReflectionProcessable#onStart() */ @Override public void onStart() { history = new DataHistory(historySize); historyTracking = new HistoryTracking(); } /* (non-Javadoc) * @see org.squidy.manager.ReflectionProcessable#onStop() */ @Override public void onStop() { if (historyTracking != null) { historyTracking.finish(); } } /** * @param dataButton * @return */ public synchronized IData process(DataButton dataButton) { if (dataButton.getFlag()) { DataPosition2D tmp = lockPos(); if(tmp!=null && lastPos != null){ publish(lastPos, dataButton, tmp); return null; } } else { if (ifInstantDrag && keeping) { DataPosition2D tmp = clickPos.getClone(); tmp.setTimestamp(System.currentTimeMillis()); publish(lastPos, dataButton, tmp); unlockPos(); return null; } unlockPos(); return dataButton; } return dataButton; } /** * @param dataPosition2D * @return */ public synchronized IData process(DataPosition2D dataPosition2D) { lastPos = dataPosition2D.getClone(); history.process(dataPosition2D); if (keeping && !ifInstantDrag) { return null; } return dataPosition2D; } private synchronized DataPosition2D lockPos() { keeping = true; waiting = false; DataPosition2D last = (DataPosition2D) history.getLastObject(); if (last == null) { unlockPos(); LOG.info("No last object found in history."); return null; } long now = System.currentTimeMillis(); DataPosition2D dataX = getTimeClosedData(last, now - historyTimeDifferenceX, histTimeThresholdX); DataPosition2D dataY = getTimeClosedData(last, now - historyTimeDifferenceY, histTimeThresholdY); if (dataX == null) { dataX = last; LOG.info("No closed object (time) for X found in history."); } if (dataY == null) { dataY = last; LOG.info("No closed object (time) for Y found in history."); } // System.out.println("x:"); double x = getPosClosed(dataX.getX(), last.getX(), maxDistanceX, "x"); // System.out.println("y:"); double y = getPosClosed(dataY.getY(), last.getY(), maxDistanceY, "y"); unlockPosTimer(); clickPos = new DataPosition2D(last.getSource(), x, y); clickPos.setAttribute(EASY_CLICKED, true); return clickPos; } private DataPosition2D getTimeClosedData(DataPosition2D ref, long time, int thres) { DataPosition2D data = (DataPosition2D) history.getObjectAt(time, thres); if (data == null && time < ref.getTimestamp()) { data = getTimeClosedData(ref, time + thres, thres); LOG.info("time diff had to be reduced: time:" + time + " ref:" + ref); } return data; } private double getPosClosed(double pos, double ref, double dist, String direction) { double tmp = pos; double delta = ref - pos; if (delta < 0) delta = delta * (-1.0); if (delta > dist) { LOG.info("estimated pos too far away in "+direction+"-direction"); // tmp = getPosClosed((pos + ref) / 2.0, ref, dist); // double res = (pos + ref) / 2.0; // LOG.debug("pos had to be interpolated: pos:" + pos + " ref:" + ref + " delta:" + // delta + " res:" + res); return ref; } return tmp; } private synchronized void unlockPos() { if (!isProcessing()) return; keeping = waiting = false; } private synchronized void unlockPosTimer() { waiting = true; historyTracking.unlock(); } private class HistoryTracking extends Thread { // Allows locking and unlocking history tracking. private Object lock = new Object(); private boolean running = true; private HistoryTracking() { start(); } public void unlock() { synchronized (lock) { lock.notify(); } } public void finish() { running = false; synchronized (lock) { lock.notify(); } } /* * (non-Javadoc) * * @see java.lang.Thread#run() */ @Override public void run() { while (running) { synchronized (lock) { try { lock.wait(); for (int i = 0; i < timer && waiting; i++) { Thread.sleep(1); } if (waiting) { keeping = waiting = false; } } catch (InterruptedException e) { if (LOG.isErrorEnabled()) { LOG.error(e.getMessage(), e); } } } } } } }