-
Notifications
You must be signed in to change notification settings - Fork 123
New sample: Edit features with feature-linked annotation #543
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Merge branch 'master' into dev
Merge pull request master into dev (following sketch on map sample implementation)
Merge branch "master" into "dev"
Merge master into dev following FeatureCollectionLayerQuery implementation
Update README to alert Java 11 users to potential exceptions which may occur when running the project. Providing a suggested workaround from the OpenJavaFX docs.
Update README.md
ListenableFuture<Void> editResult = selectedFeature.getFeatureTable().updateFeatureAsync(selectedFeature); | ||
editResult.addDoneListener(() -> { | ||
try { | ||
if (editResult.isDone()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need to check isDone
? I think just calling get()
will either work or throw an exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @sclaridge .
I think this looks good now. There's a couple of smaller things to fix and a few suggestions on documentation, but other than that it's nearly finished. I'll approve so that you can move on to get a second review once you've done the changes 👍
// set ST_STR_NAM value to the string from the text field | ||
selectedFeature.getAttributes().put("ST_STR_NAM", streetNameTextField.getText()); | ||
|
||
return selectedFeature; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work on tidying this up.
I've had a closer look and there's actually no need to return the selected feature here. This is because there's a handle on it in the main sample code already (from when we run mapView.identifyLayersAsync). So for the sake of simplicty let's convert back to return null;
...t_features_with_feature_linked_annotation/EditFeaturesWithFeatureLinkedAnnotationSample.java
Outdated
Show resolved
Hide resolved
clearSelection(); | ||
|
||
// identify across all layers | ||
ListenableFuture<List<IdentifyLayerResult>> identifyLayerResultsFuture = mapView.identifyLayersAsync(screenPoint, 1, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can be a bit more generous here and give a larger tolerance. That way it'll be a bit easier to select the lines.
ListenableFuture<List<IdentifyLayerResult>> identifyLayerResultsFuture = mapView.identifyLayersAsync(screenPoint, 1, false); | |
ListenableFuture<List<IdentifyLayerResult>> identifyLayerResultsFuture = mapView.identifyLayersAsync(screenPoint, 10, false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Increasing the tolerance forces you to zoom in to select a feature, as annotation layers are identified as the top most layer. A for loop can be added to ignore the annotation layers and select the feature. I think this implementation is cleaner, and is similar to the identify workflow in the Android sample.
// identify across all layers
ListenableFuture<List<IdentifyLayerResult>> identifyLayerResultsFuture = mapView.identifyLayersAsync(screenPoint, 10, false);
identifyLayerResultsFuture.addDoneListener(() -> {
try {
// get the list of results from the future
List<IdentifyLayerResult> identifyLayerResults = identifyLayerResultsFuture.get();
// for each layer from which an element was identified
for (IdentifyLayerResult layerResult : identifyLayerResults) {
// check if the layer is a feature layer, thereby excluding annotation layers
if (layerResult.getLayerContent() instanceof FeatureLayer) {
// get a reference to the identified feature
selectedFeature = (Feature) layerResult.getElements().get(0);
// check the geometry and select the feature
selectFeature(layerResult);
return;
}
}
// if the selected feature is a polyline with any part containing more than one segment | ||
// (i.e. a curve) | ||
if (part.getPointCount() > 2) { | ||
selectedFeature = null; | ||
// show message reminding user to select straight (single segment) polylines only | ||
new Alert(Alert.AlertType.WARNING, "Select straight (single segment) polylines only.").show(); | ||
// return early, effectively disallowing selection of multi segmented polylines | ||
return; | ||
} else { | ||
// select the polyline feature | ||
((FeatureLayer) layerResult.getLayerContent()).selectFeature(selectedFeature); | ||
selectedFeatureIsPolyline = true; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I personally think we can simplify this by turning the if-statment around. Doing an 'early return' is very much a C# pattern, as far as I'm aware.
// if the selected feature is a polyline with any part containing more than one segment | |
// (i.e. a curve) | |
if (part.getPointCount() > 2) { | |
selectedFeature = null; | |
// show message reminding user to select straight (single segment) polylines only | |
new Alert(Alert.AlertType.WARNING, "Select straight (single segment) polylines only.").show(); | |
// return early, effectively disallowing selection of multi segmented polylines | |
return; | |
} else { | |
// select the polyline feature | |
((FeatureLayer) layerResult.getLayerContent()).selectFeature(selectedFeature); | |
selectedFeatureIsPolyline = true; | |
} | |
// only select single segment lines | |
if (part.getPointCount() <= 2) { | |
// select the polyline feature | |
((FeatureLayer) layerResult.getLayerContent()).selectFeature(selectedFeature); | |
selectedFeatureIsPolyline = true; | |
} else { | |
selectedFeature = null; | |
// show message reminding user to select straight (single segment) polylines only | |
new Alert(Alert.AlertType.WARNING, "Select straight (single segment) polylines only.").show(); | |
} |
...t_features_with_feature_linked_annotation/EditFeaturesWithFeatureLinkedAnnotationSample.java
Outdated
Show resolved
Hide resolved
...t_features_with_feature_linked_annotation/EditFeaturesWithFeatureLinkedAnnotationSample.java
Outdated
Show resolved
Hide resolved
...t_features_with_feature_linked_annotation/EditFeaturesWithFeatureLinkedAnnotationSample.java
Outdated
Show resolved
Hide resolved
...t_features_with_feature_linked_annotation/EditFeaturesWithFeatureLinkedAnnotationSample.java
Outdated
Show resolved
Hide resolved
} catch (Exception e) { | ||
e.printStackTrace(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like there's any need for this try/catch block anymore, since all the asynchronous actions are now being done in other methods? In which case we can just remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sclaridge this is a really great effort on this rather complex sample!
My comments here so far are more from a readability/code tidiness perspective and I think there's some more we can do here to neaten up this sample. Let's have a pair programming call and I can talk you through them.
To discuss:
- Java SE samples and single use methods
- edge case user input control
- sample bug when moving polylines (it looks like additional polylines are being created, rather than just an individual line being moved)
- samples-data directory and some background there for the settings.gradle and build.gradle file (including removing the actual geodatabase from this repo)
- garbage collection
editing/edit-features-with-feature-linked-annotation/build.gradle
Outdated
Show resolved
Hide resolved
|
||
## How to use the sample | ||
|
||
Pan and zoom the map to see that the text on the map is annotation, not labels. Click one of the address points to update the house number (AD_ADDRESS) and street name (ST_STR_NAM). Click one of the dashed parcel polylines and click another location to change its geometry. NOTE: Selection is only enabled for points and straight (single segment) polylines. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this description doesn't include actually moving an address point on the sample. This threw me initially as I clicked "OK" or "Cancel" and then clicked on the map/another point: wasn't expecting the address point to move too. We should add in some additional documentation for this:
Pan and zoom the map to see that the text on the map is annotation, not labels. Click one of the address points to update the house number (AD_ADDRESS) and street name (ST_STR_NAM). Click one of the dashed parcel polylines and click another location to change its geometry. NOTE: Selection is only enabled for points and straight (single segment) polylines. | |
Pan and zoom the map to see that the text on the map is annotation, not labels. Click one of the address points to update the house number (AD_ADDRESS) and street name (ST_STR_NAM). Once you have edited the feature attributes, click "OK" and then click again on the map to move the address point to a new location. You can also click one of the dashed parcel polylines and click another location to change its geometry and update its annotation (distance in feet). NOTE: Selection is only enabled for points and straight (single segment) polylines. |
) | ||
ant.unzip( | ||
src: file("./data.zip"), | ||
dest: file("./src/main/resources/edit_features_with_feature_linked_annotation/loudon") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment in Jon's review, but just in case it gets lost: samples data should always be saved into a samples-data directory. The resources are reserved for UI or style files.
dest: file("./src/main/resources/edit_features_with_feature_linked_annotation/loudon") | |
dest: file("./samples-data/loudon") |
...java/com.esri.samples.edit_features_with_feature_linked_annotation/EditAttributesDialog.java
Outdated
Show resolved
Hide resolved
// create feature layers from tables in the geodatabase | ||
addressPointFeatureLayer = new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("Loudoun_Address_Points_1")); | ||
parcelLinesFeatureLayer = new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("ParcelLines_1")); | ||
// create annotation layers from tables in the geodatabase | ||
addressPointsAnnotationLayer = new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("Loudoun_Address_PointsAnno_1")); | ||
parcelLinesAnnotationLayer = new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("ParcelLinesAnno_1")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by the time we're using local variables for the FeatureLayer's here (see comment above), that's a lot of references to the word FeatureLayer in this block of code. For situations like this, we can make use of Java 10's keyword "var". This replaces the type information when declaring local variables and clears the readability up a little.
Also since these are quite long lines of code, we should stagger them a little:
// create feature layers from tables in the geodatabase | |
addressPointFeatureLayer = new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("Loudoun_Address_Points_1")); | |
parcelLinesFeatureLayer = new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("ParcelLines_1")); | |
// create annotation layers from tables in the geodatabase | |
addressPointsAnnotationLayer = new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("Loudoun_Address_PointsAnno_1")); | |
parcelLinesAnnotationLayer = new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("ParcelLinesAnno_1")); | |
// create feature layers from tables in the geodatabase | |
var addressPointFeatureLayer = | |
new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("Loudoun_Address_Points_1")); | |
var parcelLinesFeatureLayer = | |
new FeatureLayer(geodatabase.getGeodatabaseFeatureTable("ParcelLines_1")); | |
// create annotation layers from tables in the geodatabase | |
var addressPointsAnnotationLayer = | |
new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("Loudoun_Address_PointsAnno_1")); | |
var parcelLinesAnnotationLayer = | |
new AnnotationLayer(geodatabase.getGeodatabaseAnnotationTable("ParcelLinesAnno_1")); |
new Alert(Alert.AlertType.ERROR, "Error loading Geodatabase.").show(); | ||
} | ||
}); | ||
geodatabase.loadAsync(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for a readability perspective, it makes more sense for us to move this loadAsync
call up (to above the geodatabase.addDoneLoadingListener()
even though it doesn't make much of a difference from a code execution perspective.
}); | ||
geodatabase.loadAsync(); | ||
|
||
// set on click behaviour |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with moving the logic from the single use method directly into this listener, we can borrow some of the original method description and use it here instead:
// set on click behaviour | |
// select the nearest feature from where the user clicked, or move the selected feature to the given screen point |
|
||
// set on click behaviour | ||
mapView.setOnMouseClicked(event -> { | ||
// check that the primary mouse button was clicked |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can remove this comment: the code below it is clear enough to describe it is checking the primary mouse button was clicked.
// check that the primary mouse button was clicked |
// create a point where the user clicked | ||
Point2D screenPoint = new Point2D(event.getX(), event.getY()); | ||
// call select or move method to move to the point | ||
selectOrMove(screenPoint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's common with the Android implementations to have single use methods, but with Java SE samples we don't do this as often. The logic in this method isn't too verbose, so let's just remove the method and insert the code from that method directly here instead.
selectOrMove(screenPoint); | |
// if a feature hasn't been selected, select the feature | |
if (selectedFeature == null) { | |
identifyFeature(screenPoint); | |
} else { | |
// convert the screen point to a map point | |
Point mapPoint = mapView.screenToLocation(screenPoint); | |
// if the feature is a polyline, move the polyline | |
if (selectedFeatureIsPolyline) { | |
movePolylineVertex(mapPoint); | |
} else { | |
// if the feature is a point, move the point | |
movePoint(mapPoint); | |
} | |
}``` |
GeometryEngine.nearestVertex(polyline, (Point) GeometryEngine.project(mapPoint, polyline.getSpatialReference())); | ||
|
||
// get the part of the polyline nearest to the map point | ||
Part part = polylineBuilder.getParts().get((int) nearestVertex.getPartIndex()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than casting this long
to an int
(which could result in a stack overflow if the long
is greater than the maximum allowed int
value), we can use the following method which would throw an informative exception in case of the above happening. Very unlikely in this sample but good practice I believe.
Part part = polylineBuilder.getParts().get((int) nearestVertex.getPartIndex()); | |
Part part = polylineBuilder.getParts().get(Math.toIntExact(nearestVertex.getPartIndex())); |
List<Part> parts = polylineBuilder.getParts(); | ||
parts.forEach(part -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we don't use the variable parts
more than once, we can directly add the forEach like so:
List<Part> parts = polylineBuilder.getParts(); | |
parts.forEach(part -> { | |
polylineBuilder.getParts().forEach(part -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sclaridge this is looking great. I've suggested just a few really minor comment updates.
Before merging, please can you ensure the rogue geodatabase is deleted from this PR? https://github.com/Esri/arcgis-runtime-samples-java/blob/924c240f55b27603be178aef26019340df599dc7/editing/edit-features-with-feature-linked-annotation/src/main/resources/edit_features_with_feature_linked_annotation/loudon/loudoun_anno.geodatabase
Once you've done this I'm happy for you to go ahead and merge. Good job!
var parcelLinesAnnotationLayer = new AnnotationLayer( | ||
geodatabase.getGeodatabaseAnnotationTable("ParcelLinesAnno_1")); | ||
|
||
// add the feature layers and annotation layers to the map |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just some wordsmithing here:
// add the feature layers and annotation layers to the map | |
// add the feature and annotation layers to the map |
|
||
// if the feature is a point, move the point | ||
} else if (selectedFeature.getGeometry().getGeometryType() == GeometryType.POINT) { | ||
// set the selected features' geometry to a new map point |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correcting apostrophe
// set the selected features' geometry to a new map point | |
// set the selected feature's geometry to a new map point |
// update the selected feature's feature table | ||
updateAttributes(selectedFeature); | ||
|
||
// clear the selected feature |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a suggestion: I think this might be a littler clearer, but up to you if you want to accept this change!
// clear the selected feature | |
// remove the selection of the feature |
Hello,
Some areas I would like some feedback are:
the
identifyFeature()
method identifies a feature in the topmost layer. This implementation handles the case where the user moves a point very close to a line, then wants to move either feature later. Otherwise, an exception is thrown when both features are selected, then moved.I created
updateAtttributes()
to update the feature when it is moved, but the logic to update the user entered attributes is located withinEditAttributesDialog
in the samples review, I received feedback that the sample does not need to prompt the user if they want to move the point, as that is outlined in the README.
is README Java specific enough?
Thanks!