window.multigraph.util.namespace("window.multigraph.core", function (ns) {
"use strict";
/**
* Core functionality for the javascript implementation of multigraph.
*
* @module multigraph
* @submodule core
* @main core
*/
var $ = window.multigraph.jQuery;
// define empty object for holding data adpaters
window.multigraph.adapters = {};
/**
* The Multigraph Jermaine model is the root class for the js-multigraph project.
*
* @class Multigraph
* @for Multigraph
* @constructor
*/
var Multigraph = new window.jermaine.Model("Multigraph", function () {
/**
* Jermiane Attr_List of all the graphs in a Multigraph.
*
* @property graphs
* @type {Graph}
* @author jrfrimme
*/
this.hasMany("graphs").eachOfWhich.validateWith(function (graph) {
return graph instanceof ns.Graph;
});
/**
* The div the multigraph is rendered in.
*
* @property div
* @type {HTML Element}
* @author jrfrimme
*/
this.hasA("div"); // the actual div element
/**
* The url for the mugl file this graph was created from, if any
*
* @property mugl
* @type {string}
* @author mbp
*/
this.hasA("mugl");
/**
* JavaScript array of ajax throttles; each entry in this array is an
* object with the following properties:
* regex : regular expression for matching URLs
* ajaxthrottle : instance of $.ajaxthrottle
*
* @property ajaxthrottles
* @type {Array}
* @author mbp
*/
this.hasA("ajaxthrottles");
this.isBuiltWith(function() {
this.ajaxthrottles([]);
});
this.respondsTo("addAjaxThrottle", function (pattern, requests, period, concurrent) {
this.ajaxthrottles().push({
regex : pattern ? new RegExp(pattern) : undefined,
ajaxthrottle : $.ajaxthrottle({
numRequestsPerTimePeriod : parseInt(requests,10),
timePeriod : parseInt(period, 10),
maxConcurrent : parseInt(concurrent, 10)
})
});
});
this.respondsTo("getAjaxThrottle", function (url) {
var throttle = undefined;
$.each(this.ajaxthrottles(), function() {
if (!this.regex || this.regex.test(url)) {
throttle = this.ajaxthrottle;
return false;
}
return true;
});
return throttle;
});
/*
* This function transforms a given URL so that it
* is relative to the same base as the URL from which the MUGL
* file was loaded. If this graph was not created from a MUGL
* file (either it came from a MUGL string, or was created programmatically),
* the URL is returned unchanged.
*
* If the URL to be rebased is absolute (contains '://')
* or root-relative (starts with a '/'), it is returned unchanged.
*
* Otherise, the given URL is relative, and whhat is returned is a
* new URL obtained by interpreting it relative to the URL
* from which the MUGL was loaded.
*/
this.respondsTo("rebaseUrl", function(url) {
var baseurl = this.mugl();
if (! baseurl) {
return url;
}
if (/^\//.test(url)) {
// url is root-relative (starts with a '/'); return it unmodified
return url;
}
if (/:\/\//.test(url)) {
// url contains '://', so assume it's a full url, return it unmodified
return url;
}
// convert baseurl to a real base path, by eliminating any url args and
// everything after the final '/'
if (!/^\//.test(baseurl) && !/:\/\//.test(baseurl) && !/^\.\//.test(baseurl)) {
// first make sure that if baseurl is relative, it starts with './'
baseurl = './' + baseurl;
}
baseurl = baseurl.replace(/\?.*$/, ''); // remove everything after the first '?'
baseurl = baseurl.replace(/\/[^\/]*$/, '/'); // remove everything after the last '/'
return baseurl + url;
});
/**
* The busy spinner
*
* @property busySpinner
* @type {HTML Element}
* @author mbp
*/
this.hasA("busySpinner"); // the busy_spinner div
this.respondsTo("busySpinnerLevel", function (delta) {
if (this.busySpinner()) {
$(this.busySpinner()).busy_spinner('level', delta);
}
});
/**
* Initializes the Multigraph's geometry by calling the `initializeGeometry` function of
* each of its graph children.
*
* @method initializeGeometry
* @param {Integer} width Width of the multigraph's div.
* @param {Integer} height Height of the multigraph's div.
* @param {Object} graphicsContext
* @author jrfrimme
*/
this.respondsTo("initializeGeometry", function (width, height, graphicsContext) {
var i;
for (i = 0; i < this.graphs().size(); ++i) {
this.graphs().at(i).initializeGeometry(width, height, graphicsContext);
}
});
/**
* Convience function for registering callback functions for data becoming ready.
*
* @method registerCommonDataCallback
* @param {Function} callback Callback function to be registered.
* @author jrfrimme
*/
this.respondsTo("registerCommonDataCallback", function (callback) {
var i;
for (i = 0; i < this.graphs().size(); ++i) {
this.graphs().at(i).registerCommonDataCallback(callback);
}
});
});
/**
* Determines if the browser supports canvas elements.
*
* @method browserHasCanvasSupport
* @private
* @static
* @author jrfrimme
*/
ns.browserHasCanvasSupport = function () {
return (
(!!window.HTMLCanvasElement) &&
(!!window.CanvasRenderingContext2D) &&
(function (elem) {
return !!(elem.getContext && elem.getContext('2d'));
}(document.createElement('canvas')))
);
};
ns.browserHasSVGSupport = function () {
return !!document.createElementNS &&
!!document.createElementNS('http://www.w3.org/2000/svg', "svg").createSVGRect;
};
/**
* Creates a Multigraph according to specified options. Returns a
* jQuery `promise` which interacts with the multigraph through its
* `done` function.
*
* @method createGraph
* @param {Object} options
*
* @param {String|HTML Element|jQuery Object} options.div (REQUIRED)
* The DOM element div into which the multigraph should be
* placed; this value may be either (a) a string which is taken
* to be the id attribute of a div in the page, (b) a reference
* to the div DOM element itself, or (c) a jQuery object
* corresponding to the div DOM element.
*
* @param {URI} options.mugl (REQUIRED, unless muglString is present)
* the URL from which the MUGL
* file for the Multigraph can be loaded
*
* @param {String} options.muglString (REQUIRED, unless mugl is present)
* a string containing the MUGL XML for the graph
*
* @param {String} options.driver (OPTIONAL) Indicates which
* graphics driver to use; should be one of the strings
* "canvas", "raphael", or "auto". The default (which is
* used if the 'driver' tag is absent) is "auto", which
* causes Multigraph to check the features of the browser
* it is running in and choose the most appropriate driver.
*
* @param {Function} options.error (OPTIONAL) A function for
* displaying error messages to the user. Multigraph will
* call this function if and when it encounters an error. The
* function should receive a single argument which is an
* instance of the JavaScript Error object. The default is to
* use Multigraph's own internal mechanism for displaying user
* messages.
*
* @param {Function} options.warning (OPTIONAL) A function for
* displaying warning messages to the user. Multigraph will
* call this function if and when it needs to display a
* warning message. The function should receive a single
* argument which is an instance of the JavaScript Error
* object. The default is to use Multigraph's own internal
* mechanism for displaying user messages.
* @return {Promise} jQuery promise which provides interaction with
* the graph through its `done` function.
* @author mbp
*/
Multigraph.createGraph = function (options) {
var div = options.div,
messageHandler,
defaultMessageHandler;
// if driver wasn't specified, choose the best based on browser capability
if (!options.driver) {
if (ns.browserHasCanvasSupport()) {
options.driver = "canvas";
} else {
options.driver = "raphael";
}
}
// if div is a string, assume it's an id, and convert it to the div element itself
if (typeof(div) === "string") {
div = $("#" + div)[0];
}
// Force the div to have the specific width or height given in the options, if any.
// I'm adding this code to resolve a problem with the div size sometimes not being
// available when src/graphics/canvas/multigraph.js:createCanvasGraphFromString()
// is used; see the notes in that file.
if (options.width !== undefined && options.width > 0) {
$(div).width(options.width);
}
if (options.height !== undefined && options.height > 0) {
$(div).height(options.height);
}
//
// NOTE: each of the Multigraph.create{DRIVER}Graph functions below takes an
// "options" object argument just like Multigraph.createGraph does. In general this
// "options" object is the same as the one passed to this Multigraph.createGraph
// function, but it differs in one way: Instead of containing separate "error" and
// "warning" properties which are optional, the "options" object passed to the
// Multigraph.create{DRIVER}Graph functions requires a single (non-optional!)
// "messageHandler" property, which in turn contains "error" and "warning" properties
// which are functions for handling errors and warnings, respectively. Both the
// "error" and a "warning" properties must be present in the "messageHandler" object
// and must point to valid functions.
//
// The rationale behind this is to allow convenience for callers of the more "public"
// Multigraph.createGraph function, so that they don't have to specify an error or
// warning handler function unless they want to use custom ones. The internal
// Multigraph.create{DRIVER}Graph functions, however, always need access to error and
// warning functions, and often need to pass both of them on to other functions, so
// they're encapsulated together into a single messageHandler object to make this
// easier.
//
// Build the messageHandler object:
messageHandler = {};
if (typeof(options.error) === "function") {
messageHandler.error = options.error;
}
if (typeof(options.warning) === "function") {
messageHandler.warning = options.warning;
}
if (! messageHandler.error || ! messageHandler.warning) {
defaultMessageHandler = Multigraph.createDefaultMessageHandlers(div);
if (! messageHandler.error) {
messageHandler.error = defaultMessageHandler.error;
}
if (! messageHandler.warning) {
messageHandler.warning = defaultMessageHandler.warning;
}
}
options.messageHandler = messageHandler;
if (options.muglString !== undefined) {
// delegate to the driver-specific create function
if (options.driver === "canvas") {
return Multigraph.createCanvasGraphFromString(options);
} else if (options.driver === "raphael") {
return Multigraph.createRaphaelGraphFromString(options);
} else {
options.messageHanlder.error(new Error("invalid graphic driver '" + options.driver + "' specified to Multigraph.createGraph"));
return undefined;
}
}
// delegate to the driver-specific create function
if (options.driver === "canvas") {
return Multigraph.createCanvasGraph(options);
} else if (options.driver === "raphael") {
return Multigraph.createRaphaelGraph(options);
} else {
options.messageHanlder.error(new Error("invalid graphic driver '" + options.driver + "' specified to Multigraph.createGraph"));
return undefined;
}
};
/**
* `window.multigraph.create` is an alias for `window.multigraph.core.Multigraph.createGraph`.
*
* @method window.multigraph.create
* @param {Object} options
*
* @param {String|HTML Element|jQuery Object} options.div (REQUIRED)
* The DOM element div into which the multigraph should be
* placed; this value may be either (a) a string which is taken
* to be the id attribute of a div in the page, (b) a reference
* to the div DOM element itself, or (c) a jQuery object
* corresponding to the div DOM element.
*
* @param {URI} options.mugl (REQUIRED) the URL from which the MUGL
* file for the Multigraph can be loaded
*
* @param {String} options.driver (OPTIONAL) Indicates which
* graphics driver to use; should be one of the strings
* "canvas", "raphael", or "auto". The default (which is
* used if the 'driver' tag is absent) is "auto", which
* causes Multigraph to check the features of the browser
* it is running in and choose the most appropriate driver.
*
* @param {Function} options.error (OPTIONAL) A function for
* displaying error messages to the user. Multigraph will
* call this function if and when it encounters an error. The
* function should receive a single argument which is an
* instance of the JavaScrip Error object. The default is to
* use Multigraph's own internal mechanism for displaying user
* messages.
*
* @param {Function} options.warning (OPTIONAL) A function for
* displaying warning messages to the user. Multigraph will
* call this function if and when it needs to display a
* warning message. The function should receive a single
* argument which is an instance of the JavaScript Error
* object. The default is to use Multigraph's own internal
* mechanism for displaying user messages.
* @return {Promise} jQuery promise which provides interaction with
* the graph through its `done` function.
* @static
* @author jrfrimme
*/
window.multigraph.create = Multigraph.createGraph;
/**
* Creates default error and warning functions for multigraph.
*
* @method createDefaultMessageHandlers
* @param {HTML Element} div
* @static
* @return {Object} Object keyed by `error` and `warning` which respectively point to
* the generated default error and warning functions.
* @author jrfrimme
*/
Multigraph.createDefaultMessageHandlers = function (div) {
$(div).css('position', 'relative');
$(div).errorDisplay({});
return {
error : function(e) {
var stackTrace = (e.stack && typeof(e.stack) === "string") ? e.stack.replace(/\n/g, "</li><li>") : e.message;
$(div).errorDisplay("displayError", stackTrace, e.message, {
fontColor : '#000000',
backgroundColor : '#ff0000',
indicatorColor : '#ff0000'
});
},
warning : function (w) {
// w can be either a string, or a Warning instance
var message = "Warning: " + ((typeof(w) === "string") ? w : w.message),
stackTrace = (typeof(w) !== "string" && w.stack && typeof(w.stack) === "string") ? w.stack.replace(/\n/g, "</li><li>") : message;
$(div).errorDisplay("displayError", stackTrace, message, {
fontColor : '#000000',
backgroundColor : '#e06a1b',
indicatorColor : '#e06a1b'
});
}
};
};
ns.Multigraph = Multigraph;
});