Tuesday, October 22, 2013

Using JSRender For creating ARCGIS related components


1      Introduction



In a previous document I explained how you can use the JQuery template plug in for creating a legend component for the different layers used in a ArcGIS map application. However the bad news is that the JQuery template plug in is a beta release and will stay that way.  Microsoft stopped new development on this JQuery template .

The good news is that there is an excellent library replacement JSRender for JQuery templates.  This extension can be used as an independent library or as a plug in for JQuery. In this document I will explain the use of the JQuery plug in version of JSRender for creating a legend component.

One of the benefits of using JSRender for building legend or other modules is the use of the separation of the visual representation and the business logic through the use of models. So JSRender fits perfectly into the pattern separation of concerns as I already illustrated with the use of the JavaScript library KnockoutJS for using MVVM (Model View ViewModel) pattern.

Through the use of counters, I keep track of the different state of the asynchronous REST calls to know when to start the creation of the legend component with JSRender.  As is the case for other components, the building of the legend component only starts after the map processing is completed. This is done using the event aggregate pattern publish and subscribe implemented in JavaScript that I described in a previous document.

The code illustrated is based on JavaScript files, template files and support for the ArcGIS framework outlined before when creating the model contents.

Below is an example of how  a legend components looks after using JSRender plug in of JQuery.



2      Model


The first import JavaScript class I need for the legend is the model. The model is the blueprint that will be used in the template for building the legend. Its contents is built during the creation of the map with the layers specified in a configuration file. The model consists of an array of  the type ‘LayerLegendInfo’ described below.

require(['dojo/_base/declare'], function (declare) {
   declare("gis.LayerLegendInfo", null, {
     constructor: function (url,id,title,renderer,renderType,
          serviceType,groupLayer,parentId,
          group, groupName,key) {
       this.url = url;
       this.id = id;
       this.parentId = parentId;
       this.renderer = renderer;
       this.renderType = renderType;
       this.serviceType = serviceType;
       this.title = title;
       this.groupLayer = groupLayer;
       this.group = group;
       this.groupName = groupName;
       this.sequence = Number(key.slice(-2));
       this.key = key;
       this.selected = true;
     }
  });
});

It is important that all relevant data is filled in by the map initialization process as they will serve for the rendering process of the templates without interaction with the ArcGIS JavaScript API. The goal of module components is to be independent of the ArcGIS JavaScript API.

Important properties for the legend are :
                Title                  Description of the layer
                Sequence          Used to sort the list into the correct sequence
                                        on the legend component.
                RenderType       Definition of the different render types supported.
                                        Possible values are :
                                        ESRI defined types as of
· Simplefillsymbol /esriSFS
· Simplelinesymbol / esriSLS
· simplemarkersymbol
· picturemarkersymbol / esriPMS
                                         Custom type : picturewmssymbol
                Renderer            Renderer object as defined by ESRI. The legend
                                         template use the contents  of this object to
                                         display the symbols used in the layer,
                                         including WMS layers defined outside the
                                         ArcGIS JavaScript API.

3      Renderer information


For creating a legend, the most difficult part is to populate the renderer information for the different types of layers.  The handling of the retrieving of renderer data is split into three types of layers. This is done because of the nature of the layers. Non ArcGIS layers have restricted data available for the rendering of templates. This is the case for WMS layers which are integrated into the legend component.

3.1    WMS Layer

As I stated earlier, the creation of the legend is based on the definition of the renderer class defined in the ARCGIS JavaScript API. In the case of WMS layer a custom renderer is used, the ArcGIS JavaScript API has no standard support for the legend of a WMS layer.

To simplify the building of a renderer for a WMS layer, I simplified two things.

·         First to get to the layer symbols,  I added an URL in the layer configuration file for each sub layer pointing to the legend image from the sub layer of the WMS layer. This could be done dynamic through  a call to the WMS service using GetLegendGraphic, but this need asynchronous REST service calls. Doing REST service calls introduce extra complex processing, a direct link seems more simpler and result in better performance because no processing is required.

·         Second, I need to create a renderer derived from an ESRI class to unify the processing for renderer symbols used in the templates.  WMS layers have a picture symbol as legend symbol,. The renderer symbol used for WMS layers is derived from a picture symbol of the ArcGIS API. As renderer type I defined a new symbol type :  picturewmssymbol, this will be used to verify the type of symbol linked to the layer. I only need to overwrite the type property.

Dojo class definition of the symbol class :

// Create a new class named "esri.symbol.PictureWmsSymbol"
dojo.declare("esri.symbol.PictureWmsSymbol", [
    esri.symbol.PictureMarkerSymbol
],
{
    type: "picturewmssymbol"
});
                                         

3.2    ARCGIS Dynamic Layer

Using the ArcGIS JavaScript API there is no direct possibility to retrieve the renderer information for the dynamic layer. Even after using the layer initialization event, renderer information is not returned at the client side.  In the wake of a standard solutions from the ArcGIS API, we had to retrieve the rendering information exposed by the ArcGIS server through REST API calls.

The  solution is doing a REST API call to the ARCGIS server for each layer of the ARCGIS Dynamic layer service. The best way for retrieving render information is after the initialization event. After the initialization event of the ARCGIS dynamic layer, a list of all layer id’s is returned. The id’s can be used for doing the REST API calls. Some filtering has to be done to limit the renderer information to feature layers only which have the symbol information.

The JSON object returned from the REST API call can be used as is, no transformation is required.

Step 1 : Loop through the list of dynamic layers and start a REST API for each sub layer.

Step 2: Extract the renderer data from the JSON object returned and update the legend model.

STEP 1

function updateLegend4DynamicServices(onFinished) {
 /// <summary>
/// </summary>
/// <param name="onFinished"> Next step</param>
  var baseMapLayerInfo;
  dynamicLayerProcessed = 0;
  dynamicLayerCount = 0;
  dynamicProcessedComplete = onFinished;
  var layerInfo;
  try {
     $.each(layersSelectionInfos, function (index, layerSelection) {
     if (layerSelection.serviceType === layerTypes.Dynamic) {
         dynamicLayerCount++;
         if (commonData.DebugMode)
            logging.logMessage("I", "Get dynamic layer legend:" +

               layerSelection.url,
               "gisOperation/updateLegend4DynamicServices");
         var layersRequest = esri.request({
              url: layerSelection.url,
              content: { f: "json" },
              handleAs: "json",
              callbackParamName: "callback"
            });
            layersRequest.then(
               function (response) {
                updateLayerLegendList(response);
                verifyProcessDynamicServices();
               }, function (e) {                                      
                 logging.logMessage("E", "legend request process

                 failed. " + e.message,         
                 "gisOperation/updateLegend4DynamicServices");
            });
         }
     })
   } catch (e) {
    logging.logMessage("E", "process failed. " + e.name +

      " ini- " + e.message,
      "gisOperation/ updateLegend4DynamicServices");
  }
}

 STEP 2:
function updateLayerLegendList(data) {
 /// Update render information for of a dynamic layer, callback

 /// method for getting feature layer data
 /// <param name="layer">data</param>
 /// </summary>
 try {
  if (data.error) {
    logging.logMessage("E", "error in legendbuild JSON non

       feature layers>" +
       data.error.message + " - " + data.error.details, 

      "gisOperation/updateLegendList");
  }
  else {
    if (commonData.DebugMode)
      logging.logMessage("I", "Legend rest service processed :" +
        data.name, "gisOperation/updateLegendList");
    if (data.type === "Feature Layer") {
    // Find legend
      $.each(layersLegendInfos, function (index, layersLegendInfo) {
         if (layersLegendInfo.title === data.name) {
         // Update legend info
            layersLegendInfo.renderer = data.drawingInfo.renderer;
            layersLegendInfo.renderType =

                   data.drawingInfo.renderer.type;
          }
      });
    }
    else if (data.type === "Group Layer") {
      }
      else {
      }
    }
  } catch (e) {
   logging.logMessage("E", "error in legendbuild JSON non

      feature layers-> "
      + e.name + " - " + e.message, 

      "gisOperation/updateLegendList");
  }
}

3.3    Feature layer
The handling of renderer for feature layers is a lot easier than the previous layer types. As soon as a feature layer has been initialized on the map all renderer information is available. The only thing that has to be done is handling the initialization event of a feature layer. As with other asynchronous processing a counter is used to know when all feature layers has been initialized. Here the selection model is also updated for the selection layer list component. To simplify the template processing of the model the class name of the renderer is used as render type.

function featureLayer_Initialized(layer) {
/// <summary>
/// Initialize event of a feature layer
/// </summary>
/// <param name "layer">feature layer</param>
  var featureLayerTemplates = layer.templates;
  var featureLayerTypes = layer.types;
  var featureRenderer = layer.renderer;
  var featureTypes = [];
  var featureTemplates = [];
  var attributes,templates,type,typeName,prototype,info,key;
  try {
    if (commonData.DebugMode)
      logging.logMessage("I", layer.url,

         "gisOperation/featureLayer_Initialized");
    featureLayersInitialized.push(layer);
    featureLayerInfos.push(new gis.FeatureLayerInfo(layer.url,

      layer.templates,layer.types, layer.name,layer.id,
      layer.geometryType, layer.objectIdField, 
      layerTypes.Feature));
    // Legend table
    for (j = 0;

      j < configuration.getConfig().MapConfig.FeatureLayers.length;
      j++) {
      if (configuration.getConfig()

           .MapConfig.FeatureLayers[j].Title === layer.id)
         break;
    }
    key = "F" + zeroPad(j.toString(), 2);
    layersLegendInfos.push(new gis.LayerLegendInfo(

      layer.url, layer.id, layer.name,
      layer.renderer,layer.renderer.declaredClass,

      layerTypes.Feature,"", "", false, "", key));
    // Selection table
    layersSelectionInfos.push(new gis.LayerSelectionInfo(

      layer.url, layer.id,layer.description, layer.name,
      layer.fields,layer.displayField, key,
      layer,layerTypes.Feature, ""));
    //
    If (commonData.DebugMode)
      logging.logMessage("I", "Verify end of layer loaded " +

        layer.url,"gisOperation/featureLayer_Initialized");
    verifyInitialisationMap(layer.url);
  } catch (e) {
    logging.logMessage("E", "featurelayer " + layerUrl +
    " initialize process failed. " + e.name + " - " + e.message,
    "gisOperation/featureLayer_Initialized");
    verifyInitialisationMap(layer.url);
  }
}

4         Legend Templates


    4.1    Template construction

Templates are the basic elements of JSRender. But besides simple templates, JSRender also allows a more complex usage of templates. With JSRender you can use a hierarchy of templates with  conditional branches or loops.

In our implementation of a legend module the use of templates reflects the different layer types previous described. There are also a separate templates for the treatment of the rendering of the symbols used for the layers.

The structure of the templates is

·         Main template (_legend.main.tmpl.html)

o   Base layer template (_legend.baseLayer.tmpl)

§  Simple renderer template (_legend.simpleRender.tmpl.html)

§  Unique value renderer template (_legend.uniqueValueRender.tmpl)

o   Feature layer template (_legend.featureLayer.tmpl)

§  Simple renderer template (_legend.simpleRender.tmpl.html)

§  Unique value renderer template (_legend.uniqueValueRender.tmpl)

The renderer templates are responsible for the creation of the different symbols for a layer. The  legend model provides all the data needed to create the graphics for the legend. One of the useful features of JSRender is the possibility to create functions that can be used in a template. The use of template functions makes it possible to do transformation of contents during the rendering of the template.

The first thing you must do is the loading of the templates into memory, this results in quick processing of the templates. Because JSRender is integrated into JQuery, the whole loading of the templates can be done through one call as illustrated below. This way of working simplifies the asynchronous process.

You can add for each template additional methods using the property helpers of the template object.

$.when(
  $.get(getPath('legend.main')),
  $.get(getPath('legend.baseLayer')),
  $.get(getPath('legend.featureLayer')),
  $.get(getPath('legend.uniqueValueRender')),
  $.get(getPath('legend.simpleRender'))
)
 .done(function (main, baseLayer, featureLayer, uniqueValueRender,
                 simpleRender) {
      $.templates({
           mainTmpl: {
                markup: main[0],
                helpers: {
               }
           },
           baseLayerTmpl: {
                markup: baseLayer[0],
                helpers: {
                    clicked: {
                    }
                }
          },
          featureLayerTmpl: {
                markup: featureLayer[0],
                helpers: {
                     clicked: {
                         click: function () {
                              …

                          }
                     }
                }
          },
          uniqueValueRenderTmpl: {
                 markup: uniqueValueRender[0]
           },
           simpleRenderTmpl: {
                 markup: simpleRender[0]
           }
});

As an example of the use of templates and its possibilities, have a look at the simple renderer template. Its logic is simple and consists of a number of if then constructions.

<div style="display: table; margin: 0px 5px;">
  {{if symbol.type == 'picturemarkersymbol' || 
    symbol.type == 'esriPMS'}}
    <div style="display: table; margin: 0px 5px; 
         align-content: flex-start;height:20px;">
    <img style="padding-right: 10px; float: left;
         display: inline-block;vertical-align:middle;" 
         src="data:{{:symbol.contentType}};base64,
             {{:symbol.imageData}}" />
    <span class="renderTitle">{{:~label}}</span>
    </div>
  {{else}}
    {{if symbol.type == 'picturewmssymbol'}}
      <div style="display: table; margin: 0px 5px; 
           align-content: flex-start;height:20px;">
      <img style="padding-right: 10px; float: left; 
           display: table-cell;vertical-align:middle;"
           src="{{:symbol.url}}" />
      </div>
    {{else}}
      {{if symbol.type == 'simplefillsymbol' ||
           symbol.type == 'esriSFS'}}
        <div style="display: table; vertical-align: middle;
             width: 16px;height: 16px;
             background: {{colorConvert symbol/}};">
        </div>
        <span class="renderTitle">{{:~label}}</span>
      {{else}}
        {{if symbol.type == 'simplelinesymbol' ||
             symbol.type == 'esriSLS'}}
          <div style="margin: 8px 5px -15px 5px; width: 20px;
              height: {{:symbol.width}}px; 
              background: {{colorConvert symbol/}};">
          </div>
          <span class="renderTitle">{{:~label}}</span>
        {{else}}
          {{if symbol.type == 'simplemarkersymbol' ||
               symbol.type == 'esriSMS'}}
            {{if symbol.style == 'STYLE_CIRCLE'}}
              <svg id="circle" height="20"
               xmlns="http://www.w3.org/2000/svg">
              <circle id="greencircle" cx="10" cy="10" r="16"
                 fill={{colorConvert symbol.color/}}/>
              </svg>
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
            {{if symbol.style == 'STYLE_SQUARE'}}
              <div style="display: table-cell; 
                 vertical-align: middle; 
                 width: 16px; height: 16px; 
                 background: {{colorConvert symbol/}}; 
                 border: {{borderOutline symbol.outline/}};">
              </div>
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
            {{if symbol.style == 'STYLE_CROSS'}}
              <div style="display: table-cell; 
                vertical-align: middle; 
                width: 16px; height: 16px; 
                color: {{colorConvert symbol/}};"></div>
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
            {{if symbol.style == 'STYLE_X'}}
              <div style="display: table-cell; 
                verticalalign: middle; width: 16px;
                height: 16px;color: {{colorConvert symbol/}};">
              </div>
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
            {{if symbol.style == 'STYLE_DIAMOND'}}
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
            {{if symbol.style == 'STYLE_PATH'}}
              <span class="renderTitle">{{:~label}}</span>
            {{/if}}
          {{/if}}
        {{/if}}
      {{/if}}
    {{/if}}
  {{/if}}
</div>
To make the rendering working, I has to introduce a couple of template functions that will create the desired rendering for creating a symbol. An example of such a function is ‘colorConvert’. This function takes as input a color object and create as output the html code for a color symbol in the format of rgba(255,255,255,0) or rgb(255,255,255). Below is the JavaScript for this function.

$.views.tags({
….
  colorConvert: function (symbol) {
    var colorObject = symbol.color;
    if (colorObject === null) {
      colorObject = symbol.outline.color;
    }
    if ($.type(colorObject) === 'object') {
      return buildColor(colorObject);
    }
    else {
      if ($.type(colorObject) === 'array') {
        return buildColorArray(colorObject);
      }
    }
  },
  ….
});

A supporting function is also used for creating the string representation of the color. As you can see, JSRender give you a lot of possibilities for doing some extra processing during the rendering of the templates. Because symbols can have different formats, some checks are done to determine the nature of the symbol passed.

function buildColor(col) {
  if (col === null) {
    col = [0, 0, 0, 0];
  }
  var prefix =
    ($.support.opacity) ? "rgba" : "rgb",    
      color = (!$.support.opacity) ? col.r + "," + col.g + "," +
      col.b : col.r + "," +    col.g + "," + col.b + "," + 
      col.a;
  return prefix + "(" + color + ")";
}

A more complex type of template is the one that handles the base map layers. Because there is a variety of layer types, a number of ‘if then else’ blocks are required. I also has to make a difference between group layer items and detail layer items that will contain legends if available. A template for the base map layers could look like this:

<li style="list-style: none; margin-left: -10px; padding: 0px;">
{{if !group}}
  <br />
  <div style="margin-left: 0px; align-content: flex-start;">
  <input type="checkbox" class="oncheck" style="float: left"
   id="{{:key}}" checked="{{checked selected/}}" />
  <h3 class="legendTitle" id="G{{:key}}" 
   style="margin: 0 0 0 0px; padding: 0;">{{:title}}</h3>
  </div>
{{else}}
  {{if (group && sequence == 0)}}
    <br />
    <div style="margin: 0px; padding: 0px; 
      align-content: flex-start;">
    <input type="checkbox" class="oncheck" style="float: left;"
      id="GG{{:key}}" checked="{{checked selected/}}" />
    <h3 class="agsLegendOpen legendTitle" id="G{{:key}}"
      style="margin: 0 0 0 0px; padding: 0;">{{:groupName}}</h3>
    </div>
{{/if}}
<div style="margin: 0 0 0 20px; padding: 0px; 
 align-content: flex-start" class="{{groupClass key/}}">
{{if serviceType == 2 || serviceType == 5}}
  <input type="checkbox" class="oncheck" style="float: left;"
    id="{{:key}}" checked="{{checked selected/}}" />
  <h3 class="legendTitle" id="GT{{:key}}" style="">{{:title}}
  </h3>
{{/if}}
{{if renderType == 'simple'}}
  {{for renderer tmpl='simpleRenderTmpl' ~label=title/}}
{{else}}
  {{if renderType == 'esri.renderer.UniqueValueRenderer' ||
    renderType == 'uniqueValue'}}
    <ul>
    {{for renderer.uniqueValueInfos 
          tmpl='uniqueValueRenderTmpl'/}}
    </ul>
  {{else}}
  {{/if}}
{{/if}}
</div>
{{/if}}
</li>

As you can see, the template represents one item of the legend model.  The main tasks for this template is to call the different renderer templates based on the type of renderer associated with the layer in the legend model. By creating a hierarchical template structure you can simplify the logic of the templates. JSRender fits nice into the MVVM pattern as there is a clear separation of data and presentation. The only extra code required for the presentation of the legend is the use of template functions.

The legend module has more than simply displaying legends for the different layers. In the ArcGIS desktop application the legend representation is also used to perform action as hide or display layers, add or remove layers, zoom to layers and other custom actions.

Interacting with the legend can easily done through adding event handling to the different nodes of the legend. To do this I used the power of JQuery based on class selections. Below You can see how different actions has been implemented by connecting functions to different events :

·         The setting of the visibility for a layer is done through adding the event handling to the change event of the check box of the layer legend.

·         Clicking on the title is used to expand or collapse the different renderer symbols

·         The right click on the title is used to display a pop up menu with additional actions possible for the layer concerned.  Something similar exists on the ArcGIS desktop application.

$('input.oncheck').on("change", function (event) {
  handleVisibility($(this).context.id,
     $(this).context.checked);
});
$('.legendTitle').on("click", function (event) {
  $target = $(event.target);
  $("." + $target.context.id).toggle();
  $target.toggleClass("agsLegendOpen")
    .toggleClass("agsLegendClosed");
});
$('.legendTitle').on("mouseout", function (event) {
  handleRightClick($(this).context.id);
});
// Collapse the legend to the minimum and add a popup menu
$('.legendTitle').each(function () {
  if ($(this).hasClass("agsLegendOpen")) {
    $("." + this.id).toggle();
    $(this).toggleClass("agsLegendOpen")
      .toggleClass("agsLegendClosed");
    popupMenu.bindDomNode($(this).context);
  }
});

The nice thing you can see here is that building of a legend component can be done without any use of the ArcGIS API. All the renderer is done through the use of the model with the template as blue print for the layout. Interacting can be done with standard JavaScript event handling. The heavy lifting processing of setting visibility or hiding can be done in separate functions. The function ‘handleVisibility’ takes as input the key value of the legend model and the value of the check box. Using this information the corresponding layer can be retrieved and using the GIS classes the appropriate action can be done.

    4.2    Custom actions

As an example of adding extra functionality to a template is the adding of a pop up menu to the legend. For each legend line I will add the possibility to have a pop up menu displayed when the user is doing right click on the legend.

First, I create a pop up menu using the menu UI from DOJO. This looks like this :

// Create popup menu
popupMenu = new dijit.Menu({
  targetNodeIds: [configuration.getConfig().LegendRegionDiv]
});
popupMenu.addChild(new dijit.MenuItem({
  label: "Attribute List",
  onClick: function (evt) {
    attributeList(evt);
  }
}));
popupMenu.addChild(new dijit.MenuItem({
  label: "Zoom Layer",
  onClick: function (evt) {
    zoomLayer(evt);
  }
}));
popupMenu.addChild(new dijit.MenuItem({
  label: "Zoom to selected",
  onClick: function (evt) {
    zoomSelected(evt);
  }
}));
popupMenu.addChild(new dijit.MenuItem({
  label: "Select all",
  onClick: function (evt) {
    selectAll(evt);
  }
}));
popupMenu.startup();

Next I have to connect the display of the pop up menu to the different legend items of the legend component. This can be done during the  creation of the template as outlined earlier.

$('.legendTitle').each(function () {
  if ($(this).hasClass("agsLegendOpen")) {
    $("." + this.id).toggle();
    $(this).toggleClass("agsLegendOpen")
    .toggleClass("agsLegendClosed");
    popupMenu.bindDomNode($(this).context);
  }
});

Next we connect a right event handling to the legend component. The best thing is doing it through the use of a class used in the template.

$('.legendTitle').on("mouseout", function (event) {
  handleRightClick($(this).context.id);
});

The actual processing is done on a separate function. This function simply set the layer selected so that when a click on the menu of the pop up menu the related action can be done on the selected layer.

function handleRightClick(key) {
// Handling of the different menu options
  var legendLayers = gisOperation.getLayersLegendInfos();
  layerSelected = $(legendLayers).filter(function () {
    return this.key === key;
  }).first();
}

The pop up menu action looks like the functions below, the implementation is left over. The functions are called on the click event of the pop up menu described earlier.

function zoomLayer(evt) {/// <summary>/// /// </summary>/// <param name="evt"></param>

... use layerSelected to do some action, the event passed is not relevant here.

}

function zoomSelected(evt) {/// <summary>/// /// </summary>/// <param name="evt"></param>

….

}

function selectAll(evt) {/// <summary>/// /// </summary>/// <param name="evt"></param>

….

}


5      Conclusion


The same technique can be used for other components. As an example is the layer selection tab for adding a functionality similar as what can be found on the selection tab of the ArcGIS desktop application.  With the use of template files it is also easier to have a different look and feel of components with the same JavaScript code in the module. Most important of all is that JSRender fits into the pattern of MVVM with a clear distinction of the presentation and the model.