Edit feature attachments

View on GitHubSample viewer app

Add, delete, and download attachments for features from a service.

Image of edit feature attachments

Use case

Attachments provide a flexible way to manage additional information that is related to your features. Attachments allow you to add files to individual features, including: PDFs, text documents, or any other type of file. For example, if you have a feature representing a building, you could use attachments to add multiple photographs of the building taken from several angles, along with PDF files containing the building's deed and tax information.

How to use the sample

Click a feature on the map to select it. Click 'Add Attachment' to add an attachment to the selected feature. The list view will update to show the attachments of the selected feature (if any). To delete an attachment, first select the feature, and then select an attachment from the list view. Then click 'Delete Attachment' to delete the selected attachment.

How it works

  1. Create a ServiceFeatureTable from a URL.
  2. Create a FeatureLayer object from the service feature table.
  3. Select features from the feature layer with selectFeatures.
  4. To fetch the feature's attachments, cast to an ArcGISFeature and useArcGISFeature.fetchAttachmentsAsync().
  5. To add an attachment to the selected ArcGISFeature, create an attachment and use ArcGISFeature.addAttachmentAsync().
  6. To delete an attachment from the selected ArcGISFeature, use the ArcGISFeature.deleteAttachmentAsync().
  7. After a change, apply the changes to the server using ServiceFeatureTable.applyEditsAsync().

Additional information

Attachments can only be added to and accessed on service feature tables when their hasAttachments property is true.

Relevant API

  • ArcGISFeature
  • FeatureLayer
  • ServiceFeatureTable

Tags

Edit and Manage Data, image, picture, JPEG, PNG, PDF, TXT

Sample Code

EditFeatureAttachmentsSample.java
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
/*
 * Copyright 2017 Esri.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.esri.samples.edit_feature_attachments;

import org.apache.commons.io.IOUtils;
import java.util.List;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;

import com.esri.arcgisruntime.ArcGISRuntimeEnvironment;
import com.esri.arcgisruntime.data.ArcGISFeature;
import com.esri.arcgisruntime.data.Attachment;
import com.esri.arcgisruntime.data.ServiceFeatureTable;
import com.esri.arcgisruntime.layers.FeatureLayer;
import com.esri.arcgisruntime.loadable.LoadStatus;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.BasemapStyle;
import com.esri.arcgisruntime.mapping.GeoElement;
import com.esri.arcgisruntime.mapping.Viewpoint;
import com.esri.arcgisruntime.mapping.view.MapView;

public class EditFeatureAttachmentsSample extends Application {

  private ListView<String> attachmentList;
  private Label attachmentsLabel;
  private ArcGISFeature selected;
  private List<Attachment> attachments;
  private ServiceFeatureTable featureTable;
  private MapView mapView;

  @Override
  public void start(Stage stage) {

    try {
      // create stack pane and application scene
      var stackPane = new StackPane();
      var scene = new Scene(stackPane);
      scene.getStylesheets().add(getClass().getResource("/edit_feature_attachments/style.css").toExternalForm());

      // set title, size, and add scene to stage
      stage.setTitle("Edit Feature Attachments Sample");
      stage.setWidth(800);
      stage.setHeight(700);
      stage.setScene(scene);
      stage.show();

      // authentication with an API key or named user is required to access basemaps and other location services
      String yourAPIKey = System.getProperty("apiKey");
      ArcGISRuntimeEnvironment.setApiKey(yourAPIKey);

      // create a control panel
      VBox controlsVBox = new VBox(6);
      controlsVBox.setBackground(new Background(new BackgroundFill(Paint.valueOf("rgba(0,0,0,0.3)"), CornerRadii.EMPTY,
        Insets.EMPTY)));
      controlsVBox.setPadding(new Insets(10.0));
      controlsVBox.setMaxSize(180, 250);
      controlsVBox.getStyleClass().add("panel-region");

      // create add/delete buttons
      Button addAttachmentButton = new Button("Add Attachment");
      addAttachmentButton.setMaxWidth(Double.MAX_VALUE);
      addAttachmentButton.setDisable(true);

      Button deleteAttachmentButton = new Button("Delete Attachment");
      deleteAttachmentButton.setMaxWidth(Double.MAX_VALUE);
      deleteAttachmentButton.setDisable(true);

      // create a list to show selected feature's attachments
      attachmentList = new ListView<>();
      attachmentsLabel = new Label("Attachments: ");
      attachmentsLabel.getStyleClass().add("panel-label");
      attachmentList.getSelectionModel().selectedItemProperty().addListener(event -> deleteAttachmentButton.setDisable(attachmentList.getSelectionModel().getSelectedIndex() == -1));

      // get image attachment
      byte[] image = IOUtils.toByteArray(getClass().getResourceAsStream("/edit_feature_attachments/destroyed.png"));

      // button click to add image attachment to selected feature
      addAttachmentButton.setOnAction(e -> addAttachment(image));

      // button click to delete selected attachment
      deleteAttachmentButton.setOnAction(e -> deleteAttachment(attachmentList.getSelectionModel().getSelectedIndex()));

      // add controls to the panel
      controlsVBox.getChildren().addAll(addAttachmentButton, deleteAttachmentButton, attachmentsLabel, attachmentList);

      // create a map with the streets basemap style
      ArcGISMap map = new ArcGISMap(BasemapStyle.ARCGIS_STREETS);

      // create a map view and set the map to it
      mapView = new MapView();
      mapView.setMap(map);

      // set a viewpoint on the map view
      mapView.setViewpoint(new Viewpoint(40, -95, 36978595));

      // set selection color
      mapView.getSelectionProperties().setColor(Color.BLUE);

      // create service feature table from URL
      featureTable = new ServiceFeatureTable("https://sampleserver6.arcgisonline.com/arcgis/rest/services/DamageAssessment/FeatureServer/0");

      // create a feature layer from service feature table
      var featureLayer = new FeatureLayer(featureTable);

      // add the feature layer to the ArcGISMap
      map.getOperationalLayers().add(featureLayer);

      // show alert if layer fails to load
      featureLayer.addDoneLoadingListener(() -> {
        if (featureLayer.getLoadStatus() != LoadStatus.LOADED) {
          displayMessage("Error", "Error loading feature layer");
        }
      });

      mapView.setOnMouseClicked(event -> {
        if (event.isStillSincePress() && event.getButton() == MouseButton.PRIMARY) {
          // create a map point from a point
          Point2D point = new Point2D(event.getX(), event.getY());

          // clear previous results
          featureLayer.clearSelection();
          addAttachmentButton.setDisable(true);
          attachmentList.getItems().clear();

          // get the clicked feature
          mapView.identifyLayerAsync(featureLayer, point, 1, false, 1).toCompletableFuture()
            .whenComplete((identifyLayerResult, exception) -> {
              if (exception == null) {
                // if the identification operation completes successfully, get the list of identified GeoElements
                // if nothing is identified, an empty list will be returned
                List<GeoElement> identified = identifyLayerResult.getElements();
                if (!identified.isEmpty()) {
                  // only proceed if an element was identified
                  GeoElement element = identified.get(0);
                  if (element instanceof ArcGISFeature) {
                    // if the element is an ArcGISFeature, select the feature
                    selected = (ArcGISFeature) element;
                    featureLayer.selectFeature(selected);
                    selected.loadAsync();
                    selected.addDoneLoadingListener(() -> {
                      if (selected.getLoadStatus() == LoadStatus.LOADED) {
                        fetchAttachmentsAsync(selected);
                      } else {
                        displayMessage("Error", "Element failed to load!");
                      }
                    });
                    addAttachmentButton.setDisable(false);
                  }
                }
              } else {
                // if the identification operation completes with an exception, display an error
                displayMessage("Exception getting identify result", exception.getCause().getMessage());
              }
            });
        }
      });

      // add map view and control panel to stack pane
      stackPane.getChildren().addAll(mapView, controlsVBox);
      StackPane.setAlignment(controlsVBox, Pos.TOP_LEFT);
      StackPane.setMargin(controlsVBox, new Insets(10, 0, 0, 10));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  /**
   * Updates the UI with a list of a feature's attachments.
   */
  private void fetchAttachmentsAsync(ArcGISFeature feature) {

    feature.fetchAttachmentsAsync().toCompletableFuture().whenComplete(
      (attachmentResults, exception) -> {
        if (exception == null) {
          // if the attachments were fetched successfully, update the UI attachments list
          attachments = attachmentResults;

          Platform.runLater(() -> {
            attachmentList.getItems().clear();
            if (!attachments.isEmpty()) {
              attachmentsLabel.setText("Attachments: ");
              attachments.forEach(attachment -> attachmentList.getItems().add(attachment.getName()));
            } else {
              attachmentsLabel.setText("No Attachments!");
            }
          });
        } else {
          displayMessage("Exception getting feature attachments", exception.getCause().getMessage());
        }
      });
  }

  /**
   * Adds an attachment to a Feature.
   *
   * @param attachment byte array of attachment
   */
  private void addAttachment(byte[] attachment) {

    if (selected.canEditAttachments()) {
      // update feature table and apply update to server when new feature is added
      selected.addAttachmentAsync(attachment, "image/png", "edit_feature_attachments/destroyed.png")
        .toCompletableFuture()
        .thenCompose(addedAttachment -> featureTable.updateFeatureAsync(selected).toCompletableFuture())
        .thenRun(() -> applyEdits(featureTable));
    } else {
      displayMessage(null, "Cannot add attachment.");
    }
  }

  /**
   * Deletes a selected attachment from a Feature.
   */
  private void deleteAttachment(int attachmentIndex) {

    if (selected.canEditAttachments()) {
      // update feature table and apply update to server when new feature is deleted
      selected.deleteAttachmentAsync(attachments.get(attachmentIndex)).toCompletableFuture()
        .thenCompose(unused -> featureTable.updateFeatureAsync(selected).toCompletableFuture())
        .thenRun(() -> applyEdits(featureTable));
    } else {
      displayMessage(null, "Cannot delete attachment");
    }
  }

  /**
   * Sends any edits on the ServiceFeatureTable to the server.
   *
   * @param featureTable service feature table
   */
  private void applyEdits(ServiceFeatureTable featureTable) {

    // apply the changes to the server
    featureTable.applyEditsAsync().toCompletableFuture().whenComplete(
      (editResults, exception) -> {
        // check if the server edit was successful
        if (exception == null) {
          if (!editResults.isEmpty()) {
            // check for any errors. In this case we are only updating 1 feature so just check the first result
            if (!editResults.get(0).hasCompletedWithErrors()) {
              // if there are no errors, display a success message
              displayMessage("Success", "Edited feature successfully");
            } else {
              if (editResults.get(0).getError() != null) {
                // if there is an error, throw
                throw editResults.get(0).getError();
              }
            }
          }
          // update the displayed list of attachments
          fetchAttachmentsAsync(selected);
        } else {
          displayMessage("Error applying edits on server ", exception.getCause().getMessage());
        }
      });
  }

  /**
   * Shows a message in an alert dialog.
   *
   * @param title   title of alert
   * @param message message to display
   */
  private void displayMessage(String title, String message) {

    Platform.runLater(() -> {
      Alert dialog = new Alert(Alert.AlertType.INFORMATION);
      dialog.initOwner(mapView.getScene().getWindow());
      dialog.setHeaderText(title);
      dialog.setContentText(message);
      dialog.showAndWait();
    });
  }

  /**
   * Stops and releases all resources used in application.
   */
  @Override
  public void stop() {

    // release resources when the application closes
    if (mapView != null) {
      mapView.dispose();
    }
  }

  /**
   * Opens and runs application.
   *
   * @param args arguments passed to this application
   */
  public static void main(String[] args) {

    Application.launch(args);
  }

}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.