/* global define, tmpl, $, console */
/**
* @module RAMP
* @submodule FilterManager
* @main FilterManager
*/
/**
* Create a layer item for each map layer to be displayed in the layer selector. Allows for dynamic changing of the layer item state.
*
* ####Imports RAMP Modules:
* {{#crossLink "Util"}}{{/crossLink}}
* {{#crossLink "TmplHelper"}}{{/crossLink}}
* {{#crossLink "TmplUtil"}}{{/crossLink}}
* {{#crossLink "Array"}}{{/crossLink}}
* {{#crossLink "Dictionary"}}{{/crossLink}}
*
*
* ####Uses RAMP Templates:
* {{#crossLink "templates/layer_selector_template.json"}}{{/crossLink}}
*
* @class LayerItem
* @constructor
* @uses dojo/Evented
* @uses dojo/_base/declare
* @uses dojo/lang
*
* @param {Object} config a config definition of the layer
* @param {Object} [options] Additional options
*
* @param {String} [options.state] Specifies the initial state of the LyerItem; must be one of the `LayerItem.state` defaults
* @param {String} [options.type] Specifies type of this LayerItem and the name of the layer item template to use
*
* @param {Object} [options.stateMatrix] additional state matrix records to be mixed into the default
* @param {Object} [options.transitionMatrix] additional state transition matrix records to be mixed into the default
*
* @return {LayerItem} A control object representing a layer allowing to dynamically change its state.
*/
define([
"dojo/Evented", "dojo/_base/declare", "dojo/_base/lang",
/* Text */
"dojo/text!./templates/layer_selector_template.json",
/* Util */
"utils/util", "utils/tmplHelper", "utils/tmplUtil", "utils/array", "utils/dictionary"
],
function (
Evented, declare, lang,
layer_selector_template,
Util, TmplHelper, TmplUtil, UtilArray, UtilDict
) {
"use strict";
var LayerItem,
ALL_STATES_CLASS;
LayerItem = declare([Evented], {
constructor: function (config, options) {
// declare individual properties inside the constructor: http://dojotoolkit.org/reference-guide/1.9/dojo/_base/declare.html#id6
lang.mixin(this,
{
/**
* Layer id. Upon initialization, `id` can be overwritten by `config.id` value.
*
* @property id
* @type String
* @default null
*/
id: null,
/**
* A node of the LayerItem.
*
* @property node
* @type JObject
* @default null
*/
node: null,
/**
* A copy of the layer config supplied during LayerItem creation; is set to `config` value.
*
* @property _config
* @private
* @type Object
* @default null
*/
_config: null,
/**
* A node of the image container.
*
* @property _imageContainerNode
* @private
* @type JObject
* @default null
*/
_imageContainerNode: null,
/**
* A node of the layer display name.
*
* @property _displayNameNode
* @private
* @type JObject
* @default null
*/
_displayNameNode: null,
/**
* A node of the layer controls.
*
* @property _controlsNode
* @private
* @type JObject
* @default null
*/
_controlsNode: null,
/**
* A node of the layer toggles.
*
* @property _togglesNode
* @private
* @type JObject
* @default null
*/
_togglesNode: null,
/**
* A dictionary of control nodes available for this layer.
*
* @property _controlStore
* @private
* @type Object
* @default {}
*/
_controlStore: {},
/**
* A dictionary of toggle nodes available for this layer.
*
* @property _toggleStore
* @private
* @type Object
* @default {}
*/
_toggleStore: {},
/**
* A dictionary of notice nodes available for this layer.
*
* @property _noticeStore
* @private
* @type Object
* @default {}
*/
_noticeStore: {},
/**
* Templates to be used in construction of the layer nodes.
*
* @property templates
* @type Object
* @default layer_selector_template.json
*/
templates: JSON.parse(TmplHelper.stringifyTemplate(layer_selector_template)),
/**
* State of this LayerItem; can be overwritten by `options.state`.
*
* @property state
* @type String
* @default LayerItem.state.DEFAULT
*/
state: LayerItem.state.DEFAULT,
/**
* Specifies type of this LayerItem and the name of the layer item template to use; can be overwritten by `options.type`.
*
* @property type
* @type String
* @default null
*/
type: null
},
options,
{
id: config.id,
_config: config,
/**
* Specifies a state matrix for this particular LayerItem. The default is mixed with `options.stateMatrix` upon initialization.
* The state matrix prescribes what controls, toggles, and notices are present in specific states.
*
* @property stateMatrix
* @type Object
* @default LayerItem.stateMatrix
*/
stateMatrix: lang.mixin(
lang.clone(LayerItem.stateMatrix),
options.stateMatrix
),
/**
* Specifies a state transition matrix for this particular LayerItem. The default is mixed with `options.transitionMatrix` upon initialization.
* The state transition matrix prescribes the direction of state changes for specific states.
*
* @property transitionMatrix
* @type Object
* @default LayerItem.transitionMatrix
*/
transitionMatrix: lang.mixin(
lang.clone(LayerItem.transitionMatrix),
options.transitionMatrix
)
}
);
this.node = $(this._template(this.type, this._config));
this._imageBoxNode = this.node.find(".layer-details > div:first");
this._displayNameNode = this.node.find(".layer-name > span");
this._controlsNode = this.node.find(".layer-controls-group");
this._togglesNode = this.node.find(".layer-checkboxes");
this._noticesNode = this.node.find(".layer-notices");
this._generateParts("controls", "layer_control_", this._controlStore);
this._generateParts("toggles", "layer_toggle_", this._toggleStore);
this._generateParts("notices", "layer_notice_", this._noticeStore);
this.setState(this.state, options, true);
console.debug("-->", this.state, options);
},
/**
* Generates control, toggle, and notice nodes for the LayerItem object to be used in different states.
*
* @param {String} partType name of the part type - "controls", "toggles", or "notices"
* @param {String} templateKey a template name prefix for the template parts
* @param {Object} partStore a dictionary to store generated nodes
* @method _generateParts
* @private
*/
_generateParts: function (partType, templateKey, partStore) {
var that = this,
stateKey,
partKeys = [],
part;
Object
.getOwnPropertyNames(LayerItem.state)
.forEach(function (s) {
stateKey = LayerItem.state[s];
partKeys = partKeys.concat(that.stateMatrix[stateKey][partType]);
});
partKeys = UtilArray.unique(partKeys);
partKeys.forEach(function (pKey) {
part = that._generatePart(templateKey, pKey);
partStore[pKey] = (part);
});
},
/**
* Generates a control given the template name and additional data object to pass to the template engine.
*
* @param {String} templateKey a template name prefix for the template parts
* @param {String} pKey name of the template to build
* @param {Object} [data] optional data to pass to template engine; used to update strings on notice objects
* @method _generatePart
* @private
* @return Created part node
*/
_generatePart: function (templateKey, pKey, data) {
var part = $(this._template(templateKey + pKey,
{
id: this.id,
config: this._config,
nameKey: pKey,
data: data
}
));
return part;
},
/**
* Changes the state of the LayerItem and update its UI representation.
*
* @param {String} state name of the state to be set
* @param {Object} [options] additional options
* @param {Object} [options.notices] custom information to be displayed in a notice for the current state if needed; object structure is not set; look at the appropriate template;
* @example
* {
* notices: {
* error: {
* message: "I'm error"
* },
* scale: {
* message: "All your base are belong to us"
* }
* }
* }
* @param {Boolean} force if `true`, forces the state change even if it's no allowed by the `transitionMatrix`
* @method setState
*/
setState: function (state, options, force) {
var allowedStates = this.transitionMatrix[this.state],
notice,
focusedNode,
that = this;
if (allowedStates.indexOf(state) !== -1 || force) {
this.state = state;
//lang.mixin(this, options);
// set state class on the layerItem root node
this.node
.removeClass(ALL_STATES_CLASS)
.addClass(this.state);
// regenerate notice controls if extra data is provided
if (options) {
if (options.notices) {
UtilDict.forEachEntry(options.notices, function (pKey, data) {
notice = that._generatePart("layer_notice_", pKey, data);
that._noticeStore[pKey] = (notice);
});
}
}
// store reference to a focused node inside this layer item if any
focusedNode = this.node.find(":focus");
this._setParts("controls", this._controlStore, this._controlsNode);
this._setParts("toggles", this._toggleStore, this._togglesNode);
this._setParts("notices", this._noticeStore, this._noticesNode);
switch (this.state) {
case LayerItem.state.DEFAULT:
console.log(LayerItem.state.DEFAULT);
break;
case LayerItem.state.LOADING:
this.node.attr("aria-busy", true); // indicates that the region is loading
console.log(LayerItem.state.LOADING);
break;
case LayerItem.state.LOADED:
this.node.attr("aria-busy", false); // indicates that the loading is complete
this.setState(LayerItem.state.DEFAULT);
console.log(LayerItem.state.LOADED);
break;
case LayerItem.state.ERROR:
console.log(LayerItem.state.ERROR);
break;
case LayerItem.state.OFF_SCALE:
console.log(LayerItem.state.OFF_SCALE);
break;
default:
break;
}
// reset focus after changing layer item's state
if (focusedNode.length > 0) {
// if the previously focused node still in DOM, set focus to it
if (Util.containsInDom(focusedNode[0])) {
focusedNode.focus();
} else { // if this node is no longer in DOM, set focus to the first focusable element in layer item
this.node.find(":focusable:first").focus();
}
}
return true;
} else {
return false;
}
},
/**
* Sets controls, toggles, and notices of the LayerItem according to its state.
*
* @param {String} partType name of the part type - "controls", "toggles", or "notices"
* @param {Object} partStore a dictionary to store generated nodes
* @param {JObject} target a jQuery node where the nodes should be appended
* @method _setParts
* @private
*/
_setParts: function (partType, partStore, target) {
var controls = [];
this.stateMatrix[this.state][partType].forEach(function (pKey) {
controls.push(partStore[pKey]);
});
// use detach instead of empty to preserve handlers that jQuery deletes on empty
// http://stackoverflow.com/questions/2027706/why-do-registered-events-disappear-when-an-element-is-removed-from-dom/14900035#14900035
// http://api.jquery.com/detach/
target
.children()
.detach()
.end()
.append(controls)
;
},
/**
* Populates a template specified by the key with the supplied data.
*
* @param {String} key template name
* @param {Object} data data to be inserted into the template
* @method _template
* @private
* @return {String} a string template filled with supplied data
*/
_template: function (key, data) {
tmpl.cache = {};
tmpl.templates = this.templates;
data = data || {};
data.fn = TmplUtil;
return tmpl(key, data);
}
});
lang.mixin(LayerItem,
{
/**
* A default collection of possible LayerItem states.
*
* @property LayerItem.state
* @static
* @type Object
* @example
* state: {
* DEFAULT: "layer-state-default",
* LOADING: "layer-state-loading",
* LOADED: "layer-state-loaded",
* UPDATING: "layer-state-updating",
* ERROR: "layer-state-error",
* OFF_SCALE: "layer-state-off-scale"
* }
*/
state: {
DEFAULT: "layer-state-default",
LOADING: "layer-state-loading",
LOADED: "layer-state-loaded",
UPDATING: "layer-state-updating",
ERROR: "layer-state-error",
OFF_SCALE: "layer-state-off-scale"
},
/**
* A default collection of possible LayerItem controls.
*
* @property LayerItem.controls
* @static
* @type Object
* @example
* controls: {
* METADATA: "metadata",
* SETTINGS: "settings",
* LOADING: "loading",
* REMOVE: "remove",
* RELOAD: "reload",
* ERROR: "error"
* }
*/
controls: {
METADATA: "metadata",
SETTINGS: "settings",
LOADING: "loading",
REMOVE: "remove",
RELOAD: "reload",
ERROR: "error"
},
/**
* A default collection of possible LayerItem toggles.
*
* @property LayerItem.toggles
* @static
* @type Object
* @example
* toggles: {
* EYE: "eye",
* BOX: "box",
* RELOAD: "reload",
* HIDE: "hide",
* ZOOM: "zoom",
* PLACEHOLDER: "placeholder"
* }
*/
toggles: {
EYE: "eye",
BOX: "box",
RELOAD: "reload",
HIDE: "hide",
ZOOM: "zoom",
PLACEHOLDER: "placeholder"
},
/**
* A default collection of possible LayerItem notices.
*
* @property LayerItem.notices
* @static
* @type Object
* @example
* notices: {
* SCALE: "scale"
* ERROR: "error",
* UPDATE: "update",
* USER: "user"
* }
*/
notices: {
SCALE: "scale",
ERROR: "error",
UPDATE: "update",
USER: "user"
},
/**
* A default state matrix specifying what controls are active in which state.
*
* @property LayerItem.stateMatrix
* @static
* @type Object
* @example
* DEFAULT: {
* controls: [
* LayerItem.controls.METADATA,
* LayerItem.controls.SETTINGS
* ],
* toggles: [
* LayerItem.toggles.EYE,
* LayerItem.toggles.BOX
* ],
* notices: []
* },
*
* LOADING: {
* controls: [
* LayerItem.controls.LOADING
* ],
* toggles: [],
* notices: []
* },
*
* LOADED: {
* controls: [],
* toggles: [],
* notices: []
* },
* DEFAULT: {
* controls: [
* LayerItem.controls.METADATA,
* LayerItem.controls.SETTINGS
* ],
* toggles: [
* LayerItem.toggles.EYE,
* LayerItem.toggles.BOX
* ],
* notices: [
* LayerItem.notices.UPDATE
* ]
* },
*
* ERROR: {
* controls: [
* LayerItem.controls.RELOAD,
* LayerItem.controls.REMOVE
* ],
* toggles: [],
* notices: [
* LayerItem.notices.ERROR
* ]
* },
*
* OFF_SCALE: {
* controls: [
* LayerItem.controls.METADATA,
* LayerItem.controls.SETTINGS
* ],
* toggles: [
* LayerItem.toggles.ZOOM,
* LayerItem.toggles.EYE,
* LayerItem.toggles.BOX
* ],
* notices: [
* LayerItem.notices.SCALE
* ]
* }
*/
stateMatrix: {},
/**
* A default state transition matrix specifying to what state the LayerItem can transition.
*
* @property LayerItem.transitionMatrix
* @static
* @type Object
* @example
* DEFAULT: [
* LayerItem.state.ERROR,
* LayerItem.state.OFF_SCALE,
* LayerItem.state.UPDATING
* ],
* LOADED: [
* LayerItem.state.DEFAULT
* ],
* LOADING: [
* LayerItem.state.LOADED
* ],
* UPDATING: [
* LayerItem.state.ERROR,
* LayerItem.state.OFF_SCALE,
* LayerItem.state.DEFAULT
* ],
* ERROR: [
* LayerItem.state.LOADING
* ],
* OFF_SCALE: [
* LayerItem.state.ERROR,
* LayerItem.state.DEFAULT,
* LayerItem.state.UPDATING
* ]
*
*/
transitionMatrix: {}
}
);
// setting defaults for state matrix
LayerItem.stateMatrix[LayerItem.state.DEFAULT] = {
controls: [
LayerItem.controls.METADATA,
LayerItem.controls.SETTINGS,
LayerItem.controls.REMOVE
],
toggles: [
LayerItem.toggles.EYE,
LayerItem.toggles.BOX
],
notices: []
};
LayerItem.stateMatrix[LayerItem.state.LOADING] = {
controls: [
LayerItem.controls.LOADING
],
toggles: [],
notices: []
};
LayerItem.stateMatrix[LayerItem.state.LOADED] = {
controls: [],
toggles: [],
notices: []
};
LayerItem.stateMatrix[LayerItem.state.UPDATING] = {
controls: [
LayerItem.controls.METADATA,
LayerItem.controls.SETTINGS,
LayerItem.controls.REMOVE
],
toggles: [
LayerItem.toggles.EYE,
LayerItem.toggles.BOX
],
notices: [
LayerItem.notices.UPDATE
]
};
LayerItem.stateMatrix[LayerItem.state.ERROR] = {
controls: [
LayerItem.controls.RELOAD,
LayerItem.controls.REMOVE
],
toggles: [],
notices: [
LayerItem.notices.ERROR
]
};
LayerItem.stateMatrix[LayerItem.state.OFF_SCALE] = {
controls: [
LayerItem.controls.METADATA,
LayerItem.controls.SETTINGS,
LayerItem.controls.REMOVE
],
toggles: [
LayerItem.toggles.ZOOM,
LayerItem.toggles.EYE,
LayerItem.toggles.BOX
],
notices: [
LayerItem.notices.SCALE
]
};
// setting defaults for transition matrix
LayerItem.transitionMatrix[LayerItem.state.DEFAULT] = [
LayerItem.state.ERROR,
LayerItem.state.OFF_SCALE,
LayerItem.state.UPDATING
];
LayerItem.transitionMatrix[LayerItem.state.LOADING] = [
LayerItem.state.LOADED,
LayerItem.state.ERROR,
LayerItem.state.UPDATING
];
LayerItem.transitionMatrix[LayerItem.state.LOADED] = [
LayerItem.state.DEFAULT
];
LayerItem.transitionMatrix[LayerItem.state.UPDATING] = [
LayerItem.state.LOADED,
LayerItem.state.ERROR
];
LayerItem.transitionMatrix[LayerItem.state.ERROR] = [
LayerItem.state.LOADING
];
LayerItem.transitionMatrix[LayerItem.state.OFF_SCALE] = [
LayerItem.state.ERROR,
LayerItem.state.DEFAULT,
LayerItem.state.UPDATING
];
/**
* Modifies a given state matrix by adding specified partKey to the specified partType collection.
*
* @param {Object} stateMatrix matrix to modify
* @param {String} partType type of the parts to modify: `controls`, `toggles`, `notices`
* @param {String} partKey part key to be inserted into the collection
* @param {Boolean} prepend indicates if the part key should be prepended or appended
* @method addStateMatrixPart
* @static
* @private
*/
LayerItem.addStateMatrixPart = function (stateMatrix, partType, partKey, prepend) {
UtilDict.forEachEntry(stateMatrix, function (state, data) {
if (prepend) {
data[partType].unshift(partKey);
} else {
data[partType].push(partKey);
}
});
};
/**
* Modifies a given state matrix by removing specified partKey to the specified partType collection.
*
* @param {Object} stateMatrix matrix to modify
* @param {String} partType type of the parts to modify: `controls`, `toggles`, `notices`
* @param {String} partKey part key to be removed into the collection
* @method addStateMatrixPart
* @static
* @private
*/
LayerItem.removeStateMatrixPart = function (stateMatrix, partType, partKey) {
UtilDict.forEachEntry(stateMatrix, function (state, data) {
UtilArray.remove(data[partType], partKey);
});
};
/**
* Get a deep copy of the default stateMatrix.
*
* @method getStateMatrixTemplate
* @static
* @return {Object} a deep copy of the default stateMatrix
*/
LayerItem.getStateMatrixTemplate = function () {
return lang.clone(LayerItem.stateMatrix);
};
// a string with all possible layerItem state CSS classes joined by " "; used to clear any CSS state class from the node
ALL_STATES_CLASS =
Object
.getOwnPropertyNames(LayerItem.state)
.map(function (key) { return LayerItem.state[key]; })
.join(" ");
return LayerItem;
});