text: ///////////////////////////////////////////////////////////////////////////
  // Copyright © Esri. All Rights Reserved.
  //
  // Licensed under the Apache License Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //    http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  ///////////////////////////////////////////////////////////////////////////
  
  define([
    'dojo/_base/declare',
    'dojo/_base/array',
    'dojo/_base/lang',
    'dojo/Deferred',
    'dojo/promise/all',
    './LayerInfo',
    'dojox/gfx',
    'dojo/dom-construct',
    'dojo/dom-attr',
    'dojo/dom-class',
    'dojo/aspect',
    'jimu/portalUrlUtils',
    'jimu/portalUtils',
    'jimu/utils',
    'esri/symbols/jsonUtils',
    'esri/layers/LabelLayer',
    'esri/layers/LabelClass',
    'esri/dijit/PopupTemplate',
    'esri/dijit/Legend',
    'esri/graphic',
    'esri/geometry/Point',
    'esri/tasks/query',
    'esri/tasks/RelationshipQuery',
    'esri/tasks/QueryTask'
  ], function(declare, array, lang, Deferred, all, LayerInfo, gfx, domConstruct,
  domAttr, domClass, aspect, portalUrlUtils, portalUtils, jimuUtils, jsonUtils, LabelLayer,
  LabelClass, PopupTemplate, Legend, Graphic, Point, Query, RelationshipQuery, QueryTask) {
    var clazz = declare(LayerInfo, {
      _legendsNode: null,
      controlPopupInfo: null,
      // operLayer = {
      //    layerObject: layer,
      //    title: layer.label || layer.title || layer.name || layer.id || " ",
      //    id: layerId || " ",
      //    subLayers: [operLayer, ... ],
      //    mapService: {layerInfo: , subId: },
      //    collection: {layerInfo: }
      // };
      constructor: function() {
        /*
        this.layerLoadedDef = new Deferred();
        if(this.layerObject) {
          this.layerObject.on('load', lang.hitch(this, function(){
            this.layerLoadedDef.resolve();
          }));
        }
        */
  
        // init control popup
        this._initControlPopup();
        // update layerObject.name if it has.
        this._updateLayerObjectName();
        // show/hide labels
        try {
          this._initToShowLabels();
        } catch (err) {
          console.warn(err.message);
        }
      },
  
      _updateLayerObjectName: function() {
        if(this.layerObject &&
           !this.layerObject.empty &&
           this.layerObject.name &&
           !lang.getObject("_wabProperties.originalLayerName", false, this.layerObject)) {
          lang.setObject('_wabProperties.originalLayerName',
                         this.layerObject.name,
                         this.layerObject);
          this.layerObject.name = this.title;
          // "arcgisProps.title" will be clear out if overwrites the "name".
          // reset the "arcgisProps.title" for print task to display the legend title.
          if(this.layerObject.arcgisProps === undefined || this.layerObject.arcgisProps === null) {
            this.layerObject.arcgisProps = {};
          }
          lang.setObject('arcgisProps.title',
                         this.title,
                         this.layerObject);
        }
      },
  
      _initToShowLabels: function() {
        var itemLayerInfo = lang.getObject("_wabProperties.itemLayerInfo", false, this.layerObject);
        /*
        //test code
        itemLayerInfo = {
          portalUrl: "http://www.arcgis.com",
          itemId: this.layerObject.xtnItemId
        };
        */
  
        //ignores layers of webmap. layer of webmap that does not have layerObject.itemLayerInfo property.
        if(!itemLayerInfo ||
           //!LayerInfos.getInstanceSync() ||
           this.layerObject.empty) {
          return;
        }
  
        var portal = portalUtils.getPortal(itemLayerInfo.portalUrl);
        portal.getItemData(itemLayerInfo.itemId).then(lang.hitch(this, function(itemData) {
          var currentLayerDataOfItemData;
          if(itemData && itemData.layers) {
            array.some(itemData.layers, function(layerData) {
              if(layerData.id === this.layerObject.layerId) {
                currentLayerDataOfItemData = layerData;
                return true;
              } else {
                return false;
              }
            }, this);
  
            if(currentLayerDataOfItemData && currentLayerDataOfItemData.showLabels) {
              // temporary solvtion for if itemLayer data was not merged to layerObject.
              var labelingInfoOfItemData = lang.getObject("layerDefinition.drawingInfo.labelingInfo",
                                                          false,
                                                          currentLayerDataOfItemData);
              if(labelingInfoOfItemData &&
                 !this.layerObject.labelingInfo &&
                 this.layerObject.setLabelingInfo) {
                var labelClasses = array.map(labelingInfoOfItemData, function(labelingInfoJson) {
                  var labelClass = new LabelClass(labelingInfoJson);
                  return labelClass;
                }, this);
                this.layerObject.setLabelingInfo(labelClasses);
              }
  
              // init to showLabels
              this.showLabels();
            }
          }
  
        }), lang.hitch(this, function(error) {
          if(error && error.message) {
            console.log(error.message);
          }
        }));
      },
  
      _initOldFilter: function() {
        // default value of this._oldFilter is null
        if(this.layerObject &&
           !this.layerObject.empty &&
           this.layerObject.getDefinitionExpression) {
          this._oldFilter = this.layerObject.getDefinitionExpression();
        } else {
          this._oldFilter = null;
        }
      },
  
      _getLayerOptionsForCreateLayerObject: function() {
        var options = {};
        // assign id
        options.id = this.id;
        // prepare outFileds for create feature layer.
        var outFields = [];
        var infoTemplate = this.getInfoTemplate();
        if(infoTemplate && infoTemplate.info && infoTemplate.info.fieldInfos) {
          array.forEach(infoTemplate.info.fieldInfos, function(fieldInfo) {
            if(fieldInfo.visible) {
              outFields.push(fieldInfo.fieldName);
            }
          }, this);
        } else {
          outFields = ["*"];
        }
        options.outFields = outFields;
  
        // assign capabilities
        //options.resourceInfo = {capabilities: ["Query"]};
  
        /*
        // prepare popupInfo of webmap for talbe (just for table).
        if(this.originOperLayer.popupInfo && this.isTable) {
          var popupTemplate = new PopupTemplate(this.originOperLayer.popupInfo);
          if(popupTemplate ) {
            options.infoTemplate = popupTemplate;
          }
        }
        */
        return options;
      },
  
  
      _getExtent: function() {
        var def = new Deferred();
        //var extent = null;
        var defaultExtent;
        if(this.layerObject.graphics && this.layerObject.graphics.length > 0) {
          defaultExtent = jimuUtils.graphicsExtent(this.layerObject.graphics);
        } else {
          defaultExtent = this.layerObject.fullExtent || this.layerObject.initialExtent;
        }
        if(!this.layerObject.url) {
          // feature collection
          def.resolve(defaultExtent);
        } else {
          this.getServiceDefinition().then(lang.hitch(this, function(sd) {
            var queryTask = new QueryTask(this.layerObject.url);
            var query = new Query();
            var filter = this.getFilter();
            query.where = filter ? filter : "1=1";
            query.outSpatialReference = this.map.spatialReference;
            query.returnGeometry = true;
            if(sd && sd.advancedQueryCapabilities && sd.advancedQueryCapabilities.supportsReturningQueryExtent) {
              queryTask.executeForExtent(query, lang.hitch(this, function(result) {
                var extent = result.extent;
                if(result.count === 1 && this.layerObject.geometryType === "esriGeometryPoint") {
                  /*
                  result.extent.xmin = result.extent.xmin - 0.0001;
                  result.extent.ymin = result.extent.ymin - 0.0001;
                  result.extent.xmax = result.extent.xmax + 0.0001;
                  result.extent.ymax = result.extent.ymax + 0.0001;
                  */
                  var point = new Point(result.extent.xmin, result.extent.ymin, result.extent.spatialReference);
                  if(point) {
                    extent = jimuUtils.graphicsExtent([new Graphic(point)]);
                  }
                }
                def.resolve(extent);
              }), lang.hitch(this, function() {
                // using the default extent
                def.resolve(defaultExtent);
                console.log("executeForExtent failed.");
              }));
            } else {
              this.getSupportTableInfo().then(lang.hitch(this, function(result) {
                if(result.isSupportQuery) {
                  queryTask.execute(query).then(lang.hitch(this, function(featureSet) {
                    var extent = jimuUtils.graphicsExtent(featureSet.features);
                    def.resolve(extent);
                  }), lang.hitch(this, function() {
                    // using the default extent
                    def.resolve(defaultExtent);
                    console.log("query execute failed.");
                  }));
                } else {
                  def.resolve(defaultExtent);
                }
              }));
            }
          }));
        }
        return def;
      },
  
      _resetLayerObjectVisiblity: function(layerOptions) {
        var layerOption  = layerOptions ? layerOptions[this.id]: null;
        if(!this.originOperLayer.collection) {
          if(layerOption) {
            this.layerObject.setVisibility(layerOption.visible);
            this._visible = this.layerObject.visible;
          }
        }
      },
  
  
      /***************************************************
       * methods for control visiblility
       * **************************************************/
      _initVisible: function() {
        /*jshint unused: false*/
        var visible = false;
        if(this.originOperLayer.collection && this._notFirstInitVisilbeFlag) {
          var FCLayerInfo = this.originOperLayer.collection.layerInfo;
          var parentLayerVisible = FCLayerInfo._visible;
          var subLayerVisible = this.layerObject.visible;
  
          if(FCLayerInfo._oldIsShowInMap !== FCLayerInfo.isShowInMap()) {
            // control from collection layer
            return;
          }
  
          if(parentLayerVisible) {
            if(subLayerVisible) {
              this._visible = true;
            } else {
              this._visible = false;
            }
          } else {
            if(subLayerVisible) {
              this._visible = true;
            } else {
              this._visible = false;
            }
          }
          FCLayerInfo._onVisibilityChanged();
        } else {
          visible = this.originOperLayer.layerObject.visible;
          this._visible = visible;
        }
        this._notFirstInitVisilbeFlag = true;
      },
  
      _setTopLayerVisible: function(visible) {
        if(this.originOperLayer.collection){
          //collection
          //click directly
          if(this.originOperLayer.collection.layerInfo._visible) {
            if(visible) {
              this.layerObject.show();
              this._visible = true;
            } else {
              this.layerObject.hide();
              this._visible = false;
            }
          } else {
            if(visible) {
              this.layerObject.hide();
              this._visible = true;
            } else {
              this.layerObject.hide();
              this._visible = false;
            }
          }
        } else {
          if (visible) {
            this.layerObject.show();
          } else {
            this.layerObject.hide();
          }
          this._visible = visible;
        }
      },
  
      setLayerVisiblefromTopLayer: function() {
        //click from top collecton
        if(this.originOperLayer.collection.layerInfo._visible) {
          if(this._visible) {
            this.layerObject.show();
          }
        } else {
          this.layerObject.hide();
        }
      },
  
      _prepareCheckedInfoForShowOrHide: function(showOrHide) {
        var checkedInfo = {};
        var currentLayerInfo = this;
        while(currentLayerInfo.parentLayerInfo) {
          checkedInfo[currentLayerInfo.id] = showOrHide;
          currentLayerInfo = currentLayerInfo.parentLayerInfo;
        }
        return checkedInfo;
      },
  
      show: function() {
        if(this.isRootLayer()) {
          this._setTopLayerVisible(true);
        } else {
          var rootLayerInfo = this.getRootLayerInfo();
          if(rootLayerInfo._setSubLayerVisibleByCheckedInfo) {
            var checkedInfo = this._prepareCheckedInfoForShowOrHide(true);
            rootLayerInfo._setSubLayerVisibleByCheckedInfo(checkedInfo);
            rootLayerInfo.show();
          }
        }
      },
  
      _initControlPopup: function() {
        this.controlPopupInfo = {
          //enablePopup: this.originOperLayer.disablePopup ? false : true,
          enablePopup: this.layerObject.infoTemplate ? true: false,
          infoTemplate: this.layerObject.infoTemplate
        };
        // backup infoTemplate to layer.
        this.layerObject._infoTemplate = this.layerObject.infoTemplate;
        var handle = aspect.after(this.layerObject, "setInfoTemplate", lang.hitch(this, function(){
          this.layerObject._infoTemplate = this.layerObject.infoTemplate;
          this.controlPopupInfo.infoTemplate = this.layerObject.infoTemplate;
          if(!this.controlPopupInfo.enablePopup) {
            this.layerObject.infoTemplate = null;
          }
        }));
        this._eventHandles.push(handle);
      },
  
      /***************************************************
       * methods for control service definition
       ***************************************************/
  
      _getServiceDefinition: function() {
        var def;
        var url = this.getUrl();
        if(url && this.isRootLayer() && this.layerObject.declaredClass === "esri.layers.FeatureLayer") {
          var requestProxy = this._serviceDefinitionBuffer.getRequest(this.subId);
          def = requestProxy.request(url);
        } else {
          def = new Deferred();
          def.resolve(null);
        }
        return def;
      },
  
      //---------------new section-----------------------------------------
  
      // obtainLegendsNode: function() {
      //   var layer = this.originOperLayer.layerObject;
      //   var legendsNode = domConstruct.create("div", {
      //     "class": "legends-div"
      //   });
  
      //   if (layer && layer.renderer) {
      //     this._initLegendsNode(legendsNode);
      //   } else {
      //     this.layerLoadedDef.then(lang.hitch(this, function(){
      //       this._initLegendsNode(legendsNode);
      //     }));
      //   }
      //   return legendsNode;
      // },
  
      createLegendsNode: function() {
        var legendsNode = domConstruct.create("div", {
          // placeAt 'legendsNode' to document.body first, else can not
          // show legend on IE8.
          "class": "legends-div jimu-legends-div-flag jimu-leading-margin1",
          "legendsDivId": this.id
        }, document.body);
        domConstruct.create("img", {
          "class": "legends-loading-img",
          "src":  require.toUrl('jimu') + '/images/loading.gif'
        }, legendsNode);
        return legendsNode;
      },
  
      drawLegends: function(legendsNode) {
        var useLegendDijit = true;
        if (useLegendDijit) {
          this._initLegendsNodeByLegendDijit(legendsNode);
        } else {
          this._initLegendsNode(legendsNode);
        }
      },
  
      _initLegendsNodeByLegendDijit: function(legendsNode) {
        if( this.layerObject &&
        !this.layerObject.empty &&
        (!this.originOperLayer.subLayer || this.originOperLayer.subLayers.length === 0) &&
        this.layerObject.loaded) {
          // delete loading image
          domConstruct.empty(legendsNode);
          // remove 'jimu-legends-div-flag'
          domClass.remove(legendsNode, 'jimu-legends-div-flag');
          var layerInfos = [{
            layer: this.layerObject
          }];
          var legend = new Legend({
            map: this.map,
            layerInfos: layerInfos,
            arrangement: Legend.ALIGN_LEFT,
            respectCurrentMapScale: false,
            respectVisibility: false
          }, domConstruct.create("div", {}, legendsNode));
  
          legend.startup();
          legendsNode._legendDijit = legend;
        }
      },
  
      _initLegendsNode: function(legendsNode) {
        var legendInfos = [];
        var layer = this.layerObject;
  
        if( this.layerObject &&
            !this.layerObject.empty &&
            (!this.originOperLayer.subLayer || this.originOperLayer.subLayers.length === 0)) {
          // delete loading image
          domConstruct.empty(legendsNode);
          // layer has renderer that means layer has been loadded.
          if (layer.renderer) {
            if (layer.renderer.infos) {
              legendInfos = lang.clone(layer.renderer.infos); // todo
            } else {
              legendInfos.push({
                label: layer.renderer.label,
                symbol: layer.renderer.symbol
              });
            }
  
            if(layer.renderer && layer.renderer.defaultSymbol && legendInfos.length > 0) {
              legendInfos.push({
                label: layer.renderer.defaultLabel || "others",
                symbol: layer.renderer.defaultSymbol
              });
            }
  
            array.forEach(legendInfos, function(legendInfo) {
              legendInfo.legendDiv = domConstruct.create("div", {
                "class": "legend-div"
              }, legendsNode);
  
              legendInfo.symbolDiv = domConstruct.create("div", {
                "class": "legend-symbol jimu-float-leading"
              }, legendInfo.legendDiv);
              legendInfo.labelDiv = domConstruct.create("div", {
                "class": "legend-label jimu-float-leading",
                "innerHTML": jimuUtils.sanitizeHTML(legendInfo.label) || " "
              }, legendInfo.legendDiv);
  
              if(legendInfo.symbol.type === "textsymbol") {
                domAttr.set(legendInfo.symbolDiv, "innerHTML", jimuUtils.sanitizeHTML(legendInfo.symbol.text));
              } else {
                var mySurface = gfx.createSurface(legendInfo.symbolDiv, 50, 50);
                var descriptors = jsonUtils.getShapeDescriptors(legendInfo.symbol);
                var shape = mySurface.createShape(descriptors.defaultShape)
                            .setFill(descriptors.fill).setStroke(descriptors.stroke);
                shape.setTransform(gfx.matrix.translate(25, 25));
              }
            }, this);
          }
        }
      },
  
  
      // operLayer = {
      //    layerObject: layer,
      //    title: layer.label || layer.title || layer.name || layer.id || " ",
      //    id: layerId || " ",
      //    subLayers: [operLayer, ... ],
      //    mapService: {layerInfo: , subId: },
      //    collection: {layerInfo: }
      //    selfType: dynamic | tiled | group
      // };
  
      obtainNewSubLayers: function() {
        var newSubLayers = [];
        array.forEach(this.originOperLayer.subLayers, function(subOperLayer){
          var subLayerInfo;
          // create sub layer
          subOperLayer.parentLayerInfo = this;
          subLayerInfo = this._layerInfoFactory.create(subOperLayer);
          newSubLayers.push(subLayerInfo);
          subLayerInfo.init();
        }, this);
        return newSubLayers;
      },
  
      _isAllSubLayerVisibleOnPath: function() {
        var isVisbleOrInvisilbe = true;
        var currentLayerInfo = this;
        while(!currentLayerInfo.isRootLayer()) {
          isVisbleOrInvisilbe = isVisbleOrInvisilbe && currentLayerInfo.isVisible();
          currentLayerInfo = currentLayerInfo.parentLayerInfo;
        }
        return isVisbleOrInvisilbe;
      },
  
      _getCustomPopupInfo: function(object, fieldNames) {
        // return popupInfo with all fieldInfos if the fieldName is null;
        var popupInfo = null;
        if(object && object.fields) {
          popupInfo = {
            title: object.name,
            fieldInfos:[],
            description: null,
            showAttachments: true,
            mediaInfos: []
          };
          array.forEach(object.fields, function(field){
            var isValidField = false;
            if(fieldNames) {
              var isValidFieldName = array.some(fieldNames, lang.hitch(this, function(fieldName) {
                return fieldName && (field.name.toLowerCase() === fieldName.toLowerCase());
              }));
              if(isValidFieldName) {
                isValidField = true;
              }
            } else {
              isValidField = true;
            }
            if(isValidField) {
              var fieldInfo = jimuUtils.getDefaultPortalFieldInfo(field);
              fieldInfo.visible = true;
              fieldInfo.isEditable = field.editable;
              popupInfo.fieldInfos.push(fieldInfo);
            }
          }, this);
        }
        return popupInfo;
      },
  
      // get default popupInfo
      // Todo... improve the getPopupInfo interface.
      _getDefaultPopupInfo: function(object) {
        var defaultPopupInfo = this._getCustomPopupInfo(object);
        if(defaultPopupInfo) {
          defaultPopupInfo.fieldInfos = array.filter(defaultPopupInfo.fieldInfos, lang.hitch(this, function(fieldInfo) {
            var result;
  
            var fieldName = fieldInfo.fieldName && fieldInfo.fieldName.toLowerCase();
            if(fieldName &&
               fieldName.indexOf("object") < 0 &&
               fieldName.indexOf("globalid") < 0 &&
               fieldName.indexOf("shape") < 0 &&
               fieldName.indexOf("perimeter") < 0) {
              result = true;
            } else {
              result = false;
            }
            return result;
          }));
        }
        return defaultPopupInfo;
      },
  
      // control popup
      // this method depend on layerObject or webmap's popupInfo, otherwise will return null;
      _getDefaultPopupTemplate: function(object) {
        var popupTemplate = null;
        // Todo... improve the getPopupInfo interface.
        var popupInfo = this.getPopupInfo() || this._getDefaultPopupInfo(object);
        if(popupInfo) {
          popupTemplate = new PopupTemplate(popupInfo);
        }
        return popupTemplate;
      },
  
      enablePopup: function() {
        return this.loadInfoTemplate().then(lang.hitch(this, function() {
          if(this.controlPopupInfo.infoTemplate) {
            this.controlPopupInfo.enablePopup = true;
            this.layerObject.infoTemplate = this.controlPopupInfo.infoTemplate;
            return true;
          } else {
            return false;
          }
        }));
      },
  
      disablePopup: function() {
        this.controlPopupInfo.enablePopup = false;
        this.layerObject.infoTemplate = null;
      },
  
      isPopupEnabled: function() {
        var isPopupEnabled;
        if(this.controlPopupInfo &&
           this.controlPopupInfo.enablePopup) {
          isPopupEnabled = true;
        } else {
          isPopupEnabled = false;
        }
        return isPopupEnabled;
      },
  
      isSupportPopup: function() {
        var def = new Deferred();
        this.loadInfoTemplate().then(lang.hitch(this, function(infoTemplate) {
          if(infoTemplate) {
            def.resolve(true);
          } else {
            def.resolve(false);
          }
        }));
        return def;
      },
  
      /*
      loadInfoTemplate: function() {
        var def = new Deferred();
        if(!this.controlPopupInfo.infoTemplate) {
          this.controlPopupInfo.infoTemplate = this._getDefaultPopupTemplate(this.layerObject);
        }
        def.resolve(this.controlPopupInfo.infoTemplate);
        return def;
      },
      */
  
      // getLayerObject first because of some layerObjects has not been loaded, such as tabel.
      loadInfoTemplate: function() {
        var def = new Deferred();
        if(this.controlPopupInfo.infoTemplate) {
          def.resolve(this.controlPopupInfo.infoTemplate);
        } else {
          this.getLayerObject().then(lang.hitch(this, function() {
            this.controlPopupInfo.infoTemplate = this._getDefaultPopupTemplate(this.layerObject);
            def.resolve(this.controlPopupInfo.infoTemplate);
          }), lang.hitch(this, function() {
            def.resolve(null);
          }));
        }
        return def;
      },
  
      getInfoTemplate: function() {
        return this.controlPopupInfo.infoTemplate;
      },
  
      _getRelatedUrls: function(layerObject, relationshipRole) {
        var relatedUrls = [];
        if(!layerObject || !layerObject.url || !layerObject.relationships) {
          return relatedUrls;
        }
  
        var index = layerObject.url.lastIndexOf('/');
        var serverUrl = layerObject.url.slice(0, index);
        array.forEach(layerObject.relationships, function(relationship) {
          if (!relationshipRole ||
          !relationship.role ||
          relationship.cardinality === "esriRelCardinalityManyToMany" ||
          relationshipRole === relationship.role) {
            var subUrl = serverUrl + '/' + relationship.relatedTableId.toString();
            relatedUrls.push(subUrl);
          }
        }, this);
  
        return relatedUrls;
      },
  
      // summary:
      //   get related tableInfo array
      // parameters:
      //   relationshipRole: optional
      //       "esriRelRoleOrigin"
      //       "esriRelRoleDestination"
      getRelatedTableInfoArray: function(relationshipRole) {
        var relatedTableInfoArray = [];
        var def = new Deferred();
        this.getLayerObject().then(lang.hitch(this, function(layerObject) {
          var relatedUrls = this._getRelatedUrls(layerObject, relationshipRole);
          if(relatedUrls.length === 0) {
            def.resolve(relatedTableInfoArray);
          } else {
            this._getLayerInfosObj().traversalAll(lang.hitch(this, function(layerInfo) {
              if(relatedUrls.length === 0) {
                // all were found
                return true;
              } else {
                array.forEach(relatedUrls, function(relatedUrl, index) {
                  if(lang.getObject("layerObject.url", false, layerInfo) &&
                     relatedUrl &&
                     (portalUrlUtils.removeProtocol(relatedUrl.toString().toLowerCase()).replace(/\/+/g, '/') ===
                     portalUrlUtils.removeProtocol(
                                   layerInfo.layerObject.url.toString().toLowerCase()).replace(/\/+/g, '/'))
                  ) {
                    relatedTableInfoArray.push(layerInfo);
                    relatedUrls[index] = '';
                  }
                }, this);
                return false;
              }
            }));
            def.resolve(relatedTableInfoArray);
          }
        }), lang.hitch(this, function() {
          def.resolve(relatedTableInfoArray);
        }));
        return def;
      },
  
      _getOrderByFields: function(relationshipId) {
        var orderByFields = null;
        if(relationshipId === undefined || relationshipId === null ) {
          return orderByFields;
        }
  
        var popupInfo = this.getPopupInfo();
        if(popupInfo && popupInfo.relatedRecordsInfo && popupInfo.relatedRecordsInfo.orderByFields) {
          array.some(popupInfo.relatedRecordsInfo.orderByFields, function(orderByField) {
            var orderByFieldArray = orderByField.field.split('/');
            if(orderByFieldArray[1] === relationshipId.toString()) {
              orderByFields = [orderByFieldArray[2] + ' ' + orderByField.order];
              return true;
            }
          }, this);
        }
        return orderByFields;
      },
  
      getOriRelationshipByDestLayer: function(originalLayerObject, relatedLayerObject, relationshipIndex) {
        var queryRelationship = null;
        // compatible with arcgis service 10.0.
        var queryRelationships = array.filter(originalLayerObject.relationships, function(relationship) {
          if (relationship.relatedTableId === relatedLayerObject.layerId) {//************
            return true;
          }
        }, this);
        queryRelationship = queryRelationships[relationshipIndex] ?
                            queryRelationships[relationshipIndex] :
                            queryRelationships[0];
        return queryRelationship;
      },
  
      getRelatedRecords: function(feature, relatedLayerInfo, relationshipIndex) {
        var def = new Deferred();
        var relatedQuery = new RelationshipQuery();
        // todo...
        var originalLayerObjectDef = this.getLayerObject();
        var relatedLayerObjectDef = relatedLayerInfo.getLayerObject();
        all({
          originalLayerObject: originalLayerObjectDef,
          relatedLayerObject: relatedLayerObjectDef
        }).then(lang.hitch(this, function(result) {
          if(!result.originalLayerObject || !result.relatedLayerObject) {
            def.resolve([]);
          }
          var queryRelationship = this.getOriRelationshipByDestLayer(this.layerObject,
                                                                      result.relatedLayerObject,
                                                                      relationshipIndex);
          relatedQuery.outFields = ["*"];
          relatedQuery.relationshipId = queryRelationship && queryRelationship.id;
          var objectId =
            feature.attributes[this.layerObject.objectIdField];
          relatedQuery.objectIds = [objectId];
          relatedQuery.definitionExpression = relatedLayerInfo.getFilter();
          relatedQuery.orderByFields = this._getOrderByFields(queryRelationship && queryRelationship.id);
  
          this.layerObject.queryRelatedFeatures(
            relatedQuery,
            lang.hitch(this, function(relatedRecords) {
              var features = relatedRecords[objectId] && relatedRecords[objectId].features;
              if(features) {
                def.resolve(features);
              } else {
                def.resolve([]);
              }
            }), lang.hitch(this, function() {
              def.resolve([]);
            })
          );
        }));
        return def;
      },
  
      getFilter: function() {
        // summary:
        //   get filter from layerObject.
        // description:
        //   return null if it does not have or cannot get it.
        var filter;
        if(this.layerObject &&
           !this.layerObject.empty &&
           this.layerObject.getDefinitionExpression) {
          filter = this.layerObject.getDefinitionExpression();
        } else {
          filter = null;
        }
        return filter;
      },
  
      setFilter: function(layerDefinitionExpression, objectPassWithFilterChangeEvent) {
        // summary:
        //   set layer definition expression to layerObject.
        // paramtter
        //   layerDefinitionExpression: layer definition expression
        //   set 'null' to delete layer definition express
        // description:
        //   operation will skip layer if it does not support filter.
        if(this.layerObject &&
           !this.layerObject.empty &&
           this.layerObject.setDefinitionExpression) {
          var parameterObject = lang.mixin({}, objectPassWithFilterChangeEvent);
          lang.setObject('_wabProperties.objectPassWithFilterChangeEvent',
                         parameterObject,
                         this.layerObject);
          this.layerObject.setDefinitionExpression(layerDefinitionExpression);
        }
      },
  
      // control labels
  
      _isAlreadyInLabelLayerOfMap: function(labelLayerOfMap) {
        var isAlreadyInLabelLayerOfMap = false;
        if(labelLayerOfMap) {
          isAlreadyInLabelLayerOfMap = array.some(labelLayerOfMap.getFeatureLayers(), function(fl) {
            return this.id === fl.id;
          }, this);
        }
        return isAlreadyInLabelLayerOfMap;
      },
  
      _addToLabelLayerOfMap: function() {
        var labelLayerOfMap = this.getLabelLayerOfMap();
        if(labelLayerOfMap && !this._isAlreadyInLabelLayerOfMap(labelLayerOfMap)) {
          labelLayerOfMap.addFeatureLayer(this.layerObject);
          // workaround for cannot control labels for layers are added continuously.
          this.map.removeLayer(labelLayerOfMap);
          this.map.addLayer(labelLayerOfMap);
        }
      },
  
      _removeFromLabelLayerOfMap: function() {
        // remove featureLayer from labelLayerOfMap
        var labelLayerOfMap = this.getLabelLayerOfMap();
        labelLayerOfMap.removeFeatureLayer(this.id);
      },
  
      _isSupportLabelControl: function() {
        var isSupportLabelControl;
        // TODO... supports WFS layer.
        if(this.isRootLayer() &&
           !this.layerObject.empty &&
           this.layerObject.declaredClass === "esri.layers.FeatureLayer" &&
           this.layerObject.labelingInfo &&
           this.layerObject.labelingInfo.length > 0){
          isSupportLabelControl = true;
        } else {
          isSupportLabelControl = false;
        }
        return isSupportLabelControl;
      },
  
      getLabelLayerOfMap: function() {
        var labelLayer;
        if(this.map._labels) {
          labelLayer = this.map._labels;
        } else {
          var labelLayerId = 'labels';
          labelLayer = this.map.getLayer(labelLayerId);
          if(!labelLayer) {
            labelLayer = new LabelLayer({"id": labelLayerId});
            this.map.addLayer(labelLayer);
          }
        }
        return labelLayer;
      },
  
      obtainLabelControl: function() {
        var lableLayerId = this.id + '_labelLayer';
        var labelLayer = this.map.getLayer(lableLayerId);
  
        // TODO... supports WFS layer.
        if(!labelLayer && this._isSupportLabelControl()){
          var labelLayerOfMap = this.getLabelLayerOfMap();
          if(labelLayerOfMap && this._isAlreadyInLabelLayerOfMap(labelLayerOfMap)) {
            // create a new labelLayer
            labelLayer = new LabelLayer({"id": lableLayerId});
            labelLayer.addFeatureLayer(this.layerObject);
            this.map.addLayer(labelLayer);
            // remove featureLayer from labelLayerOfMap
            this._removeFromLabelLayerOfMap();
          }
        }
        return labelLayer ? labelLayer : null;
      },
  
      restoreLabelControl: function() {
        if(this._isSupportLabelControl()) {
          // remove labelLayer from map
          this.destroyRealtedLabelLayer();
          // add featureLayer to labelLayerOfMap
          this._addToLabelLayerOfMap();
        }
      },
  
      destroyRealtedLabelLayer: function() {
        var lableLayerId = this.id + '_labelLayer';
        var labelLayer = this.map.getLayer(lableLayerId);
        if(labelLayer) {
          this.map.removeLayer(labelLayer);
        }
      },
  
      destroyLabelLayer: function() {
        var currentLayerIsAlreadyInMap = this.map.getLayer(this.id);
        if(!currentLayerIsAlreadyInMap) {
          this._removeFromLabelLayerOfMap();
          this.destroyRealtedLabelLayer();
        }
      },
  
      canShowLabel: function() {
        return this._isSupportLabelControl();
      },
  
      isShowLabels: function() {
  
        var lableLayerId = this.id + '_labelLayer';
        var labelLayer = this.map.getLayer(lableLayerId);
        var labelLayerOfMap = this.getLabelLayerOfMap();
        var precondition = this._isSupportLabelControl() &&
                           (labelLayer || (labelLayerOfMap && this._isAlreadyInLabelLayerOfMap(labelLayerOfMap)));
  
        return precondition ? this.layerObject.showLabels : false;
      },
  
      showLabels: function() {
        if(this._isSupportLabelControl() && this.layerObject.setShowLabels) {
          var lableLayerId = this.id + '_labelLayer';
          var labelLayer = this.map.getLayer(lableLayerId);
          if(!labelLayer) {
            this._addToLabelLayerOfMap();
          }
          this.layerObject.setShowLabels(true);
        }
      },
  
      hideLabels: function() {
        if(this._isSupportLabelControl() && this.layerObject.setShowLabels) {
          this.layerObject.setShowLabels(false);
        }
      }
  
    });
    return clazz;
  });
  