view java/src/uk/ac/qmul/eecs/ccmi/gui/HapticKindle.java @ 8:ea7885bd9bff tip

fixed bug : render solid line as dotted/dashed when moving the stylus from dotted/dashed to solid
author ccmi-guest
date Thu, 03 Jul 2014 16:12:20 +0100
parents d66dd5880081
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.gui;

import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ResourceBundle;

import javax.swing.SwingUtilities;

import uk.ac.qmul.eecs.ccmi.diagrammodel.CollectionModel;
import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramElement;
import uk.ac.qmul.eecs.ccmi.haptics.HapticListener;
import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerThread;
import uk.ac.qmul.eecs.ccmi.haptics.HapticListenerCommand;
import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp;
import uk.ac.qmul.eecs.ccmi.network.Command;
import uk.ac.qmul.eecs.ccmi.network.DiagramEventActionSource;
import uk.ac.qmul.eecs.ccmi.sound.SoundEvent;
import uk.ac.qmul.eecs.ccmi.sound.SoundFactory;
import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;
import uk.ac.qmul.eecs.ccmi.utils.InteractionLog;

/**
 * 
 * An instance of HapticListener for the diagram editor. By this class visual diagrams 
 * can be manipulated by an haptic device. This class extends the {@code Thread} class,
 * and can therefore be run on a separate thread listening to the haptic commands coming from the thread
 * managing the haptic device. The commands affecting the Swing components will
 * be queued for execution on the code Event Dispatching Thread event queue. 
 *
 */
public class HapticKindle extends HapticListenerThread {
	
	public HapticKindle(){
		super();
		cmdImpl = new CommandImplementation();
		/* unselect always ends up to the same instruction. Therefore don't create a new runnable * 
		 * each time the command is issued, but rather keep and reuse always the same class       */
		unselectRunnable = new Runnable(){
			@Override
			public void run(){
				cmdImpl.executeCommand(HapticListenerCommand.UNSELECT, 0, 0, 0, 0, 0);
			}
		};
	}

	/**
	 * Implementation of the {@code executeCommand} method. All the commands that involve the 
	 * diagram model, are executed in the Event Dispatching Thread through {@code SwingUtilities} invoke
	 * methods. This prevents race conditions on the model and on diagram elements. 
	 * 
	 * @see HapticListenerThread#executeCommand(HapticListenerCommand, int, double, double, double, double)
	 */
	@Override
	public void executeCommand(final HapticListenerCommand cmd, final int ID, final double x, final double y, final double startX, final double startY) {
		switch(cmd){
		case PLAY_ELEMENT_SOUND :  
		case PLAY_ELEMENT_SPEECH : 
		case SELECT :
		case INFO :
		case ERROR :
			SwingUtilities.invokeLater(new Runnable(){
				public void run(){
					cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
				}
			});
			break;
		case UNSELECT :
			SwingUtilities.invokeLater(unselectRunnable);
			break;
		case PICK_UP :
		case MOVE : {
			/* when this block is executed we already have the lock * 
			 * on the element from the PICK_UP command execution    */
			try {
				SwingUtilities.invokeAndWait(new Runnable(){
					@Override
					public void run(){
						cmdImpl.executeCommand(cmd, ID, x, y, startX, startY);
					} // run()
				});
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
			break;
		 
		case PLAY_SOUND :
			cmdImpl.executeCommand(cmd, ID, x, y, startX, startY); // not in the Event Dispatching Thread
			break;
		}
		
	}

	/**
	 * Returns the delegate inner implementation of the {@code HapticListener} commands.
	 * When called directly from the returned {@code HapticListener} the commands won't be executed on a separate thread. 
	 * 
	 * @return the {@code HapticListener} command implementation
	 */
	@Override
	public HapticListener getNonRunnableListener(){
		return cmdImpl;
	}
		
	
	private Runnable unselectRunnable;
	private CommandImplementation cmdImpl;
	private static String INTERACTION_LOG_SOURCE = "HAPTIC"; 

	/* An inner class with the implementation of all the commands. HapticKindle runs  *
	 * on its own thread and delegates the real commands implementation to this class */
	private static class CommandImplementation implements HapticListener{
		@Override
		public void executeCommand(HapticListenerCommand cmd, int ID, double x,
				double y, double startX, double startY) {
			final EditorFrame frame = DiagramEditorApp.getFrame();
			switch(cmd){
			case PLAY_ELEMENT_SOUND : {
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				/* can be null if the tab has been switched or closed in the meantime */
				if(de == null)
					return;
				SoundFactory.getInstance().play(de.getSound());
			}break;
			case PLAY_ELEMENT_SPEECH : { 
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				if(de == null)
					return;
				SoundFactory.getInstance().play(de.getSound());
				NarratorFactory.getInstance().speak(de.getName());
				iLog("touch",((de instanceof Node) ? "node " : "edge ")+de.getName());
			}break;
			case SELECT : {
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				if(de == null)
					return;
				frame.selectHapticHighligh(de);
			}break;
			case UNSELECT : {
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				frame.selectHapticHighligh(null);
			}break;
			case MOVE : {
				/* when this block is executed we already have the lock * 
				 * on the element from the PICK_UP command execution    */
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				if(de == null)
					return;
				DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
				if(de instanceof Node){
					Node n = (Node)de;
					Rectangle2D bounds = n.getBounds();
					Point2D.Double p = new Point2D.Double(bounds.getCenterX(),bounds.getCenterY());
					double dx = x - p.getX();
					double dy = y - p.getY();
					n.getMonitor().lock();
					modelUpdater.translate(n, p, dx, dy,DiagramEventSource.HAPT);
					modelUpdater.stopMove(n,DiagramEventSource.HAPT);
					n.getMonitor().unlock();

					StringBuilder builder = new StringBuilder();
					builder.append(DiagramElement.toLogString(n)).append(" ").append(p.getX())
					.append(' ').append(p.getY());
					iLog("move node start",builder.toString());
					builder = new StringBuilder();
					builder.append(DiagramElement.toLogString(n)).append(' ')
					.append(x).append(' ').append(y);
					iLog("move node end",builder.toString());
				}else{
					Edge e = (Edge)de;
					modelUpdater.startMove(e, new Point2D.Double(startX,startY),DiagramEventSource.HAPT);
					Point2D p  = new Point2D.Double(x,y);
					e.getMonitor().lock();
					modelUpdater.bend(e, p,DiagramEventSource.HAPT);
					modelUpdater.stopMove(e,DiagramEventSource.HAPT);
					e.getMonitor().unlock();
					
					StringBuilder builder = new StringBuilder();
					builder.append(DiagramElement.toLogString(e)).append(' ').append(startX)
					.append(' ').append(startY);
					iLog("bend edge start",builder.toString());
					builder = new StringBuilder();
					builder.append(DiagramElement.toLogString(e)).append(' ')
					.append(x).append(' ').append(y);
					iLog("bend edge end",builder.toString());
				}
				modelUpdater.yieldLock(de,
						Lock.MOVE,
						new DiagramEventActionSource(
								DiagramEventSource.HAPT,
								de instanceof Node ? Command.Name.STOP_NODE_MOVE : Command.Name.STOP_EDGE_MOVE,
								de.getId(),
								de.getName()
								));
				SoundFactory.getInstance().play(SoundEvent.HOOK_OFF);
			}break;
			case INFO : {
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				if(de == null)
					return;
				SoundFactory.getInstance().stop();
				NarratorFactory.getInstance().speak(de.detailedSpokenText());
				iLog("request detailed info",((de instanceof Node) ? "node " : "edge ")+de.getName());					
			}break;
			case PLAY_SOUND : {
				switch(HapticListenerCommand.Sound.fromInt(ID) ){
				case MAGNET_OFF : 
					SoundFactory.getInstance().play(SoundEvent.MAGNET_OFF);
					iLog("sticky mode off","");
					break;
				case MAGNET_ON : 
					SoundFactory.getInstance().play(SoundEvent.MAGNET_ON);
					iLog("sticky mode on","");
					break;
				case DRAG : SoundFactory.getInstance().play(SoundEvent.DRAG);
					break;
				}
			}break;
			case PICK_UP :{
				if((frame == null)||(frame.getActiveTab() == null))
					return;
				CollectionModel<Node,Edge> collectionModel = frame.getActiveTab().getDiagram().getCollectionModel();
				DiagramElement de = Finder.findElementByHashcode(ID, collectionModel.getElements());
				if(de == null)
					return;
				DiagramModelUpdater modelUpdater = frame.getActiveTab().getDiagram().getModelUpdater();
				if(!modelUpdater.getLock(de, 
						Lock.MOVE,
						new DiagramEventActionSource(DiagramEventSource.HAPT, de instanceof Edge ? Command.Name.TRANSLATE_EDGE : Command.Name.TRANSLATE_NODE ,de.getId(),de.getName()))){
					iLog("Could not get lock on element for motion", DiagramElement.toLogString(de));
					NarratorFactory.getInstance().speak("Object is being moved by another user");
					return;
				}
				frame.hPickUp(de);
				SoundFactory.getInstance().play(SoundEvent.HOOK_ON);
				iLog("hook on","");
			}break;
			case ERROR : {
						if((frame == null)||(frame.getActiveTab() == null))
							return;
						frame.backupOpenDiagrams();
				NarratorFactory.getInstance().speak(ResourceBundle.getBundle(EditorFrame.class.getName()).getString("speech.haptic_device_crashed"));
			}break;
			}
		}
		
		private void iLog(String action, String args){
			InteractionLog.log(INTERACTION_LOG_SOURCE,action,args);
		}
	}
	
}