/*global define, window, tmpl */
/**
* FilterManager submodule
*
* @module RAMP
* @submodule FilterManager
* @main FilterManager
*/
/**
* FilterManager class. Represents the legend next to the map and the controls to toggle each map layer's visibility and boundary box.
* The FilterManager also includes a attribute filter that allows the user to hide map features based on a attribute values
*
* For a doc with diagrams on how this class works, please see
* http://ecollab.ncr.int.ec.gc.ca/projects/science-apps/priv/RAMP/RAMP%20AMD%20Filter%20Module.docx
*
* @class FilterManager
* @static
* @uses dojo/_base/declare
* @uses dojo/_base/lang
* @uses dojo/query
* @uses dojo/_base/array
* @uses dojo/dom
* @uses dojo/dom-class
* @uses dojo/dom-style
* @uses dojo/dom-construct
* @uses dojo/_base/connect
* @uses dojo/Deferred
* @uses dojo/topic
* @uses dojo/aspect
* @uses dojo/promise/all
* @uses templates/filter_manager_Template.html
* @uses esri/tasks/query
* @uses esri/layers/FeatureLayer
* @uses RAMP
* @uses GlobalStorage
* @uses GUI
* @uses Legend
* @uses Util
* @uses Array
* @uses Dictionary
* @uses PopupManager
*/
define([
/* Dojo */
"dojo/_base/declare", "dojo/_base/lang", "dojo/query", "dojo/_base/array", "dojo/dom", "dojo/dom-class",
"dojo/dom-style", "dojo/dom-construct", "dojo/_base/connect", "dojo/Deferred", "dojo/topic",
"dojo/aspect", "dojo/promise/all",
/* Text */
"dojo/text!./templates/filter_manager_Template.html",
"dojo/text!./templates/filter_row_template.json",
"dojo/text!./templates/filter_global_template.json",
/* Esri */
"esri/tasks/query", "esri/layers/FeatureLayer",
/* Ramp */
"ramp/ramp", "ramp/globalStorage", "ramp/map", "ramp/eventManager",
/* Util */
"utils/tmplHelper", "utils/util", "utils/array", "utils/dictionary", "utils/popupManager"],
function (
/* Dojo */
declare, lang, query, dojoArray, dom, domClass, domStyle, domConstruct,
connect, Deferred, topic, aspect, all,
/* Text */
filter_manager_Template,
filter_row_template_json,
filter_global_row_template_json,
/* Esri */
EsriQuery, FeatureLayer,
/* Ramp */
Ramp, GlobalStorage, RampMap, EventManager,
/* Util */
tmplHelper, utilMisc, utilArray, utilDict, popupManager) {
"use strict";
var config,
localString,
ui = (function () {
var layerList,
filterGlobalToggles;
/**
* Generates a data grid row with a checkbox.
* @method generateGlobalCheckboxes
* @returns {Object} toggleRow the generated data grid row.
*/
function generateGlobalCheckboxes() {
var toggleRow;
// reset and load global template
tmpl.cache = {};
tmpl.templates = JSON.parse(tmplHelper.stringifyTemplate(filter_global_row_template_json));
toggleRow = tmpl(config.siteTemplate.filterGlobalRowTemplate, config.globalFilter);
return toggleRow;
}
/**
* Generates legend for each interactive layer on the map (non-basemap layers).
*
* @method generateFilterCheckboxes
* @return {String} a string representation of the legends with simplified layer info for identification
*/
function generateFilterCheckboxes() {
var layers = RampMap.getMap().getLayersVisibleAtScale(),
section = "";
// layerLegend data for template,
var allLayerData = [];
dojoArray.forEach(layers, function (layer) {
if (!layer.type || layer.type === "basemap") {
return;
}
var layerConfig = Ramp.getLayerConfig(layer.url),
groupName = layer.id,
legendName = "filterGroup_" + groupName,
attr = "",
featureId = layerConfig.id;
/** Building data for template
* @example
* var visibilityLegendLabel = { "for": legendName, "attr": attr, "value": groupName,
* checked": "checked", "label": "Show", "class": "eye checked"
* };
*/
var visibilityLegendLabel = {
"for": legendName,
"attr": attr,
"value": groupName,
"checked": "checked",
"label": layerConfig.displayName,
"class": "eye checked",
"featureId": featureId
};
var boundingLegendLabel = {
"for": legendName + "1",
"attr": attr + "1",
"value": groupName,
"checked": "checked",
"label": layerConfig.displayName,
"class": "box checked",
"featureId": featureId
};
// legend data for current layer
var rowData = { "imageUrl": Ramp.getSymbolForLayer(layer).imageUrl,
"displayName": Ramp.getLayerConfig(layer.url).displayName,
"dataLayerUUID": featureId.replace("layer_", ""),
"txtMetadata": localString.txtMetadata,
"VisibilityLegend": visibilityLegendLabel,
"BoundingBoxLegend": boundingLegendLabel
};
/* End */
/* Begin legend sub row: legend for each layer */
var legend = layerConfig.symbology.icons;
var subRows = [];
utilDict.forEachEntry(legend, function (li) {
var legendKeyValue = li,
cleanValue = li.replace(/-/g, ""),
legendIcon;
legendIcon = legend[legendKeyValue].imageUrl;
var subRow = { "imageUrl": legendIcon, "title": localString[String.format("altFeature_{0}_Icon_{1}", featureId, cleanValue)] };
subRows.push(subRow);
});
var layerLegendData = {
"layerId": layer.id,
"rowData": rowData,
"subRows": subRows
};
allLayerData.push(layerLegendData);
});
var templateKey = "";
tmpl.cache = {};
tmpl.templates = JSON.parse(tmplHelper.stringifyTemplate(filter_row_template_json));
// generate the content using rowData and given template
section = tmpl(config.siteTemplate.filterRowTemplate, allLayerData);
return section;
}
/**
* Sets UI status of a layer presentation (checkbox and eye) according to the user action: select / de-select a layer.
* publishes event "filterManager/box-visibility-toggled" every time a layer status changed.
* There should only be one eye and one global checkbox, but
* we say checkbox"es" because jquery returns a list and it's
* easier to write a function that takes a list of checkboxes
* than to write two functions, one to take a list and one to
* take an individual checkbox
* @method setCheckboxEvents
* @private
*/
function setCheckboxEvents() {
var globalEyeCheckboxes,
globalBoxCheckboxes,
eyeCheckboxes,
boxCheckboxes;
globalEyeCheckboxes = utilMisc.styleCheckboxes(
filterGlobalToggles.find(".checkbox-custom .eye + input"),
"checked", "focused",
{ "checked": localString.txtHideAllFeatures, "unchecked": localString.txtShowAllFeatures }
);
// Turn off the bounding boxes by default
globalBoxCheckboxes = utilMisc
.styleCheckboxes(
filterGlobalToggles.find(".checkbox-custom .box + input"),
"checked", "focused",
{ "checked": localString.txtHideAllBounds, "unchecked": localString.txtShowAllBounds })
.setAll(false);
eyeCheckboxes = utilMisc.styleCheckboxes(
layerList.find(".checkbox-custom .eye + input"),
"checked", "focused",
{ "checked": localString.txtHideFeatures, "unchecked": localString.txtShowFeatures }
);
// Turn off the bounding boxes by default
boxCheckboxes = utilMisc
.styleCheckboxes(
layerList.find(".checkbox-custom .box + input"),
"checked", "focused",
{ "checked": localString.txtHideBounds, "unchecked": localString.txtShowBounds })
.setAll(false);
/**
* Toggles each layers visibility when the global visibility button is clicked
* @method toggleGlobalEye
* @param {Boolean} checked The value of the global visibility button's check status (on or off)
*/
function toggleGlobalEye(checked) {
eyeCheckboxes.setAll(checked);
topic.publish(EventManager.FilterManager.GLOBAL_LAYER_VISIBILITY_TOGGLED, {
checked: checked
});
}
/**
* Toggles each layers boundary box display check box when the global boundary box button is clicked
* @method toggleGlobalBox
* @param {Boolean} checked The value of the global boundary box button's check status (on or off)
*/
function toggleGlobalBox(checked) {
boxCheckboxes.setAll(checked);
topic.publish(EventManager.FilterManager.GLOBAL_BOX_VISIBILITY_TOGGLED, {
checked: checked
});
}
topic.subscribe(EventManager.FilterManager.TOGGLE_GLOBAL_LAYER_VISIBILITY, function (evt) {
globalEyeCheckboxes.setAll(evt.visible);
toggleGlobalEye(evt.visible);
});
topic.subscribe(EventManager.FilterManager.TOGGLE_GLOBAL_BOX_VISIBILITY, function (evt) {
globalBoxCheckboxes.setAll(evt.visible);
toggleGlobalBox(evt.visible);
});
/* START GLOBAL "EYE" AND BOUNDING BOX BUTTON EVENTS */
globalEyeCheckboxes.getNodes().on("change", function () {
// True if the checkbox got selected, false otherwise
var checked = $(this).is(':checked');
toggleGlobalEye(checked);
});
globalBoxCheckboxes.getNodes().on("change", function () {
// True if the checkbox got selected, false otherwise
var checked = $(this).is(':checked');
toggleGlobalBox(checked);
});
/* END GLOBAL "EYE" AND BOUNDING BOX BUTTONS */
/* START INDIVIDUAL "EYE" AND BOUNDING BUTTON EVENTS */
/**
* Toggles the visibility button (or eye) beside a given layer in the legend. Fires the layer_visibility event.
* @method toggleEye
* @param {Boolean} checked The check status of the visibility button next to the target layer (on or off)
* @param {Object} node The legend item representing the target layer
*/
function toggleEye(checked, node) {
// Figure out whether or not all the checkboxes are selected
var allChecked = dojoArray.every(eyeCheckboxes.getNodes(), function (checkbox) {
return $(checkbox).is(':checked');
});
globalEyeCheckboxes.setAll(allChecked);
// True if the checkbox got selected, false otherwise
topic.publish(EventManager.FilterManager.LAYER_VISIBILITY_TOGGLED, {
checked: checked,
node: node[0]
});
}
/**
* Toggles the boundary box button beside a given layer in the legend. Fires the box_visibility event.
* @method toggleBox
* @param {Boolean} checked The check status of the boundary box button next to the target layer (on or off)
* @param {Object} node The legend item representing the target layer
*/
function toggleBox(checked, node) {
// Figure out whether or not all the checkboxes are selected
var allChecked = dojoArray.every(boxCheckboxes.getNodes(), function (checkbox) {
return $(checkbox).is(':checked');
});
globalBoxCheckboxes.setAll(allChecked);
topic.publish(EventManager.FilterManager.BOX_VISIBILITY_TOGGLED, {
checked: checked,
node: node[0]
});
}
topic.subscribe(EventManager.FilterManager.TOGGLE_LAYER_VISIBILITY, function (evt) {
// Set the checkboxes visually, checkboxes with an id in evt.layerIds gets
// turned on, the rest gets turned off
eyeCheckboxes.setState(function (checkbox) {
var layerId = $(checkbox).findInputLabel().data("layer-id");
return evt.layerIds.contains(layerId);
});
dojoArray.forEach(eyeCheckboxes.getNodes(), function (checkbox) {
checkbox = $(checkbox);
var layerId = checkbox.findInputLabel().data("layer-id");
toggleEye(evt.layerIds.contains(layerId), checkbox);
});
});
topic.subscribe(EventManager.FilterManager.TOGGLE_BOX_VISIBILITY, function (evt) {
// Set the checkboxes visually, checkboxes with an id in evt.layerIds gets
// turned on, the rest gets turned off
boxCheckboxes.setState(function (checkbox) {
var layerId = $(checkbox).findInputLabel().data("layer-id");
return evt.layerIds.contains(layerId);
});
dojoArray.forEach(boxCheckboxes.getNodes(), function (checkbox) {
checkbox = $(checkbox);
var layerId = checkbox.findInputLabel().data("layer-id");
toggleBox(evt.layerIds.contains(layerId), checkbox);
});
});
// Event handling for individual "eye" and "box" toggle
eyeCheckboxes.getNodes().on("change", function () {
var node = $(this),
checked = node.is(':checked');
toggleEye(checked, node);
});
boxCheckboxes.getNodes().on("change", function () {
var node = $(this),
// True if the checkbox got selected, false otherwise
checked = node.is(':checked');
toggleBox(checked, node);
});
/* END INDIVIDUAL "EYE" AND BOUNDING BUTTON EVENTS */
}
/**
* initialize a tooltip for each layer, using the layer name.
* @method initTooltips
* @private
*/
function initTooltips() {
popupManager.registerPopup(layerList, "hoverIntent",
function () {
if (this.target.attr("title")) {
if (this.target.isOverflowed()) {
this.target.tooltipster({ theme: '.tooltipster-dark' }).tooltipster("show");
} else {
this.target.removeAttr("title");
}
}
},
{
handleSelector: ".layer-name span",
useAria: false,
timeout: 500
}
);
}
/**
* Adjusts UI layout according to a layer event.
* @method setButtonEvents
* @private
*/
function setButtonEvents() {
var expandAllButton = filterGlobalToggles.find(".global-button"),
expandAllPopupHandle,
expandNodes = layerList.find(".layerList-container:hidden"),
expandButtons = layerList.find("button.legend-button");
/**
* Changes the width of the layers pane to accommodate for the scrollbar if it's needed.
* @method adjustPaneWidth
* @private
*/
function adjustPaneWidth() {
utilMisc.adjustWidthForSrollbar(layerList, [filterGlobalToggles]);
}
/**
* Changes the state of the expand all control if all the nodes are expanded.
* @method adjustExpandAllButtonState
* @private
*/
function adjustExpandAllButtonState() {
var count = expandNodes.length,
hiddenCount = expandNodes.filter(":hidden").length;
if (hiddenCount === 0) {
expandAllPopupHandle.open();
} else if (hiddenCount === count) {
expandAllPopupHandle.close();
}
}
expandButtons.map(function () {
var handle = $(this),
target = handle.parents("fieldset").find("> .layerList-container");
popupManager.registerPopup(handle, "state-expanded", target, "click",
function (d) {
target.slideToggle(400, function () {
adjustPaneWidth();
adjustExpandAllButtonState();
d.resolve();
});
},
"same"
);
});
expandAllPopupHandle = popupManager.registerPopup(expandAllButton, "state-expanded", expandNodes, "click",
function (d) {
expandNodes.slideDown(400, function () {
expandButtons.addClass("state-expanded");
adjustPaneWidth();
d.resolve();
});
},
function (d) {
expandNodes.slideUp(400, function () {
expandButtons.removeClass("state-expanded");
$("#tabs1_1-parent").scrollTop(0);
adjustPaneWidth();
d.resolve();
});
});
// metadata buttons
// to be changed...
layerList.find("legend button.metadata-button").on("click", function () {
var node = $(this).parents("legend");
if (!node.hasClass("selected-row")) {
//var guid = $(this).data("guid") || $(this).data("guid", utilMisc.guid()).data("guid");
var guid = $(this).data("layer-uuid"),
metadataUrl;
topic.publish(EventManager.GUI.SUBPANEL_OPEN, {
panelName: localString.txtMetadata,
title: node.find(".layer-name span").text(), // + " " + guid,
content: null,
target: node.find(".layer-details"),
origin: "filterManager",
guid: guid,
doOnOpen: function () { node.addClass("selected-row"); },
doOnHide: function () { layerList.find(".selected-row").removeClass("selected-row"); }
});
metadataUrl = "assets/metadata/" + guid + ".xml";
utilMisc.transformXML(metadataUrl, "assets/metadata/xstyle_default_" + config.lang + ".xsl",
function (data) {
topic.publish(EventManager.GUI.SUBPANEL_OPEN, {
content: $(data),
origin: "filterManager",
update: true,
guid: guid
});
});
} else {
topic.publish(EventManager.GUI.SUBPANEL_CLOSE, { origin: "filterManager" });
}
});
}
/**
* Adjusts filter style according to the scroll action on the layers.
* @method initScrollListeners
* @private
*/
function initScrollListeners() {
layerList.scroll(function () {
var currentScroll = layerList.scrollTop();
if (currentScroll === 0) {
filterGlobalToggles.removeClass("scroll");
} else {
filterGlobalToggles.addClass("scroll");
}
});
}
return {
init: function () {
var globalCheckboxes = generateGlobalCheckboxes(),
filterCheckboxes = generateFilterCheckboxes(),
sectionNode = $("#searchMapSectionBody"),
section;
section = String.format(filter_manager_Template,
globalCheckboxes,
filterCheckboxes);
// fade out the loading animation
sectionNode.addClass('animated fadeOut');
window.setTimeout(
function () {
sectionNode
.empty().append(section)
.removeClass("fadeOut")
.addClass('animated fadeIn');
// remove the animating css class
window.setTimeout(function () { sectionNode.removeClass('animated fadeIn'); }, 300);
layerList = $("#layerList");
if (layerList.find("> li").length > 1) {
layerList.sortable(
{
axis: "y",
handle: ".sort-handle",
placeholder: "sortable-placeholder",
update: function (event, ui) {
var layerId = ui.item[0].id,
index = dojoArray.indexOf($("#layerList").sortable("toArray"), layerId);
topic.publish(EventManager.GUI.SUBPANEL_CLOSE, { origin: "rampPopup,datagrid" });
topic.publish(EventManager.FilterManager.SELECTION_CHANGED, {
id: layerId,
index: index
});
}
}
);
}
filterGlobalToggles = $('#filterGlobalToggles');
setCheckboxEvents();
initTooltips();
setButtonEvents();
initScrollListeners();
// ui initialization complets
console.log(EventManager.FilterManager.UI_COMPLETE);
topic.publish(EventManager.FilterManager.UI_COMPLETE);
},
300
);
}
};
} ());
/**
* Initiates a listener to handle tab deselected event
*
* @method initListeners
* @private
*/
function initListeners() {
topic.subscribe(EventManager.GUI.TAB_DESELECTED, function (arg) {
if (arg.tabName === "filterManager") {
topic.publish(EventManager.GUI.SUBPANEL_CLOSE, { origin: "filterManager" });
}
});
}
return {
/**
* Reads the application configuration and creates the legend and filter management widget
* @method init
* @constructor
*/
init: function () {
// Convenience config objects
config = GlobalStorage.config;
localString = GlobalStorage.config.stringResources;
localString.txtShowFeatures = "Show {0}";
localString.txtHideFeatures = "Hide {0}";
localString.txtShowAllFeatures = "Show All";
localString.txtHideAllFeatures = "Hide All";
localString.txtShowBounds = "Show Bounds of {0}";
localString.txtHideBounds = "Hide Bounds of {0}";
localString.txtShowAllBounds = "Show All Bounds";
localString.txtHideAllBounds = "Hide All Bounds";
initListeners();
ui.init();
},
/**
* Queries all map points on a given feature layer and returns their attributes
* @method _getFeatures
* @param {Object} fl A feature layer to query
* @return {Object} An array of attributes from the designated feature layer
*/
_getFeatures: function (fl) {
//do a query on ALL the map points.
var queryTask = new EsriQuery();
queryTask.returnGeometry = false; //only return attributes
queryTask.maxAllowableOffset = 1000;
//query.outFields = outFieldsList; //note: this list is overridden by fields in featurelayer constructor
queryTask.where = fl.objectIdField + ">0";
return fl.queryFeatures(queryTask);
},
/**
* Grabs all distinct values of the given field from a featureLayer.
* @method _getField
* @param {Object} fl A feature layer to query
* @param {String} The field (or column) to query in the feature layer
* @return {Object} deferred A deferred object which will resolve to an array of unique values
*/
_getField: function (fl, field) {
var deferred = new Deferred();
this._getFeatures(fl).then(function (featureSet) {
deferred.resolve(dojoArray.map(featureSet.features, function (feature) {
return feature.attributes[field];
}));
});
return deferred.promise;
}
};
});