Map Feedback

Map feedback is visual feedback that is displayed to the user as they are editing. The purpose of the feedback is give direct intelligence to the user, of the impact of their edits allowing them to make more informed decisions.

Types of Feedback

There are several different forms of map feedback.

Layer Feedback: Layer feedback is dynamically generated map graphics that are added to the map. The feedback can change with different events that occur in the application. For example changing map tools, updating settings, panning the map or digitising.

HTML Overlay: This is a versatile form of feedback that allows for HTML, labels, as well as graphics to be added to the map. It is more flexible than layer feedback, as it allows tables or other overlays to be shown over the map. The feedback can change with different events that occur in the application. For example changing map tools, updating settings, panning the map or digitising.

Post edit: This type of feedback allows for graphics to be added temporarily to the map, directly after an edit operation has finished. It is used to flag consequential changes to the user.

All of the feedback is written and specified using Arcade scripts.

Configuration

To add and configure the Map Feedback follow these steps:

  1. Open the Data panel by selecting Choose Data in the wireframe
  2. Expand Map feedback and use the add button
  3. Choose either Layer or Post edit or HTML overlay for the feedback type
  4. Add the logic for the feedback data

The different feedback types have different requirements. The sub-sections below explain how to use and configure the different types.

In order to provide layer feedback, it is necessary to provide the following configuration settings.

Layer Feedback

  • Feedback data: This is the script which is used to generate graphics on the map.
  • Layers to watch: The layers to monitor in the application. This is used to determine when to re-calculate the feedback data. Please see below to understand when the feedback data is recalculated.
  • Labelling Field: When data is returned, this option identifies a field to be used for labelling.
  • Pointer move data: This is an advanced form of feedback that will show graphics on the map as the user moves their pointer or hovers.

Key to using layer feedback is understanding when the script will run and display graphics. The following explains when the feedback data will be calculated

  • Current selected map operation changes. This will cause the feedback data to be recalculated.
  • Selection changes. This will cause the feedback data to be recalculated, if the script references $selection.
  • Settings / Session variable changes. This will cause the feedback data to be recalculated, if the script references $session.
  • Map extent changes. The feedback will be recalculated if the script uses $currentMapExtent. Note. If the data is showing a layer, then changing the map extent may just cause may data to be fetched.
  • Application Properties. If the script uses the applicationproperty function, and a property changes, then the script will recalculate.
  • The user starts digitising on the map, as part of a ‘New’, ‘Add’ or ‘Subtract’ operation. If the active layer is being watched, the data will be regenerated as the user draws on the map.

HTML overlay feedback – Data Script

The data script may return the following

  • graphics: An array of graphics to show on the map
  • html: An html block to display
  • labels: An array of html labels to show on the map.

The script can return all of these at once.

If the script returns null, then all graphics, html, and/or labels will be removed.

return {
    graphics: <Array of Graphics>,
    html: '<An HTML overlay>',
    labels: <Array of HTML labels>,
    pointermove: <Optional [Advanced] can be used to switch off the move script>,
    action: <Optional [Advanced] return 'noop' to keep graphics>
};

To understand how to return graphics, HTML, and labels, please see the sub-sections below.

The following advanced script will cause the app to ‘keep’ its graphics and overlays. It can be used to reduce the amount of ‘graphics’ churn. All graphics will still be removed at the end of the operation.

return {
    action: "noop"
};

The data script will be passed a number of globals. These are as follows

  • $activeLayer: The id of the layer in the webmap on which the current map tool is working on. This may be “”. It will be populated when the user is creating new features or appending / subtracting from an existing feature
  • $activeTool: The name of the current active tool. This may be ‘new’ | ‘append’ | ‘subtract’
  • $drawingShape: The current drawing shape being digitised. It will start of as a point, then a line, then a polygon (if digitising a polygon), as the user progressively digitises the vertices.
  • $drawingShapeLastPoint: The last point the user entered during the digitization process
  • $map: The layers of the map, as FeatureSets
  • $selection: The current selection, organised into FeatureSets for each layer in the webmap.

Defining a return Graphic

The following is an example script that returns a graphic to be added to the map. It is possible to add more than 1 graphic, in conjunction with labels and/or HTML overlays

var graphs = [];
push(graphs, {
    geometry: geom,
    symbol: `{
                "type": "esriSFS",
                "color": [0, 0, 0, 64],
                "outline": {
                "type": "esriSLS",
                "color": [0, 0, 0, 255],
                "width": 0.75,
                "style": "esriSLSSolid"
                },
                "style": "esriSFSSolid"
                }`
});

return {
    graphics: graphs
};

Defining a return Label

Labels are effectively pull out notices that are shown on the map. They consist of a line connector, and some HTML content.

Sweet will automatically lay out the labels as best it can using heuristics to prevent label overlap.

A label requires a map point, the HTML to show with the connector, and optionally a color, if the default is not the preference.

var labels = [];
var pt = Point({ x: 100, y: 100, spatialReference: { wkid: 102100 } });
var name = "Hello world";

push(labels, {
    pos: pt,
    // color: '#ff49e1', // Optional
    html: `<div style="padding:5px; max-width:100px; color:#ff49e1; font-size: 12px; font-family: system-ui;">
        ${name}
    </div>`
});

return {
    labels: labels
};

Defining a return Overlay

Any HTML can be added to the map. It will be ‘draped’ over the map.

The engine will only accept a subset of HTML. The HTML will be validated and checked before it is added to the page.

var somevalue = 10.222;
var html = `
<div class="float-right" style="width:200px; margin-left:30px; margin-top:30px; margin-right: 74px;">
    <div class="card" style="background-color: white; border-radius: 20px;">
        <div class="card-body rounded p-3">

      <table class="table table-sm mb-0">

  <tbody>
    <tr>
      <td class="border-top-0">Materials</td>
      <td class="border-top-0">
        <span class="badge badge-success">£0</span>
       </td>
    </tr>
    <tr>
      <td>Reinstatement</td>
      <td>
        <span class="badge badge-info">£${round(somevalue, 2)}</span>
      </td>
    </tr>
    <tr>
      <th>Total</th>
      <th>
        <span class="badge badge-danger">£${round(somevalue, 2)}</span>
      </th>
    </tr>
  </tbody>
</table>

        </div>
    </div>
</div>
`;

return {
    html: html
};

Post edit feedback

Post edit feedback appears once an operation has been completed. It allows graphics to be temporarily displayed on the map, to bring attention to changes.

In order to provide layer feedback, it is necessary to provide the following configuration settings

  • Feedback data: This is the script which is used to generate graphics on the map.
  • Layers to watch: The layers to monitor in the application. This is used to determine if the operation has affected data in the layers to which this feedback is applicable.
  • Labelling Field: When data is returned, this option identifies a field to be used for labelling.

The data script will be run and display graphics calculated, when an operation has been completed, and it involves changes in the layers being watched.

Post edit feedback – Data Script

The data script may return the following

A Feature. This consists of a geometry and set of fields. This will be converted to a single layer, with one feature in.

return Feature(pt, { fieldA: "valueA" });

A geometry. This will be converted into a graphic and added to a single layer.

return Point({ x: 100, y: 100, spatialReference: { wkid: 102100 } });

An array of geometry. This will be converted into a graphic and added to a single layer.

return [
    Point({ x: 100, y: 100, spatialReference: { wkid: 102100 } }),
    Point({ x: 200, y: 200, spatialReference: { wkid: 102100 } })
];

An array of Features. These features will be converted a layer and added to the map.

return [Feature(pt, { fieldA: "valueA" }), Feature(pt2, { fieldA: "valueA" })];

A featureset.

var f = FeatureSetByName($map, "Ineligible Land");
return intersects(f, $drawingshape);

If the data script returns null, there will be no feedback added to the map.

The data script will be passed a number of globals. These are as follows

  • $editedFeatures: The list of features being edited
  • $originalEditedFeatures: The list of edited features, as they were before the edits.
  • $unchangedMap: The state of the data, as it was before the operation ran.
  • $typeOfEditOperation: The name of the operation (causing the edit), that was run. This maybe different to $currentTool which represents the current active tool.
  • $activeLayer: The id of the layer in the webmap on which the current map tool is working on. This may be “”. It will be populated when the user is creating new features or appending / subtracting from an existing feature
  • $activeTool: The name of the current active tool. This may be ‘new’ | ‘append’ | ‘subtract’
  • $map: The layers of the map, as FeatureSets
  • $selection: The current selection, organised into FeatureSets for each layer in the webmap.

Pointer move feedback

Layer Feedback and HTML overlay feedback can respond as the user moves their mouse or hovers over the map.

Mouse/Pointer feedback is considered an advanced customization and comes with a number of limitations/expectations

  • The script must be fast. The script is executed continuously and therefore if used incorrectly, the user will have a poor experience.
  • The script is synchronous. No data can be fetched in the script, and access to map layers / featuresets has been disabled.
  • Mouse/pointer move feedback will not execute, if the the data feedback script has returned null. It will also not run, if the data feedback script has returned { pointermove: false }. In both these cases all existing move feedback will be removed from the map.
  • If the pointer move script is called in a ‘Layer feedback’ configuration, it can return graphics. If it is called in an ‘HTML overlay feedback’ it can return graphics, labels and HTML. [See earlier examples for the format used.]

When the script is called as part of a dataupdate rather than a mouse move, the following can be returned

return { action: "noop" };

This will retain all previous graphics and alike on the map.

Script globals for Pointer move

When executing the ‘script’, the engine will have access to a number of globals. These will change depending on the event. The following explains their purpose

  • $event: The type of mouse operation. This maybe ‘move’, ‘hover’, or ‘dataupdate’. ‘dataupdate’ will be used when the script is executed directly after the data feedback has run, but not in response to a mouse move
  • $drawingShape: This will be the shape that is being interactively digitised. It will start of as a point, but as the user digitises more vertices, will change.
  • $pointerPoint: This is the current pointer location. Will be null in a ‘dataupdate’ call.
  • $drawingSegment: The current segment being drawn.

Recommended approach

As the pointer move script runs synchronously, and does not have access to any map layers, it is common to use the script in tandem with the standard data script.

For example, the following script used in the data feedback expression will ‘collect’, all the relevant features in the map, and store it in temporary state as an array

var noticelines = intersects(FeatureSetByName($map,"Notices"), $currentMapExtent);

// Now loop through the lines, and store for use in mousemove/hover
var data = []
for (var n in noticelines) {
    push(data, n);
}

// Store this array in temporary state
state("notices", data);

// Return a value. If null is returned, all feedback will be removed and there will be no
// pointer move feedback
return {
  ignore: true;
};

The pointer move data expression, can now access this data and use it accordingly

// If no pointer point, then return noop
// This occurs during a 'dataupdate'
if ($pointerPoint == null) {
    return {
        action: "noop"
    };
}

var labels = [];

// Only calculate this when hovering
if ($event == "hover") {
    // Find notices in 3meters of the current point
    var buf = buffer($pointerPoint, 3); //pixelDistanceToMapUnit(40));

    // Get the notices from state, and search for labels
    var tr = state("notices");
    if (tr != null) {
        for (var t in tr) {
            var tree = tr[t];
            if (intersects(buf, tree)) {
                push(labels, {
                    pos: geometry(tree),
                    html: `<div style="padding:5px; max-width:100px; color:#ff49e1; font-size: 12px; font-family: system-ui;"> 
                        ${tree.name}
                    </div>`
                });
            }
        }
    }
}

return {
    labels: labels
};

Script helpers

Batch creating features using tasks and Arcade, can be challenging and verbose. For this reason a number of helper functions are included to help generate new features.

The following Arcade functions can be used both in returning Feedback, and constructing features

  • createFeature: A function which will build a new Arcade Feature for a layer. It will have all the correct fields for the layer. Optionally a template can be used. In this case, the feature will use the default values from the template
    var f = createFeature({layername:'Trees', geometry: pt})
    
  • symbolFromFeature: This function will get the correct symbol for a Feature in a given layer. This is helpful for constructing dynamic feedback, and adding temporary graphics to the map
    var sym = symbolFromFeature(feat, { layername:'Trees'})
    
  • featureBuilder: This function will create a set of features, given an input shape. This function works in a similar fashion to ArcGIS Pro Group Feature Templates and Feature Builders.

/** Get a new point feature using 'multiple-point-features'
 *
 * This operation expects a point geometry to be used. It will create a
 * new feature initialised with the same geometry
 */
var feats = FeatureBuilder({
    method: "multiple-point-features",
    geometry: pointGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get a new point feature at the start of a line 'point-at-start-of-line'
 *
 * This operation get a feature at the start of a line. It can optionally
 * take an offset-distance and side (left or right), and/or a distance-along.
 * The distance-along should specify the type of distance, 'proportional'
 * or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-start-of-line",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get a new point feature at the end of a line 'point-at-end-of-line''
 *
 * This operation get a feature at the end of a line. It can optionally
 * take an offset-distance and side (left or right), and/or a distance-along.
 * The distance-along should specify the type of distance, 'proportional'
 * or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-end-of-line",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get new point features at every vertex of a line
 * 'point-at-every-vertex-of-line''
 *
 * This operation gets features for every vertex of a line. It can optionally
 * take an offset-distance and side (left or right), and/or a distance-along.
 * The distance-along should specify the type of distance, 'proportional'
 * or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-every-vertex-of-line",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get new point features at every vertex of a line, except the end.
 * point-at-every-vertex-of-line-except-end'
 *
 * This operation gets features for every vertex of a line except the end
 * vertex. It can optionally take an offset-distance and side (left or right),
 * and/or a distance-along. The distance-along should specify the type of
 * distance, 'proportional' or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-every-vertex-of-line-except-end",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get new point features at every vertex of a line, except the start.
 * point-at-every-vertex-of-line-except-start'
 *
 * This operation gets features for every vertex of a line except the start
 * vertex. It can optionally take an offset-distance and side (left or right),
 * and/or a distance-along. The distance-along should specify the type of
 * distance, 'proportional' or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-every-vertex-of-line-except-start",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get new point features at every vertex of a line, except the start and end.
 * point-at-every-vertex-of-line-except-start'
 *
 * This operation gets features for every vertex of a line except the start
 * and end vertex. It can optionally take an offset-distance and side (left
 * or right), and/or a distance-along. The distance-along should specify
 * the type of distance, 'proportional' or 'distance'
 */

var feats = FeatureBuilder({
    method: "point-at-every-vertex-of-line-except-start-and-end",
    geometry: lineGeom,
    layername: "Trees",
    template: "Birch", //optional
    "offset-distance": 10, //optional
    "offset-side": "left", //optional
    "distance-along": 10, //optional
    "distance-along-type": "distance" //optional
});

/** Get new point feature at the centroid of a geometry. 'centroid'
 *
 * This operation gets a new feature at the centroid of a Geometry
 */

var feats = FeatureBuilder({
    method: "centroid",
    geometry: Geom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get a new polygon feature using 'multiple-polygon-features'
 *
 * This operation expects a polygon geometry to be used. It will create
 * a new feature initialised with the same geometry
 */

var feats = FeatureBuilder({
    method: "multiple-polygon-features",
    geometry: PolygonGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get a new polyline feature using the boundary of a polyline, using
 * 'single-line-feature-boundary'
 *
 * This operation expects a polygon geometry to be used. It will create
 * a new feature initialised with a line representing the boundary
 */

var feats = FeatureBuilder({
    method: "single-line-feature-boundary",
    geometry: PolygonGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get a new point from the first vertex of a polygon using
 * 'point-at-start-of-polygon'
 *
 * This operation gets a features for the first vertex of a polygon
 */

var feats = FeatureBuilder({
    method: "point-at-start-of-polygon",
    geometry: PolygonGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get new point features at every vertex of a polygon using
 *  'point-at-every-vertex-of-polygon'
 *
 * This operation gets features for every vertex of a polygon
 */

var feats = FeatureBuilder({
    method: "point-at-start-of-polygon",
    geometry: PolygonGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get new point features at every vertex of a polygon except the first
 *  point using 'point-at-every-vertex-of-polygon-except-start'
 *
 * This operation gets features for every vertex of a polygon except the start
 * vertex
 */

var feats = FeatureBuilder({
    method: "point-at-every-vertex-of-polygon-except-start",
    geometry: PolygonGeom,
    layername: "Trees",
    template: "Birch" //optional
});

/** Get new feature by buffering an existing geometry 'buffer'
 *
 * This operation gets a new feature as a buffer of an existing one
 */

var feats = FeatureBuilder({
    method: "buffer",
    geometry: Geom,
    layername: "Trees",
    template: "Birch", //optional
    distance: 10
});