f@0: /*
f@0: CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool
f@0:
f@0: Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/)
f@0:
f@0: This program is free software: you can redistribute it and/or modify
f@0: it under the terms of the GNU General Public License as published by
f@0: the Free Software Foundation, either version 3 of the License, or
f@0: (at your option) any later version.
f@0:
f@0: This program is distributed in the hope that it will be useful,
f@0: but WITHOUT ANY WARRANTY; without even the implied warranty of
f@0: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
f@0: GNU General Public License for more details.
f@0:
f@0: You should have received a copy of the GNU General Public License
f@0: along with this program. If not, see .
f@0: */
f@0:
f@0: package uk.ac.qmul.eecs.ccmi.haptics;
f@0:
f@0: import java.awt.Dimension;
f@0: import java.awt.Toolkit;
f@0: import java.awt.geom.Line2D;
f@0: import java.io.IOException;
f@0: import java.net.URL;
f@0: import java.util.ArrayList;
f@0: import java.util.BitSet;
f@0: import java.util.HashMap;
f@0: import java.util.ListIterator;
f@0:
f@0: import uk.ac.qmul.eecs.ccmi.utils.ResourceFileWriter;
f@0: import uk.ac.qmul.eecs.ccmi.utils.OsDetector;
f@0: import uk.ac.qmul.eecs.ccmi.utils.PreferencesService;
f@0:
f@0: /*
f@0: *
f@0: * The implementation of the Haptics interface which uses SensableŽ
f@0: * PHANTOM OmniŽ haptic device.
f@0: *
f@0: */
f@0: class OmniHaptics extends Thread implements Haptics {
f@0:
f@0: static Haptics createInstance(HapticListenerThread listener) {
f@0: if(listener == null)
f@0: throw new IllegalArgumentException("listener cannot be null");
f@0:
f@0: if(OsDetector.has64BitJVM()){
f@0: return null;// no 64 native library supported yet
f@0: }
f@0:
f@0: if(OsDetector.isWindows()){
f@0: /* try to load .dll. First copy it in the home/ccmi_editor_data/lib directory */
f@0: URL url = OmniHaptics.class.getResource("OmniHaptics.dll");
f@0: ResourceFileWriter fileWriter = new ResourceFileWriter(url);
f@0: fileWriter.writeOnDisk(
f@0: PreferencesService.getInstance().get("dir.libs", System.getProperty("java.io.tmpdir")),
f@0: "OmniHaptics.dll");
f@0: String path = fileWriter.getFilePath();
f@0: try{
f@0: if(path == null)
f@0: throw new UnsatisfiedLinkError("OmniHaptics.dll missing");
f@0: System.load( path );
f@0: }catch(UnsatisfiedLinkError e){
f@0: e.printStackTrace();
f@0: return null;
f@0: }
f@0: }else{
f@0: return null;
f@0: }
f@0:
f@0: OmniHaptics omniHaptics = new OmniHaptics("Haptics");
f@0: omniHaptics.hapticListener = listener;
f@0: /* start up the listener which immediately stops, waiting for commands */
f@0: if(!omniHaptics.hapticListener.isAlive())
f@0: omniHaptics.hapticListener.start();
f@0: /* start up the haptics thread which issues commands from the java to the c++ thread */
f@0: omniHaptics.start();
f@0: /* wait for the haptics thread (now running native code) to initialize (need to know if initialization is successful) */
f@0: synchronized(omniHaptics){
f@0: try {
f@0: omniHaptics.wait();
f@0: }catch (InterruptedException ie) {
f@0: throw new RuntimeException(ie); // must never happen
f@0: }
f@0: }
f@0: if(omniHaptics.hapticInitFailed){
f@0: /* the initialization has failed, the haptic thread is about to die */
f@0: /* don't kill the listener as initialization will be tried on other devices */
f@0: // while(!omniHaptics.hapticListener.isInterrupted()){
f@0: // omniHaptics.hapticListener.interrupt();
f@0: // }
f@0: // omniHaptics.hapticListener = null; //leave the listener to the GC
f@0: return null;
f@0: }else{
f@0: return omniHaptics;
f@0: }
f@0: }
f@0:
f@0: private OmniHaptics(String threadName){
f@0: super(threadName);
f@0: /* get the screen size which will be passed to init methos in order to set up a window
f@0: * for the haptic with the same size as the swing one
f@0: */
f@0: Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
f@0:
f@0: int screenWidth = (int)screenSize.getWidth();
f@0: int screenHeight = (int)screenSize.getHeight();
f@0:
f@0: width = screenWidth * 5 / 8;
f@0: height = screenHeight * 5 / 8;
f@0:
f@0: newHapticId = false;
f@0: dumpHapticId = false;
f@0: shutdown = false;
f@0: hapticInitFailed = false;
f@0: nodes = new HashMap>();
f@0: edges = new HashMap>();
f@0: currentNodes = Empties.EMPTY_NODE_LIST;
f@0: currentEdges = Empties.EMPTY_EDGE_LIST;
f@0: nodesMaps = new HashMap>();
f@0: edgesMaps = new HashMap>();
f@0: currentNodesMap = Empties.EMPTY_NODE_MAP;
f@0: currentEdgesMap = Empties.EMPTY_EDGE_MAP;
f@0: }
f@0:
f@0:
f@0: public native int initOmni(int width, int height) throws IOException;
f@0:
f@0: @Override
f@0: public void addNewDiagram(String diagramName){
f@0: ArrayList cNodes = new ArrayList(30);
f@0: ArrayList cEdges = new ArrayList(30);
f@0: nodes.put(diagramName, cNodes);
f@0: edges.put(diagramName, cEdges);
f@0:
f@0: HashMap cNodesMap = new HashMap();
f@0: HashMap cEdgesMap = new HashMap();
f@0: nodesMaps.put(diagramName, cNodesMap);
f@0: edgesMaps.put(diagramName, cEdgesMap);
f@0:
f@0: synchronized(this){
f@0: currentNodes = cNodes;
f@0: currentEdges = cEdges;
f@0: currentNodesMap = cNodesMap;
f@0: currentEdgesMap = cEdgesMap;
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void switchDiagram(String diagramName){
f@0: // check nodes only, as the edges and nodes maps are strongly coupled
f@0: if(!nodes.containsKey(diagramName))
f@0: throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramName);
f@0:
f@0: currentNodes = nodes.get(diagramName);
f@0: currentEdges = edges.get(diagramName);
f@0: currentNodesMap = nodesMaps.get(diagramName);
f@0: currentEdgesMap = edgesMaps.get(diagramName);
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void removeDiagram(String diagramNameToRemove, String diagramNameNext){
f@0: if(!nodes.containsKey(diagramNameToRemove))
f@0: throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameToRemove);
f@0:
f@0: nodes.remove(diagramNameToRemove);
f@0: edges.remove(diagramNameToRemove);
f@0: nodesMaps.remove(diagramNameToRemove);
f@0: edgesMaps.remove(diagramNameToRemove);
f@0: if(diagramNameNext == null){
f@0: currentNodes = Empties.EMPTY_NODE_LIST;
f@0: currentEdges = Empties.EMPTY_EDGE_LIST;
f@0: currentNodesMap = Empties.EMPTY_NODE_MAP;
f@0: currentEdgesMap = Empties.EMPTY_EDGE_MAP;
f@0: }else{
f@0: if(!nodes.containsKey(diagramNameNext))
f@0: throw new IllegalArgumentException("Diagram not found among added diagrams:" + diagramNameNext);
f@0: currentNodes = nodes.get(diagramNameNext);
f@0: currentEdges = edges.get(diagramNameNext);
f@0: currentNodesMap = nodesMaps.get(diagramNameNext);
f@0: currentEdgesMap = edgesMaps.get(diagramNameNext);
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void addNode(double x, double y, int nodeHashCode, String diagramName){
f@0: newHapticId = true;
f@0: // waits for an identifier from the openGL thread
f@0: try{
f@0: wait();
f@0: }catch(InterruptedException ie){
f@0: wasInterrupted();
f@0: }
f@0:
f@0: Node n = new Node(x,y,nodeHashCode, currentHapticId);
f@0: if(diagramName == null){
f@0: currentNodes.add(n);
f@0: currentNodesMap.put(currentHapticId, n);
f@0: }else{
f@0: nodes.get(diagramName).add(n);
f@0: nodesMaps.get(diagramName).put(currentHapticId, n);
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void removeNode(int nodeHashCode, String diagramName){
f@0: ListIterator itr = (diagramName == null) ? currentNodes.listIterator() : nodes.get(diagramName).listIterator();
f@0: boolean found = false;
f@0: int hID = -1;
f@0: while(itr.hasNext()){
f@0: Node n = itr.next();
f@0: if(n.diagramId == nodeHashCode){
f@0: hID = n.hapticId;
f@0: itr.remove();
f@0: found = true;
f@0: break;
f@0: }
f@0: }
f@0: assert(found);
f@0:
f@0: /* remove the node from the map as well */
f@0: if(diagramName == null)
f@0: currentNodesMap.remove(hID);
f@0: else
f@0: nodesMaps.get(diagramName).remove(hID);
f@0:
f@0: /* set the flag to ask the haptic thread to free the id of the node that's been deleted */
f@0: dumpHapticId = true;
f@0: /* share the id to free with the other thread */
f@0: currentHapticId = hID;
f@0: try{
f@0: wait();
f@0: }catch(InterruptedException ie){
f@0: wasInterrupted();
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void moveNode(double x, double y, int nodeHashCode, String diagramName){
f@0: ArrayList iterationList = (diagramName == null) ? currentNodes : nodes.get(diagramName);
f@0: for(Node n : iterationList){
f@0: if(n.diagramId == nodeHashCode){
f@0: n.x = x;
f@0: n.y = y;
f@0: break;
f@0: }
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void addEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, int stipplePattern, Line2D attractLine, String diagramName){
f@0: // flag the openGL thread the fact we need an identifier
f@0: newHapticId = true;
f@0: // waits for an identifier from the openGL thread
f@0: try{
f@0: wait();
f@0: }catch(InterruptedException ie){
f@0: wasInterrupted();
f@0: }
f@0:
f@0: // find the mid point of the line of attraction
f@0: double pX = Math.min(attractLine.getX1(), attractLine.getX2());
f@0: double pY = Math.min(attractLine.getY1(), attractLine.getY2());
f@0: pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2;
f@0: pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2;
f@0:
f@0: Edge e = new Edge(edgeHashCode,currentHapticId, xs, ys, adjMatrix, nodeStart, stipplePattern, pX, pY);
f@0: if(diagramName == null){
f@0: /* add the edge reference to the Haptic edges list */
f@0: currentEdges.add(e);
f@0: /* add the edge reference to the Haptic edges map */
f@0: currentEdgesMap.put(currentHapticId, e);
f@0: }else{
f@0: /* add the edge reference to the Haptic edges list */
f@0: edges.get(diagramName).add(e);
f@0: /* add the edge reference to the Haptic edges map */
f@0: edgesMaps.get(diagramName).put(currentHapticId, e);
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void updateEdge(int edgeHashCode, double[] xs, double[] ys, BitSet[] adjMatrix, int nodeStart, Line2D attractLine, String diagramName){
f@0: assert(xs.length == ys.length);
f@0:
f@0: for(Edge e : currentEdges){
f@0: if(e.diagramId == edgeHashCode){
f@0: e.xs = xs;
f@0: e.ys = ys;
f@0: e.size = xs.length;
f@0: e.adjMatrix = adjMatrix;
f@0: e.nodeStart = nodeStart;
f@0: // find the mid point of the line of attraction
f@0: double pX = Math.min(attractLine.getX1(), attractLine.getX2());
f@0: double pY = Math.min(attractLine.getY1(), attractLine.getY2());
f@0: pX += Math.abs(attractLine.getX1() - attractLine.getX2())/2;
f@0: pY += Math.abs(attractLine.getY1() - attractLine.getY2())/2;
f@0: e.attractPointX = pX;
f@0: e.attractPointY = pY;
f@0: }
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void removeEdge(int edgeHashCode, String diagramName){
f@0: ListIterator itr = (diagramName == null) ? currentEdges.listIterator() : edges.get(diagramName).listIterator();
f@0: boolean found = false;
f@0: int hID = -1;
f@0: while(itr.hasNext()){
f@0: Edge e = itr.next();
f@0: if(e.diagramId == edgeHashCode){
f@0: hID = e.hapticId;
f@0: itr.remove();
f@0: found = true;
f@0: break;
f@0: }
f@0: }
f@0: assert(found);
f@0:
f@0: /* remove the edge from the map as well */
f@0: if(diagramName == null)
f@0: currentEdgesMap.remove(hID);
f@0: else
f@0: edgesMaps.get(diagramName).remove(hID);
f@0: /* set the flag to ask the haptic thread to free the id of the node that's been deleted */
f@0: dumpHapticId = true;
f@0: /* share the id to free with the other thread */
f@0: currentHapticId = hID;
f@0: try{
f@0: wait();
f@0: }catch(InterruptedException ie){
f@0: wasInterrupted();
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void attractTo(int elementHashCode){
f@0: attractToHapticId = findElementHapticID(elementHashCode);
f@0: attractTo = true;
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void pickUp(int elementHashCode){
f@0: pickUpHapticId = findElementHapticID(elementHashCode);
f@0: pickUp = true;
f@0: }
f@0:
f@0: private int findElementHapticID(int elementHashCode){
f@0: int hID = -1;
f@0: boolean found = false;
f@0: for(Node n : currentNodes){
f@0: if(n.diagramId == elementHashCode){
f@0: hID = n.hapticId;
f@0: found = true;
f@0: break;
f@0: }
f@0: }
f@0:
f@0: if(!found)
f@0: for(Edge e : currentEdges){
f@0: if(e.diagramId == elementHashCode){
f@0: hID = e.hapticId;
f@0: found = true;
f@0: break;
f@0: }
f@0: }
f@0: assert(found);
f@0: return hID;
f@0: }
f@0:
f@0: @Override
f@0: public void setVisible(boolean visible){
f@0: // not implemented but required by Haptic interface
f@0: }
f@0:
f@0: @Override
f@0: public synchronized void dispose(){
f@0: shutdown = true;
f@0: /* wait for the haptic thread to shut down */
f@0: try {
f@0: wait();
f@0: } catch (InterruptedException e) {
f@0: wasInterrupted();
f@0: }
f@0: }
f@0:
f@0: @Override
f@0: public void run() {
f@0: try {
f@0: initOmni(width,height);
f@0: } catch (IOException e) {
f@0: throw new RuntimeException();// OMNI haptic device doesn't cause any exception
f@0: }
f@0: }
f@0:
f@0: private void wasInterrupted(){
f@0: throw new UnsupportedOperationException("Haptics thread interrupted and no catch block implemented");
f@0: }
f@0:
f@0: private Node getNodeFromID(int hID){
f@0: return currentNodesMap.get(hID);
f@0: }
f@0:
f@0: private Edge getEdgeFromID(int hID){
f@0: return currentEdgesMap.get(hID);
f@0: }
f@0:
f@0: /* the diagram currently selected */
f@0: private ArrayList currentNodes;
f@0: private ArrayList currentEdges;
f@0: private HashMap currentNodesMap;
f@0: private HashMap currentEdgesMap;
f@0:
f@0: /* maps with all the diagrams in the editor */
f@0: private HashMap> nodes;
f@0: private HashMap> edges;
f@0: private HashMap> nodesMaps;
f@0: private HashMap> edgesMaps;
f@0: private int width;
f@0: private int height;
f@0: private int attractToHapticId;
f@0: private int pickUpHapticId;
f@0: /* flag for synchronization with the haptic thread*/
f@0: private boolean newHapticId;
f@0: private boolean dumpHapticId;
f@0: private boolean shutdown;
f@0: boolean hapticInitFailed;
f@0: private boolean attractTo;
f@0: private boolean pickUp;
f@0: /* currentHapticId is used to share haptic ids between the threads */
f@0: private int currentHapticId;
f@0: private HapticListenerThread hapticListener;
f@0:
f@0: }