view java/src/uk/ac/qmul/eecs/ccmi/haptics/OmniHaptics.java @ 1:e3935c01cde2 tip

moved license of PdPersistenceManager to the beginning of the file
author Fiore Martin <f.martin@qmul.ac.uk>
date Tue, 08 Jul 2014 19:52:03 +0100
parents 78b7fc5391a2
children
line wrap: on
line source
/*  
 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(HapticListenerThread listener) {
		if(listener == null)
			throw new IllegalArgumentException("listener cannot be null");
		
		if(OsDetector.has64BitJVM()){
			return null;// no 64 native library supported yet
		}
		
		if(OsDetector.isWindows()){
			/* try to load .dll. First copy it in the home/ccmi_editor_data/lib directory  */
			URL url = OmniHaptics.class.getResource("OmniHaptics.dll");
			ResourceFileWriter fileWriter = new ResourceFileWriter(url);
			fileWriter.writeOnDisk(
					PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")),
					"OmniHaptics.dll");
			String path = fileWriter.getFilePath();
			try{
				if(path == null)
					throw new UnsatisfiedLinkError("OmniHaptics.dll missing");
				System.load( path );
			}catch(UnsatisfiedLinkError e){
				e.printStackTrace();
				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         */
				/* don't kill the listener as initialization will be tried on other devices */
//				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 = Empties.EMPTY_NODE_LIST;
		currentEdges = Empties.EMPTY_EDGE_LIST;
		nodesMaps = new HashMap<String,HashMap<Integer,Node>>();
		edgesMaps = new HashMap<String,HashMap<Integer,Edge>>();
		currentNodesMap = Empties.EMPTY_NODE_MAP;
		currentEdgesMap = Empties.EMPTY_EDGE_MAP;
	}	
	
	
	public native int initOmni(int width, int height) throws IOException;
	
	@Override
	public void addNewDiagram(String diagramName){
		ArrayList<Node> cNodes = new ArrayList<Node>(30);
		ArrayList<Edge> cEdges = new ArrayList<Edge>(30);
		nodes.put(diagramName, cNodes);
		edges.put(diagramName, cEdges);
		
		HashMap<Integer,Node> cNodesMap = new HashMap<Integer,Node>();
		HashMap<Integer,Edge> cEdgesMap = new HashMap<Integer,Edge>();
		nodesMaps.put(diagramName, cNodesMap);
		edgesMaps.put(diagramName, cEdgesMap);
		
		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 not found among added diagrams:" + diagramName);
		
		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("Diagram not found among added diagrams:" + diagramNameToRemove);
		
		nodes.remove(diagramNameToRemove);
		edges.remove(diagramNameToRemove);
		nodesMaps.remove(diagramNameToRemove);
		edgesMaps.remove(diagramNameToRemove);
		if(diagramNameNext == null){
			currentNodes = Empties.EMPTY_NODE_LIST;
			currentEdges = Empties.EMPTY_EDGE_LIST;
			currentNodesMap = Empties.EMPTY_NODE_MAP;
			currentEdgesMap = Empties.EMPTY_EDGE_MAP;
		}else{
			if(!nodes.containsKey(diagramNameNext))
					throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameNext);
			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 void setVisible(boolean visible){
		// not implemented but required by Haptic interface 
	}
	
	@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 {
			initOmni(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 HapticListenerThread hapticListener;

}