//
// SocketSlaveDisplay.java
//
/*
VisAD system for interactive analysis and visualization of numerical
data. Copyright (C) 1996 - 2017 Bill Hibbard, Curtis Rueden, Tom
Rink, Dave Glowacki, Steve Emmerson, Tom Whittaker, Don Murray, and
Tommy Jasmin.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library 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
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place - Suite 330, Boston,
MA 02111-1307, USA
*/
package visad;
import java.awt.Component;
import java.awt.event.MouseEvent;
import java.net.*;
import java.io.*;
import java.rmi.RemoteException;
import java.util.*;
import visad.browser.*;
/** A SocketSlaveDisplay server wraps around a VisAD display, providing support
for stand-alone remote displays (i.e., not dependent on the VisAD packages)
that communicate with the server using sockets. For an example, see
examples/Test68.java together with the stand-alone VisAD applet
visad.browser.VisADApplet, usable from within a web browser. */
public class SocketSlaveDisplay implements RemoteSlaveDisplay {
/** debugging flag */
private static final boolean DEBUG = false;
/** the default port for server/client communication */
private static final int DEFAULT_PORT = 4567;
/** list of control classes that support socket-based collaboration */
private static final Class[] supportedControls = {
GraphicsModeControl.class, ContourControl.class
};
/** the port at which the server communicates with clients */
private int port;
/** the server's associated VisAD display */
private DisplayImpl display;
/** flag that prevents getImage() calls from signaling a FRAME_DONE event */
private boolean flag;
/** array of image data extracted from the VisAD display */
private byte[] pix;
/** height of image */
private int h;
/** width of image */
private int w;
/** the server's associated socket */
private ServerSocket serverSocket;
/** vector of client sockets connected to the server */
private Vector clientSockets = new Vector();
/** vector of client sockets' input streams */
private Vector clientInputs = new Vector();
/** vector of client sockets' output streams */
private Vector clientOutputs = new Vector();
/** vector of client socket ids */
private Vector clientIds = new Vector();
/** thread monitoring incoming clients */
private Thread connectThread = null;
/** thread monitoring communication between server and clients */
private Thread commThread = null;
/** whether the server is still alive */
private boolean alive = true;
/** next available client ID number */
private int clientID = 0;
/** this socket slave display */
private final SocketSlaveDisplay socketSlave = this;
/** contains the code for monitoring incoming clients */
private Runnable connect = new Runnable() {
public void run() {
while (alive) {
try {
// wait for a new socket to connect
Socket socket = serverSocket.accept();
if (!alive) break;
if (socket != null) {
synchronized (clientSockets) {
// add client to the list
clientSockets.add(socket);
// add client's input and output streams to the list
DataInputStream in =
new DataInputStream(socket.getInputStream());
clientInputs.add(in);
DataOutputStream out =
new DataOutputStream(socket.getOutputStream());
clientOutputs.add(out);
// assign client an ID number
out.writeInt(++clientID);
clientIds.add(new Integer(clientID));
}
}
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
}
}
};
/** contains the code for monitoring server/client communication */
private Runnable comm = new Runnable() {
public void run() {
while (alive) {
boolean silence = true;
Object[] sockets, inputs, outputs, cids;
synchronized (clientSockets) {
sockets = clientSockets.toArray();
inputs = clientInputs.toArray();
outputs = clientOutputs.toArray();
cids = clientIds.toArray();
}
for (int i=0; i<sockets.length; i++) {
Socket socket = (Socket) sockets[i];
DataInputStream in = (DataInputStream) inputs[i];
DataOutputStream out = (DataOutputStream) outputs[i];
int cid = ((Integer) cids[i]).intValue();
// check for client requests in the form of MouseEvent data
try {
if (in.available() > 0) {
silence = false;
// receive the client data
int sid = in.readInt();
if (DEBUG && sid != cid) {
System.err.println("Warning: client #" + cid + " believes " +
"its ID number is " + sid);
}
int eventType = in.readInt();
if (eventType == VisADApplet.REFRESH) {
// send latest display image to the client
updateClient(socket, in, out);
// send latest supported control states to the client
for (int j=0; j<supportedControls.length; j++) {
Class c = supportedControls[j];
Vector v = display.getControls(c);
// send control state message to each client
for (int k=0; k<v.size(); k++) {
Control control = (Control) v.elementAt(k);
String message = c.getName() + "\n" +
k + "\n" + control.getSaveString();
updateClient(message, socket, in, out);
}
}
// send latest ScalarMap states to the client
Vector maps = display.getMapVector();
for (int j=0; j<maps.size(); j++) {
ScalarMap map = (ScalarMap) maps.elementAt(j);
ScalarType scalar = map.getScalar();
DisplayRealType displayScalar = map.getDisplayScalar();
double[] range = map.getRange();
String message = "visad.ScalarMap\n" +
scalar.getName() + " " + displayScalar.getName() + " " +
range[0] + " " + range[1];
updateClient(message, socket, in, out);
}
}
else if (eventType == VisADApplet.MOUSE_EVENT) {
int mid = in.readInt();
long when = in.readLong();
int mods = in.readInt();
int x = in.readInt();
int y = in.readInt();
int clicks = in.readInt();
boolean popup = in.readBoolean();
// construct resulting MouseEvent and process it
Component c = display.getComponent();
MouseEvent me =
new MouseEvent(c, mid, when, mods, x, y, clicks, popup);
MouseBehavior mb = display.getMouseBehavior();
MouseHelper mh = mb.getMouseHelper();
mh.processEvent(me, cid);
}
else if (eventType == VisADApplet.MESSAGE) {
int len = in.readInt();
char[] c = new char[len];
for (int j=0; j<len; j++) c[j] = in.readChar();
String message = new String(c);
StringTokenizer st = new StringTokenizer(message, "\n");
String controlClass = st.nextToken();
int index = Convert.getInt(st.nextToken());
String save = st.nextToken();
Class cls = null;
try {
cls = Class.forName(controlClass);
}
catch (ClassNotFoundException exc) {
if (DEBUG) exc.printStackTrace();
}
if (cls != null) {
Control control = display.getControl(cls, index);
if (control != null) {
// verify that control state has actually changed
if (!save.equals(control.getSaveString())) {
try {
synchronized (socketSlave) {
// temporarily disable control change notification
display.removeSlave(socketSlave);
control.setSaveString(save);
display.addSlave(socketSlave);
}
// notify clients of change
for (int k=0; k<sockets.length; k++) {
// skip event source client
int kid = ((Integer) cids[k]).intValue();
if (kid != cid) {
Socket ksocket = (Socket) sockets[k];
DataInputStream kin = (DataInputStream) inputs[k];
DataOutputStream kout =
(DataOutputStream) outputs[k];
updateClient(message, ksocket, kin, kout);
}
}
}
catch (VisADException exc) {
if (DEBUG) exc.printStackTrace();
}
catch (RemoteException exc) {
if (DEBUG) exc.printStackTrace();
}
}
}
else {
if (DEBUG) System.err.println("Warning: ignoring " +
"change to unknown control from client #" + cid);
}
}
}
else { // Unknown event type
if (DEBUG) System.err.println("Warning: " +
"ignoring unknown event type from client #" + cid);
}
}
}
catch (SocketException exc) {
// there is a problem with this socket, so kill it
killSocket(socket, in, out);
break;
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
}
if (silence) {
try {
Thread.sleep(200);
}
catch (InterruptedException exc) { }
}
}
}
};
/** construct a SocketSlaveDisplay for the given VisAD display */
public SocketSlaveDisplay(DisplayImpl d) throws IOException {
this(d, DEFAULT_PORT);
}
/** construct a SocketSlaveDisplay for the given VisAD display,
and communicate with clients using the given port */
public SocketSlaveDisplay(DisplayImpl d, int port) throws IOException {
display = d;
this.port = port;
// create a server socket at the given port
serverSocket = new ServerSocket(port);
// create a thread that listens for connecting clients
connectThread = new Thread(connect,
"SocketSlaveDisplay-Connect-" + display.getName());
connectThread.start();
// create a thread for client/server communication
commThread = new Thread(comm,
"SocketSlaveDisplay-Comm-" + display.getName());
commThread.start();
// register socket server as a slaved display
display.addSlave(this);
}
/** get the socket port used by this SocketSlaveDisplay */
public int getPort() {
return port;
}
/** send the latest display image to the given socket */
private void updateClient(Socket socket, DataInputStream in,
DataOutputStream out)
{
if (pix != null) {
try {
// send image width, height and array length to the output stream
out.writeInt(w);
out.writeInt(h);
out.writeInt(pix.length);
// send pixel data to the output stream
out.write(pix);
}
catch (SocketException exc) {
// there is a problem with this socket, so kill it
killSocket(socket, in, out);
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
}
else if (DEBUG) System.err.println("Null pixels!");
}
/** send a message to the given client */
private void updateClient(String message, Socket socket,
DataInputStream in, DataOutputStream out)
{
try {
// send message to the output stream
out.writeInt(-1); // special code of width -1 indicates message
out.writeInt(message.length());
out.writeChars(message);
}
catch (SocketException exc) {
// there is a problem with this socket, so kill it
killSocket(socket, in, out);
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
}
/** display automatically calls sendImage when its content changes */
public synchronized void sendImage(int[] pixels, int width, int height,
int type) throws RemoteException
{
// convert pixels to byte array
pix = Convert.intToBytes(pixels);
w = width;
h = height;
// update all clients with the new image
int numSockets;
Object[] sockets, inputs, outputs;
synchronized (clientSockets) {
sockets = clientSockets.toArray();
inputs = clientInputs.toArray();
outputs = clientOutputs.toArray();
}
for (int i=0; i<sockets.length; i++) {
updateClient((Socket) sockets[i], (DataInputStream) inputs[i],
(DataOutputStream) outputs[i]);
}
}
/** send the given message to this slave display */
public synchronized void sendMessage(String message) throws RemoteException {
Object[] sockets, inputs, outputs;
synchronized (clientSockets) {
sockets = clientSockets.toArray();
inputs = clientInputs.toArray();
outputs = clientOutputs.toArray();
}
for (int i=0; i<sockets.length; i++) {
updateClient(message, (Socket) sockets[i],
(DataInputStream) inputs[i], (DataOutputStream) outputs[i]);
}
}
/** shut down the given socket */
private void killSocket(Socket socket, DataInputStream in,
DataOutputStream out)
{
// shut down socket input stream
try {
in.close();
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
// shut down socket output stream
try {
out.close();
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
// shut down socket itself
try {
socket.close();
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
// remove socket from socket vectors
synchronized (clientSockets) {
int index = clientSockets.indexOf(socket);
clientSockets.removeElementAt(index);
clientInputs.removeElementAt(index);
clientOutputs.removeElementAt(index);
clientIds.removeElementAt(index);
}
}
/** destroy this server and kills all associated threads */
public void killServer() {
// set flag to cause server's threads to stop running
alive = false;
// shut down all client sockets
while (true) {
Socket socket = null;
DataInputStream in = null;
DataOutputStream out = null;
synchronized (clientSockets) {
if (clientSockets.size() > 0) {
socket = (Socket) clientSockets.elementAt(0);
in = (DataInputStream) clientInputs.elementAt(0);
out = (DataOutputStream) clientOutputs.elementAt(0);
}
}
if (socket == null) break;
else killSocket(socket, in, out);
}
// shut down server socket
try {
serverSocket.close();
}
catch (IOException exc) {
if (DEBUG) exc.printStackTrace();
}
}
}