Select to view content in your preferred language

Using selectedFeature when browsing cluster's features from popup

1451
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
1 Solution

Accepted Solutions
JoelBennett
MVP Regular Contributor

This is a bug in the API, particularly the PopupViewModel, which I'll try to summarize here, and then provide recommendations.

Basically, the PopupViewModel stores a reference to all the features in the vicinity that you clicked on the map.  When clicking on a cluster, you can browse the cluster's features as shown in your screenshots.  When you click the next button, it takes that corresponding feature's Graphic reference and adds it to the MapView's graphics collection, thus enabling you to temporarily see that particular feature on the map (i.e. in addition to the cluster graphic).  If you click next again, it removes the current Graphic, and then adds the newly selected feature's Graphic.  It is this adding and removing from the MapView's graphics that causes the Graphics' layer property to get severed.

 

My recommendation for you:

There are various ways to work around this,, and all will fall under the category of a hack.  My recommendation is to apply the simplest one, and replace the reference to "layer" with the undocumented property "sourceLayer" like so:

 

buildLayerPopupTemplate(selectedFeature.attributes.objectid, selectedFeature.sourceLayer);

 

You can change this back if it ever gets fixed.

 

My recommendation for ESRI:

Since we have to wait three and a half weeks to download 4.27, I'm still looking at 4.26.  The problem occurs in the PopupViewModel module, particularly in the "_selectedFeatureChange" function.  When setting the "_selectedClusterFeature" property, this can be resolved by setting it to a clone of the graphic, rather than the graphic itself.  That is, replace:

 

this._selectedClusterFeature=b

 

with:

 

this._selectedClusterFeature=b.clone()

 

 

 

View solution in original post

0 Kudos
12 Replies
JoelBennett
MVP Regular Contributor

This is a bug in the API, particularly the PopupViewModel, which I'll try to summarize here, and then provide recommendations.

Basically, the PopupViewModel stores a reference to all the features in the vicinity that you clicked on the map.  When clicking on a cluster, you can browse the cluster's features as shown in your screenshots.  When you click the next button, it takes that corresponding feature's Graphic reference and adds it to the MapView's graphics collection, thus enabling you to temporarily see that particular feature on the map (i.e. in addition to the cluster graphic).  If you click next again, it removes the current Graphic, and then adds the newly selected feature's Graphic.  It is this adding and removing from the MapView's graphics that causes the Graphics' layer property to get severed.

 

My recommendation for you:

There are various ways to work around this,, and all will fall under the category of a hack.  My recommendation is to apply the simplest one, and replace the reference to "layer" with the undocumented property "sourceLayer" like so:

 

buildLayerPopupTemplate(selectedFeature.attributes.objectid, selectedFeature.sourceLayer);

 

You can change this back if it ever gets fixed.

 

My recommendation for ESRI:

Since we have to wait three and a half weeks to download 4.27, I'm still looking at 4.26.  The problem occurs in the PopupViewModel module, particularly in the "_selectedFeatureChange" function.  When setting the "_selectedClusterFeature" property, this can be resolved by setting it to a clone of the graphic, rather than the graphic itself.  That is, replace:

 

this._selectedClusterFeature=b

 

with:

 

this._selectedClusterFeature=b.clone()

 

 

 

0 Kudos
JoelBennett
MVP Regular Contributor

I have replied twice to this with a solution and my posts keep getting deleted almost immediately.  Did you see either of those before they disappeared?

0 Kudos
BlakeTerhune
MVP Regular Contributor

I've not seen anything. 😕

0 Kudos
JoelBennett
MVP Regular Contributor

The third attempt seems to have worked; good thing I made a backup of the second attempt...

0 Kudos
BlakeTerhune
MVP Regular Contributor

Haha, nope! I still don't see it. Maybe try contacting the community team.

0 Kudos
JoelBennett
MVP Regular Contributor

Sure enough, it disappeared...I took a screenshot last time; maybe just posting that will work.  Here goes:

 

image.png

0 Kudos
BriannaEttley
Esri Community Manager

Apologies on that, @JoelBennett!

Our automated spam filter isn't always as smart as it thinks it is and can occasionally make a mistake in what it quarantines for our team's review.

I've released your comment to this thread so that it's now public again.

Brianna Ettley
Community Manager, Engagement & Content
OlivierLefevre
Occasional Contributor

I just tried your hack @JoelBennett and it works pretty well.

Thank you very much for your help.

 
0 Kudos
LaurenBoyd
Esri Contributor

Hi @OlivierLefevre -

I haven't been able to reproduce the issue you're experiencing with any of our clustering samples. We'd like to look into this issue. Would you be able to send over a CodePen with a simplified sample and let us know what type of data is in your webmap (e.g. feature, map image, etc.)?

Accessing the selectedFeature's layer for MapImageLayers does not work because the layer property is not exposed for graphics from a MapImageLayer. It doesn't seem like your data is from a map image service since you're checking for the "feature" type, but I would like to make sure that's not the case. I haven't seen this issue before with clustering so any extra information you can provide would be helpful!

Just note the sourceLayer property isn't an officially documented property so this might not always work.

Thanks!

Lauren
0 Kudos