/***********************************************************************
* mt4j Copyright (c) 2008 - 2009, C.Ruff, Fraunhofer-Gesellschaft All rights reserved.
*
* 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.mt4j.input.inputData;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.mt4j.components.interfaces.IMTComponent3D;
import org.mt4j.input.inputProcessors.componentProcessors.AbstractCursorProcessor;
import org.mt4j.util.math.Vector3D;
/**
* This is a container for AbstractCursorInputEvt Events with a unique ID, identifying the cursor.
* The cursor contains all cursor events of the correspinding cursor (or finger when using multi-touch).
* Also, the cursor allows the input processors to negotiate who has priority to use (lock) this cursor.
* @author Christopher Ruff
*/
public class InputCursor{
private static final Logger logger = Logger.getLogger(InputCursor.class.getName());
private static final int EVENT_HISTORY_DEPTH = 99;
static{
logger.setLevel(Level.ERROR);
SimpleLayout l = new SimpleLayout();
ConsoleAppender ca = new ConsoleAppender(l);
logger.addAppender(ca);
}
/** The events. */
private List<AbstractCursorInputEvt> events;
/** The current id. */
private static long currentID;
/** The ID. */
private long ID;
private TreeMap<AbstractCursorProcessor, Integer> lockSeekingProcessorsToPriority;
private TreeMap<AbstractCursorProcessor, Integer> interestedProcessorsToPriority;
/**
* Instantiates a new input cursor.
*/
public InputCursor(){
this.ID = generateNewID();
events = new ArrayList<AbstractCursorInputEvt>(100);
// events = new LinkedList<AbstractCursorInputEvt>();
lockSeekingProcessorsToPriority = new TreeMap<AbstractCursorProcessor, Integer>(new Comparator<AbstractCursorProcessor>() {
//@Override //TODO make comparater inner clas and reuse
public int compare(AbstractCursorProcessor o1, AbstractCursorProcessor o2) {
if (o1.getLockPriority() < o2.getLockPriority()){
return -1;
}else if (o1.getLockPriority() > o2.getLockPriority()){
return 1;
}else{
if (!o1.equals(o2)
&& o1.getLockPriority() == o2.getLockPriority()){
return -1;
}
return 0;
}
}
});
interestedProcessorsToPriority = new TreeMap<AbstractCursorProcessor, Integer>(new Comparator<AbstractCursorProcessor>() {
//@Override
public int compare(AbstractCursorProcessor o1, AbstractCursorProcessor o2) {
if (o1.getLockPriority() < o2.getLockPriority()){
return -1;
}else if (o1.getLockPriority() > o2.getLockPriority()){
return 1;
}else{
if (!o1.equals(o2)
&& o1.getLockPriority() == o2.getLockPriority()){
return -1;
}
return 0;
}
}
});
}
/**
* Gets the priority by which this cursor is locked.
*
* @return the current lock priority
*/
public int getCurrentLockPriority(){
if (lockSeekingProcessorsToPriority.isEmpty()){
return 0;
}else{
return lockSeekingProcessorsToPriority.lastKey().getLockPriority();
}
}
public boolean canLock(AbstractCursorProcessor ia){
int currentLockPriority = this.getCurrentLockPriority();
if (currentLockPriority == ia.getLockPriority()){
return true;
}else if (currentLockPriority < ia.getLockPriority()){
return true;
}else{ //cursor claimed by higher priority already
return false;
}
}
/**
* Locks this cursor with the specified processor if the processors lock priority
* is higher or equal than the current lock priority of this cursor.
*
* @param ia the AbstractCursorProcessor
*
* @return true if sucessfully locked
*/
public boolean getLock(AbstractCursorProcessor ia){
// if (ia instanceof AbstractCursorProcessor){
// AbstractCursorProcessor a = (AbstractCursorProcessor)ia;
// System.out.println(a.getName() + " trying to LOCK cursor: " + this.getId());
logger.debug(ia.getName() + " trying to LOCK cursor: " + this.getId());
// }
int currentLockPriority = this.getCurrentLockPriority();
if (currentLockPriority == ia.getLockPriority()){
lockSeekingProcessorsToPriority.put(ia, ia.getLockPriority());
logger.debug("Cursor: " + this.getId() + " LOCKED sucessfully, dont send lock signal because cursor was already locked by same priority (" + currentLockPriority + ")");
return true;
}else if (currentLockPriority < ia.getLockPriority()){
lockSeekingProcessorsToPriority.put(ia, ia.getLockPriority());
//FIXME MTInputPositionEvtEST - ONLY KEEPING MTInputPositionEvtHE HIGHEST PRIORITY ANALYZERS - just keep an array with the current highest priority analyzers?
//Just keep the head of the map
//sprich bei drag : 1 entry mit drag
//bei rotate/scale : 2 entries aber kein drag entry mehr gebraucht
SortedMap<AbstractCursorProcessor, Integer> m = lockSeekingProcessorsToPriority.headMap(ia);
Set<AbstractCursorProcessor> k = m.keySet();
for (Iterator<AbstractCursorProcessor> iterator = k.iterator(); iterator.hasNext();) {
AbstractCursorProcessor processor = (AbstractCursorProcessor) iterator.next();
logger.debug("itereating and removing old, lower priority processor: " + processor);
iterator.remove();
}
logger.debug("Cursor: " + this.getId() + " LOCKED sucessfully, send lock signal - Cursor priority was lower " + "(" + currentLockPriority + ")" + " than the gesture priority (" + ia.getLockPriority() + ")");
//send only to ones lower than this priority
cursorLockedByHigherPriorityGesture(ia, ia.getLockPriority());
return true;
}else{ //cursor locked by higher priority already
// lockSeekingAnalyzersToPriority.put(ia, ia.getLockPriority()); //TODO REMOVE?
logger.debug("Cursor: " + this.getId() + " LOCKED UN-sucessfully, send no lock signal - Cursor priority " + "(" + currentLockPriority + ")" + " higher than the gesture priority (" + ia.getLockPriority() + ")");
return false;
}
}
public boolean isLockedBy(AbstractCursorProcessor cp){
// return lockSeekingProcessorsToPriority.containsKey(cp);
for (AbstractCursorProcessor abstractCursorProcessor : lockSeekingProcessorsToPriority.keySet()) {
if (abstractCursorProcessor.equals(cp)){
return true;
}
}
return false;
}
//only do this when previous highest priority strictly < the new priority!
private void cursorLockedByHigherPriorityGesture(AbstractCursorProcessor ia, int gesturePriority){
if (!interestedProcessorsToPriority.isEmpty()){
SortedMap<AbstractCursorProcessor, Integer> lesserPriorityGestureMap = interestedProcessorsToPriority.headMap(ia); //get analyzers with strictly lower priority than the locking one
Set<AbstractCursorProcessor> lesserPriorityGestureKeys = lesserPriorityGestureMap.keySet();
for (Iterator<AbstractCursorProcessor> iterator = lesserPriorityGestureKeys.iterator(); iterator.hasNext();) {
AbstractCursorProcessor processor = (AbstractCursorProcessor) iterator.next();
//Only send lock signal to the processors whos priority is lower than the current locking cursor priority
if (processor instanceof AbstractCursorProcessor){
AbstractCursorProcessor a = (AbstractCursorProcessor)processor;
logger.debug("Cursor: " + this.getId() + " Sending cursor LOCKED signal to: " + a.getName());
}
processor.cursorLocked(this, ia);
}
}
}
//TODO how to call implicitly in analyzters?
/**
* Input processors should call this when new input has started to be
* able to use the cursor locking mechanisms.
*
* @param ia the ia
*/
public void registerForLocking(AbstractCursorProcessor ia) {
interestedProcessorsToPriority.put(ia, ia.getLockPriority());
}
/**
* Input processors should call this when input has ended.
*
* @param ia the ia
*/
public void unregisterForLocking(AbstractCursorProcessor ia){
Set<AbstractCursorProcessor> keys = interestedProcessorsToPriority.keySet();
for (Iterator<AbstractCursorProcessor> iterator = keys.iterator(); iterator.hasNext();) {
AbstractCursorProcessor inputAnalyzer = (AbstractCursorProcessor) iterator.next();
if (inputAnalyzer.equals(ia)){
iterator.remove();
}
}
// if (interestedAnalyzersToPriority.containsKey(ia)){ //FIXME REMOVE, NOT RELIABLE - BUG?
// interestedAnalyzersToPriority.remove(ia);
// }
}
/**
* Unlocks this cursor from the specified processor.
* If the priority by which this cursor is locked changes by that,
* the <code>cursorUnlocked</code> method is invoked on processors
* with a lower priority who by that get a chance to lock this cursor again.
*
* @param ia the AbstractCursorProcessor
*/
public void unlock(AbstractCursorProcessor ia){
logger.debug(ia.getName() + " UNLOCKING cursor: " + this.getId());
int beforeLockPriority = this.getCurrentLockPriority();
int unlockingGesturePriority = ia.getLockPriority();
// if (lockSeekingAnalyzersToPriority.containsKey(ia)){ //FIXME WARUM MANCHE NICHT IN LISTE DIE SEIN SOLLTEN??
// //remove the analyzer from the priority map in any case
// lockSeekingAnalyzersToPriority.remove(ia);
// }
Set<AbstractCursorProcessor> keys = lockSeekingProcessorsToPriority.keySet();
for (Iterator<AbstractCursorProcessor> iterator = keys.iterator(); iterator.hasNext();) {
AbstractCursorProcessor inputProcessor = iterator.next();
if (inputProcessor.equals(ia)){
iterator.remove();
logger.debug("Removed " + ia + " from lockSeekingAnalyzersToPriority list.");
}
}
//dont send released signal if cursor was consumed by higher priority anyway
//should actually not occur because we should only call release when we have a lock on the cursor
if (beforeLockPriority > unlockingGesturePriority){
logger.debug("Trying to unlock cursor, but cursor was already locked by higher priority.");
return;
}
int afterRemoveLockPriority = this.getCurrentLockPriority();
//Only send released signal if the priority really was lowered by releasing (there can be more than 1 lock with the same lock priority)
if (beforeLockPriority > afterRemoveLockPriority){
if (!interestedProcessorsToPriority.isEmpty()){
//Get strictly smaller priority gestures than the one relreasing, so that the ones with same priority dont get a signal
SortedMap<AbstractCursorProcessor, Integer> lesserPriorityGestureMap = interestedProcessorsToPriority.headMap(interestedProcessorsToPriority.lastKey());
// SortedMap<IInputAnalyzer, Integer> lesserPriorityGestureMap = watchingAnalyzersToPriority.headMap(ia);
Set<AbstractCursorProcessor> lesserPriorityGestureKeys = lesserPriorityGestureMap.keySet();
for (Iterator<AbstractCursorProcessor> iterator = lesserPriorityGestureKeys.iterator(); iterator.hasNext();) {
AbstractCursorProcessor processor = (AbstractCursorProcessor) iterator.next();
//Only send released signal to the analyzers whos priority is higher than the current cursor priority
//the current highest priority of the cursor can change when released is called on a gesture that successfully
//locks this cursor, so check each loop iteration
if ( processor.getLockPriority() < unlockingGesturePriority //Only call on gestures with a lower priority than the one releasing the lock
&& this.getCurrentLockPriority() <= processor.getLockPriority() //only call unLocked on analyzers with a lower or equal lockpriority
){
processor.cursorUnlocked(this);
//FIXME funktioniert das, wenn bei claim in anderer geste wieder was in die liste geadded wird etc?
}
}
}
}
}
/**
* Adds the event.
*
* @param te the te
*/
protected void addEvent(AbstractCursorInputEvt te){
this.events.add(te);
// if (events.size() > EVENT_HISTORY_DEPTH && events.size() > 30){
// events.subList(0, 30).clear();
// }
if (events.size() > EVENT_HISTORY_DEPTH ){
events.remove(0);
//logger.debug(this.getId() + " - First event removed!");
// System.out.println("First event removed!");
}
}
/**
* Contains event.
*
* @param te the te
*
* @return true, if successful
*/
public boolean containsEvent(AbstractCursorInputEvt te){
return this.events.contains(te);
}
/**
* Generate new id.
*
* @return the long
*/
synchronized private long generateNewID(){
return currentID++;
}
// /**
// * Gets the events.
// *
// * @return the events
// */
// public MTConcretePositionEvt[] getEvents(){
// return this.events.toArray(new MTConcretePositionEvt[this.events.size()]);
// }
/**
* Gets the events.
*
* @return the events
*/
public List<AbstractCursorInputEvt> getEvents(){
return this.events;
}
/**
* Gets the events.
*
* @param millisAgo the millis ago
*
* @return the events
*/
public List<AbstractCursorInputEvt> getEvents(int millisAgo){
ArrayList<AbstractCursorInputEvt> result = new ArrayList<AbstractCursorInputEvt>();
List<AbstractCursorInputEvt> allEvents = this.getEvents();
long now = System.currentTimeMillis();
// for (int i = 0; i < allEvents.size(); i++) {
// if(now-allEvents.get(i).getWhen()<millisAgo){
// result.add(allEvents.get(i));
// }
// }
for (int i = allEvents.size()-1; i > 0; i--) {
if((now - allEvents.get(i).getWhen()) < millisAgo){
result.add(allEvents.get(i));
}
else{// schleife abbrechen wenn falsch damit rest nicht durchsucht werden muss
break;
}
}
return result;
}
/**
* Gets the last event.
*
* @return the last event
*/
public AbstractCursorInputEvt getCurrentEvent(){
if(this.events.size()==0){
return null;
}else{
return this.events.get(this.getEventCount()-1);
}
}
/**
* Gets the evt before last event.
*
* @return the evt before last event
*/
public AbstractCursorInputEvt getPreviousEvent(){
if(this.events.size()<2){
return null;
}else{
return this.events.get(this.getEventCount()-2);
}
}
/**
* Gets the current events position x.
*
* @return the current events position x
*/
public float getCurrentEvtPosX(){
return this.getCurrentEvent().getPosX();
}
/**
* Gets the current events position y.
*
* @return the current events position y
*/
public float getCurrentEvtPosY(){
return this.getCurrentEvent().getPosY();
}
/**
* Gets the position.
* @return the position
*/
public Vector3D getPosition(){
return new Vector3D(getCurrentEvtPosX(), getCurrentEvtPosY());
}
/**
* Gets the target component of this cursor. (The component the cursor started on)
* or null if it has no target or the cursor has no input events yet.
*
* @return the target
*/
public IMTComponent3D getTarget(){
if (this.getCurrentEvent() != null){
return this.getCurrentEvent().getTargetComponent();
}else{
return null;
}
}
/**
* Gets the start position x.
*
* @return the start position x
*/
public float getStartPosX(){
return this.getFirstEvent().getPosX();
}
/**
* Gets the start position y.
*
* @return the start position y
*/
public float getStartPosY(){
return this.getFirstEvent().getPosY();
}
/**
* Gets the start position.
* @return the start position
*/
public Vector3D getStartPosition(){
return new Vector3D(getStartPosX(), getStartPosY());
}
/**
* Gets the previous event of.
*
* @param te the te
*
* @return the previous event of
*/
public AbstractCursorInputEvt getPreviousEventOf(AbstractCursorInputEvt te){
List<AbstractCursorInputEvt> allEvents = this.getEvents();
AbstractCursorInputEvt returnEvent = null;
// for (int i = 0; i < allEvents.length; i++) {
// T event = allEvents[i];
//
// if (event.equals(te) && allEvents[i-1] != null) {
// returnEvent = allEvents[i-1] ;
// }
// }
// return returnEvent;
for (int i = 0; i < allEvents.size(); i++) {
AbstractCursorInputEvt event = allEvents.get(i);
if (event.equals(te)
&& (allEvents.size() >= 2)
&& i-1 > 0
&& allEvents.get(i-1) != null)
{
returnEvent = allEvents.get(i-1);
}
}
return returnEvent;
}
/**
* Gets the first event.
*
* @return the first event
*/
public AbstractCursorInputEvt getFirstEvent(){
if(this.events.size()==0){
return null;
}else{
return this.events.get(0);
}
}
/**
* Gets the event count.
*
* @return the event count
*/
public int getEventCount(){
return this.events.size();
}
/**
* Gets the id.
*
* @return the id
*/
public long getId() {
return this.ID;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object obj) {
if(obj instanceof InputCursor){
InputCursor compare = (InputCursor)obj;
return this.getId() == compare.getId();
}else{
return false;
}
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return (""+this.ID).hashCode();
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
String s=("Cursor id=" +this.ID) + "\n";
for (int i = 0; i < this.events.size(); i++) {
s += "\t" + i + ": " + this.events.get(i)+ "\n";
}
return s;
}
public void printLockSeekingAnalyzerList() {
Set<AbstractCursorProcessor> claimed = lockSeekingProcessorsToPriority.keySet();
logger.debug("Lock seeking processors list of cursor: " + this.getId());
for (Iterator<AbstractCursorProcessor> iterator = claimed.iterator(); iterator.hasNext();) {
AbstractCursorProcessor inputAnalyzer = (AbstractCursorProcessor) iterator.next();
logger.debug(inputAnalyzer.getClass() + " " + " Priority: " + inputAnalyzer.getLockPriority());
}
}
public void printInterestedAnalyzersList() {
Set<AbstractCursorProcessor> watching = interestedProcessorsToPriority.keySet();
logger.debug("Interested processors list of cursor: " + this.getId());
for (Iterator<AbstractCursorProcessor> iterator = watching.iterator(); iterator.hasNext();) {
AbstractCursorProcessor inputAnalyzer = (AbstractCursorProcessor) iterator.next();
logger.debug(inputAnalyzer.getClass() + " " + " Priority: " + inputAnalyzer.getLockPriority());
}
}
/*
//TODO make velocity time based?
//FIXME EXPERIMENTAL!
public float getVelocityX(){
if (this.events.isEmpty() || this.events.size() < 2)
return 0;
AbstractCursorInputEvt posEvt = events.get(events.size()-1);
AbstractCursorInputEvt prev = events.get(events.size()-2);
if (prev == null)
prev = posEvt;
Vector3D pos = new Vector3D(posEvt.getPosX(), posEvt.getPosY(), 0);
Vector3D prevPos = new Vector3D(prev.getPosX(), prev.getPosY(), 0);
float invWidth = 1.0f/MT4jSettings.getInstance().getScreenWidth();
// System.out.println("Pos: " + pos);
float mouseNormX = pos.x * invWidth;
float mouseVelX = (pos.x - prevPos.x) * invWidth;
System.out.println("Mouse vel X: " + mouseVelX + " mouseNormX:" + mouseNormX);
return mouseVelX;
}
//FIXME EXPERIMENTAL!
public float getVelocityY(){
if (this.events.isEmpty() || this.events.size() < 2)
return 0;
AbstractCursorInputEvt posEvt = events.get(events.size()-1);
AbstractCursorInputEvt prev = events.get(events.size()-2);
if (prev == null)
prev = posEvt;
Vector3D pos = new Vector3D(posEvt.getPosX(), posEvt.getPosY(), 0);
Vector3D prevPos = new Vector3D(prev.getPosX(), prev.getPosY(), 0);
float invHeight = 1.0f/MT4jSettings.getInstance().getScreenHeight();
float mouseNormY = pos.y * invHeight;
float mouseVelY = (pos.y - prevPos.y) * invHeight;
System.out.println("Mouse vel Y: " + mouseVelY + " mouseNormY:" + mouseNormY);
return mouseVelY;
}
*/
public Vector3D getDirection(){
if (this.events.isEmpty() || this.events.size() < 2)
return Vector3D.ZERO_VECTOR;
AbstractCursorInputEvt posEvt = events.get(events.size()-1);
AbstractCursorInputEvt prev = events.get(events.size()-2);
if (prev == null)
prev = posEvt;
//TODO normalize direction or not?
return new Vector3D(posEvt.getPosX() - prev.getPosX(), posEvt.getPosY() - prev.getPosY(), 0);
}
/**
* Calculates and returns the velocity vector.
* The calculation takes the events of the last milliseconds into account.
* The calculation is not physically correct but provides a good vector to use as
* inertia.
*
* @return the velocity vector
*/
public Vector3D getVelocityVector(){
return getVelocityVector(120);
}
/**
* Calculates and returns the velocity vector.
* The calculation takes the events of the last milliseconds into account.
* The calculation is not physically correct but provides a good vector to use as
* inertia.
*
* @param millisAgo the all events from millis ago are taken into calculation
*
* @return the velocity vector
*/
public Vector3D getVelocityVector(int millisAgo){
List<AbstractCursorInputEvt> lastEvents = getEvents(millisAgo);
//System.out.println("Events " + millisAgo + "ms ago: " + lastEvents.size());
float lastX = 0;
float lastY = 0;
float totalX = 0;
float totalY = 0;
for (int i = 0; i < lastEvents.size(); i++) {
AbstractCursorInputEvt ce = lastEvents.get(i);
float x = ce.getPosX();
float y = ce.getPosY();
if (i == 0){
lastX = x;
lastY = y;
}
totalX += x - lastX;
totalY += y - lastY;
lastX = x;
lastY = y;
}
// totalX /= 20f;
// totalY /= 20f;
totalX *= -0.2f;
totalY *= -0.2f;
//works ok with damping float dampingValue = 0.85f; later
//System.out.println("X total: " + totalX);
//System.out.println("Y total: " + totalY);
return new Vector3D(totalX, totalY);
}
// public double getAngleFromStartPoint() {
// if(this.getEventCount()<=1){
// return 0.0;
// }
// else{
// int lastElemIndex = getEventCount()-1;
// CursorEvent[] events = this.getEvents();
// double x1 = events[0].getXRel();
// double x2 = events[lastElemIndex].getXRel();
// double y1 = events[0].getYRel();
// double y2 = events[lastElemIndex].getYRel();
//
// Position p1 = events[0].getPosition();
// Position p2 = events[lastElemIndex].getPosition();
// return AngleUtil.calcAngle(p1,p2);
// }
// }
//
}