Source: Pathfinder.js

/**
 * Represents a pathfinder application. This object can be used
 * to query cluster data or request commodity transit.
 *
 * @param {string} url - WebSocket url to the Pathfinder service
 * @param {string} applicationIdentifier - The application identifier for your Pathfinder application
 * @param {string} userCredentials - Unique key from google id toolkit TODO figure out exactly what this is
 * @constructor
 */
function Pathfinder(url, applicationIdentifier, userCredentials) {
    this.url = url;
    this.applicationIdentifier = applicationIdentifier;

    // TODO figure out what to do with this later
    this.userCredentials = userCredentials;
    this.webSocket = new WebSocket(this.url);
    this.webSocket.onmessage = this.onmessage.bind(this);

    this.pendingRequests = {
        "created" : {
            "Cluster" : [],
            "Commodity" : [],
            "Transport" : []
        },
        "deleted" : {
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        },
        "read" : {
            "ApplicationCluster" : {},
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        },
        "routed" : {
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        },
        "routeSubscribed" : {
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        },
        "subscribed" : {
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        },
        "updated" : {
//            "Cluster" : {}, Not currently in documentation
            "Commodity" : {},
            "Transport" : {}
        }
    };

    this.subscriptions = {
        "Cluster" : {},
        "Commodity" : {},
        "Transport" : {},
        "Route" : {
            "Cluster" : {},
            "Commodity" : {},
            "Transport" : {}
        }
    };
}


/**
 * Closes the Pathfinder service's WebSocket
 */
Pathfinder.prototype.close = function() {
  this.webSocket.close();
};

Pathfinder.prototype.onmessage = function(msg) {

    var data = JSON.parse(msg.data);

    if(data.hasOwnProperty("error")) {
        console.error("Server error occured: " + JSON.stringify(data));
        return;
    }

    if(data.hasOwnProperty("routed")) {
        this.handleRouted(data.routed);
    } else if(data.hasOwnProperty("updated")) {
        this.handleUpdated(data.updated);
    } else if(data.hasOwnProperty("created")) {
        this.handleCreated(data.created);
    } else if(data.hasOwnProperty("model")) {
        this.handleRead(data.model);
    } else if(data.hasOwnProperty("deleted")) {
        this.handleDeleted(data.deleted);
    } else if(data.hasOwnProperty("routeSubscribed")) {
        this.handleRouteSubscribed(data.routeSubscribed)
    } else if(data.hasOwnProperty("subscribed")) {
        this.handleSubscribed(data.subscribed);
    } else if(data.hasOwnProperty("applicationCluster")) {
        this.handleReadDefaultClusterId(data.applicationCluster);
    } else {
        console.error("Unknown message: " + JSON.stringify(data));
    }

    // I think that's all of them, for now ...

};

Pathfinder.prototype.constructCluster = function(data) {

    var commodities = data.commodities.map(
        this.constructCommodity,
        this
    );

    var transports = data.transports.map(
        this.constructTransport,
        this
    );

    return new PFCluster(
        data.id,
        data.parent,
        commodities,
        transports,
        this
    );
};

Pathfinder.prototype.constructCommodity = function(data) {
    return new PFCommodity(
        data.id,
        data.startLatitude,
        data.startLongitude,
        data.endLatitude,
        data.endLongitude,
        data.status,
        data.capacity,
        this
    );
};

Pathfinder.prototype.constructTransport = function(data) {
    return new PFTransport(
        data.id,
        data.longitude,
        data.latitude,
        data.status,
        data.capacity,
        this
    );
};

Pathfinder.prototype.constructModel = function(value, model) {
    switch(model) {
        case "Cluster" :
            return this.constructCluster(value);
        case "Commodity" :
            return this.constructCommodity(value);
        case "Transport" :
            return this.constructTransport(value);
    }
};

Pathfinder.prototype.handleCreated = function(data) {
    var request = this.pendingRequests.created[data.model].pop();

    if(request === undefined) {
        console.error("Create " + data.model + " request failed: " + data);
    } else {
        var callback = request.callback;
        callback(this.constructModel(data.value, data.model));

    }
};

Pathfinder.prototype.handleHelper = function(value, type, model) {
    var id = value.id;
    var request = this.pendingRequests[type][model][id];
    var subscription = this.subscriptions[model][value.id];

    var obj = this.constructModel(value, model);
    if(request !== undefined) {
        var callback = request.callback;
        delete this.pendingRequests[type][model][id];

        callback(obj);
    }

    if(subscription !== undefined) {
        var subCallback = subscription.callback;

        subCallback(subscription.obj, obj);
    }
};

Pathfinder.prototype.handleDeleted = function(data) {
    this.handleHelper(data.value, "deleted", data.model);
};

Pathfinder.prototype.handleRead = function(data) {
    this.handleHelper(data.value, "read", data.model);
};

Pathfinder.prototype.handleRouted = function(data) {
    var id = data.value.id;
    var request = this.pendingRequests.routed[data.model][id];
    var subscription = this.subscriptions.Route[data.model][id];

    if(request !== undefined) {
        var callback = request.callback;
        delete this.pendingRequests.routed[data.model][id];

        callback(data.route);
    }

    if(subscription !== undefined) {
        var subCallback = subscription.callback;

        subCallback(subscription.obj, data.route);
    }
};

Pathfinder.prototype.handleUpdated = function(data) {
    this.handleHelper(data.value, "updated", data.model);
};

Pathfinder.prototype.handleSubscribedHelper = function(type, data) {
    var id = data[type].id;
    var request = this.pendingRequests[type][data.model][id];

    if(request === undefined) {
        console.error(type + " " + data.model + " request failed: " + data);
    } else {
        var callback = request.callback;
        delete this.pendingRequests[type][data.model][id];

        callback(data.id);
    }
};

Pathfinder.prototype.handleRouteSubscribed = function(data) {
    this.handleSubscribedHelper("routeSubscribed", data);
};

Pathfinder.prototype.handleSubscribed = function(data) {
    this.handleSubscribedHelper("subscribed", data);
};

Pathfinder.prototype.handleReadDefaultClusterId = function(data) {

    var request = this.pendingRequests.read.ApplicationCluster[data.id];

    if(request === undefined) {
        console.error("Get default cluster request failed: " + data);
    } else {
        var callback = request.callback;
        delete this.pendingRequests.read.ApplicationCluster[data.id];
        callback(data.clusterId);
    }
};

Pathfinder.prototype.requestHelper = function(type, model, id, obj, callback) {

    var requestInFlight = false;

    if(id !== null) {
        requestInFlight = this.pendingRequests[type][model][id] !== undefined;
        this.pendingRequests[type][model][id] = {
            "callback" : callback
        };
    } else {
        this.pendingRequests[type][model].push(
            {
                "callback" : callback
            }
        );
    }

    console.log(JSON.stringify(obj));

    if(!requestInFlight) {
        this.webSocket.send(JSON.stringify(obj));
    }
};

/**
 * Gets the default cluster id for the specific application.
 * @param {Pathfinder~getDefaultClusterIdCallback} callback - A callback that handles the response
 */
Pathfinder.prototype.getDefaultClusterId = function(callback) {

    var obj = {
        "getApplicationCluster": {
            "id": this.applicationIdentifier
        }
    };

    this.requestHelper("read", "ApplicationCluster", this.applicationIdentifier, obj, callback);
};

/**
 * This callback is called after the getDefaultClusterId function receives a response.
 * @callback Pathfinder~getDefaultClusterIdCallback
 * @param {number} clusterId - The default cluster id
 */


Pathfinder.prototype.readRequestHelper = function(model, id, callback) {

    var obj = {
        "read" : {
            "model" : model,
            "id" : id
        }
    };

    this.requestHelper("read", model, id, obj, callback);
};

/**
 * Gets a cluster with the specified id.
 * @param id - The id of the cluster to retrieve
 * @param {Pathfinder~getClusterCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.getCluster = function(id, callback) {
    this.readRequestHelper("Cluster", id, callback);
};

/**
 * This callback is called after the getCluster function receives a response.
 * @callback Pathfinder~getClusterCallback
 * @param {PFCluster} cluster - The cluster received
 */

/**
 * Gets a commodity with the specified id.
 * @param id - The id of the commodity to retrieve
 * @param {Pathfinder~getCommodityCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.getCommodity = function(id, callback) {
    this.readRequestHelper("Commodity", id, callback);
};

/**
 * This callback is called after the getCommodity function receives a response.
 * @callback Pathfinder~getCommodityCallback
 * @param {PFCommodity} commodity - The commodity received
 */

/**
 * Gets a transport with the specified id.
 * @param id - The id of the transport to retrieve
 * @param {Pathfinder~getTransportCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.getTransport = function(id, callback) {
    this.readRequestHelper("Transport", id, callback);
};

/**
 * This callback is called after the getTransport function receives a response.
 * @callback Pathfinder~getTransportCallback
 * @param {PFTransport} transport - The transport received
 */

Pathfinder.prototype.createRequestHelper = function(model, value, callback) {
    var obj = {
        "create" : {
            "model" : model,
            "value" : value
        }
    };

    this.requestHelper("created", model, null, obj, callback);
};

/**
 * Creates a cluster.
 * @param {Pathfinder~createClusterCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.createCluster = function(callback) {
    // TODO figure out if this needs an id or something.
    this.createRequestHelper("Cluster", {}, callback);
};

/**
 * This callback is called after the createCluster function receives a response.
 * @callback Pathfinder~createClusterCallback
 * @param {PFCluster} cluster - The newly created cluster
 */

/**
 * Creates a commodity
 * @param {number} startLat - The starting latitude of the commodity
 * @param {number} startLong - The starting longitude of the commodity
 * @param {number} endLat - The ending latitude of the commodity
 * @param {number} endLong - The ending longitude of the commodity
 * @param {number} param - The capacity taken up by the commodity
 * @param {string} status - The status of the commodity
 * @param {number} clusterId - The cluster the commodity will be created under
 * @param {Pathfinder~createCommodityCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.createCommodity = function(startLat, startLong, endLat, endLong, param, status, clusterId, callback) {
    var val = {
        "startLatitude" : startLat,
        "startLongitude" : startLong,
        "endLatitude" : endLat,
        "endLongitude" : endLong,
        "param" : param,
        "status" : status,
        "clusterId" : clusterId
    };

    this.createRequestHelper("Commodity", val, callback);
};

/**
 * This callback is called after the createCommodity function receives a response.
 * @callback Pathfinder~createCommodityCallback
 * @param {PFCommodity} commodity - The newly created commodity
 */

/**
 * Creates a transport.
 * @param {number} latitude - The current latitude of the transport
 * @param {number} longitude - The current longitude of the transport
 * @param {number} capacity - The capacity of the transport
 * @param {string} status - The status of the transport
 * @param {number} clusterId - The cluster the transport will be created under
 * @param {Pathfinder~createTransportCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.createTransport = function(latitude, longitude, capacity, status, clusterId, callback) {
    var val = {
        "latitude" : latitude,
        "longitude" : longitude,
        "capacity" : capacity,
        "status" : status,
        "clusterId" : clusterId
    };

    this.createRequestHelper("Transport", val, callback);
};

/**
 * This callback is called after the createTransport function receives a response.
 * @callback Pathfinder~createTransportCallback
 * @param {PFTransport} transport - The newly created transport
 */

Pathfinder.prototype.updateRequestHelper = function(model, id, value, callback) {
    var obj = {
        "update" : {
            "model" : model,
            "id" : id,
            "value" : value
        }
    };

    this.requestHelper("updated", model, id, obj, callback);
};

/**
 * Updates a commodity. Use null for any parameter that should not change.
 * @param {number} startLat - The starting latitude of the commodity
 * @param {number} startLong - The starting longitude of the commodity
 * @param {number} endLat - The ending latitude of the commodity
 * @param {number} endLong - The ending longitude of the commodity
 * @param {number} param - The capacity taken up by the commodity
 * @param {string} status - The status of the commodity
 * @param {number} id - The id of the commodity to be updated
 * @param {Pathfinder~updateCommodityCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.updateCommodity = function(startLat, startLong, endLat, endLong, status, param, id, callback) {
    var value = {};

    if(startLat !== null) {
        value.startLatitude = startLat;
    }

    if(startLong !== null) {
        value.startLongitude = startLong;
    }

    if(endLat !== null) {
        value.endLatitude = endLat;
    }

    if(endLong !== null) {
        value.endLongitude = endLong;
    }

    if(status !== null) {
        value.status = status;
    }

    if(param !== null) {
        value.param = param;
    }

    this.updateRequestHelper("Commodity", id, value, callback);
};

/**
 * This callback is called after the updateCommodity function receives a response.
 * @callback Pathfinder~updateCommodityCallback
 * @param {PFCommodity} commodity - The updated commodity
 */

/**
 * Updates a transport. Use null for any parameter that should not be updated.
 * @param {number} lat - The current latitude of the transport
 * @param {number} long - The current longitude of the transport
 * @param {number} capacity - The capacity of the transport
 * @param {string} status - The status of the transport
 * @param {number} id - The id of the transport to be updated
 * @param {Pathfinder~updateTransportCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.updateTransport = function(lat, long, status, capacity, id, callback) {
    var value = {};

    if(lat !== null) {
        value.latitude = lat;
    }

    if(long !== null) {
        value.longitude = long;
    }

    if(status !== null) {
        value.status = status;
    }

    if(capacity !== null) {
        value.capacity = capacity;
    }

    this.createRequestHelper("Transport", value, callback);
};

/**
 * This callback is called after the updateTransport function receives a response.
 * @callback Pathfinder~updateTransportCallback
 * @param {PFTransport} transport - The updated transport
 */


Pathfinder.prototype.deleteRequestHelper = function(model, id, callback) {
    var obj = {
        "delete" : {
            "model" : model,
            "id" : id
        }
    };

    this.requestHelper("deleted", model, id, obj, callback);
};

/**
 * Deletes a cluster with the specified id.
 * @param {number} id - Id of the cluster to be deleted
 * @param {Pathfinder~deleteClusterCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.deleteCluster = function(id, callback) {
    this.deleteRequestHelper("Cluster", id, callback);
};

/**
 * This callback is called after the deleteCluster function receives a response.
 * @callback Pathfinder~deleteClusterCallback
 * @param {PFCluster} cluster - The deleted cluster
 */

/**
 * Deletes a commodity with the specified id.
 * @param {number} id - Id of the commodity to be deleted
 * @param {Pathfinder~deleteCommodityCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.deleteCommodity = function(id, callback) {
    this.deleteRequestHelper("Commodity", id, callback);
};

/**
 * This callback is called after the deleteCommodity function receives a response.
 * @callback Pathfinder~deleteCommodityCallback
 * @param {PFCommodity} cluster - The deleted commodity
 */

/**
 * Deletes a transport with the specified id.
 * @param {number} id - Id of the transport to be deleted
 * @param {Pathfinder~deleteTransportCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.deleteTransport = function(id, callback) {
    this.deleteRequestHelper("Transport", id, callback);
};

/**
 * This callback is called after the deleteTransport function receives a response.
 * @callback Pathfinder~deleteTransportCallback
 * @param {PFTransport} transport - The deleted transport
 */

Pathfinder.prototype.routeRequestHelper = function(model, id, callback) {
    var obj = {
        "route" : {
            "model" : model,
            "id" : id
        }
    };
    this.requestHelper("routed", model, id, obj, callback);
};

/**
 * Gets the routes for a cluster with the specified id.
 * @param {number} id - Id of the cluster to get the routes for
 * @param {Pathfinder~routeClusterCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.routeCluster = function(id, callback) {
    this.routeRequestHelper("Cluster", id, callback);
};

/**
 * This callback is called after the routeCluster function receives a response.
 * @callback Pathfinder~routeClusterCallback
 * @param {object} routes - The routes received
 */

/**
 * Gets the route for a commodity with the specified id.
 * @param {number} id - Id of the commodity to get the route for
 * @param {Pathfinder~routeCommodityCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.routeCommodity = function(id, callback) {
    this.routeRequestHelper("Commodity", id, callback);
};

/**
 * This callback is called after the routeCommodity function receives a response.
 * @callback Pathfinder~routeCommodityCallback
 * @param {object} route - The route received
 */

/**
 * Gets the route for a transport with the specified id.
 * @param {number} id - Id of the transport to get the route for
 * @param {Pathfinder~routeTransportCallback} callback - The callback that handles the response
 */
Pathfinder.prototype.routeTransport = function(id, callback) {
    this.routeRequestHelper("Transport", id, callback);
};

/**
 * This callback is called after the routeTransport function receives a response.
 * @callback Pathfinder~routeTransportCallback
 * @param {object} route - The route received
 */

Pathfinder.prototype.modelSubscribeRequestHelper = function(model, id, callback) {
    var obj = {
        "subscribe" : {
            "model" : model,
            "id" : id
        }
    };
    this.requestHelper("subscribed", model, id, obj, callback);
};

Pathfinder.prototype.modelSubscribeHelper = function(model, obj, onSubscribeCallback, updateCallback) {
    if(this.pathfinder.subscriptions[model][this.id] === undefined) {
        this.pathfinder.subscriptions[model][this.id] = {
            "obj" : obj,
            "callback" : updateCallback
        };
        this.modelSubscribeRequestHelper(model, obj.id, onSubscribeCallback);
    } else {
        this.pathfinder.subscriptions[model][this.id].callback = updateCallback;
    }
};

Pathfinder.prototype.modelUnsubscribeHelper = function(model, id) {
    delete this.pathfinder.subscriptions[model][id];
    // need to tell the server to stop sending updates when this gets implemented
};

Pathfinder.prototype.routeSubscribeRequestHelper = function(model, id, callback) {
    var obj = {
        "routeSubscribe" : {
            "model" : model,
            "id" : id
        }
    };
    this.requestHelper("routeSubscribed", model, id, obj, callback);
};

Pathfinder.prototype.routeSubscribeHelper = function(model, obj, onSubscribeCallback, updateCallback) {
    if(this.pathfinder.subscriptions.Route[model][this.id] === undefined) {
        this.pathfinder.subscriptions.Route[model][this.id] = {
            "obj" : obj,
            "callback" : updateCallback
        };
        this.routeSubscribeRequestHelper(model, obj.id, onSubscribeCallback);
    } else {
        this.pathfinder.subscriptions.Route[model][this.id].callback = updateCallback;
    }
};

Pathfinder.prototype.routeUnsubscribeHelper = function(model, id) {
    delete this.pathfinder.subscriptions.Route[model][id];
    // need to tell the server to stop sending updates when this gets implemented
};