Display LayerList with Legend using a MapImageLayer's sublayers

5188
8
Jump to solution
10-18-2018 12:05 PM
deleted-user-9_yPCHk-_Xlk
New Contributor II

I am trying to modify one of the samples that combines the Legend and LayerList widgets to also accommodate sublayers. It seems that if leave the default layerlist declaration (as the sample provides), it gives me all of the symbology for the sublayers at the top, and nothing inside of each sublayer. 

Originally, I had thought that I would just assign the "legend" to the panel of the children the parent layer, but that didn't seem to work. Any thoughts or references where this has been done before? 

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">

    <title>Add a Legend to LayerList - 4.9</title>

    <link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/themes/light/main.css">

    <style>
        html,
        body,
        #viewDiv {
            padding: 0;
            margin: 0;
            height: 100%;
            width: 100%;
            overflow: hidden;
        }
    </style>

    <script src="https://js.arcgis.com/4.9/"></script>

    <script>
        require([
            "esri/Map",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/widgets/LayerList"
        ], function (
            Map, MapView, MapImageLayer, LayerList
        ) {

            const citiesStatesHighways = new MapImageLayer({
                url: "https://sampleserver6.arcgis.com/arcgis/rest/services/USA/MapServer",
                sublayers: [{
                        id: 2,
                        title: "States"
                    },
                    {
                        id: 1,
                        title: "Highways"
                    },
                    {
                        id: 0,
                        title: "Cities"
                    }
                ]
            });

            const map = new Map({
                basemap: "dark-gray",
                layers: [citiesStatesHighways]
            });

            // Add the map to a MapView
            const view = new MapView({
                container: "viewDiv",
                map: map
            });

            // Add a legend instance to the panel of a
            // ListItem in a LayerList instance
            const layerList = new LayerList({
                view: view,
                listItemCreatedFunction: function (event) {
                    const item = event.item;
                    if (item.layer.type != "group") { // don't show legend twice
                        item.panel = {
                            content: "legend",
                            open: true
                        };
                        console.log(item.panel.content);
                    }
                }
            });


            // Assigning "legend" to children of parent layer - not working
            // const layerList = new LayerList({
            //     view: view,
            //     listItemCreatedFunction: function (event) {
            //         const item = event.item;
            //         console.log(item);

            //         // target elements without children (the sublayers)
            //         if (item.children.items.length !== 0) {
            //             let childrenArr = item.children.items
            //             for (let i = 0; i < childrenArr.length; i++) {
            //                 childrenArr.panel = {
            //                     content: "legend",
            //                     open: true
            //                 }
            //             }
            //         }
            //     }
            // });

            view.ui.add(layerList, "top-right");


        });
    </script>

</head>

<body>
    <div id="viewDiv"></div>
</body>

</html>
1 Solution

Accepted Solutions
DheerajGambhir
New Contributor II

Hi All,
I did not find any solution for this anywhere and few solutions were leading to too many network calls, so developed a workaround. Sharing the code below might help others:

1. Querying the legend images url and store in an object

let that = this;
    query: {
        f: 'json'
    },
    responseType: "json"
  }).then(function (response) {
   
    response.data?.layers?.forEach((element: any) => {
      let layerId: string = element.layerId;
      if(element.legend.length>1){
        let legendArr: any = [];
        element.legend.forEach((item: any) => {
          legendArr.push({url: item.url, label: item.label})
        });
        that.legendImage[layerId] = legendArr;
      }
      else{
        that.legendImage[layerId] = element.legend[0].url
      }
    });
  })


2. Adding Image to Legend sublayer panel

 
const layerList = new LayerList({
    view: view,
    listItemCreatedFunction: (event) => {
      const items = event.item.children._items;
      if(items){
        for(let i=0; i<items.length; i++){
          let img = document.createElement("img");
          if(that.legendImage.hasOwnProperty(items[i].layer.id)){
            if(typeof(that.legendImage[items[i].layer.id])=='object'){
              let div = document.createElement("div");
              let count = 0;
              that.legendImage[items[i].layer.id].forEach((el: any) => {
                div.innerHTML += "<img src='" + el.url + "'>  " + el.label;
                if(count<that.legendImage[items[i].layer.id].length -1)
                  div.innerHTML += "</br></br>";
                items[i].panel = {
                  content: div,
                  open: true
                };
                count++;
              });
            }
            else{
              img.src = that.legendImage[items[i].layer.id];
              items[i].panel = {
                content: img,
                open: true
              };
            }
           
          }
         
        }
      }
    }
  });

View solution in original post

8 Replies
deleted-user-9_yPCHk-_Xlk
New Contributor II

I'm not really sure if this was a good solution, but I was able to put something together. Does anybody have any suggestions? In addition, it seems like this 'listItemCreatedFunction' is happening a bunch of times (executing more times than the number of layers'. It doesn't seem to interfere with anything, but I'm wondering if it will slow down the map when expanding to multiple services with many sublayers.

See codepen for live example: Add a Legend to a Layerlist (Sublayers Edition) 

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">

    <title>Add a Legend to LayerList - 4.9</title>

    <link rel="stylesheet" href="https://js.arcgis.com/4.9/esri/themes/light/main.css">

    <style>
        html,
        body,
        #viewDiv {
            padding: 0;
            margin: 0;
            height: 100%;
            width: 100%;
            overflow: hidden;
        }
    </style>

    <script src="https://js.arcgis.com/4.9/"></script>

    <script>
        require([
            "esri/Map",
            "esri/request",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/widgets/LayerList",
            "esri/widgets/Legend"
        ], function (
            Map, esriRequest, MapView, MapImageLayer, LayerList, Legend
        ) {

            const citiesStatesHighways = new MapImageLayer({
                url: "https://sampleserver6.arcgisonline.com/arcgis/rest/services/USA/MapServer",
                sublayers: [{
                        id: 2,
                        title: "States"
                    },
                    {
                        id: 1,
                        title: "Highways"
                    },
                    {
                        id: 0,
                        title: "Cities"
                    }
                ]
            });

            const map = new Map({
                basemap: "dark-gray",
                layers: [citiesStatesHighways]
            });

            // Add the map to a MapView
            const view = new MapView({
                container: "viewDiv",
                map: map
            });

            // Assigning "legend" to children of parent layer
            const layerList = new LayerList({
                view: view,
                listItemCreatedFunction: function (event) {
                    const item = event.item;
                    console.log(item);

                    // target elements without children (the sublayers)
                    if (item.children.items.length !== 0) {

                        // make array of the sublayers
                        let childrenArr = item.children.items
                        console.log(childrenArr);
                        for (let i = 0; i < childrenArr.length; i++) {

                            // make a request to the server to retrieve the layer image url
                            esriRequest(childrenArr[i].layer.url.slice(0, -1) + "legend", {
                                query: {
                                    f: 'json'
                                },
                                responseType: "json"
                            }).then(function (response) {
                                let img = document.createElement("img");
                                // build unique url for the legend symbol
                                img.src = childrenArr[i].layer.url + "/images/" + response.data
                                    .layers[i].legend[0].url;

                                // assign image to the sublayers in layerlist
                                childrenArr[i].panel = {
                                    content: img,
                                    open: true
                                }
                            });
                        }
                    }
                }
            });

            view.ui.add(layerList, "top-right");

        });
    </script>

</head>

<body>
    <div id="viewDiv"></div>
</body>

</html>‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍
ae
by
Occasional Contributor II

I have the exact same requirement, and I actually think it's really strange that the arcgis js dev team haven't provided a widget or example that properly combines legend with layerlist, ALSO for sublayers. This is something that I think a lot of people would appreciate, as it greatly improves the usability of any map that uses dynamic services with sublayers. 

mattprice
New Contributor II

Hello,

I noticed that it is making several calls as well.  I just made one call to the legend.  Below is my solution.  Its a lot, but it looks and acts the way I want.  I would like to remove the opacity setting for non-polygon layers and add a slider, but that is for a latter time. Thanks for sharing your code it got me started.  

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>Add a Legend to LayerList - 4.11</title>
   <!-- <link rel="stylesheet" href="https://js.arcgis.com/4.11/esri/themes/light/main.css"> -->
 <link rel="stylesheet" href="https://js.arcgis.com/4.11/esri/css/main.css">
    <style>
        html,
        body,
        #viewDiv {
            padding: 0;
            margin: 0;
            height: 100%;
            width: 100%;
            overflow: hidden;
        }
  

  .esri-layer-list__child-toggle + .esri-layer-list__item-label:not([role="radio"]) > .esri-layer-list__item-toggle {
     display:none;
    }

.esri-icon-non-visible::before {
content: "\e610";
}
.esri-icon-visible::before {
content: "\e611";
}
 
/*
.esri-icon-right-triangle-arrow::before  {
content: "\e63c";
}
.esri-icon-down-arrow::before  {
content: "\e63b";
}
*/

 .esri-layer-list__list {
    list-style: none;
    margin: 0 0 0 0;
    padding: 0;
  }
  .esri-layer-list__item--has-children {
    padding-bottom: 0;
  }

.esri-layer-list-panel__content esri-layer-list-panel__content--html-element{
margin: 0 0 0 0;
}
 .esri-layer-list__item-container {
     display: flex;
     justify-content: flex-start;
     align-items: flex-start;
   padding: 1px 1px 1px 1px;
   }
.esri-layer-list__item--has-children > .esri-layer-list__item-container {
       padding-left: 1px;
       padding-right: 1px;
    }
.esri-layer-list__item {
    background-color: $background-color;
    border-bottom: 1px solid $border-color;
     position: relative;
     overflow: hidden;
     list-style: none;
     margin: 0 5px;
     padding: 0;
   }
.esri-layer-list-panel {
     margin: 3px 20px;
   }
 
   .esri-layer-list-panel__content--legend .esri-legend__service {
     padding:  0 10px 0  0;
   }
 .esri-layer-list__list {
       margin: 0 3px 3px 0;
     }
.esri-layer-list__item-actions {
    position: relative;
    background-color: $background-color--offset;
    color: $interactive-font-color;
    margin: -1px  2px 2px;
    height: auto;
  }
.esri-layer-list__item-actions-list {
    display: flex;
    flex-flow: column;
    justify-content: flex-start;
    align-items: flex-start;
    padding: 2px 0;
    list-style: none;
    border-top: 2px solid $background-color;
  }
 </style>
    <script src="https://js.arcgis.com/4.11/"></script>
    <script>
var layerList;
var CountyBaseMapURL = "https://gis.santacruzcounty.us/arcserver/rest/services/Cache/CountyBasemap/MapServer";
var mapImageLayerURL = "https://gis.santacruzcounty.us/arcserver/rest/services/giswebp/MapServer";
        require([
            "esri/Map",
            "esri/request",
     "esri/Basemap",
      "esri/layers/TileLayer",
            "esri/views/MapView",
            "esri/layers/MapImageLayer",
            "esri/widgets/LayerList",
            "esri/widgets/Legend",
     "dojo/_base/array"
        ], function (
            Map, esriRequest, Basemap, TileLayer, MapView, MapImageLayer, LayerList, Legend, arrayUtils
        ) {
            const layer1 = new MapImageLayer({
                url: mapImageLayerURL
            });
  var imageryTilelayer = new TileLayer({ url: CountyBaseMapURL});
   var customBasemap = new Basemap({
     baseLayers: [imageryTilelayer],
     title: "Custom Basemap",
     id: "myBasemap"
    });

            const map = new Map({
                basemap: customBasemap
            });
            // Add the map to a MapView
            const view = new MapView({
                container: "viewDiv",
                map: map
            });
    map.add(layer1);

var theURL = "https://gis.santacruzcounty.us/arcserver/rest/services/" + layer1.title.toLowerCase() + "/MapServer/legend";
esriRequest(theURL, {
                                query: {
                                    f: 'json'
                                },
                                responseType: "json"
                            }).then(function (response) {
   layerList = new LayerList({
                 view: view,
                  listItemCreatedFunction: function (event) {
                      const item = event.item;
    if (item.title === layer1.title) {
    item.title = "Legend";
    } 
      event.item.layer.opacity = 1;  //added opacity here, was set to undefined
     if (item.children.items.length === 0 && item.title != "Legend") {
  
     //let theArray = [];
     var aDiv = document.createElement("Div");
          
     var layerNumber = -1;     
      for (let i = 0; i < response.data.layers.length; i++) {
      if(response.data.layers.layerId === item.layer.id) { 
      layerNumber = i;
      }
       }
          if(layerNumber != -1){
       for (let j = 0; j < response.data.layers[layerNumber].legend.length; j++) {  
      
       var para = document.createElement("P");
       para.style.margin = "2px";
       para.style.verticalAlign = "middle";     
             
       var img = document.createElement("img");
       img.style.height = "20px";
       img.style.verticalAlign = "bottom";
       img.src = item.layer.url + "/images/" + response.data.layers[layerNumber].legend.url;
       
       theLabel = response.data.layers[layerNumber].legend.label;
       var t =  document.createTextNode(theLabel);
              
       para.appendChild(img);
       para.appendChild(t);                                         
       aDiv.appendChild(para);
        } 
         
         item.panel = {
      
      className: "esri-icon-drag-horizontal",
        content: aDiv,
                                              open: false,
        visible: true
                                       }
          } // if(layerNumber != -1){

      item.actionsSections = [
                  [ {
                    title: "Layer information",
                    className: "esri-icon-description",
                      id: "information"
                  }],
                  [{
                    title: "Increase opacity",
                    className: "esri-icon-up",
                    id: "increase-opacity"
                  }, {
                    title: "Decrease opacity",
                    className: "esri-icon-down",
                    id: "decrease-opacity"
                  }]
                ];
       }//if (item.chil
     else{ // added this so users can turn off group layers by clicking on the name
       item.layer.watch("visible", function (){
       item.layer.visible = true; 
      });     
     }
     
    } // listItem

                              }); // new layerlist

 view.ui.add(layerList, "top-right");

 layerList.on("trigger-action", function(event) {
 var visibleLayer = event.item.layer;
          // Capture the action id.
          var id = event.action.id;
          if (id === "full-extent") {
            // to the full extent of the visible layer
            view.goTo(visibleLayer.layer.fullExtent);
          } else if (id === "information") {
            // open the item details page of the service layer
  var themetaURL = "NONE";
   
 switch(visibleLayer.parent.title) {
 
case "Parcel Related":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Parcel%20Related.pdf";
      break;
  case "Transportation":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Transportation.PDF";
      break;
 case "Biotic and Water Resources":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Biotic%20and%20Water%20Resources.PDF";
      break;
 case "Hazards and Geophysical":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Hazards%20and%20Geophysical.PDF";
      break;
 case "Zoning":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Zoning.PDF";
      break;
 case "Land Use and General Plan":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Land%20Use%20and%20General%20Plan.PDF";
      break;
 case "Special Districts":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Special%20Districts.PDF";
      break;
 case "Jurisdictional, Elections, Census":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Jurisdictional,%20Elections,%20Census.pdf";
      break;
 case "School Districts and CSAs":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/School%20Districts%20and%20CSAs.pdf";
      break;
 case "Utilities":
     themetaURL = "https://gis.santacruzcounty.us/gisweb/help/Utilities.pdf";
      break;
  default:
     alert("No additonal information available");
  }
 if(themetaURL != "NONE"){
            window.open(themetaURL);
 } 
          } else if (id === "increase-opacity") {
            // increase the opacity of the GroupLayer by 0.25
            if (visibleLayer.opacity < 1) {
              visibleLayer.opacity += 0.2;
            }
          } else if (id === "decrease-opacity") {
            // if the decrease-opacity action is triggered, then
            // decrease the opacity of the GroupLayer by 0.25
            if (visibleLayer.opacity > 0) {
              visibleLayer.opacity -= 0.2;
            }
          }
        });
       
                
}); // then(function (response
         
        });
    </script>
</head>
<body>
    <div id="viewDiv"></div>
</body>
</html>
KevinMacLeod3
New Contributor III

matt price‌ awesome, great work! I did have to do one update.. toggling Group on/off did not work (for me).  With a dynamic map service with some Groups and subGroups loaded as a mapImageLayer.

I first noticed interestingly that Group layer items (parents with children or subchildren sublayers) were not showing up with checkboxes, at all. Just the triangle expand/minimize arrows. So there was no way to turn on layers in a group if the parent Group was off. 

So...I removed this chunk of CSS, below. Ok, so the Group checkboxes came back and appear as normally. But clicking the parent/group checkboxes doesn't do anything. 

https://codepen.io/kevinsagis/pen/RwajJRa 

.esri-layer-list__child-toggle + .esri-layer-list__item-label:not([role="radio"]) > .esri-layer-list__item-toggle {
display:none;
}

However, the fix was that I deleted this part of a line:

item.children.items.length === 0 && 

So instead of  if (item.children.items.length === 0 && item.title != "Legend") {   

it's now just  if (item.title != "Legend") {

Now it works. Hope this helped everyone.  

0 Kudos
MichailMarinakis1
Occasional Contributor II

Hi all,

In the latest version of the API, 4.12, the function "listItemCreatedFunction" is still loading multiple times and there is no solution to show legend for sublayers (other than the custom ones above, that they work). 

Are there any updates about that? 

Thanks in advance!

GiovanniPopulis
New Contributor II

This still seems to be an issue in 4.15, is there any intention to address this issue?

KevinMacLeod3
New Contributor III

Still present 4.16 - 4.17.  mapImage services with grouped sublayers layers are best for various scenarios.  Matt's approach above works with 4.17 with a small update as noted (and in my Pen example https://codepen.io/kevinsagis/full/RwajJRa  )

It would be great to see this integrated into the widget itself, with Legend properties such as "Legend: true" to simply enable legend for all layers and all sublayers. Expanded should be an option for all; or per layer by layer. And other options like in WebApp Builder, like hiding certain layers. And even allow per-sublayer exclusion of the legend as override.

DheerajGambhir
New Contributor II

Hi All,
I did not find any solution for this anywhere and few solutions were leading to too many network calls, so developed a workaround. Sharing the code below might help others:

1. Querying the legend images url and store in an object

let that = this;
    query: {
        f: 'json'
    },
    responseType: "json"
  }).then(function (response) {
   
    response.data?.layers?.forEach((element: any) => {
      let layerId: string = element.layerId;
      if(element.legend.length>1){
        let legendArr: any = [];
        element.legend.forEach((item: any) => {
          legendArr.push({url: item.url, label: item.label})
        });
        that.legendImage[layerId] = legendArr;
      }
      else{
        that.legendImage[layerId] = element.legend[0].url
      }
    });
  })


2. Adding Image to Legend sublayer panel

 
const layerList = new LayerList({
    view: view,
    listItemCreatedFunction: (event) => {
      const items = event.item.children._items;
      if(items){
        for(let i=0; i<items.length; i++){
          let img = document.createElement("img");
          if(that.legendImage.hasOwnProperty(items[i].layer.id)){
            if(typeof(that.legendImage[items[i].layer.id])=='object'){
              let div = document.createElement("div");
              let count = 0;
              that.legendImage[items[i].layer.id].forEach((el: any) => {
                div.innerHTML += "<img src='" + el.url + "'>  " + el.label;
                if(count<that.legendImage[items[i].layer.id].length -1)
                  div.innerHTML += "</br></br>";
                items[i].panel = {
                  content: div,
                  open: true
                };
                count++;
              });
            }
            else{
              img.src = that.legendImage[items[i].layer.id];
              items[i].panel = {
                content: img,
                open: true
              };
            }
           
          }
         
        }
      }
    }
  });