odoe

How to make a ToC

Blog Post created by odoe on Feb 11, 2015

table_contents.png

Ok, let's talk the ToC.

Adding a Table of Contents/Legend widget to ArcGIS web maps is one of those things that unfortunately is a necessary evil. I really think you should strive to design your map and your application in such a way that a ToC isn't needed. But sometimes, in those worst times, you have to do what you have to do.

 

I'm guilty of it. I'm aware that users may want it and developers have to deal with it. I even added an example in a chapter to my ArcGIS WebDev book.

 

Questions in the forum always seem to pop up about it. This ToC Widget from nilu appears to be pretty popular. It's a neat widget, tons of features, generally good stuff.

 

Learn to walk the ToC

But you as a developer should have an idea of how a ToC widget is built. I explain the concepts in the above linked sample chapter from my book. But here are the basic steps.

 

  • Make a request to the legend endpoint of the MapService.
  • Parse the legend response into a sweet looking list of the layers
  • Wire up click events to turn layers on/off
  • Bonus - Wire up click events to sub layers on/off
  • Do the happy dance!

happy_dance.gif

 

That's all there is to it. Simple right?

There is probably a dozen different ways you could accomplish this. You could use the dijit/Menu, you could a dijit/Tree, you could just use regular DOM elements, take your pick.

 

Here is some code that does just this.

define([
  'dojo/_base/declare',
  'dojo/Deferred',
  'dojo/on',
  'dojo/topic',
  'dojo/query',
  'dojo/dom',
  'dojo/dom-attr',
  'put-selector',
  'dojo/dom-class',
  'dojo/Evented',
  'esri/lang',
  './layerservice',
  'dojox/lang/functional/curry',
  'dijit/_WidgetBase',
  'dijit/_TemplatedMixin',
  'dojo/text!./templates/layertoc.tpl.html'
], function(
  declare, Deferred, on, topic,
  query, dom, domAttr, put, domClass,
  Evented, esriLang,
  getLayers, curry,
  _WidgetBase, _TemplatedMixin,
  template
) {
  var labelName = curry(function(a, b) {
    if (b.label && b.label.length > 1) {
      return b.label;
    } else if (a.layerName && a.layerName.length) {
      return a.layerName;
    } else {
      return 'Layer Item';
    }
  });
  var sub = esriLang.substitute;
  var layertitle = '<span class="pull-right">${title}</span>';
  return declare([_WidgetBase, _TemplatedMixin, Evented], {
    templateString: template,
    postCreate: function() {
      var node = dom.byId('map_root');
      put(node, this.domNode);
      var map = this.get('map');
      var layerIds = this.get('layerIds');
      // map over the layer ids and pull
      // the layers designated as part of legend
      this.tocLayers = map.layerIds.map(function(x) {
        if (layerIds.indexOf(x) > -1) {
          return map.getLayer(x);
        } else {
          return false;
        }
      }).filter(function(a) { return a; });
      // map over those layers and create some DOM element containers
      this.tocLayers.map(function(x) {
        var visible = x.visible ? 'glyphicon-ok' : 'glyphicon-ban-circle';
        var panel = put(this.tocInfo, 'div.panel.panel-default');
        var pheading = put(panel, 'div.panel-heading');
        var ptitle = put(pheading, 'h4.panel-title');
        put(
          ptitle,
          'span.glyphicon.' + visible +
            '.layer-item[data-layer-id=' + x.id + ']'
        );
        var node =
          put(ptitle,
              'span',
              { innerHTML: sub(x, layertitle) }
             );
        this._getDetails(x, node, panel);
      }.bind(this));
      // this will handle turning the whole service on/off
      var layerHandle = on(this.tocInfo, '.layer-item:click', function(e) {
        e.preventDefault();
        e.stopPropagation();
        domClass.toggle(e.target, 'glyphicon-ok glyphicon-ban-circle');
        var id = domAttr.get(e.target, 'data-layer-id');
        var lyr = map.getLayer(id);
        lyr.setVisibility(!lyr.visible);
      });
      // this will turn individual layers on/off
      var itemHandle = on(this.tocInfo, '.sublayer-item:click', function(e) {
        e.preventDefault();
        e.stopPropagation();
        domClass.toggle(e.target, 'glyphicon-ok glyphicon-ban-circle');
        var id = domAttr.get(e.target, 'data-layer-id');
        var subid = parseInt(domAttr.get(e.target, 'data-sublayer-id'));
        var lyr = map.getLayer(id);
        var lyrs = lyr.visibleLayers;
        var visibleLayers = [];
        // this bit will adjust the visible layers based on what was clicked
        if (lyrs.indexOf(subid) > -1) {
          visibleLayers = lyrs.filter(function(x) {
            return x !== subid;
          });
        } else {
          visibleLayers = lyrs.concat([subid]);
        }
        lyr.setVisibleLayers(visibleLayers);
      });
      this.own(layerHandle, itemHandle);
    },
    // do a quick check that a URL has been provided
    _getDetails: function(layer, node, panel) {
      if (!layer.url) { return; }
      var pbody = put(panel, 'div.panel-body');
      this._getLegend(layer, pbody);
    },
    // here is the workhorse
    _getLegend: function(layer, pbody) {
      var url = layer.url + '/legend';
      var id = layer.id;
      // this is a just a wrapper module I use for
      // esri/request. see source at https://github.com/odoe/esri-layertoc-sample
      getLayers(url).then(function(layers) {
        var tbl = put(pbody, 'table.table');
        // iterate over the layers and
        // add items to the table
        layers.map(function(a) {
          var lbl = labelName(a);
          var layerId;
          var hasLayerId = false;
          if (a.hasOwnProperty('layerId')) {
            hasLayerId = true;
            layerId = a.layerId;
          }
          // iterate over the legend and add items
          // to the table-row
          a.legend.map(function(b) {
            var tr = put(tbl, 'tr');
            if (hasLayerId) {
              hasLayerId = false;
              var lyrCheck = put(
                'span.glyphicon.glyphicon-ok' +
                '.sublayer-item[data-layer-id=' + id + ']' +
                '.[data-sublayer-id=' + layerId + ']'
              );
              put(tr, 'td', lyrCheck);
            } else {
              put(tr, 'td');
            }
            var td1 = put(tr, 'td.layer-image');
            put(tr, 'td', {
              innerHTML: lbl(b)
            });
            // I just add the base64 image, but you could also
            // use the URL to image provided in Legend endpoint
            put(td1, 'img', {
              src: 'data:image/png;base64,' + b.imageData
            });
          });
        });
      }, function(err) { console.debug('error in request', err); });
    }
  });
});

 

Woah, that's a lot of code. Hey you wanted to learn about a ToC widget, that's going to take a bit of code. I added some comments in there to help you out. You might be able to break out some of the functionality into smaller modules, but I'll leave that up to you.

 

I'm using the put-selector in this sample to create DOM elements (it's included in the ArcGIS JS API) just because I've found it makes more sense for me when composing DOM creation. I'm also using Bootstrap to make it look nice, which is where some of the css class names are defined.

 

When it's all said and done, this sample will look something like this.

toc-sample.jpg

Pretty snazzy

What this does is turns off individual services and the individual layers in the visibleLayers. This sample is only set up for an ArcGISDynamicMapServiceLayer, but you could tweak it for FeatureLayers and if you're bold add support for custom renderers. I try to avoid this when I can as I always seem to muck something up, but it can be done. Good luck. I remember this being a lot harder a long time ago when I did this in Flex, as I don't think the REST API had a Legend endpoint back then, so this is actually easier than it would have been.

 

I just wanted to give you a decent overview of how you can go about accessing the legend endpoint of a map service to pull all the data you need to make your own ToC widget. Maybe this will help you troubleshoot issues you have using other ToC widgets. It's a good exercise in learning to display data in the DOM and sometimes, you may just need a ToC... maybe.

 

The full source code for this sample can be found here.

 

Be sure to check out my blog for more geodev tips and tricks!

Outcomes