Tuesday, March 13, 2012

ARCGIS JavaScript API – BUILDING A LEGEND COMPONENT – PART II


1.     Introduction


As we see in the ArcGis Desktop application (ArcMap), the use of the legend consists of more than just displaying the rendering information. Behind the legend extra functionality is exposed through context menus. In the Silverlight desktop application I wrote some of this functionality was added to the Silverlight API Legend. This extra functionality was done through extending the Silverlight API legend control with extra methods and attached in the XAML through commands. The Microsoft Silverlight framework and MVVM makes this possible.

In the JavaScript API however, the legend control has only limited functionality making it very difficult to create extended functionality. To solve this issue, a custom legend control must be created. A solution could be creating the legend with the help of the Dojo library, but for a generic solution this is not a simple job. Another solution is using jQuery templates. The jQuery templates is an extension created by Microsoft  and added to the community. The interesting aspect is the use of html templates to define how a control is shown using all the power of CSS and a build-in processing syntax. There was an interesting session  at ESRI in 2011 explaining this technique of building a custom legend control.  To have a flexible solution, the template is saved on the server in a file that is downloaded and added to the legend region. The initial template I used has the following contents.

<li>
  <input type="checkbox" style="float:left" id="${$item.getCheckboxID()}" checked="${$item.getChecked()}" />
      <h3 class="agsLegendOpen" style="margin:0 0 0 25px;padding:0;" >${title}</h3>
     {{if serviceType == 4}}
       {{if renderer.type=="simple"}}
          {{if renderer.symbol.type=='esriPMS'}}
            <div style="display:table; margin:10px 5px;" >
              <img  style="padding-right:10px;
              float:left;
              display:inline-block;
              vertical-align:middle;"
              src="data:${renderer.symbol.contentType};base64,${renderer.symbol.imageData}"/>
            </div>
          {{else renderer.symbol.type=='esriSFS'}} 
             {{if renderer.symbol.color == null}}
                  <div style="display:table-cell;
                    vertical-align:middle;
                    width:20px;
                    height:20px;
                    background-color:rgba(151,219,242,255);
                    border:${$item.getBorder(renderer.symbol.outline)};">
                  </div>
                {{else}}
                  <div style="display:table-cell;
                    vertical-align:middle;
                    width:20px;
                    height:20px;
                    background-color:${$item.getColor(renderer.symbol.color)};
                    border:${$item.getBorder(renderer.symbol.outline)};">
                  </div>
             {{/if}}
          {{else renderer.symbol.type=='esriSMS'}}
            {{if renderer.symbol.style=="esriSMSCircle"}}
            {{/if}}
          {{/if}}
        {{else renderer.type=='uniqueValue'}}
          <ul>
          {{each renderer.uniqueValueInfos}}                
                  {{if $value.symbol.type=='esriPMS'}}
                          <div style="display:table;margin:5px 5px;">                        
                          <img src="data:${$value.symbol.contentType};base64,${$value.symbol.imageData}" style="vertical-align:middle;"/>
                          <span style="padding:0px 5px 0px 0px;display:table-cell;vertical-align:middle;">${$value.label}</span>
                          </div>
                  {{else $value.symbol.type=='esriSFS'}}
                          <li style="display:table; margin:5px 5px;" >                               
                                  <div style="display:table-cell;vertical-align:middle;width:20px;height:20px;background:${$item.getColor($value.symbol.color)};border:${$item.getBorder($value.symbol.outline)};" ></div>
                                  <span style="padding:0px 5px 0px 5px;display:table-cell;vertical-align:middle;">${$value.label}</span>
                          </li>
                  {{else $value.symbol.type=='esriSLS'}}
                    <li style="display:table;" >
                      <div style="margin:8px 5px -15px 5px;
                        width:20px;
                        height:${$value.symbol.width}px;
                        background:${$item.getColor($value.symbol.color)};">
                      </div>
                      <span style="padding:0px 5px 0px 5px;
                        display:table-cell;
                        vertical-align:middle;">
                        ${$value.label}
                      </span>
                    </li>
                  {{/if}}
          {{/each}}
        </ul>
       {{/if}}
     {{/if}}
</li>
<div style="clear:both"/>

As you can see, there are conditional statements and loop statements. To finalize this approach I encapsulate the template class into a custom jQuery plugin. All these classes makes part of the legend module.

2.     Legend in action

To handle this approach of program logic, I have to retrieve extra information of the ArcGis server using the REST API. This extra information is retrieved through the legend module and not during the map initialization process. Doing so we can loosely couple the map module from the legend module,  allowing this module to handle all data needed for the legend. The communication between the map module and the legend module is done through our class eventAggregator that will inform the legend module when the map is initialized (loaded). During the initialization method of the legend module a callback functions is hooked to the map loaded event  as you can see in the code below.
var legendModule = (function () {
    var divLegend;
    var layersCount;
    var layersTotal
    function legendBuild(eventName, eventData, tagData) {

       }
    return {
        initLegend: function (divElement) {
            this.divLegend = divElement;
            iEventAggregator.subscribe("mapLoaded", legendBuild, divElement);
        }
    }

Because the retrieving of all the legend information coming from the REST services is asynchronous, I use a simple counter to know when the last information is retrieved and all the legend information is available to be applied in the template. The code below shows how the build of the legend is started after the last legend has been processed. We simply called the custom legend  jQuery plug-in
    function buildLegend() {
        if (layersCount == layersTotal) {
            var legend = $("#legendRegion").agsLegend({ });
        }

To activate the templates, this can be done through this method in the jQuery plug-in.
    $.fn.agsLegend = function (options) {
        opts = $.extend({}, $.fn.agsLegend.defaults, options);     
        if (opts.autoLoadTemplates) {
            getTemplate();
        }
        return this.each(function () {
                $this = $(this);
                $this.delegate("input[type='checkbox']""click", checkLayerVisibility);  
                $this.delegate("h3","mouseup",rightClick);
                if (opts.isCollapsible) { makeCollapsible(this); }   
        }

Upload of the template from the server is done by this code through a AJAX call.
    function getTemplate() {
        $.ajax({
            url: opts.templateFileURL,
            success: opts.onTemplateLoaded || handleTemplateResponse
        });
    }

To apply the template, you can do this by this function below. You simply insert the template into a script section of the head section. Using the tmpl method of the jquery template plug-in the data is inserted into the template.

    function handleTemplateResponse(data) {
        script = document.createElement("script");
        script.style.display = "none";
        script.type = "text/x-jquery-tmpl";
        script.id = opts.templateDOMId;
        script.text = data;
        $('head').append(script);                  
        $("#"+opts.templateDOMId).tmpl(iGisOperation.GetLayersLegendInfos(),
                {
                    getColor: getColor,
                    getBorder: getBorder,
                    getChecked: getChecked,
                    getCheckboxID: getCheckboxID
                }).appendTo($this);
    }

Some methods are needed to format the values in the template.The final result of all this, is a legend that looks like this.

The legend build here is a first simple implementation. I will show later how we can enhance the look and feel of the legend by adding concept as grouping using more advanced rendering support. All legend information will be maintained in the gisOperation class so that other modules can benefit from this data.

In the coming weeks I will add a toolbar to the application to allow different GIS operations on the map as zooming and selecting.

2 comments:

  1. Johnny this looks very cool, and thank you for contributing to the community.

    Did you post a demo site showing this Legend code in action, or your site in general? This would be great to see in action.

    ReplyDelete
  2. Hi, Great.. Tutorial is just awesome..It is really helpful for a newbie like me..
    I am a regular follower of your blog. Really very informative post you shared here.
    Kindly keep blogging. If anyone wants to become a Front end developer learn from Javascript Training in Chennai .
    or Javascript Training in Chennai.
    Nowadays JavaScript has tons of job opportunities on various vertical industry. ES6 Training in Chennai

    ReplyDelete