view java/src/uk/ac/qmul/eecs/ccmi/haptics/MouseHaptics.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.AWTException;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Robot;
import java.awt.Stroke;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.BitSet;

import javax.swing.JFrame;
import javax.swing.JPanel;

import uk.ac.qmul.eecs.ccmi.gui.DiagramPanel;
import uk.ac.qmul.eecs.ccmi.main.DiagramEditorApp;
import uk.ac.qmul.eecs.ccmi.speech.NarratorFactory;

@SuppressWarnings("serial")
class MouseHaptics extends JPanel implements Haptics, KeyListener, MouseListener, MouseMotionListener {
	static Haptics createInstance(HapticListener listener){
		MouseHaptics instance = new MouseHaptics(listener);
		try{
			instance.initMouseHaptics();
			return instance;
		}catch(IOException ioe){
			return null;
		}
	}
	
	private MouseHaptics(HapticListener listener){
		this.listener = listener;
		solidStroke = new BasicStroke(EDGE_THICKNESS);
		dottedStroke = new BasicStroke(EDGE_THICKNESS,
				BasicStroke.CAP_ROUND, 
				BasicStroke.JOIN_ROUND, 
				0.0f, 
				new float[]{1.0f,30.0f}, 
				0.0f);
		dashedStroke = new BasicStroke(EDGE_THICKNESS,
				BasicStroke.CAP_ROUND, 
				BasicStroke.JOIN_ROUND, 
				0.0f, 
				new float[]{80.0f,50.0f}, 
				0.0f);
	}

	@Override
	public void paintComponent(Graphics g){
		super.paintComponent(g);
		
		Graphics2D g2 = (Graphics2D)g;
		
		Stroke oldStroke = g2.getStroke();
		// draw edges  
		g2.setColor(EDGE_COLOR);
		for(Edge e : listSupport.getCurrentEdges()){
			switch(e.stipplePattern){
			case Edge.SOLID_LINE :
				g2.setStroke(solidStroke);break;
			case Edge.DOTTED_LINE : 
				g2.setStroke(dottedStroke);break;
			case Edge.DASHED_LINE : 
				g2.setStroke(dashedStroke);break;
			}
			for(int i=0; i< e.adjMatrix.length; i++){
				 BitSet adj = e.adjMatrix[i];
				 for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) {
				     g2.drawLine((int)e.xs[i], (int)e.ys[i], (int)e.xs[j], (int)e.ys[j]);
				 }
			}
		}
		
		g2.setStroke(oldStroke);
		// draw nodes 
		g2.setColor(NODE_COLOR);
		for(Node n : listSupport.getCurrentNodes()){
			g2.fillOval((int)(n.x - NODE_DIAMETER/2), (int)(n.y - NODE_DIAMETER/2), NODE_DIAMETER, NODE_DIAMETER);
		}
	}
	
	@Override
	public void run() {}

	void initMouseHaptics() throws IOException {
		hapticFrame = new JFrame("Haptic Frame");
		try {
			robot = new Robot();
		} catch (AWTException e) {
			throw new IOException(e);
		}
		/* this is necessary to make the window screen size. it doens't work with the default layout */
		setLayout(new BorderLayout());
		
		setBackground(Color.black);
		
		listSupport = new HapticListSupport();

		hapticFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		hapticFrame.addWindowFocusListener(new WindowFocusListener(){
			@Override
			public void windowGainedFocus(WindowEvent arg0) {
				NarratorFactory.getInstance().speak("Haptic window Focused");
			}

			@Override
			public void windowLostFocus(WindowEvent arg0) {}
		});
		hapticFrame.setContentPane(this);
		hapticFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
		hapticFrame.setUndecorated(true);
		hapticFrame.addKeyListener(this);
		addMouseMotionListener(this);
		addMouseListener(this);
		hapticFrame.pack();
	}

	@Override
	public void addNewDiagram(String diagramName) {
		listSupport.addNewDiagram(diagramName);
		repaint();
	}

	@Override
	public void switchDiagram(String diagramName) {
		listSupport.switchDiagram(diagramName);
		repaint();
	}

	@Override
	public void removeDiagram(String diagramNameToRemove,
			String diagramNameOfNext) {
		listSupport.removeDiagram(diagramNameToRemove, diagramNameOfNext);
		repaint();
	}

	@Override
	public void addNode(double x, double y, int nodeHashCode, String diagramName) {
		Node n = new Node(x,y,nodeHashCode, 0);
		listSupport.addNode(n,diagramName);
		repaint();
	}

	@Override
	public void removeNode(int nodeHashCode, String diagramName) {
		listSupport.removeNode(nodeHashCode, diagramName);
		repaint();
	}

	@Override
	public void moveNode(double x, double y, int nodeHashCode,
			String diagramName) {
		Node n = listSupport.getNode(nodeHashCode, diagramName);
		n.x = x;
		n.y = y;
		repaint();
	}

	@Override
	public void addEdge(int edgeHashCode, double[] xs, double[] ys,
			BitSet[] adjMatrix, int nodeStart, int stipplePattern,
			Line2D attractLine, String diagramName) {
		// 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,0, xs, ys,
				adjMatrix, nodeStart, stipplePattern,
				pX, pY);
		listSupport.addEdge(e, diagramName);
		repaint();
	}

	@Override
	public void updateEdge(int edgeHashCode, double[] xs, double[] ys,
			BitSet[] adjMatrix, int nodeStart, Line2D attractLine,
			String diagramName) {
		Edge e = listSupport.getEdge(edgeHashCode, diagramName);
		e.xs = xs;
		e.ys = ys;
		e.adjMatrix = adjMatrix;
		e.nodeStart = nodeStart;
		repaint();
	}

	@Override
	public void removeEdge(int edgeHashCode, String diagramName) {
		listSupport.removeEdge(edgeHashCode, diagramName);
		repaint();
	}

	@Override
	public void attractTo(int elementHashCode) {

	}

	@Override
	public void pickUp(int elementHashCode) {
		//	needed to change the status of the haptic device. not needed here
	}

	@Override
	public boolean isAlive() {
		return false;
	}
	
	@Override
	public void setVisible(boolean visible){
		hapticFrame.setVisible(visible);
	}

	@Override
	public void dispose() {
		// no resources to free up
	}
	
	@Override
	public void keyPressed(KeyEvent evt) {
		DiagramPanel panel = DiagramEditorApp.getFrame().getActiveTab();
		if(panel == null)
			DiagramEditorApp.getFrame().editorTabbedPane.dispatchEvent(evt);
		else
			panel.getTree().dispatchEvent(evt);
	}

	@Override
	public void keyReleased(KeyEvent evt) {
		keyPressed(evt);
	}

	@Override
	public void keyTyped(KeyEvent evt) {
		keyPressed(evt);
	}
	
	@Override
	public void mouseDragged(MouseEvent evt) {
		/* priority to nodes: check if the mouse pointer is inside a circle centred on   *
		 * the node and with radius equal to NODE_RADIUS. If the mouse pointer is within *
		 * the radius of two or more nodes then the closest is picked up. The command is *
		 * executed once when the node is touched. In order have it executed again the   *
		 * used must get away from it and hover above it again                           */ 
		Point2D p = evt.getPoint();
		if(elementPickedUp){
			draggedDistance += evt.getPoint().distance(lastDragPoint);  
			if( draggedDistance > CHAIN_SOUND_INTERVAL){
				listener.executeCommand(HapticListenerCommand.PLAY_SOUND, 3, 0,0,0,0);
				draggedDistance = 0;
			}
			lastDragPoint = evt.getPoint();
		}
		
		Node hoveredNode = null;
		for(Node n : listSupport.getCurrentNodes()){
			double distance = p.distance(n.x, n.y); 
			if( distance < NODE_HOVER_DIST){
				if(hoveredNode == null || distance < p.distance(hoveredNode.x,hoveredNode.y) ){
					hoveredNode = n;
				}
			}
		}
		if(hoveredNode != null && hoveredNode != lastTouchedNode){
			listener.executeCommand(HapticListenerCommand.PLAY_ELEMENT_SPEECH, hoveredNode.diagramId, 0, 0, 0, 0);
			lastTouchedNode = hoveredNode;
			lastTouchedEdge = null;
			return; 
		}
		lastTouchedNode = hoveredNode;
		/* if hovering inside a node neither send the command nor take edges into account */
		if(hoveredNode != null)
			return;
		
		/* if no node is being touched, check the edges out. */
		Edge hoveredEdge = null;
		Line2D line = new Line2D.Double();
		/* look at all edges */
		for(Edge e : listSupport.getCurrentEdges()){
			/* look at all edge's lines */
			for(int i=0; i< e.adjMatrix.length; i++){
				 BitSet adj = e.adjMatrix[i];
				 for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) {
					 line.setLine(e.xs[i], e.ys[i], e.xs[j], e.ys[j]);
					 if(lastTouchedEdge != e && line.ptSegDist(p)<EDGE_HOVER_DIST){
						 hoveredEdge = e;
						 listener.executeCommand(HapticListenerCommand.PLAY_ELEMENT_SPEECH, hoveredEdge.diagramId, 0, 0, 0, 0);
						 lastTouchedEdge = hoveredEdge;
						 return;
					 }
				 }
			}
		}
		lastTouchedEdge = hoveredEdge;
	}
	
	@Override
	public void mouseMoved(MouseEvent evt) {
		/* right click on a graphic tablet used as mouse has the effect of nullifying the           *
		 * dragging. That is there is no right click but rather it's like if you untouch            *
		 * the tablet. In order to address this a robot is used in order to re-leftclick each time  *
		 * the right click is pressed. In this way we assure that the left click is always held     *
		 * and therefore the mouse is always dragging rather than moving                            */
		if(mustReclick){
			mustReclick = false;
			reclickedAfterMove = true; // this is to make this.mousePressed() have no effect, when it's the robot clicking 
			robot.mousePress(InputEvent.BUTTON1_MASK);
		}
	}
	
	@Override
	public void mousePressed(MouseEvent evt) {
		/* by clicking on the object, its name is stated by the TTS.        *
		 * Much as what happens when hovering on it with the button pressed */
		if(evt.getButton() == MouseEvent.BUTTON1){
			if(reclickedAfterMove){
				reclickedAfterMove = false;
				return;
			}
			lastTouchedEdge = null; // these two fields are used with dragging to avoid repeating 
			lastTouchedNode = null;
			
			mouseDragged(evt);//left clicking on an object is like to drag on it 
			return;
		}
		/* button 3 (right click) is for moving the objects (picking up and dropping) */
		if(evt.getButton() != MouseEvent.BUTTON3)
			return;
		if(!secondClick){ // clicked for the first time: pick up the node or edge
			Point2D p = evt.getPoint();
			Node hoveredNode = null;
			for(Node n : listSupport.getCurrentNodes()){
				double distance = p.distance(n.x, n.y); 
				if( distance < NODE_HOVER_DIST){
					if(hoveredNode == null || distance < p.distance(hoveredNode.x,hoveredNode.y) ){
						hoveredNode = n;
					}
				}
			}
			if(hoveredNode != null){ // clicked on a node 
				listener.executeCommand(HapticListenerCommand.PICK_UP, hoveredNode.diagramId, 0, 0, 0, 0);
				secondClick = true;
				startX = evt.getX();
				startY = evt.getY();
				pickedUpElementId = hoveredNode.diagramId;
				mustReclick = true;
				/* sets the variables for the chain sound when dragging the element around */
				elementPickedUp = true;
				lastDragPoint = evt.getPoint();
				return;
			}
			/* if no node is being touched, check the edges out. */
			Line2D line = new Line2D.Double();
			/* look at all edges */
			for(Edge e : listSupport.getCurrentEdges()){
				/* look at all edge's lines */
				for(int i=0; i< e.adjMatrix.length; i++){
					BitSet adj = e.adjMatrix[i];
					for (int j = adj.nextSetBit(0); j >= 0; j = adj.nextSetBit(j+1)) {
						line.setLine(e.xs[i], e.ys[i], e.xs[j], e.ys[j]);
						if(/*lastTouchedEdge != e && */line.ptSegDist(p)<EDGE_HOVER_DIST){
							listener.executeCommand(HapticListenerCommand.PICK_UP, e.diagramId, 0, 0, 0, 0);
							secondClick = true;
							startX = evt.getX();
							startY = evt.getY();
							pickedUpElementId = e.diagramId;
							mustReclick = true;
							/* sets the variables for the chain sound when dragging the element around */
							elementPickedUp = true;
							lastDragPoint = evt.getPoint();
							return;
						}
					}
				}
			}
		}else{
			listener.executeCommand(HapticListenerCommand.MOVE, pickedUpElementId, evt.getX(), evt.getY(), startX, startY);
			elementPickedUp = false;
			secondClick = false;
		}
		mustReclick = true;
	}

	@Override
	public void mouseEntered(MouseEvent evt) {}

	@Override
	public void mouseExited(MouseEvent evt) {}

	@Override
	public void mouseClicked(MouseEvent evt) {}

	@Override
	public void mouseReleased(MouseEvent evt) {}
	
	private HapticListener listener;
	private JFrame hapticFrame;
	private HapticListSupport listSupport;
	private Stroke solidStroke;
	private Stroke dashedStroke;
	private Stroke dottedStroke;
	private Node lastTouchedNode;
	private Edge lastTouchedEdge;
	private Robot robot;
	private boolean mustReclick;
	private boolean reclickedAfterMove;
	private boolean secondClick;
	private boolean elementPickedUp;
	private Point2D lastDragPoint;
	private double draggedDistance;
	private int startX;
	private int startY;
	private int pickedUpElementId;
	private static final Color EDGE_COLOR = Color.RED;
	private static final Color NODE_COLOR = Color.WHITE;
	private static final int EDGE_THICKNESS = 26;//2;
	private static final int NODE_DIAMETER = 50;//10;
	private static final int NODE_HOVER_DIST = 25;
	private static final int EDGE_HOVER_DIST = 13;
	private static final double CHAIN_SOUND_INTERVAL = 150;
	
}