/** * @name Table of Contents (TOC) widget for ArcGIS Server JavaScript API * @author: Nianwei Liu * @fileoverview *

A TOC (Table of Contents) widget for ESRI ArcGIS Server JavaScript API. The namespace is agsjs

*/ // change log: // 2015-01-15: Handle Esri REST API bug: annotation layer's sublayer; JS API workaround: invoked both load and error call back in same request. // 2014-09-24: handle uniquevaluerender's attributeField as function not string. // 2014-09-02: added support of scale dependency for feature layer. // 2014-04-08: in sample: js API 3.8, added handle node check event // 2014-04-04: fire event 'toc-node-checked' (classic onTOCNodeChecked) on click on check box. {rootLayer,serviceLayer, checked} // 2013-10-17: TOC.on('load') event style, clean up src and sample AMD style, JSAPI 3.7. // 2013-09-23: Secure service support: Integrated Windows, Token, or via Proxy (IWA or Token); listen to rootLayer onLoad if not already loaded. // 2013-09-05: JSAPI 3.6. Treat root FeatureLayer same as a layer inside a map service, i.e. move the symbol inline if there is only one symbol. // 2013-08-05: nested groups fix, findTOCNode, onLoad event, css change to a new folder and in sample, added autoToggle option // 2013-07-24: FeatureLayer, JSAPI3.5, removed a few functionalities: uniqueValueRenderer generated checkboxes; dynamically created layer from TOC config. // 2012-08-21: fix dojo.fx load that caused IE has to refresh to see TOC. // 2012-07-26: add ready so it works with compact built (missing dijit._Widget, dijit._Templated). // 2012-07-23: sync and honor setVisibleLayers. // 2012-07-19: xdomain build // 2012-07-18: upgrade to JSAPI v3.0 // 2012-02-02: fix IE7&8 problem when there is "all other value"(default symbol) // 2011-12-20: refresh method // 2011-11-04: v1.06: uniquevalue renderer check on/off using definitions. group layer on/off. change css class name. inline style as default. deprecate standard style // 2011-08-11: support for not showing legend or layer list; slider at service level config; removed style background. /*global dojo esri*/ // reference: http://dojotoolkit.org/reference-guide/quickstart/writingWidgets.html define("agsjs/dijit/TOC", ['dojo/_base/declare', "dojo/has", "dojo/aspect", "dojo/_base/lang", "dojo/_base/array", "dojo/dom-construct", "dojo/dom-class", "dojo/dom-style", "dojo/dom-attr", 'dijit/_Widget', 'dijit/_Templated', 'dojo/Evented', 'dojox/gfx', 'dojo/fx', 'dojo/fx/Toggler', "esri/symbols/jsonUtils", "esri/geometry/scaleUtils", "esri/config", "esri/layers/ArcGISDynamicMapServiceLayer", "esri/layers/ArcGISTiledMapServiceLayer", "dojo/_base/sniff"], function(declare, has, aspect, lang, array, domConstruct, domClass, domStyle, domAttr, _Widget, _Templated, Evented, gfx, coreFx, Toggler, jsonUtils, scaleUtils, esriConfig, ArcGISDynamicMapServiceLayer, ArcGISTiledMapServiceLayer){ /** * _TOCNode is a node, with 3 possible types: root layer|serviceLayer|legend * @private */ var _TOCNode = declare([_Widget, _Templated], { templateString: '
' + '
' + '' + '' + '' + '' + '
' + '
', // each node contains reference to rootLayer, servierLayer(layer within service), legend // the reason not to use a "type" property is because in the case of legend, it is necessary // to access meta data of the serviceLayer and rootLayer as well. rootLayer: null, serviceLayer: null, legend: null, rootLayerTOC: null, data: null, // could be one of rootLayer, serviceLayer, or legend _childTOCNodes: [], constructor: function(params, srcNodeRef){ lang.mixin(this, params); }, // extension point. called automatically after widget DOM ready. postCreate: function(){ domStyle.set(this.rowNode, 'paddingLeft', '' + this.rootLayerTOC.tocWidget.indentSize * this.rootLayerTOC._currentIndent + 'px'); // using the availability of certain property to decide what kind of node to create. // priority is legend/serviceLayer/rootLayer this.data = this.legend || this.serviceLayer || this.rootLayer; this.blank = this.iconNode.src; if (this.legend) { this._createLegendNode(this.legend); } else if (this.serviceLayer) { this._createServiceLayerNode(this.serviceLayer); } else if (this.rootLayer) { this._createRootLayerNode(this.rootLayer); } if (this.containerNode && Toggler) { // if containerNode was not removed, it means this is some sort of group. this.toggler = new Toggler({ node: this.containerNode, showFunc: coreFx.wipeIn, hideFunc: coreFx.wipeOut }) } if (!this._noCheckNode) { // typically _noCheckNode means it is a tiledlayer, or legend item that should not have a checkbox var chk; //2013-10-17: do not want to load dijit form in this widget, //but seems no good way to check if it is already been loaded by external code // using AMD style without rely on classic dijit namespace. // maybe will require TOC constructor explicitly set if want to plain HTML checkbox or dijit.form.CheckBox if (dijit.form && dijit.form.CheckBox) { chk = new dijit.form.CheckBox({ // looks a bug in dijit. image not renderered until mouse over. bug was closed but still exist. // use attr('checked', true) not working either. checked: this.data.visible }); chk.placeAt(this.checkContainerNode); chk.startup(); } else { chk = domConstruct.create('input', { type: 'checkbox', checked: this.data.visible }, this.checkContainerNode); } this.checkNode = chk; } var showChildren = this.data.visible; // if it is a group layer and no child layer is visible, then collapse if (this.data._subLayerInfos) { var noneVisible = true; array.every(this.data._subLayerInfos, function(info){ if (info.visible) { noneVisible = false; return false; } return true; }); if (noneVisible) showChildren = false; } if (this.data.collapsed) showChildren = false; if (this.iconNode && this.iconNode.src == this.blank) { domClass.add(this.iconNode, 'dijitTreeExpando'); domClass.add(this.iconNode, showChildren ? 'dijitTreeExpandoOpened' : 'dijitTreeExpandoClosed'); } if (this.iconNode) { domClass.add(this.iconNode, 'agsjsTOCIcon'); } if (this.containerNode) { domStyle.set(this.containerNode, 'display', showChildren ? 'block' : 'none'); } this.domNode.id = 'TOCNode_' + this.rootLayer.id + (this.serviceLayer ? '_' + this.serviceLayer.id : '') + (this.legend ? '_' + this.legend.id : ''); }, // root level node, layers directly under esri.Map _createRootLayerNode: function(rootLayer){ domClass.add(this.rowNode, 'agsjsTOCRootLayer'); domClass.add(this.labelNode, 'agsjsTOCRootLayerLabel'); var title = this.rootLayerTOC.config.title; // if it is '' then it means we do not title to be shown, i.e. not indent. if (title === '') { // we do not want to show the first level, typically in the case of a single map service esri.hide(this.rowNode); rootLayer.show(); this.rootLayerTOC._currentIndent--; } else if (title === undefined) { // no title is set, try to find default if (rootLayer.name) { // this is a featureLayer title = rootLayer.name; } else { var start = rootLayer.url.toLowerCase().indexOf('/rest/services/'); var end = rootLayer.url.toLowerCase().indexOf('/mapserver', start); title = rootLayer.url.substring(start + 15, end); } } rootLayer.collapsed = this.rootLayerTOC.config.collapsed; if (this.rootLayerTOC.config.slider) { this.sliderNode = domConstruct.create('div', { 'class': 'agsjsTOCSlider' }, this.rowNode, 'last');// var me = this; require(["dijit/form/HorizontalSlider", "dojo/domReady!"], function(HorizontalSlider){ me.slider = new HorizontalSlider({ showButtons: false, value: rootLayer.opacity * 100, intermediateChanges: true, //style: "width:100%;padding:0 20px 0 20px", tooltip: 'adjust transparency', onChange: function(value){ rootLayer.setOpacity(value / 100); }, layoutAlign: 'right' }); me.slider.placeAt(me.sliderNode); // the new on method lost context compare to the old-good connect. //dojo .connect(rootLayer, 'onOpacityChange', this, function(op){ rootLayer.on('opacity-change', function(evt){ me.slider.setValue(evt.opacity * 100); }); }); } if (!this.rootLayerTOC.config.noLegend) { if (rootLayer._tocInfos) { this._createChildrenNodes(rootLayer._tocInfos, 'serviceLayer'); } else if (rootLayer.renderer) { // for feature layers var r = rootLayer.renderer; if (r.infos) { //UniqueValueRenderer |ClassBreaksRenderer var legs = r.infos; if (r.defaultSymbol && legs.length > 0 && legs[0].label != '[all other values]') { // insert at top legs.unshift({ label: '[all other values]', symbol: r.defaultSymbol }) } var af = ""; if (typeof r.attributeField == "string"){ af = r.attributeField + (r.normalizationField ? '/' + r.normalizationField : ''); af += (r.attributeField2 ? '/' + r.attributeField2 : '') + (r.attributeField3 ? '/' + r.attributeField3 : ''); } var anode = domConstruct.create('div', {}, this.containerNode); domStyle.set(anode, 'paddingLeft', '' + this.rootLayerTOC.tocWidget.indentSize * (this.rootLayerTOC._currentIndent + 2) + 'px'); anode.innerHTML = af; this._createChildrenNodes(legs, 'legend'); } else { //this._createChildrenNodes([rootLayer.renderer], 'legend'); this._setIconNode(rootLayer.renderer, this.iconNode, this); domConstruct.destroy(this.containerNode); this.containerNode = null; } } else { domStyle.set(this.iconNode, 'visibility', 'hidden'); } } else { // no legend means no need for plus/minus sign domStyle.set(this.iconNode, 'visibility', 'hidden'); } this.labelNode.innerHTML = title; //dojo .attr(this.rowNode, 'title', title); domAttr.set(this.rowNode, 'title', title); }, // a layer inside a map service. _createServiceLayerNode: function(serviceLayer){ // layer: layerInfo with nested subLayerInfos if (this.rootLayer instanceof (ArcGISDynamicMapServiceLayer)) { this.labelNode.innerHTML = serviceLayer.name; } // 2015-01-15: workaround: ESRI rest API incorrectly returns empty array for annotation layer, which confuses with empty group layer. // was told arcgis.com's TOC also suffered this problem. REST API should fix it. if (serviceLayer._subLayerInfos && serviceLayer._subLayerInfos.length>0) {// group layer domClass.add(this.rowNode, 'agsjsTOCGroupLayer'); domClass.add(this.labelNode, 'agsjsTOCGroupLayerLabel'); this._createChildrenNodes(serviceLayer._subLayerInfos, 'serviceLayer'); } else { domClass.add(this.rowNode, 'agsjsTOCServiceLayer'); domClass.add(this.labelNode, 'agsjsTOCServiceLayerLabel'); if (this.rootLayer.tileInfo) { // can not check on/off for tiled this._noCheckNode = true; } if (serviceLayer._legends && !this.rootLayerTOC.config.noLegend) { if (serviceLayer._legends.length == 1) { this.iconNode.src = this._getLegendIconUrl(serviceLayer._legends[0]); domConstruct.destroy(this.containerNode); this.containerNode = null; } else { this._createChildrenNodes(serviceLayer._legends, 'legend'); } } else { domConstruct.destroy(this.iconNode); this.iconNode = null; domConstruct.destroy(this.containerNode); this.containerNode = null; } } }, /* a legend data normally have: {description,label,symbol,value} */ _createLegendNode: function(rendLeg){ this._noCheckNode = true; domConstruct.destroy(this.containerNode); domClass.add(this.labelNode, 'agsjsTOCLegendLabel'); this._setIconNode(rendLeg, this.iconNode, this); var label = rendLeg.label; if (rendLeg.label === undefined) { if (rendLeg.value !== undefined) { label = rendLeg.value; } if (rendLeg.maxValue !== undefined) { label = '' + rendLeg.minValue + ' - ' + rendLeg.maxValue; } } this.labelNode.appendChild(document.createTextNode(label)); }, // set url or replace node _setIconNode: function(rendLeg, iconNode, tocNode){ var src = this._getLegendIconUrl(rendLeg); if (!src) { if (rendLeg.symbol) { var w = this.rootLayerTOC.tocWidget.swatchSize[0]; var h = this.rootLayerTOC.tocWidget.swatchSize[1]; if (rendLeg.symbol.width && rendLeg.symbol.height) { w = rendLeg.symbol.width; h = rendLeg.symbol.height; } var node = domConstruct.create('span', {}); domStyle.set(node, { 'width': w + 'px', 'height': h + 'px', 'display': 'inline-block' }); domConstruct.place(node, iconNode, 'replace'); tocNode.iconNode = node; var descriptors = jsonUtils.getShapeDescriptors(rendLeg.symbol); var mySurface = gfx.createSurface(node, w, h);//dojox. if (descriptors) { //if (dojo .isIE) { if (has('ie')) { // 2013076: see http://mail.dojotoolkit.org/pipermail/dojo-interest/2009-December/041404.html window.setTimeout(lang.hitch(this, '_createSymbol', mySurface, descriptors, w, h), 500); } else { this._createSymbol(mySurface, descriptors, w, h); } } } else { if (console) console.log('no symbol in renderer'); } } else { iconNode.src = src; if (rendLeg.symbol && rendLeg.symbol.width && rendLeg.symbol.height) { iconNode.style.width = rendLeg.symbol.width; iconNode.style.height = rendLeg.symbol.height; } } }, _createSymbol: function(mySurface, descriptors, w, h){ var shape = mySurface.createShape(descriptors.defaultShape); if (descriptors.fill) { shape.setFill(descriptors.fill); } if (descriptors.stroke) { shape.setStroke(descriptors.stroke); } shape.applyTransform({ dx: w / 2, dy: h / 2 }); }, _getLegendIconUrl: function(legend){ var src = legend.url; // in some cases NULL value may cause #legend != #of renderer entry. if (src != null && src.indexOf('data') == -1) { if (!has('ie') && legend.imageData && legend.imageData.length > 0) { src = "data:image/png;base64," + legend.imageData; } else { if (src.indexOf('http') !== 0) { // resolve relative url src = this.rootLayer.url + '/' + this.serviceLayer.id + '/images/' + src; } if (this.rootLayer.credential && this.rootLayer.credential.token) { src = src + "?token=" + this.rootLayer.credential.token; } else if (esriConfig.defaults.io.alwaysUseProxy) { src = esriConfig.defaults.io.proxyUrl + "?" + src; } } } return src; }, /** * Create children nodes, for serviceLayers, subLayers of a group layer, or legends within a serviceLayer. * @param {Object} chdn children nodes data * @param {Object} type rootLayer|serviceLayer|legend. It's name will be passed in constructor of _TOCNode. */ _createChildrenNodes: function(chdn, type){ this.rootLayerTOC._currentIndent++; var c = []; for (var i = 0, n = chdn.length; i < n; i++) { var chd = chdn[i]; var params = { rootLayerTOC: this.rootLayerTOC, rootLayer: this.rootLayer, serviceLayer: this.serviceLayer, legend: this.legend }; params[type] = chd; params.data = chd; //var node = new agsjs.dijit._TOCNode(params); if (type == 'legend') { chd.id = 'legend' + i; } var node = new _TOCNode(params); node.placeAt(this.containerNode); c.push(node); } this._childTOCNodes = c; // for refreshTOC use recursively. this.rootLayerTOC._currentIndent--; }, _toggleContainer: function(o){ if (domClass.contains(this.iconNode, 'dijitTreeExpandoClosed') || domClass.contains(this.iconNode, 'dijitTreeExpandoOpened')) { // make sure its not clicked on legend swatch if (o) { domClass.remove(this.iconNode, 'dijitTreeExpandoClosed'); domClass.add(this.iconNode, 'dijitTreeExpandoOpened'); } else if (o === false) { domClass.remove(this.iconNode, 'dijitTreeExpandoOpened'); domClass.add(this.iconNode, 'dijitTreeExpandoClosed'); } else { domClass.toggle(this.iconNode, 'dijitTreeExpandoClosed'); domClass.toggle(this.iconNode, 'dijitTreeExpandoOpened'); } if (domClass.contains(this.iconNode, 'dijitTreeExpandoOpened')) { if (this.toggler) { this.toggler.show(); } else { esri.show(this.containerNode); } } else { if (this.toggler) { this.toggler.hide(); } else { esri.hide(this.containerNode); } } // remember it's state for refresh if (this.rootLayer && !this.serviceLayer && !this.legend) { this.rootLayerTOC.config.collapsed = domClass.contains(this.iconNode, 'dijitTreeExpandoClosed'); } } }, /** * Expand the node's children if applicable */ expand: function(){ this._toggleContainer(true); }, /** * collapse the node's children if applicable */ collapse: function(){ this._toggleContainer(false); }, /** * Show the TOC Node */ show: function(){ esri.show(this.domNode); }, /** Hide TOC node * */ hide: function(){ esri.hide(this.domNode); }, // change UI according to the state of map layers. _adjustToState: function(){ if (this.checkNode) { var checked = this.legend ? this.legend.visible : this.serviceLayer ? this.serviceLayer.visible : this.rootLayer ? this.rootLayer.visible : false; if (this.checkNode.set) { //checkNode is a dojo .forms.CheckBox this.checkNode.set('checked', checked); } else { // checkNode is a simple HTML element. this.checkNode.checked = checked; } // 2014-04-08: automatically expand/collapse? if (this.rootLayerTOC.config.autoToggle !== false) { this._toggleContainer(this.checkNode && this.checkNode.checked); } } //20140902: handle out of scale for feature layer var scale = scaleUtils.getScale(this.rootLayerTOC.tocWidget.map);//esri.geometry var outScale = false; if (this.serviceLayer) { outScale = (this.serviceLayer.maxScale != 0 && scale < this.serviceLayer.maxScale) || (this.serviceLayer.minScale != 0 && scale > this.serviceLayer.minScale); } else if (this.rootLayer.renderer) { // this is a feature layer outScale = (this.rootLayer.maxScale != 0 && scale < this.rootLayer.maxScale) || (this.rootLayer.minScale != 0 && scale > this.rootLayer.minScale); } if (outScale) { domClass.add(this.domNode, 'agsjsTOCOutOfScale'); } else { domClass.remove(this.domNode, 'agsjsTOCOutOfScale'); } if (this.checkNode) { if (this.checkNode.set) { this.checkNode.set('disabled', outScale); } else { this.checkNode.disabled = outScale; } } if (this._childTOCNodes.length > 0) { array.forEach(this._childTOCNodes, function(child){ child._adjustToState(); }); } }, _onClick: function(evt){ var t = evt.target; var lay; if (t == this.checkNode || dijit.getEnclosingWidget(t) == this.checkNode) { // 2013-07-23: remove this most complex checkable legend functionality to simplify the widget if (this.serviceLayer) { this.serviceLayer.visible = this.checkNode && this.checkNode.checked; // if a sublayer is checked on, force it's group layer to be on. // 2013-08-01 handler multiple level of groups if (this.serviceLayer.visible) { lay = this.serviceLayer; while (lay._parentLayerInfo) { if (!lay._parentLayerInfo.visible) { lay._parentLayerInfo.visible = true; } lay = lay._parentLayerInfo; } } // if a layer is on, it's service must be on. if (this.serviceLayer.visible && !this.rootLayer.visible) { this.rootLayer.show(); } if (this.serviceLayer._subLayerInfos) { // this is a group layer; // 2013-08-01 handler multiple level of groups this._setSubLayerVisibilitiesFromGroup(this.serviceLayer); } /* 2013-07-23: do not deal with checkbox legend any more.*/ this.rootLayer.setVisibleLayers(this._getVisibleLayers(), true); this.rootLayerTOC._refreshLayer(); } else if (this.rootLayer) { this.rootLayer.setVisibility(this.checkNode && this.checkNode.checked); } // 2014-04-04: emit event with infomation about the root layer, service layer, and on/off /** * @event */ this.rootLayerTOC.tocWidget.emit('toc-node-checked', { rootLayer: this.rootLayer, serviceLayer: this.serviceLayer, checked: this.checkNode.checked }); // classic style. this.rootLayerTOC.tocWidget.onTOCNodeChecked(this.rootLayer, this.serviceLayer, this.checkNode.checked); // automatically expand/collapse? if (this.rootLayerTOC.config.autoToggle !== false) { this._toggleContainer(this.checkNode && this.checkNode.checked); } this.rootLayerTOC._adjustToState(); } else if (t == this.iconNode) { this._toggleContainer(); } }, _setSubLayerVisibilitiesFromGroup: function(lay){ if (lay._subLayerInfos && lay._subLayerInfos.length > 0) { array.forEach(lay._subLayerInfos, function(info){ info.visible = lay.visible; if (info._subLayerInfos && info._subLayerInfos.length > 0) { this._setSubLayerVisibilitiesFromGroup(info); } }, this); } }, _getVisibleLayers: function(){ var vis = []; array.forEach(this.rootLayer.layerInfos, function(layerInfo){ if (layerInfo.subLayerIds) { // if a group layer is set to vis, all sub layer will be drawn regardless it's sublayer status return; } else if (layerInfo.visible) { vis.push(layerInfo.id); } }); if (vis.length === 0) { vis.push(-1); } else if (!this.rootLayer.visible) { this.rootLayer.show(); } return vis; }, _findTOCNode: function(layerId){ if (this.serviceLayer && this.serviceLayer.id == layerId) { return this; } if (this._childTOCNodes.length > 0) { var n = null; for (var i = 0, c = this._childTOCNodes.length; i < c; i++) { n = this._childTOCNodes[i]._findTOCNode(layerId); if (n) return n; } } return null; } }); var _RootLayerTOC = declare([_Widget], { _currentIndent: 0, rootLayer: null, tocWidget: null, /** * * @param {Object} params: noLegend: true|false, collapsed: true|false, slider: true|false * @param {Object} srcNodeRef */ constructor: function(params, srcNodeRef){ this.config = params.config || {}; this.rootLayer = params.config.layer; this.tocWidget = params.tocWidget; }, // extenstion point called by framework postCreate: function(){ if ((this.rootLayer instanceof (ArcGISDynamicMapServiceLayer) || this.rootLayer instanceof (ArcGISTiledMapServiceLayer))) { if (this._legendResponse) { this._createRootLayerTOC(); } else { this._getLegendInfo(); } } else { this._createRootLayerTOC(); } }, _getLegendInfo: function(){ var url = ''; if (this.rootLayer.version >= 10.01) { url = this.rootLayer.url + '/legend'; } else { url = 'http://www.arcgis.com/sharing/tools/legend'; var i = this.rootLayer.url.toLowerCase().indexOf('/rest/'); var soap = this.rootLayer.url.substring(0, i) + this.rootLayer.url.substring(i + 5); url = url + '?soapUrl=' + escape(soap); } var handle = esri.request({ url: url, content: { f: "json" }, callbackParamName: 'callback', handleAs: 'json', load: lang.hitch(this, this._processLegendInfo), error: lang.hitch(this, this._processLegendError) }); }, _processLegendError: function(err){ // 2015-01-01: workaround: In some cases ESRI JSAPI will call load and error handler in same request, resulted in double entry in TOC. // This change surpress one. JS API should fix to make sure either load or error is called but not both. if (console) console.log("esri.request error callback. Is this error legit?"); if (!this._legendResponse) this._createRootLayerTOC(); }, _processLegendInfo: function(json){ this._legendResponse = json; var layer = this.rootLayer; if (!layer._tocInfos) { // create a lookup map, key=layerId, value=LayerInfo // generally id = index, this is to assure we find the right layer by ID // note: not all layers have an entry in legend response. var layerLookup = {}; array.forEach(layer.layerInfos, function(layerInfo){ layerLookup['' + layerInfo.id] = layerInfo; // used for later reference. layerInfo.visible = layerInfo.defaultVisibility; if (layer.visibleLayers && !layerInfo.subLayerIds) { if (array.indexOf(layer.visibleLayers, layerInfo.id) == -1) { layerInfo.visible = false; } else { layerInfo.visible = true; } } }); // attached legend Info to layer info if (json.layers) { array.forEach(json.layers, function(legInfo){ var layerInfo = layerLookup['' + legInfo.layerId]; if (layerInfo && legInfo.legend) { layerInfo._legends = legInfo.legend; } }); } // nest layer Infos array.forEach(layer.layerInfos, function(layerInfo){ if (layerInfo.subLayerIds) { var subLayerInfos = []; array.forEach(layerInfo.subLayerIds, function(id, i){ subLayerInfos[i] = layerLookup[id]; subLayerInfos[i]._parentLayerInfo = layerInfo; }); layerInfo._subLayerInfos = subLayerInfos; } }); //2012-07-21: if setVisibility is called before this widget is built, we want to use the actual visibility instead of the layerInfo. //finalize the tree structure in _tocInfos, skipping all sublayers because they were nested already. var tocInfos = []; array.forEach(layer.layerInfos, function(layerInfo){ if (layerInfo.parentLayerId == -1) { tocInfos.push(layerInfo); } }); layer._tocInfos = tocInfos; } this._createRootLayerTOC(); }, _createRootLayerTOC: function(){ // sometimes IE may fail next step ///this._rootLayerNode = new agsjs.dijit._TOCNode({ if (this.rootLayer.loaded) { this._rootLayerNode = new _TOCNode({ rootLayerTOC: this, rootLayer: this.rootLayer }); this._rootLayerNode.placeAt(this.domNode); //this._visHandler = dojo .connect(this.rootLayer, "onVisibilityChange", this, "_adjustToState"); this._visHandler = this.rootLayer.on("visibility-change", lang.hitch(this, this._adjustToState)); // this will make sure all TOC linked to a Map synchronized. if (this.rootLayer instanceof (ArcGISDynamicMapServiceLayer)) { // this._visLayerHandler = dojo .connect(this.rootLayer, "setVisibleLayers", this, "_onSetVisibleLayers"); // 2013-10-17: in AMD aspect is used to replace connect to regular method, use recieveArgs=true to avoid argument shift and hitch to keep scope. this._visLayerHandler = aspect.after(this.rootLayer, "setVisibleLayers", lang.hitch(this, this._onSetVisibleLayers), true); } this._adjustToState(); this._loaded = true; this.onLoad(); } else { //dojo .connect(this.rootLayer, 'onLoad', dojo .hitch(this, this._createRootLayerTOC)); this.rootLayer.on('load', lang.hitch(this, this._createRootLayerTOC)); } }, /** * @event */ onLoad: function(){ }, _refreshLayer: function(){ var rootLayer = this.rootLayer; var timeout = this.tocWidget.refreshDelay; if (this._refreshTimer) { window.clearTimeout(this._refreshTimer); this._refreshTimer = null; } this._refreshTimer = window.setTimeout(function(){ rootLayer.setVisibleLayers(rootLayer.visibleLayers); }, timeout); }, _onSetVisibleLayers: function(visLayers, doNotRefresh){ // 2012-07-23: // set the actual individual layerInfo's visibility after service's setVisibility call. if (!doNotRefresh) { array.forEach(this.rootLayer.layerInfos, function(layerInfo){ if (array.indexOf(visLayers, layerInfo.id) != -1) { layerInfo.visible = true; } else if (!layerInfo._subLayerInfos) { layerInfo.visible = false; } }); this._adjustToState(); } }, _adjustToState: function(){ this._rootLayerNode._adjustToState(); }, destroy: function(){ if (this._visHandler) { this._visHandler.remove(); this._visHandler = null; } if (this._visLayerHandler) { this._visLayerHandler.remove(); this._visLayerHandler = null; } } }); var TOC = declare("agsjs.dijit.TOC", [_Widget, Evented], { indentSize: 18, swatchSize: [30, 30], refreshDelay: 500, layerInfos: null, /** * @name TOCLayerInfo * @class This is an object literal that specify the options for each map rootLayer layer. * @property {Layer} [layer] ArcGIS Server layer. * @property {string} [title] title. optional. If not specified, rootLayer name is used. * @property {Boolean} [slider] whether to show slider for each rootLayer to adjust transparency. default is false. * @property {Boolean} [noLegend] whether to skip the legend, and only display layers. default is false. * @property {Boolean} [collapsed] whether to collapsed the rootLayer layer at beginning. default is false, which means expand if visible, collapse if not. * @property {Boolean} [suppressGroup] whether to supress the automatic action regarding groups: sync parent/child layer status. * */ /** * @name TOCOptions * @class This is an object literal that specify the option to construct a {@link TOC}. * @property {esri.Map} [map] the map instance. required. * @property {Object[]} [layerInfos] a subset of layers in the map to show in TOC. each object is a {@link TOCLayerInfo} * @property {Number} [indentSize] indent size of tree nodes. default to 18. */ /** * Create a Table Of Contents (TOC) * @name TOC * @constructor * @class This class is a Table Of Content widget. * @param {ags.TOCOptions} opts * @param {DOMNode|id} srcNodeRef */ constructor: function(params, srcNodeRef){ params = params || {}; if (!params.map) { throw new Error('no map defined in params for TOC'); } this.layerInfos = params.layerInfos; this.indentSize = params.indentSize || 18; lang.mixin(this, params); }, // extension point postCreate: function(){ this._createTOC(); }, /** @event the widget DOM is loaded * */ onLoad: function(){ }, /**@event * */ onTOCNodeChecked: function(rootLayer, serviceLayer, checked){ }, _createTOC: function(){ domConstruct.empty(this.domNode); this._rootLayerTOCs = []; for (var i = 0, c = this.layerInfos.length; i < c; i++) { // attach a title to rootLayer layer itself var info = this.layerInfos[i]; ///var rootLayerTOC = new agsjs.dijit._RootLayerTOC({ var rootLayerTOC = new _RootLayerTOC({ config: info, tocWidget: this }); this._rootLayerTOCs.push(rootLayerTOC); this._checkLoadHandler = rootLayerTOC.on('load', lang.hitch(this, '_checkLoad'));//dojo .connect(rootLayerTOC, 'onLoad', this, '_checkLoad'); rootLayerTOC.placeAt(this.domNode); this._checkLoad(); } if (!this._zoomHandler) { this._zoomHandler = this.map.on('zoom-end', lang.hitch(this, "_adjustToState")); } }, _checkLoad: function(){ var loaded = true; array.every(this._rootLayerTOCs, function(widget){ if (!widget._loaded) { loaded = false; return false; } return true; }); if (loaded) { this.onLoad(); this.emit('load'); } }, _adjustToState: function(){ array.forEach(this._rootLayerTOCs, function(widget){ widget._adjustToState(); }); }, /** * Refresh the TOC to reflect */ refresh: function(){ this._createTOC(); }, destroy: function(){ this._zoomHandler.remove(); this._zoomHandler = null; this._checkLoadHandler.remove(); this._checkLoadHandler = null; }, /** * Find the TOC Node based on root layer and optional serviceLayer ID. * @param {Object} layer root Layer of a map * @param {Object} serviceLayerId id of a ArcGIS Map Service Layer * @return {TOCNode} TOC node, it has public methods: collapse, expand, show, hide */ findTOCNode: function(layer, serviceLayerId){ var w; array.every(this._rootLayerTOCs, function(widget){ if (widget.rootLayer == layer) { w = widget; return false; } return true; }); if (serviceLayerId !== null && serviceLayerId !== undefined && w.rootLayer instanceof (ArcGISDynamicMapServiceLayer)) { return w._rootLayerNode._findTOCNode(serviceLayerId); } else if (w) { return w._rootLayerNode; } return null; } }); return TOC; });