diff java/src/uk/ac/qmul/eecs/ccmi/haptics/OmniHaptics.java @ 3:9e67171477bc

PHANTOM Omni Heptic device release
author Fiore Martin <fiore@eecs.qmul.ac.uk>
date Wed, 25 Apr 2012 17:09:09 +0100
parents
children d66dd5880081
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/java/src/uk/ac/qmul/eecs/ccmi/haptics/OmniHaptics.java	Wed Apr 25 17:09:09 2012 +0100
@@ -0,0 +1,431 @@
+/*  
+ CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
+  
+ Copyright (C) 2011  Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
+
+ 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 uk.ac.qmul.eecs.ccmi.haptics;
+
+import java.awt.Dimension;
+import java.awt.Toolkit;
+import java.awt.geom.Line2D;
+import java.io.IOException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.ListIterator;
+
+import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter;
+import uk.ac.qmul.eecs.ccmi.utils.OsDetector;
+import uk.ac.qmul.eecs.ccmi.utils.PreferencesService;
+
+/*
+ *
+ * The implementation of the Haptics interface which uses SensableŽ
+ * PHANTOM OmniŽ haptic device.
+ *
+ */
+class OmniHaptics extends Thread implements Haptics {
+	
+	static Haptics createInstance(HapticListener listener) {
+		if(listener == null)
+			throw new IllegalArgumentException("listener cannot be null");
+		
+		if(OsDetector.isWindows()){
+			/* try to load .dll. First copy it in the home/ccmi_editor_data/lib directory  */
+			URL url = OmniHaptics.class.getResource("Haptics.dll");
+			ResourceFileWriter fileWriter = new ResourceFileWriter(url);
+			fileWriter.writeToDisk(
+					PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")),
+					"Haptics.dll");
+			String path = fileWriter.getFilePath();
+			if(path == null)
+				return null;
+			try{
+				System.load( path );
+			}catch(UnsatisfiedLinkError e){
+				return null; 
+			}
+		}else{
+			return null;
+		}
+		
+		OmniHaptics omniHaptics = new OmniHaptics("Haptics");
+		omniHaptics.hapticListener = listener;
+		/* start up the listener which immediately stops, waiting for commands */
+		if(!omniHaptics.hapticListener.isAlive())
+			omniHaptics.hapticListener.start();
+		/* start up the haptics thread which issues commands from the java to the c++ thread */
+		omniHaptics.start();
+		/* wait for the haptics thread (now running native code) to initialize (need to know if initialization is successful) */
+		synchronized(omniHaptics){
+			try {
+				omniHaptics.wait();
+			}catch (InterruptedException ie) {
+				throw new RuntimeException(ie); // must never happen 
+			}
+		}
+		if(omniHaptics.hapticInitFailed){
+				/* the initialization has failed, the haptic thread is about to die */
+				while(!omniHaptics.hapticListener.isInterrupted()){
+					omniHaptics.hapticListener.interrupt();
+				}
+				omniHaptics.hapticListener = null; //leave the listener to the GC
+				return null;
+		}else{ 
+			return omniHaptics;
+		}
+	}
+	
+	private OmniHaptics(String threadName){
+		super(threadName);
+		/* get the screen size which will be passed to init methos in order to set up a window
+		 * for the haptic with the same size as the swing one
+		 */
+		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+
+		int screenWidth = (int)screenSize.getWidth();
+		int screenHeight = (int)screenSize.getHeight();
+
+		width =  screenWidth * 5 / 8;
+		height = screenHeight * 5 / 8;
+		
+		newHapticId = false;
+		dumpHapticId = false;
+		shutdown = false;
+		hapticInitFailed = false;
+		nodes = new HashMap<String,ArrayList<Node>>();
+		edges = new HashMap<String,ArrayList<Edge>>();
+		currentNodes = EMPTY_NODE_LIST;
+		currentEdges = EMPTY_EDGE_LIST;
+		nodesMaps = new HashMap<String,HashMap<Integer,Node>>();
+		edgesMaps = new HashMap<String,HashMap<Integer,Edge>>();
+		currentNodesMap = EMPTY_NODE_MAP;
+		currentEdgesMap = EMPTY_EDGE_MAP;
+	}	
+	
+	
+	@Override
+	public native int init(int width, int height) throws IOException;
+	
+	@Override
+	public void addNewDiagram(String name, boolean switchAfter){
+		ArrayList<Node> cNodes = new ArrayList<Node>(30);
+		ArrayList<Edge> cEdges = new ArrayList<Edge>(30);
+		nodes.put(name, cNodes);
+		edges.put(name, cEdges);
+		
+		HashMap<Integer,Node> cNodesMap = new HashMap<Integer,Node>();
+		HashMap<Integer,Edge> cEdgesMap = new HashMap<Integer,Edge>();
+		nodesMaps.put(name, cNodesMap);
+		edgesMaps.put(name, cEdgesMap);
+		
+		if(switchAfter){
+			synchronized(this){
+				currentNodes = cNodes;
+				currentEdges = cEdges;
+				currentNodesMap = cNodesMap;
+				currentEdgesMap = cEdgesMap;
+			}
+		}
+	}
+	
+	@Override
+	public synchronized void switchDiagram(String diagramName){
+		// check nodes only, as the edges and nodes maps are strongly coupled
+		if(!nodes.containsKey(diagramName))
+			throw new IllegalArgumentException("Diagram " + diagramName + " not present among the current ones");
+		
+		currentNodes = nodes.get(diagramName);
+		currentEdges = edges.get(diagramName);
+		currentNodesMap = nodesMaps.get(diagramName);
+		currentEdgesMap = edgesMaps.get(diagramName);
+	}
+	
+	@Override
+	public synchronized void removeDiagram(String diagramNameToRemove, String diagramNameNext){
+		if(!nodes.containsKey(diagramNameToRemove))
+			throw new IllegalArgumentException("Id " + diagramNameToRemove + " not present aong the current ones");
+		
+		nodes.remove(diagramNameToRemove);
+		edges.remove(diagramNameToRemove);
+		nodesMaps.remove(diagramNameToRemove);
+		edgesMaps.remove(diagramNameToRemove);
+		if(diagramNameNext == null){
+			currentNodes = EMPTY_NODE_LIST;
+			currentEdges = EMPTY_EDGE_LIST;
+			currentNodesMap = EMPTY_NODE_MAP;
+			currentEdgesMap = EMPTY_EDGE_MAP;
+		}else{
+			if(!nodes.containsKey(diagramNameNext))
+					throw new IllegalArgumentException("Id " + diagramNameNext + " not present aong the current ones");
+			currentNodes = nodes.get(diagramNameNext);
+			currentEdges = edges.get(diagramNameNext);
+			currentNodesMap = nodesMaps.get(diagramNameNext);
+			currentEdgesMap = edgesMaps.get(diagramNameNext);
+		}
+	}
+	
+	@Override
+	public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName){
+		newHapticId = true;
+		// waits for an identifier from the openGL thread
+		try{
+			wait();
+		}catch(InterruptedException ie){
+			wasInterrupted();
+		}
+		
+		Node n = new Node(x,y,nodeHashCode, currentHapticId);
+		if(diagramName == null){
+			currentNodes.add(n);
+			currentNodesMap.put(currentHapticId, n);
+		}else{
+			nodes.get(diagramName).add(n);
+			nodesMaps.get(diagramName).put(currentHapticId, n);
+		}
+	}
+
+	@Override
+	public synchronized void removeNode(int nodeHashCode, String diagramName){
+		ListIterator<Node> itr = (diagramName == null) ? currentNodes.listIterator() : nodes.get(diagramName).listIterator();
+		boolean found = false;
+		int hID = -1;
+		while(itr.hasNext()){
+			Node n = itr.next();
+			if(n.diagramId == nodeHashCode){
+				hID = n.hapticId;
+				itr.remove();
+				found = true;
+				break;
+			}
+		}
+		assert(found);
+		
+		/* remove the node from the map as well */
+		if(diagramName == null)
+			currentNodesMap.remove(hID);
+		else
+			nodesMaps.get(diagramName).remove(hID);
+		
+		/* set the flag to ask the haptic thread to free the id of the node that's been deleted */
+		dumpHapticId = true;
+		/* share the id to free with the other thread */
+		currentHapticId = hID;
+		try{
+			wait();
+		}catch(InterruptedException ie){
+			wasInterrupted();
+		}
+	}
+
+	@Override
+	public synchronized void moveNode(double x, double y, int nodeHashCode, String diagramName){
+		ArrayList<Node> iterationList = (diagramName == null) ? currentNodes : nodes.get(diagramName); 
+		for(Node n : iterationList){
+			if(n.diagramId == nodeHashCode){
+				n.x = x;
+				n.y = y;
+				break;
+			}
+		}
+	}
+
+	@Override
+	public synchronized void addEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine, String diagramName){
+		// flag the openGL thread the fact we need an identifier
+		newHapticId = true;
+		// waits for an identifier from the openGL thread
+		try{
+			wait();
+		}catch(InterruptedException ie){ 
+			wasInterrupted();
+		}
+		
+		// find the mid point of the line of attraction
+		double pX = Math.min(attractLine.getX1(), attractLine.getX2());
+		double pY = Math.min(attractLine.getY1(), attractLine.getY2());
+		pX += Math.abs(attractLine.getX1() -  attractLine.getX2())/2;
+		pY += Math.abs(attractLine.getY1() -  attractLine.getY2())/2;
+		
+		Edge e = new Edge(edgeHashCode,currentHapticId, xs, ys, adjMatrix, nodeStart, stipplePattern, pX, pY);
+		if(diagramName == null){
+			/* add the edge reference to the Haptic edges list */
+			currentEdges.add(e);
+			/* add the edge reference to the haptic edges map */
+			currentEdgesMap.put(currentHapticId, e);
+		}else{
+			/* add the edge reference to the Haptic edges list */
+			edges.get(diagramName).add(e);
+			/* add the edge reference to the haptic edges map */
+			edgesMaps.get(diagramName).put(currentHapticId, e);
+		}
+	}
+
+	@Override
+	public synchronized void updateEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart,  Line2D attractLine, String diagramName){
+		assert(xs.length == ys.length);
+		
+		for(Edge e : currentEdges){
+			if(e.diagramId == edgeHashCode){
+				e.xs = xs;
+				e.ys = ys;
+				e.size = xs.length;
+				e.adjMatrix = adjMatrix;
+				e.nodeStart = nodeStart;
+				// find the mid point of the line of attraction
+				double pX = Math.min(attractLine.getX1(), attractLine.getX2());
+				double pY = Math.min(attractLine.getY1(), attractLine.getY2());
+				pX += Math.abs(attractLine.getX1() -  attractLine.getX2())/2;
+				pY += Math.abs(attractLine.getY1() -  attractLine.getY2())/2;
+				e.attractPointX = pX;
+				e.attractPointY = pY;
+			}
+		}
+	}
+
+	@Override
+	public synchronized void removeEdge(int edgeHashCode, String diagramName){
+		ListIterator<Edge> itr = (diagramName == null) ? currentEdges.listIterator() : edges.get(diagramName).listIterator(); 
+		boolean found = false;
+		int hID = -1;
+		while(itr.hasNext()){
+			Edge e = itr.next();
+			if(e.diagramId == edgeHashCode){
+				hID = e.hapticId;
+				itr.remove();
+				found = true;
+				break;
+			}
+		}
+		assert(found);
+		
+		/* remove the edge from the map as well */
+		if(diagramName == null)
+			currentEdgesMap.remove(hID);
+		else
+			edgesMaps.get(diagramName).remove(hID);
+		/* set the flag to ask the haptic thread to free the id of the node that's been deleted */
+		dumpHapticId = true;
+		/* share the id to free with the other thread */
+		currentHapticId = hID;
+		try{
+			wait();
+		}catch(InterruptedException ie){
+			wasInterrupted();
+		}
+	}
+
+	@Override
+	public synchronized void attractTo(int elementHashCode){
+		attractToHapticId = findElementHapticID(elementHashCode);
+		attractTo = true;
+	}
+	
+	@Override
+	public synchronized void pickUp(int elementHashCode){
+		pickUpHapticId = findElementHapticID(elementHashCode);
+		pickUp = true;
+	}
+	
+	private int findElementHapticID(int elementHashCode){
+		int hID = -1;
+		boolean found = false;
+		for(Node n : currentNodes){
+			if(n.diagramId == elementHashCode){
+				hID = n.hapticId;
+				found = true;
+				break;
+			}
+		}
+		
+		if(!found)
+			for(Edge e : currentEdges){
+				if(e.diagramId == elementHashCode){
+					hID = e.hapticId;
+					found = true;
+					break;
+				}
+			}
+		assert(found);
+		return hID;
+	}
+	
+	@Override
+	public synchronized void dispose(){
+		shutdown = true;
+		/* wait for the haptic thread to shut down */
+		try {
+			wait();
+		} catch (InterruptedException e) {
+			wasInterrupted();
+		}
+	}
+	
+	@Override
+	public void run() {
+		try {
+			init(width,height);
+		} catch (IOException e) {
+			throw new RuntimeException();// OMNI haptic device doesn't cause any exception
+		}
+	}
+	
+	private void wasInterrupted(){
+		throw new UnsupportedOperationException("Haptics thread interrupted and no catch block implemented");
+	}
+	
+	private Node getNodeFromID(int hID){
+		return currentNodesMap.get(hID);
+	}
+	
+	private Edge getEdgeFromID(int hID){
+		return currentEdgesMap.get(hID);
+	}
+	
+	/* the diagram currently selected */
+	private ArrayList<Node> currentNodes;
+	private ArrayList<Edge> currentEdges;
+	private HashMap<Integer,Node> currentNodesMap;
+	private HashMap<Integer,Edge> currentEdgesMap;
+	
+	/* maps with all the diagrams in the editor */
+	private HashMap<String,ArrayList<Node>> nodes;
+	private HashMap<String,ArrayList<Edge>> edges;
+	private HashMap<String,HashMap<Integer,Node>> nodesMaps;
+	private HashMap<String,HashMap<Integer,Edge>> edgesMaps;
+	private int width;
+	private int height;
+	private int attractToHapticId;
+	private int pickUpHapticId;
+	/* flag for synchronization with the haptic thread*/
+	private boolean newHapticId;
+	private boolean dumpHapticId;
+	private boolean shutdown;
+	boolean hapticInitFailed;
+	private boolean attractTo;
+	private boolean pickUp;
+	/* currentHapticId is used to share haptic ids between the threads */
+	private int currentHapticId;
+	private HapticListener hapticListener;
+	
+	private static final ArrayList<Node> EMPTY_NODE_LIST = new ArrayList<Node>(0);
+	private static final ArrayList<Edge> EMPTY_EDGE_LIST = new ArrayList<Edge>(0);
+	private static final HashMap<Integer,Node> EMPTY_NODE_MAP = new HashMap<Integer,Node>();
+	private static final HashMap<Integer,Edge> EMPTY_EDGE_MAP = new HashMap<Integer,Edge>();
+
+}