org.rdv.rbnb.RBNBController.java Source code

Java tutorial

Introduction

Here is the source code for org.rdv.rbnb.RBNBController.java

Source

/*
 * RDV
 * Real-time Data Viewer
 * http://rdv.googlecode.com/
 * 
 * Copyright (c) 2005-2007 University at Buffalo
 * Copyright (c) 2005-2007 NEES Cyberinfrastructure Center
 * Copyright (c) 2008 Palta Software
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * 
 * $URL$
 * $Revision$
 * $Date$
 * $Author$
 */

package org.rdv.rbnb;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rdv.DataViewer;
import org.rdv.data.Channel;
import org.rdv.data.LocalChannel;
import org.rdv.data.LocalChannelManager;

import com.rbnb.sapi.ChannelMap;
import com.rbnb.sapi.ChannelTree;
import com.rbnb.sapi.LocalChannelMap;
import com.rbnb.sapi.SAPIException;
import com.rbnb.sapi.Sink;

/**
 * A class to manage a connection to an RBNB server and to post channel data to
 * interested listeners.
 * 
 * @author Jason P. Hanley
 */
public class RBNBController implements Player {

    static Log log = LogFactory.getLog(RBNBController.class.getName());

    /** the single instance of this class */
    protected static RBNBController instance;

    private String rbnbSinkName = "RDV";

    private Thread rbnbThread;

    private int state;

    private Sink sink;

    private String rbnbHostName;
    private int rbnbPortNumber;

    private static final String DEFAULT_RBNB_HOST_NAME = "localhost";
    private static final int DEFAULT_RBNB_PORT_NUMBER = 3333;

    private boolean requestIsMonitor;

    private ChannelMap requestedChannels;

    private ChannelManager channelManager;
    private MetadataManager metadataManager;
    private MarkerManager markerManager;

    private List<TimeListener> timeListeners;
    private List<StateListener> stateListeners;
    private List<SubscriptionListener> subscriptionListeners;
    private List<PlaybackRateListener> playbackRateListeners;
    private List<TimeScaleListener> timeScaleChangeListeners;
    private List<MessageListener> messageListeners;
    private List<ConnectionListener> connectionListeners;

    private LocalChannelMap preFetchChannelMap;
    private Object preFetchLock = new Object();
    private boolean preFetchDone;

    private double location;
    private double playbackRate;
    private double timeScale;

    private double updateLocation = -1;
    private Object updateLocationLock = new Object();

    private double updateTimeScale = -1;
    private Object updateTimeScaleLock = new Object();

    private double updatePlaybackRate = -1;
    private Object updatePlaybackRateLock = new Object();

    private List<Integer> stateChangeRequests = new ArrayList<Integer>();
    private List<SubscriptionRequest> updateSubscriptionRequests = new ArrayList<SubscriptionRequest>();

    private boolean dropData;

    private final double PLAYBACK_REFRESH_RATE = 0.05;

    private final long LOADING_TIMEOUT = 30000;

    protected RBNBController() {
        // get the system host name and append it to the sink name
        try {
            InetAddress addr = InetAddress.getLocalHost();
            String hostname = addr.getHostName();
            rbnbSinkName += "@" + hostname;
        } catch (UnknownHostException e) {
        }

        state = STATE_DISCONNECTED;

        rbnbHostName = DEFAULT_RBNB_HOST_NAME;
        rbnbPortNumber = DEFAULT_RBNB_PORT_NUMBER;

        requestIsMonitor = false;

        dropData = false;

        location = System.currentTimeMillis() / 1000d;
        playbackRate = 1;
        timeScale = 1;

        requestedChannels = new ChannelMap();

        channelManager = new ChannelManager();
        metadataManager = new MetadataManager(this);
        markerManager = new MarkerManager(this);

        timeListeners = new ArrayList<TimeListener>();
        stateListeners = new ArrayList<StateListener>();
        subscriptionListeners = new ArrayList<SubscriptionListener>();
        playbackRateListeners = new ArrayList<PlaybackRateListener>();
        timeScaleChangeListeners = new ArrayList<TimeScaleListener>();
        messageListeners = new ArrayList<MessageListener>();
        connectionListeners = new ArrayList<ConnectionListener>();

        run();
    }

    /**
     * Get the single instance of this class.
     * 
     * @return  the instance of this class
     */
    public static RBNBController getInstance() {
        if (instance == null) {
            instance = new RBNBController();
        }

        return instance;
    }

    private void run() {
        rbnbThread = new Thread(new Runnable() {
            public void run() {
                runRBNB();
            }
        }, "RBNB");
        rbnbThread.start();
    }

    private void runRBNB() {
        log.info("RBNB data thread has started.");

        while (state != STATE_EXITING) {
            processSubscriptionRequests();
            processLocationUpdate();
            processTimeScaleUpdate();
            processPlaybackRateUpdate();
            processStateChangeRequests();

            switch (state) {
            case STATE_LOADING:
                log.warn("You must always manually transition from the loading state.");
                changeStateSafe(STATE_STOPPED);
                break;
            case STATE_PLAYING:
                updateDataPlaying();
                break;
            case STATE_MONITORING:
                updateDataMonitoring();
                break;
            case STATE_STOPPED:
            case STATE_DISCONNECTED:
                try {
                    Thread.sleep(50);
                } catch (Exception e) {
                }
                break;
            }
        }

        closeRBNB();
        metadataManager.stopUpdating();

        log.info("RBNB data thread is exiting.");
    }

    // State Processing Methods

    private void processSubscriptionRequests() {
        while (!updateSubscriptionRequests.isEmpty()) {
            SubscriptionRequest subscriptionRequest;
            synchronized (updateSubscriptionRequests) {
                subscriptionRequest = (SubscriptionRequest) updateSubscriptionRequests.remove(0);
            }
            List<String> channelNames = subscriptionRequest.getChannelNames();
            DataListener listener = subscriptionRequest.getListener();
            if (subscriptionRequest.isSubscribe()) {
                subscribeSafe(channelNames, listener);
            } else {
                unsubscribeSafe(channelNames, listener);
            }
        }
    }

    private void processLocationUpdate() {
        if (updateLocation != -1) {
            double oldLocation = location;

            synchronized (updateLocationLock) {
                location = updateLocation;
                updateLocation = -1;
            }

            if (oldLocation == location) {
                return;
            }

            log.info("Setting location to " + DataViewer.formatDate(location) + ".");

            if (requestedChannels.NumberOfChannels() > 0) {
                changeStateSafe(STATE_LOADING);

                double duration;
                if (oldLocation < location && oldLocation > location - timeScale) {
                    duration = location - oldLocation;
                } else {
                    duration = timeScale;
                }
                loadData(location, duration);

                changeStateSafe(STATE_STOPPED);
            } else {
                updateTimeListeners(location);
            }
        }
    }

    private void processTimeScaleUpdate() {
        if (updateTimeScale != -1) {
            double oldTimeScale = timeScale;

            synchronized (updateTimeScaleLock) {
                timeScale = updateTimeScale;
                updateTimeScale = -1;
            }

            if (timeScale == oldTimeScale) {
                return;
            }

            log.info("Setting time scale to " + timeScale + ".");

            fireTimeScaleChanged(timeScale);

            if (timeScale > oldTimeScale && requestedChannels.NumberOfChannels() > 0) {
                //TODO make this loading smarter

                int originalState = state;

                changeStateSafe(STATE_LOADING);
                loadData();

                if (originalState == STATE_PLAYING) {
                    changeStateSafe(STATE_PLAYING);
                } else if (originalState == STATE_MONITORING) {
                    changeStateSafe(STATE_MONITORING);
                } else {
                    changeStateSafe(STATE_STOPPED);
                }
            }
        }
    }

    private void processPlaybackRateUpdate() {
        if (updatePlaybackRate != -1) {
            double oldPlaybackRate = playbackRate;

            synchronized (updatePlaybackRateLock) {
                playbackRate = updatePlaybackRate;
                updatePlaybackRate = -1;
            }

            if (playbackRate == oldPlaybackRate) {
                return;
            }

            log.info("Setting playback rate to " + playbackRate + " seconds.");

            if (state == STATE_PLAYING) {
                getPreFetchChannelMap();
                preFetchData(location, playbackRate);
            }

            firePlaybackRateChanged(playbackRate);
        }
    }

    private void processStateChangeRequests() {
        while (!stateChangeRequests.isEmpty()) {
            int updateState;
            synchronized (stateChangeRequests) {
                updateState = ((Integer) stateChangeRequests.remove(0)).intValue();
            }
            changeStateSafe(updateState);
        }
    }

    private boolean changeStateSafe(int newState) {
        if (state == newState) {
            log.info("Already in state " + getStateName(state) + ".");
            return true;
        } else if (state == STATE_PLAYING) {
            getPreFetchChannelMap();
        } else if (state == STATE_EXITING) {
            log.error("Can not transition out of exiting state to " + getStateName(state) + " state.");
            return false;
        } else if (state == STATE_DISCONNECTED && newState != STATE_EXITING) {
            fireConnecting();

            try {
                initRBNB();
            } catch (SAPIException e) {
                closeRBNB();

                String message = e.getMessage();

                // detect nested excpetions
                if (message.contains("java.io.InterruptedIOException")) {
                    log.info("RBNB server connection canceled by user.");
                } else {
                    log.error("Failed to connect to the RBNB server.");
                    fireErrorMessage("Failed to connect to the RBNB server.");
                }

                fireConnectionFailed();
                return false;
            }

            metadataManager.startUpdating();
            fireConnected();
        }

        switch (newState) {
        case STATE_MONITORING:
            if (!monitorData()) {
                fireErrorMessage(
                        "Stopping real time. Failed to load data from the server. Please try again later.");
                return false;
            }
            break;
        case STATE_PLAYING:
            preFetchData(location, playbackRate);
            break;
        case STATE_LOADING:
        case STATE_STOPPED:
        case STATE_EXITING:
            break;
        case STATE_DISCONNECTED:
            closeRBNB();
            metadataManager.stopUpdating();
            LocalChannelManager.getInstance().removeAllChannels();
            break;
        default:
            log.error("Unknown state: " + state + ".");
            return false;
        }

        int oldState = state;
        state = newState;

        notifyStateListeners(state, oldState);

        log.info("Transitioned from state " + getStateName(oldState) + " to " + getStateName(state) + ".");

        return true;
    }

    // RBNB Methods

    private void initRBNB() throws SAPIException {
        if (sink == null) {
            sink = new Sink();
        } else {
            return;
        }

        sink.OpenRBNBConnection(rbnbHostName + ":" + rbnbPortNumber, rbnbSinkName);

        log.info("Connected to RBNB server.");
    }

    private void closeRBNB() {
        if (sink == null)
            return;

        sink.CloseRBNBConnection();
        sink = null;

        log.info("Connection to RBNB server closed.");
    }

    private void reInitRBNB() throws SAPIException {
        closeRBNB();
        initRBNB();
    }

    // Subscription Methods

    private void subscribeSafe(List<String> channelNames, DataListener panel) {
        //skip subscription if we are not connected
        if (state == STATE_DISCONNECTED) {
            return;
        }

        // a list of channels to load data for
        List<String> channelsToLoad = new ArrayList<String>();

        //subscribe to channels
        for (String channelName : channelNames) {
            Channel channel = getChannel(channelName);

            // see if this is a local channel and subscribe to its server channels,
            // otherwise just subscribe to the channel
            if (channel != null && channel instanceof LocalChannel) {
                LocalChannel localChannel = (LocalChannel) channel;
                List<String> serverChannels = localChannel.getServerChannels();

                for (String serverChannel : serverChannels) {
                    try {
                        requestedChannels.Add(serverChannel);
                    } catch (SAPIException e) {
                        log.error("Failed to subscribe to channel " + serverChannel + ".");
                        e.printStackTrace();
                        continue;
                    }
                }

                log.info("Subscribed to channel " + channelName + " with server channels: " + serverChannels);

                channelsToLoad.addAll(serverChannels);
            } else {
                try {
                    requestedChannels.Add(channelName);
                } catch (SAPIException e) {
                    log.error("Failed to subscribe to channel " + channelName + ".");
                    e.printStackTrace();
                    continue;
                }

                log.info("Subscribed to channel " + channelName + ".");

                channelsToLoad.add(channelName);
            }

            //notify channel manager
            channelManager.subscribe(channelName, panel);
        }

        int originalState = state;

        changeStateSafe(STATE_LOADING);
        loadData(channelsToLoad);

        if (originalState == STATE_MONITORING) {
            changeStateSafe(STATE_MONITORING);
        } else if (originalState == STATE_PLAYING) {
            changeStateSafe(STATE_PLAYING);
        } else {
            changeStateSafe(STATE_STOPPED);
        }

        for (String channelName : channelNames) {
            fireSubscriptionNotification(channelName);
        }
    }

    private void unsubscribeSafe(List<String> channelNames, DataListener panel) {
        //skip unsubscription if we are not connected
        if (state == STATE_DISCONNECTED) {
            return;
        }

        for (String channelName : channelNames) {
            channelManager.unsubscribe(channelName, panel);

            log.info("Unsubscribed from channel " + channelName + ".");
        }

        ChannelMap newRequestedChannels = new ChannelMap();

        // recreate the channel map with the subscribed channels 
        for (String channelName : channelManager.getSubscribedChannels()) {
            Channel channel = getChannel(channelName);

            // see if this is a local channel and subscribe to its server channels,
            // otherwise just subscribe to the channel
            if (channel != null && channel instanceof LocalChannel) {
                LocalChannel localChannel = (LocalChannel) channel;
                List<String> serverChannels = localChannel.getServerChannels();

                for (String serverChannel : serverChannels) {
                    try {
                        newRequestedChannels.Add(serverChannel);
                    } catch (SAPIException e) {
                        log.error("Failed to resubscribe to channel " + serverChannel + ".");
                        e.printStackTrace();
                        continue;
                    }
                }
            } else {
                try {
                    newRequestedChannels.Add(channelName);
                } catch (SAPIException e) {
                    log.error("Failed to resubscribe to channel " + channelName + ".");
                    e.printStackTrace();
                    continue;
                }
            }
        }

        requestedChannels = newRequestedChannels;

        if (state == STATE_MONITORING) {
            monitorData();
        }

        for (String channelName : channelNames) {
            fireUnsubscriptionNotification(channelName);
        }
    }

    // Load Methods

    /**
     * Load data for all channels.
     */
    private void loadData() {
        loadData(location, timeScale);
    }

    /**
     * Load data for all channels.
     * 
     * @param location  the end time
     * @param duration  the duration
     */
    private void loadData(double location, double duration) {
        String[] subscribedChannels = requestedChannels.GetChannelList();
        loadData(Arrays.asList(subscribedChannels), location, duration);
    }

    /**
     * Load data for the specified channels.
     * 
     * @param channelNames  the names of the channels
     */
    private void loadData(List<String> channelNames) {
        loadData(channelNames, location, timeScale);
    }

    /**
     * Load data for the specified channels.
     * 
     * @param channelNames  a list of channel names
     * @param location      the end time
     * @param duration      the amount of data to load
     */
    private void loadData(List<String> channelNames, double location, double duration) {
        ChannelMap realRequestedChannels = requestedChannels;

        ChannelMap imageChannels = new ChannelMap();
        ChannelMap tabularChannels = new ChannelMap();
        ChannelMap otherChannels = new ChannelMap();

        for (String channelName : channelNames) {
            try {
                if (isVideo(channelName)) {
                    imageChannels.Add(channelName);
                } else if (channelManager.isChannelTabularOnly(channelName)) {
                    tabularChannels.Add(channelName);
                } else {
                    otherChannels.Add(channelName);
                }
            } catch (SAPIException e) {
                log.error("Failed to add channel " + channelName + ".");
                e.printStackTrace();
            }
        }

        if (imageChannels.NumberOfChannels() > 0) {
            requestedChannels = imageChannels;
            if (!requestData(location, 0)) {
                fireErrorMessage("Failed to load data from the server. Please try again later.");
                requestedChannels = realRequestedChannels;
                changeStateSafe(STATE_STOPPED);
                return;
            }
            updateDataMonitoring();
            updateTimeListeners(location);
        }

        if (tabularChannels.NumberOfChannels() > 0) {
            requestedChannels = tabularChannels;
            if (!requestData(location - 1, 1)) {
                fireErrorMessage("Failed to load data from the server. Please try again later.");
                requestedChannels = realRequestedChannels;
                changeStateSafe(STATE_STOPPED);
                return;
            }
            updateDataMonitoring();
            updateTimeListeners(location);
        }

        if (otherChannels.NumberOfChannels() > 0) {
            requestedChannels = otherChannels;
            if (!requestData(location - duration, duration)) {
                fireErrorMessage("Failed to load data from the server. Please try again later.");
                requestedChannels = realRequestedChannels;
                changeStateSafe(STATE_STOPPED);
                return;
            }
            updateDataMonitoring();
            updateTimeListeners(location);
        }

        requestedChannels = realRequestedChannels;

        log.info("Loaded " + DataViewer.formatSeconds(timeScale) + " of data at " + DataViewer.formatDate(location)
                + ".");
    }

    // Playback Methods

    private boolean requestData(double location, double duration) {
        return requestData(location, duration, true);
    }

    private boolean requestData(double location, double duration, boolean retry) {
        if (requestedChannels.NumberOfChannels() == 0) {
            return false;
        }

        if (requestIsMonitor) {
            try {
                reInitRBNB();
            } catch (SAPIException e) {
                requestIsMonitor = true;
                return false;
            }

            requestIsMonitor = false;
        }

        try {
            sink.Request(requestedChannels, location, duration, "absolute");
        } catch (SAPIException e) {
            log.error("Failed to request channels at " + DataViewer.formatDate(location) + " for "
                    + DataViewer.formatSeconds(duration) + ".");
            e.printStackTrace();

            requestIsMonitor = true;

            if (retry) {
                return requestData(location, duration, false);
            } else {
                return false;
            }
        }

        return true;
    }

    private synchronized void updateDataPlaying() {
        if (requestedChannels.NumberOfChannels() == 0) {
            fireStatusMessage("Stopping playback. No channels are selected.");
            changeStateSafe(STATE_STOPPED);
            return;
        }

        LocalChannelMap getmap = null;

        getmap = getPreFetchChannelMap();
        if (getmap == null) {
            fireErrorMessage("Stopping playback. Failed to load data from the server. Please try again later.");
            changeStateSafe(STATE_STOPPED);

            requestIsMonitor = true;
            return;
        } else if (getmap.GetIfFetchTimedOut()) {
            fireErrorMessage(
                    "Stopping playback. Failed to load enough data from server. The playback rate may be too fast or the server is busy.");
            changeStateSafe(STATE_STOPPED);
            return;
        }

        //stop if no data in fetch and past end time, most likely end of data
        if (getmap.NumberOfChannels() == 0 && !moreData(requestedChannels.GetChannelList(), location)) {
            log.warn("Received no data. Assuming end of channel.");
            changeStateSafe(STATE_STOPPED);
            return;
        }

        preFetchData(location + playbackRate, playbackRate);

        channelManager.postData(getmap);

        double playbackDuration = playbackRate;
        double playbackRefreshRate = PLAYBACK_REFRESH_RATE;
        double playbackStepTime = playbackRate * playbackRefreshRate;
        long playbackSteps = (long) (playbackDuration / playbackStepTime);

        double locationStartTime = location;
        long playbackStartTime = System.nanoTime();

        long i = 0;
        while (i < playbackSteps && stateChangeRequests.size() == 0 && updateLocation == -1 && updateTimeScale == -1
                && updatePlaybackRate == -1) {
            double timeDifference = (playbackRefreshRate * (i + 1))
                    - ((System.nanoTime() - playbackStartTime) / 1000000000d);
            if (dropData && timeDifference < -playbackRefreshRate) {
                int stepsToSkip = (int) ((timeDifference * -1) / playbackRefreshRate);
                i += stepsToSkip;
            } else if (timeDifference > playbackRefreshRate) {
                try {
                    Thread.sleep((long) (timeDifference * 1000));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            i++;

            location = locationStartTime + (playbackStepTime) * i;
            updateTimeListeners(location);
        }
    }

    private void preFetchData(final double location, final double duration) {
        preFetchChannelMap = null;
        preFetchDone = false;

        new Thread(new Runnable() {
            public void run() {
                boolean requestStatus = false;
                if (state == STATE_PLAYING) {
                    requestStatus = requestData(location, duration);
                }

                if (requestStatus) {
                    preFetchChannelMap = new LocalChannelMap();
                    try {
                        sink.Fetch(LOADING_TIMEOUT, preFetchChannelMap);
                    } catch (Exception e) {
                        log.error("Failed to fetch data.");
                        e.printStackTrace();
                    }
                } else {
                    preFetchChannelMap = null;
                }

                synchronized (preFetchLock) {
                    preFetchDone = true;
                    preFetchLock.notify();
                }
            }
        }, "prefetch").start();
    }

    private LocalChannelMap getPreFetchChannelMap() {
        synchronized (preFetchLock) {
            if (!preFetchDone) {
                log.debug("Waiting for pre-fetch channel map.");
                try {
                    preFetchLock.wait();
                } catch (Exception e) {
                    log.error("Failed to wait for channel map.");
                    e.printStackTrace();
                }
                log.debug("Done waiting for pre-fetch channel map.");
            }
        }

        LocalChannelMap fetchedMap = preFetchChannelMap;
        preFetchChannelMap = null;

        return fetchedMap;
    }

    // Monitor Methods

    private boolean monitorData() {
        return monitorData(true);
    }

    private boolean monitorData(boolean retry) {
        if (requestedChannels.NumberOfChannels() == 0) {
            return true;
        }

        if (requestIsMonitor) {
            try {
                reInitRBNB();
            } catch (SAPIException e) {
                e.printStackTrace();
                return false;
            }
        }

        log.debug("Monitoring data after location " + DataViewer.formatDate(location) + ".");

        requestIsMonitor = true;

        try {
            sink.Monitor(requestedChannels, 5);
            log.info("Monitoring selected data channels.");
        } catch (SAPIException e) {
            log.error("Failed to monitor channels.");
            e.printStackTrace();

            if (retry) {
                return monitorData(false);
            } else {
                return false;
            }
        }

        return true;
    }

    private void updateDataMonitoring() {
        //stop monitoring if no channels selected
        if (requestedChannels.NumberOfChannels() == 0) {
            fireStatusMessage("Stopping real time. No channels are selected.");
            changeStateSafe(STATE_STOPPED);
            return;
        }

        LocalChannelMap getmap = new LocalChannelMap();

        long timeout;
        if (state == STATE_MONITORING) {
            timeout = 500;
        } else {
            timeout = LOADING_TIMEOUT;
        }

        try {
            sink.Fetch(timeout, getmap);
        } catch (Exception e) {
            fireErrorMessage("Failed to load data from the server. Please try again later.");
            e.printStackTrace();

            changeStateSafe(STATE_STOPPED);
            requestIsMonitor = true;
            return;
        }

        if (getmap.GetIfFetchTimedOut()) {
            if (state == STATE_MONITORING) {
                //no data was received, this is not an error and we should go on
                //to see if more data is recieved next time around
                //TODO see if we should sleep here
                log.debug("Fetch timed out for monitor.");
                return;

            } else {
                log.error("Failed to fetch data.");
                fireErrorMessage("Failed to load data from the server. Please try again later.");
                changeStateSafe(STATE_STOPPED);
                return;
            }
        }

        //received no data
        if (getmap.NumberOfChannels() == 0) {
            return;
        }

        //post data to listeners
        channelManager.postData(getmap);

        if (state == STATE_MONITORING) {
            //update current location
            double newLocation = getLastTime(getmap);
            if (newLocation > location) {
                location = newLocation;
            }
            updateTimeListeners(location);
        }
    }

    // Listener Methods

    private void updateTimeListeners(double location) {
        for (int i = 0; i < timeListeners.size(); i++) {
            TimeListener timeListener = (TimeListener) timeListeners.get(i);
            try {
                timeListener.postTime(location);
            } catch (Exception e) {
                log.error("Failed to post time to " + timeListener + ".");
                e.printStackTrace();
            }
        }
    }

    private void notifyStateListeners(int state, int oldState) {
        for (int i = 0; i < stateListeners.size(); i++) {
            StateListener stateListener = (StateListener) stateListeners.get(i);
            stateListener.postState(state, oldState);
        }
    }

    private void fireSubscriptionNotification(String channelName) {
        SubscriptionListener subscriptionListener;
        for (int i = 0; i < subscriptionListeners.size(); i++) {
            subscriptionListener = (SubscriptionListener) subscriptionListeners.get(i);
            subscriptionListener.channelSubscribed(channelName);
        }
    }

    private void fireUnsubscriptionNotification(String channelName) {
        SubscriptionListener subscriptionListener;
        for (int i = 0; i < subscriptionListeners.size(); i++) {
            subscriptionListener = (SubscriptionListener) subscriptionListeners.get(i);
            subscriptionListener.channelUnsubscribed(channelName);
        }
    }

    private void firePlaybackRateChanged(double playbackRate) {
        PlaybackRateListener listener;
        for (int i = 0; i < playbackRateListeners.size(); i++) {
            listener = (PlaybackRateListener) playbackRateListeners.get(i);
            listener.playbackRateChanged(playbackRate);
        }
    }

    private void fireTimeScaleChanged(double timeScale) {
        TimeScaleListener listener;
        for (int i = 0; i < timeScaleChangeListeners.size(); i++) {
            listener = (TimeScaleListener) timeScaleChangeListeners.get(i);
            listener.timeScaleChanged(timeScale);
        }
    }

    // Utility Methods

    private boolean moreData(String[] channels, double time) {
        double endTime = 0;

        for (String channelName : channels) {
            Channel channel = getChannel(channelName);
            if (channel == null) {
                continue;
            }

            double channelEndTime = channel.getStart() + channel.getDuration();
            endTime = Math.max(endTime, channelEndTime);
        }

        return time < endTime;
    }

    private boolean isVideo(String channelName) {
        Channel channel = getChannel(channelName);
        if (channel == null) {
            return false;
        }

        String mime = channel.getMetadata("mime");
        if (mime != null && mime.equals("image/jpeg")) {
            return true;
        } else {
            return false;
        }
    }

    private static double getLastTime(ChannelMap channelMap) {
        double lastTime = -1;

        String[] channels = channelMap.GetChannelList();
        for (int i = 0; i < channels.length; i++) {
            String channelName = channels[i];
            int channelIndex = channelMap.GetIndex(channelName);
            double[] times = channelMap.GetTimes(channelIndex);
            double endTime = times[times.length - 1];
            if (endTime > lastTime) {
                lastTime = endTime;
            }
        }

        return lastTime;
    }

    // Player Methods

    public int getState() {
        return state;
    }

    public void monitor() {
        if (state != STATE_MONITORING) {
            setLocation(System.currentTimeMillis() / 1000d);
        }
        setState(STATE_MONITORING);
    }

    public void play() {
        setState(STATE_PLAYING);
    }

    public void pause() {
        setState(STATE_STOPPED);
    }

    public void exit() {
        setState(STATE_EXITING);

        //wait for thread to finish
        int count = 0;
        while (sink != null && count++ < 20) {
            try {
                Thread.sleep(50);
            } catch (Exception e) {
            }
        }
    }

    public void setState(int state) {
        synchronized (stateChangeRequests) {
            stateChangeRequests.add(new Integer(state));
        }
    }

    public double getLocation() {
        return location;
    }

    public void setLocation(final double location) {
        if (location < 0) {
            log.error("Location not set; location must be nonnegative.");
            return;
        }

        synchronized (updateLocationLock) {
            updateLocation = location;
        }
    }

    public double getPlaybackRate() {
        return playbackRate;
    }

    public void setPlaybackRate(final double playbackRate) {
        if (playbackRate <= 0) {
            log.error("Playback rate not set; playback rate must be positive.");
            return;
        }

        synchronized (updatePlaybackRateLock) {
            updatePlaybackRate = playbackRate;
        }
    }

    public double getTimeScale() {
        return timeScale;
    }

    public void setTimeScale(double timeScale) {
        if (timeScale <= 0) {
            log.error("Time scale not set; time scale must be positive.");
            return;
        }

        synchronized (updateTimeScaleLock) {
            updateTimeScale = timeScale;
        }
    }

    public boolean subscribe(String channelName, DataListener listener) {
        synchronized (updateSubscriptionRequests) {
            updateSubscriptionRequests.add(new SubscriptionRequest(channelName, listener, true));
        }

        return true;
    }

    /**
     * Subscribe to the list of <code>channels</code> with the data
     * <code>listener</code>.
     * 
     * @param channels  the channels to subscribe to
     * @param listener  the data listener to post data to
     */
    public void subscribe(List<String> channels, DataListener listener) {
        synchronized (updateSubscriptionRequests) {
            updateSubscriptionRequests.add(new SubscriptionRequest(channels, listener, true));
        }
    }

    public boolean unsubscribe(String channelName, DataListener listener) {
        synchronized (updateSubscriptionRequests) {
            updateSubscriptionRequests.add(new SubscriptionRequest(channelName, listener, false));
        }

        return true;
    }

    /**
     * Unsubscribe from the list of <code>channels</code> for  the data
     * <code>listener</code>.
     * 
     * @param channels  the list of channels to unsubscribe from
     * @param listener  the data listener to unsubscribe
     */
    public void unsubscribe(List<String> channels, DataListener listener) {
        synchronized (updateSubscriptionRequests) {
            updateSubscriptionRequests.add(new SubscriptionRequest(channels, listener, false));
        }
    }

    public boolean isSubscribed(String channelName) {
        return channelManager.isChannelSubscribed(channelName);
    }

    /**
     * Returns true if there is at least one listener subscribed to a channel.
     * 
     * @return  true if there are channel listener, false if there are none
     */
    public boolean hasSubscribedChannels() {
        return channelManager.hasSubscribedChannels();
    }

    public void addStateListener(StateListener stateListener) {
        stateListener.postState(state, state);
        stateListeners.add(stateListener);
    }

    public void removeStateListener(StateListener stateListener) {
        stateListeners.remove(stateListener);
    }

    public void addTimeListener(TimeListener timeListener) {
        timeListeners.add(timeListener);
        timeListener.postTime(location);
    }

    public void removeTimeListener(TimeListener timeListener) {
        timeListeners.remove(timeListener);
    }

    public void addPlaybackRateListener(PlaybackRateListener listener) {
        listener.playbackRateChanged(playbackRate);
        playbackRateListeners.add(listener);
    }

    public void removePlaybackRateListener(PlaybackRateListener listener) {
        playbackRateListeners.remove(listener);
    }

    public void addTimeScaleListener(TimeScaleListener listener) {
        listener.timeScaleChanged(timeScale);
        timeScaleChangeListeners.add(listener);
    }

    public void removeTimeScaleListener(TimeScaleListener listener) {
        timeScaleChangeListeners.remove(listener);
    }

    // Public Methods

    public void dropData(boolean dropData) {
        this.dropData = dropData;
    }

    public String getRBNBHostName() {
        return rbnbHostName;
    }

    public void setRBNBHostName(String rbnbHostName) {
        this.rbnbHostName = rbnbHostName;
    }

    public int getRBNBPortNumber() {
        return rbnbPortNumber;
    }

    public void setRBNBPortNumber(int rbnbPortNumber) {
        this.rbnbPortNumber = rbnbPortNumber;
    }

    public String getRBNBConnectionString() {
        return rbnbHostName + ":" + rbnbPortNumber;
    }

    /**
     * Gets the name of the server. If there is no active connection, null is
     * returned.
     * 
     * @return  the name of the server, or null if there is no connection
     */
    public String getServerName() {
        if (sink == null) {
            return null;
        }

        String serverName;

        try {
            serverName = sink.GetServerName();

            // strip out the leading slash that is there for some reason
            if (serverName.startsWith("/") && serverName.length() >= 2) {
                serverName = serverName.substring(1);
            }
        } catch (IllegalStateException e) {
            serverName = null;
        }

        return serverName;
    }

    public boolean isConnected() {
        return sink != null;
    }

    public void connect() {
        connect(false);
    }

    public boolean connect(boolean block) {
        if (isConnected()) {
            return true;
        }

        if (block) {
            final Thread object = Thread.currentThread();
            ConnectionListener listener = new ConnectionListener() {
                public void connecting() {
                }

                public void connected() {
                    synchronized (object) {
                        object.notify();
                    }
                }

                public void connectionFailed() {
                    object.interrupt();
                }
            };
            addConnectionListener(listener);

            synchronized (object) {
                setState(STATE_STOPPED);

                try {
                    object.wait();
                } catch (InterruptedException e) {
                    return false;
                }
            }

            removeConnectionListener(listener);
        } else {
            setState(STATE_STOPPED);
        }

        return true;
    }

    /**
     * Cancel a connection attempt.
     */
    public void cancelConnect() {
        if (rbnbThread != null) {
            rbnbThread.interrupt();
        }
    }

    /**
     * Disconnect from the RBNB server. This method will return immediately.
     */
    public void disconnect() {
        disconnect(false);
    }

    /**
     * Disconnect from the RBNB server. If block is set, this method will not
     * return until the server has disconnected.
     * 
     * @param block  if true, wait for the server to disconnect
     * @return       true if the server disconnected
     */
    public boolean disconnect(boolean block) {
        if (!isConnected()) {
            return true;
        }

        if (block) {
            final Thread object = Thread.currentThread();
            StateListener listener = new StateListener() {
                public void postState(int newState, int oldState) {
                    if (newState == STATE_DISCONNECTED) {
                        synchronized (object) {
                            object.notify();
                        }
                    }
                }
            };
            addStateListener(listener);

            synchronized (object) {
                setState(STATE_DISCONNECTED);

                try {
                    object.wait();
                } catch (InterruptedException e) {
                    return false;
                }
            }

            removeStateListener(listener);

        } else {
            setState(STATE_DISCONNECTED);
        }

        return true;
    }

    public void reconnect() {
        setState(STATE_DISCONNECTED);
        setState(STATE_STOPPED);
    }

    public void addSubscriptionListener(SubscriptionListener subscriptionListener) {
        subscriptionListeners.add(subscriptionListener);
    }

    public void removeSubscriptionListener(SubscriptionListener subscriptionListener) {
        subscriptionListeners.remove(subscriptionListener);
    }

    //Message Methods

    private void fireErrorMessage(String errorMessage) {
        for (int i = 0; i < messageListeners.size(); i++) {
            MessageListener messageListener = (MessageListener) messageListeners.get(i);
            messageListener.postError(errorMessage);
        }
    }

    private void fireStatusMessage(String statusMessage) {
        for (int i = 0; i < messageListeners.size(); i++) {
            MessageListener messageListener = (MessageListener) messageListeners.get(i);
            messageListener.postStatus(statusMessage);
        }
    }

    public void addMessageListener(MessageListener messageListener) {
        messageListeners.add(messageListener);
    }

    public void removeMessageListener(MessageListener messageListener) {
        messageListeners.remove(messageListener);
    }

    // Connection Listener Methods

    private void fireConnecting() {
        for (int i = 0; i < connectionListeners.size(); i++) {
            ConnectionListener connectionListener = (ConnectionListener) connectionListeners.get(i);
            connectionListener.connecting();
        }
    }

    private void fireConnected() {
        for (int i = 0; i < connectionListeners.size(); i++) {
            ConnectionListener connectionListener = (ConnectionListener) connectionListeners.get(i);
            connectionListener.connected();
        }
    }

    private void fireConnectionFailed() {
        for (int i = 0; i < connectionListeners.size(); i++) {
            ConnectionListener connectionListener = (ConnectionListener) connectionListeners.get(i);
            connectionListener.connectionFailed();
        }
    }

    public void addConnectionListener(ConnectionListener connectionListener) {
        connectionListeners.add(connectionListener);
    }

    public void removeConnectionListener(ConnectionListener connectionListener) {
        connectionListeners.remove(connectionListener);
    }

    //Public Metadata Methods

    /**
     * Gets the <code>MetadataManager</code>.
     * 
     * @return  the metadata manager
     */
    public MetadataManager getMetadataManager() {
        return metadataManager;
    }

    /**
     * Gets the <code>Channel</code> with this <code>channelName</code>. This is
     * a convenience method for the same method in <code>MetadataManager</code>.
     * 
     * @param channelName  the name of the channel
     * @return             the channel or null if it was not found
     * @see                MetadataManager#getChannel(String)
     */
    public Channel getChannel(String channelName) {
        return metadataManager.getChannel(channelName);
    }

    /**
     * Gets a list of <code>Channel<code>'s. This is a convenience method for the
     * same method in <code>MetadataManager</code>.
     * 
     * @return  a list of channels
     * @see     MetadataManager#getChannels()
     */
    public List<Channel> getChannels() {
        return metadataManager.getChannels();
    }

    /**
     * Gets a list of <code>Channel</code>'s with the <code>channelNames</code>.
     * This is a convenience method for the same method in
     * <code>MetadataManager</code>.
     * 
     * @param channelNames  the channel names to get
     * @return              a list of channels
     * @see                 MetadataManager#getChannels(List)
     */
    public List<Channel> getChannels(List<String> channelNames) {
        return metadataManager.getChannels(channelNames);
    }

    /**
     * Gets the <code>ChannelTree</code>. This is a convenience method for the
     * same method in <code>MetadataManager</code>.
     * 
     * @return  the channel tree
     * @see     MetadataManager#getChannelTree()
     */
    public ChannelTree getChannelTree() {
        return metadataManager.getChannelTree();
    }

    /**
     * Updates the metadata. This is a convenience method for the same method in
     * <code>MetadataManager</code>.
     * 
     * @see  MetadataManager#updateMetadataBackground()
     */
    public void updateMetadata() {
        metadataManager.updateMetadataBackground();
    }

    //Public Marker Methods

    public MarkerManager getMarkerManager() {
        return markerManager;
    }

    //Public Static Methods

    public static String getStateName(int state) {
        String stateString;

        switch (state) {
        case STATE_LOADING:
            stateString = "loading";
            break;
        case STATE_PLAYING:
            stateString = "playing";
            break;
        case STATE_MONITORING:
            stateString = "real time";
            break;
        case STATE_STOPPED:
            stateString = "stopped";
            break;
        case STATE_EXITING:
            stateString = "exiting";
            break;
        case STATE_DISCONNECTED:
            stateString = "disconnected";
            break;
        default:
            stateString = "UNKNOWN";
        }

        return stateString;
    }

    /**
     * Returns the state code for a given state name
     * 
     * @param stateName  the state name
     * @return           the state code
     */
    public static int getState(String stateName) {
        int code;

        if (stateName.equals("loading")) {
            code = STATE_LOADING;
        } else if (stateName.equals("playing")) {
            code = STATE_PLAYING;
        } else if (stateName.equals("real time")) {
            code = STATE_MONITORING;
        } else if (stateName.equals("stopped")) {
            code = STATE_STOPPED;
        } else if (stateName.equals("exiting")) {
            code = STATE_EXITING;
        } else if (stateName.equals("disconnected")) {
            code = STATE_DISCONNECTED;
        } else {
            code = -1;
        }

        return code;
    }

    class SubscriptionRequest {
        private List<String> channelNames;
        private DataListener listener;
        private boolean isSubscribe;

        public SubscriptionRequest(String channelName, DataListener listener, boolean isSubscribe) {
            this(Collections.singletonList(channelName), listener, isSubscribe);
        }

        public SubscriptionRequest(List<String> channelNames, DataListener listener, boolean isSubscribe) {
            this.channelNames = channelNames;
            this.listener = listener;
            this.isSubscribe = isSubscribe;
        }

        public List<String> getChannelNames() {
            return channelNames;
        }

        public DataListener getListener() {
            return listener;
        }

        public boolean isSubscribe() {
            return isSubscribe;
        }
    }
}