Update related features

View on GitHub
Sample viewer app

Update related features in an online feature service.

Image of update related features

Use case

Updating related features is a helpful workflow when you have two features with shared or dependent attributes. In a data collection scenario where origin tree features are related to destination inspection records, trees might undergo inspection on some regular interval to assess their size, health, and other characteristics. When logging a new inspection record that captures the latest trunk diameter and condition of a tree, updating these attributes on the origin tree feature would permit the tree point to be symbolized most accurately according to these latest observations.

How to use the sample

Once you launch the app, select a national park feature (shown in green on the map). The app will then identify it, perform a related table query, and will show you the annual visitors amount for the preserve. You can then update the visitor amount by tapping the drop-down in the Callout and selecting a different amount.

How it works

  1. Create two ServiceFeatureTables from the Feature Service URLs.
  2. Create two FeatureLayers using the previously created service feature tables.
  3. Add these feature layers to the map.
  4. When a Feature is selected, identify and highlight the selected feature.
  5. Retrieve related features by calling serviceFeatureTable.queryRelatedFeaturesAsync() and passing in the selected feature.
  6. Update feature in the feature table with serviceFeatureTable.updateFeatureAsync(selectedFeature) and apply updates to the server using serviceFeatureTable.applyEditsAsync().

Relevant API

  • ArcGISFeature
  • RelatedFeatureQueryResult
  • ServiceFeatureTable

About the data

The map opens to a view of the State of Alaska. Two related feature layers are loaded to the map and display the Alaska National Parks and Preserves.

Additional information

All the tables participating in a relationship must be present in the data source. ArcGIS Runtime supports related tables in the following data sources:

  • ArcGIS feature service
  • ArcGIS map service
  • Geodatabase downloaded from a feature service
  • Geodatabase in a mobile map package

Tags

editing, features, service, updating

Sample Code

MainActivity.java
                                                                                                                                                                                                                                                                                                                    
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
/* 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.arcgisruntime.sample.updaterelatedfeatures;

import android.graphics.Point;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.esri.arcgisruntime.ArcGISRuntimeEnvironment;
import com.esri.arcgisruntime.concurrent.ListenableFuture;
import com.esri.arcgisruntime.data.ArcGISFeature;
import com.esri.arcgisruntime.data.Feature;
import com.esri.arcgisruntime.data.FeatureEditResult;
import com.esri.arcgisruntime.data.RelatedFeatureQueryResult;
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.Viewpoint;
import com.esri.arcgisruntime.mapping.view.Callout;
import com.esri.arcgisruntime.mapping.view.DefaultMapViewOnTouchListener;
import com.esri.arcgisruntime.mapping.view.IdentifyLayerResult;
import com.esri.arcgisruntime.mapping.view.MapView;

import java.util.List;
import java.util.concurrent.ExecutionException;

public class MainActivity extends AppCompatActivity {

  private static final String TAG = MainActivity.class.getSimpleName();

  private MapView mMapView;
  private ServiceFeatureTable mParksFeatureTable;
  private FeatureLayer mParksFeatureLayer;
  private ArcGISFeature mSelectedArcGISFeature;
  private ServiceFeatureTable mPreservesFeatureTable;
  private ArcGISFeature mSelectedRelatedFeature;
  private Point mTappedPoint;
  private Callout mCallout;

  private String mAttributeValue;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // authentication with an API key or named user is required to access basemaps and other
    // location services
    ArcGISRuntimeEnvironment.setApiKey(BuildConfig.API_KEY);

    // create MapView from layout
    mMapView = findViewById(R.id.mapView);

    // create a map and set the map view viewpoint
    ArcGISMap map = new ArcGISMap(BasemapStyle.ARCGIS_TOPOGRAPHIC);
    mMapView.setMap(map);
    mMapView.setViewpoint(new Viewpoint(65.399121, -151.521682, 50000000));

    // get callout and set style
    mCallout = mMapView.getCallout();
    Callout.Style calloutStyle = new Callout.Style(this, R.xml.callout_style);
    mCallout.setStyle(calloutStyle);

    // set up feature tables and layers
    mParksFeatureTable = new ServiceFeatureTable(getString(R.string.parks_feature_table));
    mParksFeatureLayer = new FeatureLayer(mParksFeatureTable);
    mPreservesFeatureTable = new ServiceFeatureTable(getString(R.string.preserves_feature_table));
    FeatureLayer preservesFeatureLayer = new FeatureLayer(mPreservesFeatureTable);

    // add feature layers to map
    map.getOperationalLayers().add(mParksFeatureLayer);
    map.getOperationalLayers().add(preservesFeatureLayer);

    // set the mArcGISMap to be displayed in this view
    mMapView.setMap(map);

    // identify feature
    mMapView.setOnTouchListener(new DefaultMapViewOnTouchListener(this, mMapView) {
      @Override
      public boolean onSingleTapConfirmed(MotionEvent me) {
        // tapped point
        mTappedPoint = new Point((int) me.getX(), (int) me.getY());
        // clear any selected features or callouts
        mParksFeatureLayer.clearSelection();
        if (mCallout.isShowing()) {
          mCallout.dismiss();
        }

        final ListenableFuture<IdentifyLayerResult> identifyLayerResultFuture = mMapView
            .identifyLayerAsync(
                mParksFeatureLayer, mTappedPoint, 5, false, 1);
        identifyLayerResultFuture.addDoneListener(() -> {
          try {
            // call get on the future to get the result
            IdentifyLayerResult identifyLayerResult = identifyLayerResultFuture.get();

            if (!identifyLayerResult.getElements().isEmpty()) {
              mSelectedArcGISFeature = (ArcGISFeature) identifyLayerResult.getElements().get(0);
              // highlight the selected feature
              mParksFeatureLayer.selectFeature(mSelectedArcGISFeature);
              queryRelatedFeatures(mSelectedArcGISFeature);
            }
          } catch (InterruptedException | ExecutionException e) {
            String error = "Error getting identify layer result: " + e.getMessage();
            Toast.makeText(MainActivity.this, error, Toast.LENGTH_LONG).show();
            Log.e(TAG, error);
          }
        });
        return super.onSingleTapConfirmed(me);
      }
    });
  }

  /**
   * Query related features from selected feature
   *
   * @param feature selected feature
   */
  private void queryRelatedFeatures(ArcGISFeature feature) {
    final ListenableFuture<List<RelatedFeatureQueryResult>> relatedFeatureQueryResultFuture = mParksFeatureTable
        .queryRelatedFeaturesAsync(feature);

    relatedFeatureQueryResultFuture.addDoneListener(() -> {
      try {
        List<RelatedFeatureQueryResult> relatedFeatureQueryResultList = relatedFeatureQueryResultFuture
            .get();

        // iterate over returned RelatedFeatureQueryResults
        for (RelatedFeatureQueryResult relatedQueryResult : relatedFeatureQueryResultList) {
          // iterate over Features returned
          for (Feature relatedFeature : relatedQueryResult) {
            // persist selected related feature
            mSelectedRelatedFeature = (ArcGISFeature) relatedFeature;
            // get preserve park name
            String parkName = mSelectedRelatedFeature.getAttributes().get("UNIT_NAME").toString();
            // use the Annual Visitors field to use as filter on related attributes
            mAttributeValue = mSelectedRelatedFeature.getAttributes().get("ANNUAL_VISITORS")
                .toString();
            showCallout(parkName);
            // center on tapped point
            mMapView.setViewpointCenterAsync(mMapView.screenToLocation(mTappedPoint));
          }
        }
      } catch (InterruptedException | ExecutionException e) {
        String error = "Error getting related feature query result: " + e.getMessage();
        Toast.makeText(this, error, Toast.LENGTH_LONG).show();
        Log.e(TAG, error);
      }
    });
  }

  /**
   * Show a callout with Attribute Key and editable Value
   *
   * @param parkName preserves park name
   */
  private void showCallout(String parkName) {
    // create a text view for the callout
    View calloutLayout = LayoutInflater.from(this).inflate(R.layout.related_features_callout, null);
    // create a text view and add park name
    TextView parkText = calloutLayout.findViewById(R.id.park_name);
    String parkLabel = String.format(getString(R.string.callout_label), parkName);
    parkText.setText(parkLabel);
    // create spinner with selection options
    final Spinner visitorSpinner = calloutLayout.findViewById(R.id.visitor_spinner);
    // create an array adapter using the string array and default spinner layout
    final ArrayAdapter<CharSequence> adapter = ArrayAdapter
        .createFromResource(this, R.array.visitors_range, android.R.layout.simple_spinner_item);
    // Specify the layout to use when the list of choices appear
    adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
    // apply the adapter to the spinner
    visitorSpinner.setAdapter(adapter);
    visitorSpinner.setSelection(getIndex(visitorSpinner, mAttributeValue));
    // show callout at tapped location
    mCallout.setLocation(mMapView.screenToLocation(mTappedPoint));
    mCallout.setContent(calloutLayout);
    mCallout.show();
    // respond to user interaction
    visitorSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
      @Override
      public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
        // check if selection has changed
        String selectedValue = visitorSpinner.getSelectedItem().toString();
        if (!selectedValue.equalsIgnoreCase(mAttributeValue)) {
          // selection changed, update the related feature
          mCallout.dismiss();
          updateRelatedFeature(selectedValue);
        }
      }

      @Override
      public void onNothingSelected(AdapterView<?> parent) {
      }
    });
  }

  /**
   * Update the related feature table and apply update on the server
   *
   * @param visitors annual visitors value
   */
  private void updateRelatedFeature(final String visitors) {
    // load the related feature
    mSelectedRelatedFeature.loadAsync();
    mSelectedRelatedFeature.addDoneLoadingListener(() -> {
      if (mSelectedRelatedFeature.getLoadStatus() == LoadStatus.LOADED) {
        // put new attribute value
        mSelectedRelatedFeature.getAttributes().put("ANNUAL_VISITORS", visitors);
        // persist the attribute value
        mAttributeValue = visitors;
        // update feature in the related feature table
        ListenableFuture<Void> updateFeature = mPreservesFeatureTable
            .updateFeatureAsync(mSelectedRelatedFeature);
        updateFeature.addDoneListener(() -> {
          // apply update to the server
          final ListenableFuture<List<FeatureEditResult>> serverResult = mPreservesFeatureTable
              .applyEditsAsync();
          serverResult.addDoneListener(() -> {
            try {
              // check if server result successful
              List<FeatureEditResult> edits = serverResult.get();
              if (!edits.isEmpty()) {
                if (!edits.get(0).hasCompletedWithErrors()) {
                  mParksFeatureLayer.clearSelection();
                  Toast.makeText(this, getString(R.string.update_success), Toast.LENGTH_SHORT)
                      .show();
                  // show callout with new value
                  mCallout.show();
                } else {
                  Toast.makeText(this, getString(R.string.update_fail), Toast.LENGTH_LONG).show();
                }
              }
            } catch (InterruptedException | ExecutionException e) {
              String error = "Error getting feature edit result: " + e.getMessage();
              Toast.makeText(this, error, Toast.LENGTH_LONG).show();
              Log.e(TAG, error);
            }
          });
        });
      }
    });
  }

  /**
   * Get the position of attribute value
   *
   * @param spinner spinner with list of selection options
   * @param value   attribute value
   * @return position of attribute value
   */
  private int getIndex(Spinner spinner, String value) {
    if (value == null || spinner.getCount() == 0) {
      return -1;
    } else {
      for (int i = 0; i < spinner.getCount(); i++) {
        if (spinner.getItemAtPosition(i).toString().equalsIgnoreCase(value)) {
          return i;

        }
      }
    }
    return -1;
  }

  @Override
  protected void onPause() {
    mMapView.pause();
    super.onPause();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mMapView.resume();
  }

  @Override
  protected void onDestroy() {
    mMapView.dispose();
    super.onDestroy();
  }
}

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