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 forward cluster's features is fine too. But an error occurs when browsing those features backward.
At line 355 selectedFeature.layer is null.
I don't understand why and how to fix this issue.
Could you please help me ?
Solved! Go to Solution.
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.
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!
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.