Skip to content

Commit 2a0790d

Browse files
authored
Edit and sync features (#352)
1 parent ede756d commit 2a0790d

File tree

5 files changed

+542
-0
lines changed

5 files changed

+542
-0
lines changed
338 KB
Loading
Lines changed: 374 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,374 @@
1+
/*
2+
* Copyright 2019 Esri.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package com.esri.samples.editing.edit_and_sync_features;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.util.List;
22+
import java.util.concurrent.ExecutionException;
23+
24+
import javafx.fxml.FXML;
25+
import javafx.geometry.Point2D;
26+
import javafx.scene.control.Alert;
27+
import javafx.scene.control.Button;
28+
import javafx.scene.control.ProgressBar;
29+
import javafx.scene.input.MouseButton;
30+
import javafx.scene.paint.Color;
31+
32+
import com.esri.arcgisruntime.concurrent.Job;
33+
import com.esri.arcgisruntime.concurrent.ListenableFuture;
34+
import com.esri.arcgisruntime.data.Feature;
35+
import com.esri.arcgisruntime.data.Geodatabase;
36+
import com.esri.arcgisruntime.data.ServiceFeatureTable;
37+
import com.esri.arcgisruntime.data.TileCache;
38+
import com.esri.arcgisruntime.geometry.Envelope;
39+
import com.esri.arcgisruntime.geometry.GeometryEngine;
40+
import com.esri.arcgisruntime.geometry.GeometryType;
41+
import com.esri.arcgisruntime.geometry.Point;
42+
import com.esri.arcgisruntime.layers.ArcGISTiledLayer;
43+
import com.esri.arcgisruntime.layers.FeatureLayer;
44+
import com.esri.arcgisruntime.layers.LayerContent;
45+
import com.esri.arcgisruntime.loadable.LoadStatus;
46+
import com.esri.arcgisruntime.mapping.ArcGISMap;
47+
import com.esri.arcgisruntime.mapping.Basemap;
48+
import com.esri.arcgisruntime.mapping.GeoElement;
49+
import com.esri.arcgisruntime.mapping.view.DrawStatus;
50+
import com.esri.arcgisruntime.mapping.view.DrawStatusChangedEvent;
51+
import com.esri.arcgisruntime.mapping.view.DrawStatusChangedListener;
52+
import com.esri.arcgisruntime.mapping.view.Graphic;
53+
import com.esri.arcgisruntime.mapping.view.GraphicsOverlay;
54+
import com.esri.arcgisruntime.mapping.view.IdentifyLayerResult;
55+
import com.esri.arcgisruntime.mapping.view.MapView;
56+
import com.esri.arcgisruntime.mapping.view.ViewpointChangedListener;
57+
import com.esri.arcgisruntime.symbology.ColorUtil;
58+
import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
59+
import com.esri.arcgisruntime.symbology.SimpleRenderer;
60+
import com.esri.arcgisruntime.tasks.geodatabase.GenerateGeodatabaseJob;
61+
import com.esri.arcgisruntime.tasks.geodatabase.GenerateGeodatabaseParameters;
62+
import com.esri.arcgisruntime.tasks.geodatabase.GeodatabaseSyncTask;
63+
import com.esri.arcgisruntime.tasks.geodatabase.SyncGeodatabaseJob;
64+
import com.esri.arcgisruntime.tasks.geodatabase.SyncGeodatabaseParameters;
65+
import com.esri.arcgisruntime.tasks.geodatabase.SyncLayerOption;
66+
67+
public class EditAndSyncFeaturesController {
68+
69+
@FXML private Button generateButton;
70+
@FXML private MapView mapView;
71+
@FXML private ProgressBar progressBar;
72+
@FXML private Button syncButton;
73+
74+
private final Graphic downloadAreaGraphic = new Graphic();
75+
private GeodatabaseSyncTask geodatabaseSyncTask;
76+
private Geodatabase geodatabase;
77+
private ArcGISMap map;
78+
private ViewpointChangedListener viewpointChangedListener;
79+
private Feature selectedFeature;
80+
81+
@FXML
82+
private void initialize() {
83+
84+
try {
85+
// create a basemap from a local tile cache
86+
TileCache sanFranciscoTileCache = new TileCache("samples-data/sanfrancisco/SanFrancisco.tpk");
87+
ArcGISTiledLayer tiledLayer = new ArcGISTiledLayer(sanFranciscoTileCache);
88+
Basemap basemap = new Basemap(tiledLayer);
89+
90+
// create a map with the basemap and set it to the map view
91+
map = new ArcGISMap(basemap);
92+
mapView.setMap(map);
93+
94+
// create a graphics overlay for displaying the download area
95+
GraphicsOverlay graphicsOverlay = new GraphicsOverlay();
96+
graphicsOverlay.setRenderer(new SimpleRenderer(new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID,
97+
ColorUtil.colorToArgb(Color.RED), 2)));
98+
graphicsOverlay.getGraphics().add(downloadAreaGraphic);
99+
mapView.getGraphicsOverlays().add(graphicsOverlay);
100+
101+
// update the download area graphic when the map is initially drawn and when the viewpoint is changed
102+
viewpointChangedListener = viewpointChangedEvent -> updateDownloadArea();
103+
DrawStatusChangedListener drawStatusChangedListener = new DrawStatusChangedListener() {
104+
@Override
105+
public void drawStatusChanged(DrawStatusChangedEvent drawStatusChangedEvent) {
106+
if (drawStatusChangedEvent.getDrawStatus() == DrawStatus.COMPLETED) {
107+
updateDownloadArea();
108+
mapView.removeDrawStatusChangedListener(this);
109+
}
110+
}
111+
};
112+
mapView.addDrawStatusChangedListener(drawStatusChangedListener);
113+
mapView.addViewpointChangedListener(viewpointChangedListener);
114+
115+
// create a geodatabase sync task using the feature service URL
116+
String featureServiceUrl = "https://sampleserver6.arcgisonline" +
117+
".com/arcgis/rest/services/Sync/WildfireSync/FeatureServer";
118+
geodatabaseSyncTask = new GeodatabaseSyncTask(featureServiceUrl);
119+
geodatabaseSyncTask.loadAsync();
120+
121+
// load the geodatabase sync task to get its contents
122+
geodatabaseSyncTask.addDoneLoadingListener(() -> {
123+
if (geodatabaseSyncTask.getLoadStatus() == LoadStatus.LOADED) {
124+
// look through the feature service layers
125+
geodatabaseSyncTask.getFeatureServiceInfo().getLayerInfos().forEach(layerInfo -> {
126+
// get the URL for this particular layer
127+
String featureLayerURL = featureServiceUrl + "/" + layerInfo.getId();
128+
129+
// create the service feature table
130+
ServiceFeatureTable onlineFeatureTable = new ServiceFeatureTable(featureLayerURL);
131+
onlineFeatureTable.loadAsync();
132+
133+
// add feature layers to the map from feature tables with point geometries (to make editing easier)
134+
onlineFeatureTable.addDoneLoadingListener(() -> {
135+
if (onlineFeatureTable.getLoadStatus() == LoadStatus.LOADED &&
136+
onlineFeatureTable.getGeometryType() == GeometryType.POINT) {
137+
map.getOperationalLayers().add(new FeatureLayer(onlineFeatureTable));
138+
}
139+
});
140+
});
141+
142+
generateButton.setDisable(false);
143+
} else {
144+
new Alert(Alert.AlertType.ERROR, "Error loading geodatabase sync task").show();
145+
}
146+
});
147+
} catch (Exception ex) {
148+
// on any error, display the stacktrace
149+
ex.printStackTrace();
150+
}
151+
152+
}
153+
154+
/**
155+
* Generates a local geodatabase of the features in the download area and displays it in the map.
156+
*/
157+
@FXML
158+
private void generateGeodatabase() {
159+
// only allow geodatabase generation once
160+
generateButton.setDisable(true);
161+
// stop updating the download area when changing the viewpoint
162+
mapView.removeViewpointChangedListener(viewpointChangedListener);
163+
164+
// create generate geodatabase parameters for the download area
165+
final ListenableFuture<GenerateGeodatabaseParameters> generateGeodatabaseParametersFuture = geodatabaseSyncTask
166+
.createDefaultGenerateGeodatabaseParametersAsync(downloadAreaGraphic.getGeometry());
167+
generateGeodatabaseParametersFuture.addDoneListener(() -> {
168+
try {
169+
// create generate geodatabase parameters not returning attachments
170+
GenerateGeodatabaseParameters generateGeodatabaseParameters = generateGeodatabaseParametersFuture.get();
171+
generateGeodatabaseParameters.setReturnAttachments(false);
172+
173+
// create a temporary file for the geodatabase
174+
File tempFile = File.createTempFile("gdb", ".geodatabase");
175+
tempFile.deleteOnExit();
176+
177+
// create and start the generate job
178+
GenerateGeodatabaseJob generateGeodatabaseJob = geodatabaseSyncTask.generateGeodatabase(generateGeodatabaseParameters, tempFile.getAbsolutePath());
179+
generateGeodatabaseJob.start();
180+
181+
// show the job's progress in the progress bar
182+
progressBar.setVisible(true);
183+
generateGeodatabaseJob.addJobChangedListener(() ->
184+
progressBar.setProgress((double) generateGeodatabaseJob.getProgress() / 100.0)
185+
);
186+
187+
// get the geodatabase when done
188+
generateGeodatabaseJob.addJobDoneListener(() -> {
189+
if (generateGeodatabaseJob.getStatus() == Job.Status.SUCCEEDED) {
190+
geodatabase = generateGeodatabaseJob.getResult();
191+
geodatabase.loadAsync();
192+
193+
// display the contents of the geodatabase to the map
194+
geodatabase.addDoneLoadingListener(() -> {
195+
progressBar.setVisible(false);
196+
if (geodatabase.getLoadStatus() == LoadStatus.LOADED) {
197+
198+
// remove the existing layers from the map
199+
map.getOperationalLayers().clear();
200+
201+
// iterate through the feature tables in the geodatabase and add new layers to the map
202+
geodatabase.getGeodatabaseFeatureTables().forEach(geodatabaseFeatureTable -> {
203+
geodatabaseFeatureTable.loadAsync();
204+
geodatabaseFeatureTable.addDoneLoadingListener(() -> {
205+
if (geodatabaseFeatureTable.getGeometryType() == GeometryType.POINT) {
206+
// create a new feature layer from the table and add it to the map
207+
FeatureLayer featureLayer = new FeatureLayer(geodatabaseFeatureTable);
208+
map.getOperationalLayers().add(featureLayer);
209+
}
210+
});
211+
});
212+
213+
generateButton.setDisable(true);
214+
allowEditing();
215+
} else {
216+
new Alert(Alert.AlertType.ERROR, "Error loading geodatabase").show();
217+
}
218+
});
219+
} else {
220+
new Alert(Alert.AlertType.ERROR, "Error generating geodatabase").show();
221+
}
222+
});
223+
224+
} catch (InterruptedException | ExecutionException e) {
225+
new Alert(Alert.AlertType.ERROR, "Error generating geodatabase parameters").show();
226+
progressBar.setVisible(false);
227+
} catch (IOException e) {
228+
new Alert(Alert.AlertType.ERROR, "Error creating temp file for geodatabase").show();
229+
progressBar.setVisible(false);
230+
}
231+
});
232+
}
233+
234+
/**
235+
* Adds an event handler to allow the user to interactively select and move features in the map.
236+
*/
237+
private void allowEditing() {
238+
mapView.setOnMouseClicked(e -> {
239+
if (e.isStillSincePress() && e.getButton() == MouseButton.PRIMARY) {
240+
Point2D screenPoint = new Point2D(e.getX(), e.getY());
241+
if (selectedFeature != null) {
242+
// move the selected feature to the clicked location and update it in the feature table
243+
Point point = mapView.screenToLocation(screenPoint);
244+
if (GeometryEngine.intersects(point, downloadAreaGraphic.getGeometry())) {
245+
selectedFeature.setGeometry(point);
246+
selectedFeature.getFeatureTable().updateFeatureAsync(selectedFeature).addDoneListener(() -> syncButton.setDisable(false));
247+
} else {
248+
new Alert(Alert.AlertType.WARNING, "Cannot move feature outside downloaded area.").show();
249+
}
250+
} else {
251+
// identify which feature was clicked and select it
252+
ListenableFuture<List<IdentifyLayerResult>> identifyLayersFuture = mapView.identifyLayersAsync(screenPoint, 1,
253+
false);
254+
identifyLayersFuture.addDoneListener(() -> {
255+
try {
256+
// get the result of the query
257+
List<IdentifyLayerResult> identifyLayerResults = identifyLayersFuture.get();
258+
if (!identifyLayerResults.isEmpty()) {
259+
// retrieve the first result and get it's contents
260+
IdentifyLayerResult firstResult = identifyLayerResults.get(0);
261+
LayerContent layerContent = firstResult.getLayerContent();
262+
// check that the result is a feature layer and has elements
263+
if (layerContent instanceof FeatureLayer && !firstResult.getElements().isEmpty()) {
264+
FeatureLayer featureLayer = (FeatureLayer) layerContent;
265+
// retrieve the geoelements in the feature layer
266+
GeoElement identifiedElement = firstResult.getElements().get(0);
267+
if (identifiedElement instanceof Feature) {
268+
Feature feature = (Feature) identifiedElement;
269+
featureLayer.selectFeature(feature);
270+
// keep track of the selected feature to move it
271+
selectedFeature = feature;
272+
}
273+
}
274+
}
275+
} catch (InterruptedException | ExecutionException ex) {
276+
ex.printStackTrace();
277+
}
278+
});
279+
}
280+
} else if (e.isStillSincePress() && e.getButton() == MouseButton.SECONDARY) {
281+
// clear the selection on a right-click
282+
clearSelection();
283+
selectedFeature = null;
284+
}
285+
});
286+
}
287+
288+
/**
289+
* Syncs changes made on either the local or web service geodatabase with each other.
290+
*/
291+
@FXML
292+
private void syncGeodatabase() {
293+
clearSelection();
294+
syncButton.setDisable(true);
295+
selectedFeature = null;
296+
mapView.setOnMouseClicked(null);
297+
298+
// create parameters for the sync task
299+
SyncGeodatabaseParameters syncGeodatabaseParameters = new SyncGeodatabaseParameters();
300+
syncGeodatabaseParameters.setSyncDirection(SyncGeodatabaseParameters.SyncDirection.BIDIRECTIONAL);
301+
syncGeodatabaseParameters.setRollbackOnFailure(false);
302+
303+
// specify the layer IDs of the feature tables to sync (all in this case)
304+
geodatabase.getGeodatabaseFeatureTables().forEach(geodatabaseFeatureTable -> {
305+
long serviceLayerId = geodatabaseFeatureTable.getServiceLayerId();
306+
SyncLayerOption syncLayerOption = new SyncLayerOption(serviceLayerId);
307+
syncGeodatabaseParameters.getLayerOptions().add(syncLayerOption);
308+
});
309+
310+
// create a sync job with the parameters and start it
311+
final SyncGeodatabaseJob syncGeodatabaseJob = geodatabaseSyncTask.syncGeodatabase(syncGeodatabaseParameters, geodatabase);
312+
syncGeodatabaseJob.start();
313+
314+
// show the job's progress in the progress bar
315+
progressBar.setVisible(true);
316+
syncGeodatabaseJob.addJobChangedListener(() ->
317+
progressBar.setProgress((double) syncGeodatabaseJob.getProgress() / 100.0)
318+
);
319+
320+
// notify the user when the sync is complete
321+
syncGeodatabaseJob.addJobDoneListener(() -> {
322+
if (syncGeodatabaseJob.getStatus() == Job.Status.SUCCEEDED) {
323+
new Alert(Alert.AlertType.INFORMATION, "Geoatabase sync successful").show();
324+
} else {
325+
new Alert(Alert.AlertType.ERROR, "Error syncing geodatabase").show();
326+
}
327+
328+
progressBar.setVisible(false);
329+
allowEditing();
330+
});
331+
}
332+
333+
/**
334+
* Clears selection in all layers of the map.
335+
*/
336+
private void clearSelection() {
337+
map.getOperationalLayers().forEach(layer -> {
338+
if (layer instanceof FeatureLayer) {
339+
((FeatureLayer) layer).clearSelection();
340+
}
341+
});
342+
}
343+
344+
/**
345+
* Updates the download area graphic to show a red border around the current view extent that will be downloaded if
346+
* taken offline.
347+
*/
348+
private void updateDownloadArea() {
349+
if (map.getLoadStatus() == LoadStatus.LOADED) {
350+
// upper left corner of the area to take offline
351+
Point2D minScreenPoint = new Point2D(50, 50);
352+
// lower right corner of the downloaded area
353+
Point2D maxScreenPoint = new Point2D(mapView.getWidth() - 50, mapView.getHeight() - 50);
354+
// convert screen points to map points
355+
Point minPoint = mapView.screenToLocation(minScreenPoint);
356+
Point maxPoint = mapView.screenToLocation(maxScreenPoint);
357+
// use the points to define and return an envelope
358+
if (minPoint != null && maxPoint != null) {
359+
Envelope envelope = new Envelope(minPoint, maxPoint);
360+
downloadAreaGraphic.setGeometry(envelope);
361+
}
362+
}
363+
}
364+
365+
/**
366+
* Stops the animation and disposes of application resources.
367+
*/
368+
void terminate() {
369+
370+
if (mapView != null) {
371+
mapView.dispose();
372+
}
373+
}
374+
}

0 commit comments

Comments
 (0)