ArcGIS Blog

Developers

ArcGIS Maps SDK for Java

Using JavaFX Properties with the ArcGIS Maps SDK for Java

By Luis Flores Carrubio

This blog will help you discover the advantages of using JavaFX properties with the ArcGIS Maps SDK for Java to make great UI experiences while increasing your productivity as a developer.

Introduction

The release of version 200.0 of the ArcGIS Maps SDK for Java shipped with new features, designed to help developers create modern interactive Java based mapping applications. The Java Maps SDK now includes JavaFX properties on the GeoView class (the parent class to the MapView and SceneView classes which display a map or a scene), and classes which implement the loadable interface such as ArcGISMap and ArcGISScene.

  1. What is a JavaFX property?
  2. JavaFX property bindings
  3. JavaFX properties in the Java Maps SDK
  4. Unidirectional property bindings
  5. Bidirectional property bindings
  6. Using the Bindings API
  7. JavaFX property listeners
  8. JavaFX property listeners in the Java Maps SDK
  9. Why make the change to JavaFX properties?
  10. Summary

What is a JavaFX property?

A JavaFX property is an observable object whose attributes are defined in a class. The member variables are read-write or read-only property data types that are themselves wrapper classes for the primitive types. These wrappers provide the necessary functionalities for properties to become observable and participate in binding expressions. Moreover, the methods in the class used to access the property must follow the JavaFX bean pattern designed to complement the model-view-controller pattern and expand the capabilities of the JavaBeans API. The following code example showcases how a read-write object property can be defined:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;

public class SomeClass {

  // Define a variable to store the property
  private ObjectProperty<SomeType> somethingProperty = 
    new SimpleObjectProperty<>(bean, name, initialValue);

  // Define a getter for the property itself
  public ObjectProperty<SomeType> somethingProperty() {
    return somethingProperty;
  }

  // Define a getter for the property’s value
  public SomeType getSomething() {
    return somethingProperty.get();
  }
  
  // Define a setter for the property’s value
  public void setSomething(SomeType something) {
    somethingProperty.set(something);
  }
}

In this example, the object property is defined using a three-parameter constructor. This constructor takes an object representing the property owner, the object property name, and the initial value of the wrapped value. The current value of the property can be obtained using the getSomething() method and the property getter somethingProperty(), returns the property providing additional functionalities to register listeners and monitor its value. Accordingly, JavaFX properties have become essential because of the functionalities they provide and consequent advantages when creating interactive GUI applications.

JavaFX property bindings

JavaFX properties can participate in binding expressions because they are observable objects. They can be bound to other properties and have their values automatically updated when changes are detected. This is helpful for keeping the graphical user interface display synchronised with the application’s underlying data.

A binding defines a relationship between a dependent variable and its dependencies. There are various ways of expressing binding relationships with the most common achieved through using the property object methods. These include the bind() and bindBidirectional() methods corresponding to unidirectional and bidirectional binding, respectively. The bind() method enables a dependent property to update only when the value of a dependency changes. Additionally, the property value is not allowed to be set once it has been bound unidirectionally. The only way of setting its value again is to unbind the property using the unbind() method. In contrast, the bindBidirectional() method makes properties depend on each other updating their values when a change is detected in either of them. This type of binding also allows for properties to have their values set before or after the binding takes place. Note that read-only properties cannot be modified directly with setters, but the value of the property can change and therefore able to participate in binding expressions.

JavaFX properties in the Java Maps SDK

JavaFX controls like progress indicators are used to show visual information on the progress of an ongoing task. These can be useful in applications that display data layers loaded from a web service or when the view elements of an ArcGISMap or ArcGISScene are being drawn. In previous versions of the Java Maps SDK, the visibility of progress indicators needed to be manually set depending on the status of a task. However, with the addition of custom JavaFX properties, binding expressions can be created that take on the responsibility of updating control property values automatically.

An additional feature of JavaFX properties is the ability to register listeners to them. A common use case scenario in the Java Maps SDK is when code needs to execute when there is a change in the load status of a loadable resource. The registered listener will trigger when the load status changes allowing you to obtain the old and new values of the property which can then be used to execute the appropriate code. With the Java Maps SDK, the use of property listeners offers an alternative to done loading listeners which are invoked only when a loadable resource has finished loading.

Many of the sample applications in the GitHub repository of the ArcGIS Maps SDK for Java showcase the use of properties. Let’s step through a couple of examples.

Unidirectional property bindings

The following binding expression from the WMS Layer (URL) sample shows how to bind the visibleProperty() of a progress indicator to the loadStatusProperty() of a WMS layer:

progressIndicator.visibleProperty().bind(
wmsLayer.loadStatusProperty().isEqualTo(LoadStatus.LOADING));

This binding relationship causes the progress indicator to show when the WMS layer is loading. Furthermore, the indicator is not displayed for any other possible status of the loadable resource. Other binding expressions can be created depending on the values of the loadStatusProperty(). This can be useful when using property listeners discussed in a later section.

The Add Dynamic Entity Layer sample, released in version 200.1.0 of the ArcGIS Maps SDK for Java, showcases how to use unidirectional and bidirectional binding expressions. As an example, a unidirectional binding between two JavaFX controls is used to bind a slider disableProperty() to the selectedProperty() of a checkbox. The slider control is then disabled if the checkbox is not selected. The following binding expression describes the unidirectional relationship between the slider and the checkbox:

observationsSlider.disableProperty().bind(
observationsCheckBox.selectedProperty().not());

In this scenario, the slider disableProperty() updates when its dependency, the checkbox selectedProperty(), changes its value and not the other way around. The result can be seen below in the GIF of the application where the slider remains disabled until the previous observations checkbox is selected. The slider control also has a value property that is often used in bidirectional binding expressions. The next section contains an example on how to use bidirectional bindings.

Example of a unidirectional binding between a checkbox and a slider control.

Bidirectional property bindings

Bidirectional binding is particularly suited for keeping UI view components synchronised with model data. An example can be seen below in the GIF from the Add Dynamic Entity Layer sample showing how the previous observations are updated as the value of the slider control changes. This relationship can be described with the following binding expression:

observationsSlider.valueProperty().bindBidirectional(
dynamicEntityLayer.getTrackDisplayProperties().
maximumObservationsProperty());

In this case, the valueProperty() of the slider is bound bidirectionally with the maximumObservationsProperty() of the dynamicEntityLayer. When the value of the slider changes, the maximum observations are updated to this new value. Additionally, if the maximumObservationsProperty() changes, the slider also updates. A unidirectional binding would not achieve the desired result because it would describe the valueProperty() depending on the maximumObservationsProperty() locking the slider to one value. If the properties were to be swapped, then a unidirectional binding applies:

dynamicEntityLayer.getTrackDisplayProperties().
maximumObservationsProperty().bind(observationsSlider.valueProperty());

For this expression, trying to set the maximumObservationsProperty() after the binding results in a runtime exception error because it has been bound to the slider valueProperty(). In these occasions, a bidirectional binding is preferred because when a property is bound unidirectionally, its dependencies can have a default value which can update the dependent property value after it has been set prior to the binding. Additionally, the value of some properties can change internally potentially creating errors if a unidirectional binding is used.

Using the Bindings API

The two binding methods described above are ideal for properties with the same data types. However, binding relationships are not always straightforward and other expressions might be required to describe more complicated relationships. In these cases, the properties can be manipulated to achieve the desired binding relationship with the use of the Bindings API. This API contains a variety of methods that can be used to manipulate observable values for the purpose of creating binding expressions with properties of different data types.

The following expression (also from the Add Dynamic Entity Layer sample) binds unidirectionally the textProperty() of a label to the connectionStatusProperty() of an ArcGISStreamService. Looking at the application’s GIF below, a button is pressed to toggle the connection to the stream service but the button itself does not participate in the binding. The button press action is required to change the value of the connectionStatusProperty() that participates in a binding with the textProperty(). The Bindings API allows the connectionStatusProperty() to be passed into a lambda function and then formatted by the supplied function to produce the string used for the binding. This is necessary because of the type mismatch between string and object properties. The label continues to be updated throughout the life of the application once it has been bound:

connectionStatusLabel.textProperty().bind(
Bindings.createStringBinding(() -> "Status: " +
streamService.connectionStatusProperty().getValue(),
streamService.connectionStatusProperty()));

A similar expression is seen in the sample application to bind the textProperty() of a label to the maximumObservationsProperty(). Here, the createStringBinding() method is used again to bind two properties of different types:

textProperty().bind(
Bindings.createStringBinding(() -> "Observations per track (" +
dynamicEntityLayer.getTrackDisplayProperties().
maximumObservationsProperty().getValue() + ")",
dynamicEntityLayer.getTrackDisplayProperties().
maximumObservationsProperty()));

The result of this expression can be seen in the GIF below showing the text of the label representing the number of previous observations updating as the slider is moved.

Example of a bidirectional binding between a slider control and the number of dynamic entity observations.

The Create and Edit Geometries sample shows a complex graphical user interface (see below image) that follows the model-view-controller design pattern and benefits from the use of JavaFX properties to bind multiple UI controls. As an example, the following expression binds unidirectionally the disableProperty() of the delete all geometries button to the geometry editor startedProperty() and to whether there are any graphics in the graphics overlay:

deleteAllGeometriesButton.disableProperty().bind(
Bindings.when(geometryEditor.startedProperty()).then(true).
otherwise(graphicsOverlay.getGraphics().isEmpty()));

This sample application contains other interesting bindings that exercise the capabilities of the Bindings API to achieve more complex binding relationships.

The Create and Edit Geometries sample application incorporates a complex graphical user interface with user interactions handled by many JavaFX property bindings.

JavaFX property listeners

JavaFX properties are observable objects that obtain their capabilities from extending various interfaces. Internally, bindings are possible because of change and invalidation listeners which fire when a property value changes, or a binding becomes invalid, respectively. The main difference between these is on when the property value is recalculated and if eager or lazy evaluation is desired. Eager evaluation refers to recomputing a binding when dependencies change whereas lazy evaluation triggers the operation only when asked for the value. Change listeners are of particular interest because they are useful when a block of code needs to be defined ahead of time. Additionally, registering a change listener allows monitoring the observable new and old values but enforces eager evaluation.

JavaFX property listeners in the Java Maps SDK

The Web Map Tile Service Layer (WMTS) sample registers a change listener to the WMTS service load status property in favour of using a done loading listener. This can be achieved by using the property object addListener() method which enables listening for load status changes to run the appropriate code depending on the property value. The values of this property are “LOADED”, “NOT_LOADED”, “LOADING”, or “FAILED_TO_LOAD” status. If the value of the property changes to “LOADED” then an alert is displayed and the WMTS layer is displayed as the basemap (see image below). However, if the service is in “FAILED_TO_LOAD” status, then an alert is displayed with information obtained from the value of the WMTS service loadErrorProperty().

Example of the Web Map Tile Service Layer sample application using property listeners to display message alerts.

The following code showcases how to register a change listener to the loadStatusProperty() and how to use the property value to allow a particular block of code to execute after the value of the property changes:

wmtsService.loadStatusProperty().addListener((
observable, oldValue, newValue) -> { 
  switch (newValue) { 
    case LOADED -> 
      // Create WMTS layer, set it to the basemap, and display alert 
      new Alert(Alert.AlertType.INFORMATION, 
      "The WMTS layer has loaded.\n").show(); 
    case FAILED_TO_LOAD -> 
      new Alert(Alert.AlertType.ERROR, "Failed to load WMTS layer.\n" +
      wmtsService.loadErrorProperty().get().getCause().
      getMessage()).show(); 
  }; 
});

Why make the change to JavaFX properties?

Using JavaFX properties in your mapping applications enables writing simpler code with the necessary flexibility to support complex graphical user interfaces. The ArcGIS Maps SDK for Java supports JavaFX properties allowing you to modernise your applications and benefit from these advantages. The ability to bind the new custom properties to JavaFX controls offers more possibilities while reducing the complexity of your code and potentially obtaining performance gains.

For example, the Realistic Lighting and Shadows application (see gif below) benefits from using bindings to improve code logic and readability while reducing the total amount of lines in the application. Let’s have a look at how this is accomplished.

The first change is to bind bidirectionally the valueProperty() of a combo box to the sunLightingProperty():

comboBox.valueProperty().bindBidirectional(
sceneView.sunLightingProperty());

The sunLightingProperty() can have  “NO_LIGHT”, “LIGHT”, and “LIGHT_AND_SHADOWS” values which are the options displayed in the combo box to control the ambient light and shadows in the application. Additionally, a default selection value for the combo box is required because the sunLightingProperty() is a non-nullable property.

Example of using the Bindings API to bind properties of different data types reducing the amount of code needed to support user interface interactions.

The alternative option to using this binding is to register a change listener to the selectedItemProperty() of the combo box and then set the sun lighting each time the listener fires:

comboBox.getSelectionModel().selectedItemProperty().addListener(e -> {
  sceneView.setSunLighting(comboBox.getSelectionModel().
  getSelectedItem()); 
});

A second change is to bind unidirectionally the textProperty() of a label to the sunTimeProperty() for updating the label value as the Sun’s position changes. The createStringBinding() method of the Bindings API is used because the sunTimeProperty() is an object property that wraps a Calendar object. Consequently, the object property value needs to be formatted into a string that can bind to the textProperty():

timeLabel.textProperty().bind(Bindings.createStringBinding(() -> { 
  return dateFormat.format(sceneView.sunTimeProperty().
  getValue().getTime()); 
}, sceneView.sunTimeProperty()));

The alternative version sets default values for the label and sun time using the setText() and setSunTime() methods, respectively. These methods are then used again in the  updateTimeOfDay()  method (see image below) to set new values as the slider changes.

The sunTimeProperty() participates in another unidirectional binding with the valueProperty() of the slider to update the time. The createObjectBinding() method is used because the valueProperty() is a double property which does not match the sunTimeProperty() object type. To solve this, the slider property value is passed to the updateCalendar() method returning an updated Calendar object when the slider value changes:

sceneView.sunTimeProperty().bind(Bindings.createObjectBinding(() -> {
  return updateCalendar(timeSlider.valueProperty().
  getValue().intValue());
}, timeSlider.valueProperty()));

This workflow avoids registering a change listener in the updateTimeOfDay() method to the valueProperty() of the slider and having to set the label and sun time values in favour of using bindings.

The above changes result in the new updateCalendar() method to become significantly shorter than the original updateTimeOfDay() method seen in the image below. Furthermore, the entire application contains sixteen lines less of code because of using properties. As observed in these examples, incorporating JavaFX properties and bindings into your applications can significantly improve code readability and increase developer productivity. This is particularly noticeable when figuring out which UI controls modify the underlying data of your application.

Code differences in the updateTimeOfDay() method after using properties in the Realistic Lighting and Shadows sample application.

Summary

The repertoire of custom JavaFX properties that are available in the Java Maps SDK continues to grow. Look out for future releases of the ArcGIS Maps SDK for Java to include support for properties involving collections.

If you’re new to the ArcGIS Maps SDK for Java and want to have some fun testing out JavaFX properties in your app, head to the Java Maps SDK documentation! There you can sign up for a free developer account and gain access to everything you need to get started building your apps. You can also check out our starter app and sample GitHub repositories.

You can find more information on JavaFX properties in the following sites:

 

Share this article