ProGuide Custom Pop ups - Esri/arcgis-pro-sdk GitHub Wiki
Language: C#
Subject: Map Exploration
Contributor: ArcGIS Pro SDK Team <[email protected]>
Organization: Esri, http://www.esri.com
Date: 10/06/2024
ArcGIS Pro: 3.4
Visual Studio: 2022
This guide demonstrates how to create a map tool that uses a custom pop-up. You use the tool to select a set of features and display their feature content using HTML5/JavaScript in the custom pop-up control. See ProConcepts Map Exploration, MapTool for more information on the MapTool pattern.
Prerequisites
- Download and install the sample data required for this guide as instructed in Arcgis Pro SDK Community Samples Releases.
- Create a new ArcGIS Pro Module Add-in, and name the project MapToolWithCustomPopup. If you are not familiar with the ArcGIS Pro SDK, you can follow the steps in the ProGuide Build your first add-in to get started.
- Add a new ArcGIS Pro Add-ins | ArcGIS Pro Map Tool item to the add-in project, and name the item CustomPopupTool.
Step 1
Modify the Config.daml file tool item as follows:
- Change the caption to "Pop-up (Customized)".
- Change the tool heading to "Pop-up (Customized)" and the ToolTip text to "Shows a custom pop-up for features in the map."
...
<tool id="CustomPopup_CustomPopupTool" caption="Pop-up (Customized)"
className="CustomPopupTool" loadOnClick="true"
smallImage="GenericButtonRed16" largeImage="GenericButtonRed32"
condition="esri_mapping_mapPane" keytip="z2">
<tooltip heading="Pop-up (Customized)">
Shows a custom pop-up for features in the map.<disabledText /></tooltip>
</tool>
...
Build the sample and validate the UI on the ArcGIS Pro ribbon.
Step 2
The required custom pop-up functions must be implemented in the CustomPopupTool class.
First, the sketch type set in the constructor is changed to SketchGeometryType.Rectangle.
Next, SketchOutputMode in the constructor is changed to SketchOutputMode.Screen. MapView.SelectFeatures and MapView.GetFeatures throw a System.ArgumentException if a 3D query is attempted in SketchOutputMode.Map, so you need to change the sketch projection to screen coordinates.
public CustomPopupTool()
{
IsSketchTool = true;
SketchType = SketchGeometryType.Rectangle;
SketchOutputMode = SketchOutputMode.Screen; //required for 3D selection and identify.
}
Note: If IsSketchTool is not set to true, a sketch is not produced.
The OnSketchCompleteAsync method can be overwritten to implement the required custom pop-up functionality. Since you "await" functions within your identify implementation, you have to add the "async" keyword to the OnSketchCompleteAsync method declaration. The MapTool base class provides an ActiveMapView property that provides a link to the current active map view. The MapView, in turn, provides a method that can be used to get all features that intersect a given geometry. The resulting features' BasicFeatureLayer and object ID are then used to create a list of DynamicPopupContent objects (we will define this in the next step). This list of objects is used when calling the ActiveMapView's ShowCustomPopup method.
protected override async Task<bool> OnSketchCompleteAsync(ArcGIS.Core.Geometry.Geometry
geometry)
{
var popupContent = await QueuedTask.Run(() =>
{
// Get the features that intersect the sketch geometry.
var result = ActiveMapView.GetFeatures(geometry);
// For each feature in the result create a new instance of our
// custom pop-up content class.
List<PopupContent> popups = new List<PopupContent>();
foreach (var kvp in result.ToDictionary<BasicFeatureLayer>())
{
kvp.Value.ForEach(id => popups.Add(new DynamicPopupContent(kvp.Key, id)));
}
// Flash the features that intersected the sketch geometry.
ActiveMapView.FlashFeature(result);
// Return the collection of pop-up content object.
return popups;
});
//Show the custom pop-up with the custom commands and the default pop-up commands.
ActiveMapView.ShowCustomPopup(popupContent, null, true);
return true;
}
Step 3
Add a new CSharp class file to the project, name the class DynamicPopupContent, and copy the content shown below into the class body. The DynamicPopupContent will inherit from the PopupContent class found in ArcGIS.Desktop.Mapping. Each PopupContent object references one feature which are defined by its MapMember and ObjectID properties.
Override the OnCreateHtmlContent method, which is called when each feature is displayed. OnCreateHtmlContent expects that the HTML content for the current selected feature is returned. This implementation queries the visible numeric attributes of the feature, reads the template HTML page, and replaces the //data.addColumn string place holder in the template HTML string with the feature's attribute data (formatted as a JavaScript string). We will add the template.html file to our project in the next step.
internal class DynamicPopupContent : PopupContent
{
private Dictionary<FieldDescription, double> _values =
new Dictionary <FieldDescription, double>();
/// <summary>
/// Constructor initializing the base class with the mapMember and object id associated
/// with the pop-up content.
/// </summary>
public DynamicPopupContent(MapMember mapMember, long id) : base(mapMember, id)
{
// Set property indicating the html content will be generated on demand when
// the content is viewed.
IsDynamicContent = true;
}
/// <summary>
/// Called the first time the pop-up content is viewed. This is good practice
/// when you may show a pop-up for multiple items at a time.
/// This allows you to delay generating the html content until
/// the item is actually viewed.
/// </summary>
protected override Task<string> OnCreateHtmlContent()
{
return QueuedTask.Run(() =>
{
var invalidPopup = "<p>Pop-up content could not be generated for this feature.</p>";
var layer = MapMember as BasicFeatureLayer;
if (layer == null) return invalidPopup;
// Get all the visible numeric fields for the layer.
var fields = layer.GetFieldDescriptions().
Where(f => IsNumericFieldType(f.Type) && f.IsVisible);
// Create a query filter using the fields found above and a where clause for
// the object id associated with this pop-up content.
var tableDef = layer.GetTable().GetDefinition();
var oidField = tableDef.GetObjectIDField();
var qf = new QueryFilter() { WhereClause = $"{oidField} = {ObjectID}",
SubFields = string.Join(",", fields.Select(f => f.Name)) };
// search
var rows = layer.Search(qf);
// Get the first row, there should only be 1 row.
if (!rows.MoveNext()) return invalidPopup;
var row = rows.Current;
// Loop through the fields, extract the value for the row, and add to a dictionary.
foreach (var field in fields)
{
var val = row[field.Name];
if (val is DBNull || val == null) continue;
double value;
if (!Double.TryParse(val.ToString(), out value)) continue;
if (value < 0) continue;
_values.Add(field, value);
}
if (_values.Count == 0) return invalidPopup;
// Construct a new html string that we will use to update our html template.
StringBuilder sb = new StringBuilder();
sb.AppendLine("data.addColumn('string', 'Votes')");
sb.AppendLine("data.addColumn('number', 'Count')");
sb.AppendLine("data.addColumn('number', 'In %')");
sb.AppendLine("data.addRows([");
//Add each value to the html string.
var total = _values.Sum(kvp => kvp.Value);
foreach (var v in _values)
{
var percentage = (v.Value / total) * 100;
sb.AppendLine(string.Format("['{0}', {{v: {1} }}, {{v: {2} }}],",
v.Key.Alias, v.Value, percentage));
}
sb.AppendLine("]);");
//Get the html from the template file on disk that we have packaged with our add-in.
var htmlPath = Path.Combine(Path.GetDirectoryName
(Assembly.GetExecutingAssembly().Location), "template.html");
var html = File.ReadAllText(htmlPath);
// Update the template with our custom html and
// return it to be displayed in the pop-up window.
html = html.Replace("//data.addColumn", sb.ToString());
return html;
});
}
private bool IsNumericFieldType(FieldType type)
{
switch (type)
{
case FieldType.Double:
case FieldType.Integer:
case FieldType.Single:
case FieldType.SmallInteger:
return true;
default:
return false;
}
}
}
Step 4
Now we will add the pop-up content page used to display the feature records. Add a new file to the project, and name it template.html. Make sure the build action for the file is set to Content and the Set Copy to Output Directory is Copy always.
Paste the content below into the newly added file. Note: The //data.addColumn text is a place holder for the real data, which is substituted for each page of feature content that is displayed.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<style type='text/css'>
.even-row {
background: #ffffff;
font-weight: normal;
}
.odd-row {
background: #efeeef;
font-weight: normal;
}
</style>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1.1", { packages: ["table", "corechart"] });
google.setOnLoadCallback(drawTable);
function drawTable() {
var data = new google.visualization.DataTable();
//data.addColumn
var options = {
'showRowNumber': false, 'width': '100%', 'cssClassNames': {
'headerRow': 'odd-row',
'oddTableRow': 'odd-row ',
'tableRow': 'even-row',
'hoverTableRow': 'even-row'
}
};
var formatter = new google.visualization.NumberFormat({ suffix: '%',
fractionalDigits: '2' });
formatter.format(data, 2);
var table = new google.visualization.Table(document.
getElementById('table_div'));
table.draw(data, options);
var chart = new google.visualization.PieChart(
document.getElementById('chart_div'));
chart.draw(data, { is3D: true, tooltip: { trigger: 'selection' },
chartArea: { left: 0, top: 20,
width: '100%', height: '100%' }, sliceVisibilityThreshold: 0 });
google.visualization.events.addListener(table, 'select',
function (e) {
var selection = table.getSelection();
chart.setSelection(selection);
});
google.visualization.events.addListener(chart, 'onmouseover',
function (e) {
table.setSelection([{ row: e.row, column: null }]);
chart.setSelection([{ row: e.row, column: null }]);
});
google.visualization.events.addListener(chart, 'onmouseout',
function (e) {
table.setSelection();
chart.setSelection();
});
}
</script>
</head>
<body>
<div id="table_div"></div>
<div id="chart_div"></div>
</body>
</html>
Step 5
Rebuild the add-in. Fix any compilation errors.
Step 6
Debug the add-in. Run the debugger and start ArcGIS Pro. Open the C:\Data\ElectionData\Election.aprx project that contains a dataset that works the custom pop-up. Try the custom pop-up tool, and select some states as shown here: