Select to view content in your preferred language

Using selectedFeature when browsing cluster's features from popup

1452
12
Jump to solution
06-13-2023 08:33 AM
OlivierLefevre
Occasional Contributor

Hi,

I've been trying to mix two functionnalities. One is about displaying attached images in popup. And the other is about clustering.

Most of the code below behaves as expected except for browsing cluster's features.

 

 

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

        <title>Parking clusters</title>

        <link rel="stylesheet" href="https://js.arcgis.com/4.26/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.26/"></script>

        <script>
            require([
                "esri/config",
                "esri/portal/Portal",
                "esri/WebMap",
                "esri/views/MapView",
                "esri/widgets/LayerList",
                "esri/widgets/Expand",
                "esri/layers/GroupLayer",
                "esri/smartMapping/labels/clusters",
                "esri/smartMapping/popup/clusters",
                "esri/core/reactiveUtils",
                "esri/symbols/support/symbolUtils",
                "esri/geometry/geometryEngine",
                "esri/Graphic",
                "esri/popup/FieldInfo",
                "esri/popup/ExpressionInfo"
            // Fonction de callback. Attention a passer les constructeurs et les fonctions en arguments dans le même ordre que celui des modules
            ], function(esriConfig, Portal, WebMap, MapView, LayerList, Expand, GroupLayer, clusterLabelCreator, clusterPopupCreator, reactiveUtils, symbolUtils, geometryEngine, Graphic, FieldInfo, ExpressionInfo) {

                let layerAdmHorsCara;
                let basePopupTemplate = [];
                let webmapOperationalLayers;
                let layerServParkingVelo;
                let layerViewServParkingVelo;

                esriConfig.portalUrl = "https://carto.zzzzzzzzzzz.fr/argis";

                const view = new MapView({
                    container: "viewDiv"
                });

                const myPortal = new Portal();

                const webmap = new WebMap({
                    portalItem: {
                        portal: myPortal,
                        id: "xxxyyyxxxyyyxxxyyyxxxyyyxxxyyyxx"
                    }
                });

                const groupLayerVtt = new GroupLayer({
                    title: "VTT",
                    visible: false
                });
                const groupLayerVel = new GroupLayer({
                    title: "Vélo",
                    visible: true
                });
                const groupLayerCor = new GroupLayer({
                    title: "Course d'orientation",
                    visible: false
                });
                const groupLayerEqu = new GroupLayer({
                    title: "Equestre",
                    visible: false
                });
                const groupLayerPed = new GroupLayer({
                    title: "Pédestre",
                    visible: false
                });
                const groupLayerServ = new GroupLayer({
                    title: "Services",
                    visible: false
                });

                webmap.load()
                .then(function() {
                    return webmap.basemap.load();
                })
                .then(function() {
                    const layers = webmap.layers;
                    const promises = layers.map(function(layer) {
                        return layer.load();
                    });
                    return Promise.all(promises.toArray());
                })
                .then(function(layers) {
                    layers.forEach(function(element) {
                        element.title = element.title.slice(4);
                    });
                    return layers;
                })
                .then(function(layers) {
                    webmapOperationalLayers = layers;                    
                    layers.map(function(layer) {
                        if (layer.popupTemplate) {
                            basePopupTemplate[layer.id] = layer.popupTemplate;
                        }
                    });
                    const layerPedCircuit = layers[0];
                    const layerPedEquipement = layers[1];
                    const layerPedBorne = layers[2];
                    const layerEquParcours = layers[3];
                    const layerEquRevetement = layers[4];
                    const layerEquEquipement = layers[5];
                    const layerEquBorne = layers[6];
                    const layerEquAccesParcours = layers[7];
                    const layerCorEquipement = layers[8];
                    const layerCorBorne = layers[9];
                    const layerVttCircuit = layers[10];
                    const layerVttRevetement = layers[11];
                    const layerVttEquipement = layers[12];
                    const layerVttBorne = layers[13];
                    const layerVelCircuit = layers[14];
                    const layerVelEquipement = layers[15];
                    const layerVelBorne = layers[16];
                    layerAdmHorsCara = layers[17];
                    const layerServPiqueNique = layers[18];
                    const layerServEquipVelo = layers[19];
                    const layerServEauPotable = layers[20];
                    const layerServSanitaire = layers[21];
                    layerServParkingVelo = layers[22];
                    layerAdmHorsCara.listMode = "hide";
                    groupLayerVtt.addMany([layerVttCircuit, layerVttRevetement, layerVttEquipement, layerVttBorne]);
                    groupLayerVel.addMany([layerVelCircuit, layerVelEquipement, layerVelBorne]);
                    groupLayerCor.addMany([layerCorEquipement, layerCorBorne]);
                    groupLayerEqu.addMany([layerEquParcours, layerEquRevetement, layerEquEquipement, layerEquBorne, layerEquAccesParcours]);
                    groupLayerPed.addMany([layerPedCircuit, layerPedEquipement, layerPedBorne]);
                    groupLayerServ.addMany([layerServParkingVelo, layerServPiqueNique, layerServEquipVelo, layerServEauPotable, layerServSanitaire]);
                })
                .then(function(){
                    view.map = webmap;
                });

                view.when(function(){
                    webmap.removeAll();
                    webmap.addMany([layerAdmHorsCara, groupLayerPed, groupLayerEqu, groupLayerCor, groupLayerVtt, groupLayerVel, groupLayerServ]);
                })
                .then(function(){
                    const layerList = new LayerList({
                        container: document.createElement("llDiv"),
                        view: view,
                        selectionEnabled: false,
                        listItemCreatedFunction: function(event) {
                            const item = event.item;
                            if (item.layer.type != "group") { // don't show legend twice
                                item.panel = {
                                    content: "legend",
                                    open: false,
                                };
                            }                           
                        }
                    });
                    
                    // Builds popup template for layers with attachments to be displayed in popup
                    function buildLayerPopupTemplate(f_objectId, layer) {
                        layer.queryAttachments({
                                attachmentTypes: ["image/jpeg", "image/png"],
                                objectIds: f_objectId
                        })
                        .then(function(attachmentsByFeatureId) {
                            if (!attachmentsByFeatureId) {
                                return;
                            }
                            
                            if (["Parking_velo", "Pique-nique"].includes(layer.title)) {
                                let template = {title:basePopupTemplate[layer.id].title};
                            
                                let content = [{
                                    type: "fields",
                                    fieldInfos: basePopupTemplate[layer.id].fieldInfos
                                }];
                                
                                // If there are attached images
                                if (Object.keys(attachmentsByFeatureId).length > 0){
                                    const attachments = Object.values(attachmentsByFeatureId)[0];
                                    let mediaInfos =  [];
                                    if (attachments) {
                                        attachments.forEach(function (attachment) {
                                            const image_url = attachment.url;
                                            const item = {
                                                type: "image",
                                                value: {
                                                    sourceURL:image_url,
                                                    linkURL: image_url
                                                }
                                            }
                                            mediaInfos.push(item);
                                        });
                                        
                                        let typeMedia = {
                                            type: "media",
                                        }
                                        typeMedia.mediaInfos = mediaInfos;
                                        content.push(typeMedia);
                                    }
                                };
                                
                                template.content = content;
                                layer.popupTemplate = template;
                            }
                        })
                        .catch(function(error) {
                            console.log(error)
                        });
                    }

                    view.on("click", function(event) {                 
                        const opts = {
                            include: webmapOperationalLayers.filter(layer => {
                                return layer.type === "feature"
                            })
                        }
                        view.hitTest(event, opts)
                        .then(function (response) {
                            if (response.results.length) {
                                let graphic = response.results[0].graphic;
                                if (!graphic.attributes.objectid) {
                                    return;
                                }
                                buildLayerPopupTemplate(graphic.attributes.objectid, graphic.layer);
                            }
                        })
                        .catch(function(error) {
                            console.log(error)
                        });
                    });

                    layerServParkingVelo.when()
                    .then(generateClusterConfig)
                    .then(async (featureReduction) => {
                        // sets generated cluster configuration on the layer
                        layerServParkingVelo.featureReduction = featureReduction;
                        // the layer view is needed for querying clusters
                        layerViewServParkingVelo = await view.whenLayerView(layerServParkingVelo);
                    })
                    .catch((error) => {
                        console.error(error);
                    });
                
                    async function generateClusterConfig(layer) {
                        // generates default popupTemplate
                        const popupTemplate = await clusterPopupCreator
                        .getTemplates({
                            layer
                        })
                        .then(
                        (popupTemplateResponse) =>
                            popupTemplateResponse.primaryTemplate.value
                        );
                
                        // Add actions for exploring the features of each cluster
                        popupTemplate.actions = [
                          {
                            title: "Convex hull",
                            id: "convex-hull",
                            className: "esri-icon-polygon"
                          },
                          {
                            title: "Show features",
                            id: "show-features",
                            className: "esri-icon-maps"
                          }
                        ];

                        popupTemplate.title = "Groupe de {cluster_count} parkings vélo";
                        
                        const clusterSumExpressionInfo = new ExpressionInfo({
                            name: "cluster-sum",
                            title: "somme des places",
                            expression: `
                                Expects($aggregatedFeatures, 'nombre_place')
                                Text(Sum($aggregatedFeatures, 'nombre_place'), '#,###')
                            `
                            //returnType: "string"
                        })
                        popupTemplate.expressionInfos.push(clusterSumExpressionInfo)

                        popupTemplate.content = [{
                                type: "text",
                                text: "Cela représente {expression/cluster-sum} places au total."                    
                            }, {
                                type: "text",
                                text: "Un zoom avant affichera plus en détail la localisation des parkings."
                            }
                        ]

                        // generates default labelingInfo
                        const { labelingInfo, clusterMinSize } = await clusterLabelCreator
                        .getLabelSchemes({
                            layer,
                            view
                        })
                        .then((labelSchemes) => labelSchemes.primaryScheme);

                        labelingInfo[0].symbol.color = "rgba(0, 112, 255, 0.8)";
                        labelingInfo[0].symbol.haloSize = 0;

                        const clusterSymbol = {
                            type: "simple-marker",
                            style: "circle",
                            color: "rgba(255, 255, 255, 0.8)",
                            outline: {
                                color: "rgba(0, 112, 255, 0.8)",
                                width: 2
                            }
                        }

                        const clusterMaxSize = 40;
                        const clusterRadius = 80;

                        return {
                            type: "cluster",
                            popupTemplate,
                            labelingInfo,
                            clusterMinSize,
                            clusterMaxSize: clusterMaxSize,
                            clusterRadius: clusterRadius,
                            maxScale: 5000,
                            symbol: clusterSymbol
                        };
                    }

                    view.popup.on("trigger-action", (event) => {
                        clearViewGraphics();
                        const id = event.action.id;
                        if (id === "convex-hull") {
                          displayConvexHull(view.popup.selectedFeature);
                        }
                        if (id === "show-features") {
                          displayFeatures(view.popup.selectedFeature);
                        }
                    });
              
                    reactiveUtils.watch(
                        () => [view.scale, view.popup.selectedFeature, view.popup.visible],
                        ([scale, selectedFeature, visible]) => {
                            clearViewGraphics()
                            if (selectedFeature) {
                                if (!selectedFeature.isAggregate) {
                                    buildLayerPopupTemplate(selectedFeature.attributes.objectid, selectedFeature.layer);
                                }
                            }
                        }
                    );
              
                    let convexHullGraphic = null;
                    let clusterChildGraphics = [];
              
                    function clearViewGraphics() {
                        view.graphics.remove(convexHullGraphic);
                        view.graphics.removeMany(clusterChildGraphics);
                    }

                    // displays all features from a given cluster in the view
                    async function displayFeatures(graphic) {
                        processParams(graphic, layerViewServParkingVelo);

                        const query = layerViewServParkingVelo.createQuery();
                        query.aggregateIds = [graphic.getObjectId()];
                        const { features } = await layerViewServParkingVelo.queryFeatures(query);

                        features.forEach(async (feature) => {
                            const symbol = await symbolUtils.getDisplayedSymbol(feature);
                            feature.symbol = symbol;
                            view.graphics.add(feature);
                        });
                        clusterChildGraphics = features;
                    }

                    async function displayConvexHull(graphic) {
                        processParams(graphic, layerViewServParkingVelo);

                        const query = layerViewServParkingVelo.createQuery();
                        query.aggregateIds = [graphic.getObjectId()];
                        const { features } = await layerViewServParkingVelo.queryFeatures(query);
                        const geometries = features.map((feature) => feature.geometry);
                        const [convexHull] = geometryEngine.convexHull(geometries, true);

                        convexHullGraphic = new Graphic({
                            geometry: convexHull,
                            symbol: {
                                type: "simple-fill",
                                outline: {
                                    width: 1.5,
                                    color: [75, 75, 75, 1]
                                },
                                style: "none",
                                color: [0, 0, 0, 0.1]
                            }
                        });
                        view.graphics.add(convexHullGraphic);
                    }

                    function processParams(graphic, layerView) {
                        if (!graphic || !layerView) {
                            throw new Error("Graphic or layerView not provided.");
                        }
              
                        if (!graphic.isAggregate) {
                            throw new Error("Graphic must represent a cluster.");
                        }
                    }

                    const layerListExpand = new Expand({
                        expandIconClass: "esri-icon-layer-list",
                        expandTooltip: "Légende",
                        collapseTooltip: "Replier la légende",
                        view: view,
                        content: layerList.domNode,
                        group: "top-right",
                        expanded: true
                    });

                    view.ui.add([layerListExpand], "bottom-right");
                });
            });
        </script>

    </head>

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

 

 

When browsing clicked features (forward and backward) everything is fine.

browsing clicked featuresbrowsing clicked features

Browsing forward cluster's features is fine too. But an error occurs when browsing those features backward.

browsing cluster's featuresbrowsing cluster's features

At line 355 selectedFeature.layer is null.

I don't understand why and how to fix this issue.

Could you please help me ?

 

0 Kudos
12 Replies
JoelBennett
MVP Regular Contributor

This can be reproduced in the Intro to Clustering sample:

1. Go to the "Intro to Clustering" sample sandbox.

2. Add the following code between lines 169 and 170 (i.e. somewhere between the instantiation of the View and the instantiation of the Legend):

 

        require(["esri/core/reactiveUtils"], function(reactiveUtils) {
          reactiveUtils.watch(
            () => view.popup?.selectedFeature,
            (selectedFeature) => {
              if ((selectedFeature) && (!selectedFeature.layer)) alert("no layer");
            }
          );
        });

 

 

3. Click "Refresh" above the top right of the map.

4. After the map loads, click a cluster.

5. On the popup, click "Browse features".

6. Click the "Next feature" icon.

7. Click the "Next feature" icon again.

8. Click the "Previous feature" icon - the alert displays, showing that the feature's "layer" reference has been lost.

0 Kudos
LaurenBoyd
Esri Contributor

Thank you for providing this! I was expecting the popup to not show the previous feature as depicted in the images above, but that doesn't seem to be the case: https://codepen.io/laurenb14/pen/JjebjQN

The main issue is that the selectedFeature's layer reference is lost. We will take a look at this and get back to this post!

Lauren
OlivierLefevre
Occasional Contributor

Thank you @LaurenBoyd for your interest in this issue.

I am sorry not to have been able to get back to you sooner as I was off last week.

Thank you @JoelBennett for the extra information you provided LaurenBoyd with.

0 Kudos