1 Introduction

In this module, we will discuss the following concepts:

  1. The GEE objects used in generating a graphical user interface.
  2. How to develop panels with interactive elements.
  3. How to connect geoprocessing elements to interactive elements.

2 Background

Over the past ten modules we have shown that Google Earth Engine can be a significant and efficient resource for visualizing and running analyses with remotely sensed data. Given the range of topics addressed over these modules it is also clear that many different fields of science and management can benefit from the application of remotely sensed data. That said, being comfortable enough to engage with GEE and create your own scientifically sound analysis is something that not everyone will have the time or energy to do. More often than not, project partners are not going to be excited if the final deliverable they receive is simply a bunch of code. In this module we will cover the process of creating a Graphical User Interface (GUI) that will allow a more straightforward, point-and-click experience of the geoprocessing that the code is performing.

2.1 About the Data

For this module, we will be adapting a preexisting project completed by the Colorado NASA DEVELOP program in Summer of 2018. The DEVELOP team created a GUI in GEE that allowed National Park Service employees to observe changes in fluvial and vegetative morphology of flood plains along stretches of the Colorado, Yampa, and Green Rivers in Utah and Colorado. The project used data from Landsat 5, Landsat 7, Landsat 8, and Sentinel 2 to provide a visualization of 32 years of morphological changes within these river systems. The analysis performed within the code is relatively simple because the goal of the project was to provide end users with a resource that was easy to understand and flexible enough to allow them to ask their own questions with the data. In this module we will adapt this work to visualize the changes in vegetation in three major river deltas from around the world. There are many interesting scientific questions that can be asked with the remote sensing of deltas. But this module will focus on the technical explanation of the GUI development more than the details of the scientific methods being presented.



3 Understanding the Graphical User Interface

The Google Earth Engine Code Editor is a graphical user interface on its own. While we cannot alter the base elements (map, task pane, etc.), we can add to their functionality. Depending on the need, one can add three different classes of elements (widgets, panels, and events) to the existing GUI. These three class elements are interconnected. To learn a bit more about each element we will use an example where we ask a simple question about the capabilities of a Landsat image.

Using NDVI derived from Landsat can we effectively distinguish the “A” on the hogback just west of Fort Collins from the surrounding vegetation in the summer?*


We will begin with simplified examples to provide a conceptual understanding of how the elements of GUI development work in GEE. Once we cover these elements we will move on to a more complex presentation of this material by reproducing the River Morphology Evaluation Toolbox (RMET) code to map the vegetation change of river deltas across the Landsat 5-8 collection timeframe (1984 to present).

3.1 Widgets

Widgets are the base level functions for adding interactivity to your code. There are many widgets available to choose from. Before continuing this module, we highly suggest visiting the documentation for widgets and running through all the presented examples by copying and pasting the code examples into a GEE script.

To demonstrate a widget, we are going to generate a histogram of values from the region surrounding the “A” in the image below to see if there is a bimodal pattern to the distribution due to the white color of the “A”.



A high resolution, less than one meter, image of the hillslope on the outskirts of Fort Collins, CO that has a large white A painted on it each year.




A histogram of NDVI values within the black square geometry feature that encompasses the A.

The end result of our script is a histogram printed in the console tab. You can alter the geometry object’s area and the date range of the imagery to further refine the data and potentially get at the answer to our question. As the data are presented now it appears that the “A” is neither large enough or white enough to be easily differentiated from the surrounding landscape. The small peak in value at the very low end of the distribution is due to the water.

3.2 Panels

Panels and Layouts are used to generate containers at different places in the map, which can be used to hold specific widgets. If no panels are specified, most widgets appear in the console, as shown in the previous example. By applying panels, you can create a layout that emphasizes the use of the map and not the code. Look at the example from the RMET tool below.



The RMET tool uses a panel to hold multiple widgets that allow the user to ask questions without engaging with the code.

The end user need only to hit the run button. Then, all the elements they need to engage with the code will be presented and described on the panel. They can effectively ignore the code and console and focus on the map. To learn more, we suggest reviewing the documentation of panels and layouts to learn more.

As we mentioned above, printing elements to the console is great for individuals who are using the code editor. But for individuals who will not be editing the code, it is best to place the elements of interest on the map. In the script below, we create a panel to store text about what we are showing and hold our histogram for more convenient viewing. Copy this code into your code editor and run it to view the changes.

While panels do not have much to offer in terms of analytical processes they are great resource for organization.



A histogram of NDVI values within the black square geometry features added to a panel on the map.

We can see that the information in the histogram has not changed, it is just presented in a different location.

3.3 Events

Events are triggers that perform a predefined action once selected by the user and are associated with widgets or map functions. The example below incorporates a widget, panel, and event. The panel is used to display the result of the event. In this case the mean NDVI at a user selected location. More information about events can be found in the Events Documentation.

When the user clicks on a location on the map, the Map.onClick() event passes coordinates into the function. This will then run the remaining elements of the function. The first widget inspector.widgets() is used to acknowledge that the users input, through clicking, was received. The loading message should help stop the user from continuously clicking and thereby prevent them from restarting the event process. Once the underlying geoprocessing code has returned a value, the inspector.widgets() is used again to replace the “loading” statement with the value the user requested.



When the user clicks on locations of interest, the on.Click() event function will return the mean NDVI value for the location.

The geoprocessing is taking place within a function which means the variables within the function (point, meanNDVI, sample, etc.) only exist within that function. You can test this by attempting to print the computedValue variable.

This will return the warning message "computedValue is not defined in this scope." What this means is that the variable is not defined in the local environment, so GEE does not have any information to print. This can get confusing but is an important element to understand when working with GUI development.

3.4 Tying it all Together

As these examples have shown, the development of a GUI in GEE can quickly become a complex process. In order to work efficiently with GUI development, it is important to have a solid understanding of Functions and Client vs. Server side environments. More detail on both these topics can be found in the online documentation.;

Client Side Objects are native JavaScript elements that exist in the local environment.

Server Side Objects are JavaScript wrappers that are used to communication information to the functions that we never see on Google’s Servers.

To illustrate this let us look at a previous code chuck for defining the area of interest. Note: You do not need to run this section of code

The numerical values of the coordinates and the string ‘geometry’ text and even the Map.addLayer function are all client side objects. When we call ee functions we are creating elements that need to communicate with the server to work as we expect them to. In this example, ee.Geometry.Polygon is a server side object. The server side objects do not mean anything in JavaScript themselves. They are used as references, basically a set of instructions for what the servers that run GEE should do within this request.

For example, this text is a JavaScript string (“aString”). Yet, if you tried to call the ee.String() function length() on this JavaScript string you would get an error “JavaScriptString.length is not a function”. You would need to change your client side string “aString” to a server side string object by calling ee.String("aString") before you could apply any of the ee.String functions.

4 RMET: River Monitoring and Evaluation Tool

The RMET script created by the NASA DEVELOP team was developed over a 10 week term by individuals with previous experience using GEE for spatial analysis, yet limited experience with GUI development. The code itself is more than 1,000 lines and contains over 60 unique functions. We will not be examining the full functionality of the RMET code within this module but we will pull some important elements from the RMET code to construct our own GUI: Delta Watch. The full code for a version of the RMET product can be found here.

4.1 RMET Components

If we load the RMET code using the link above and run it, we can see many user defined options in the panel on the left side of the map. An important thing to notice is that nothing is added to the map after running the script. In effect, all actions of the RMET code are dependent on a user’s input.

Before digging into the code, look at each element of the GUI. Our first goal is try to understand what each GUI element is actually doing. From this, we will want to understand what is required in order for that element to function as expected.

Select Area of Interest

This button allows the user to define a study area based on preloaded features and also gives the option to import their own study area.

Requirements

  • Geometry features of the predefined study areas

  • Mechanism for importing your own geometry

Select Year for Landsat 30m Binary Map

This step allows the user to define a year of interest, which then creates a binary image and adds it to the map. The user can adjust the thresholds for the binary distinction.

Requirements

  • Use the input year to select specific data

  • Load all Landsat data from 1984 to 2018

  • Generate a specific image based on the selected index

  • Remake the image based on the user defined threshold value

  • Visualize elements

Select Years for Landsat 30 m Change Map

This step produces a change map by comparing differences in images between two user-defined years for a given index.

Requirements

  • Use the input year to select specific data

  • Load all Landsat data from 1984 to 2018

  • Generate two images based on the selected years and index

  • Perform a differencing between the data.

  • Visualize elements

The last two steps of the RMET GUI are effectively the same products (binary maps and change maps) but for a different sensor.

In an attempt to stay organized, here is a general list of the elements we need to incorporate into our GUI in order to match the functionality of the RMET tool

A List of the Required Elements of the Delta Watch GUI

  • Define the area of interest

  • Define year(s) of interest

  • Bring together imagery

  • Generate indices from imagery

  • Compare imagery

  • Visualize imagery

4.2 Experiment with the Interface

Before we jump into the details of how the code is functioning, we need to spend some time testing the different elements in the GUI. For example, if you want to clear all the features you have added to the map simply refresh the page and rerun or hit the reset map button at the bottom.



The RMET tool showing an area of vegetation encroachment between 1985 and 2018 in Dinosaur National Park.

The image above shows an area in the northern most reaches of the Colorado River in Dinosaur National Park where vegetation has encroached on the river over time effectively narrowing the channel. This type of channel narrowing dramatically reduces the stream bed complexity and is having negative impacts on the breeding habitat of multiple endangered fish within this river. This is the type of information that can help park managers qualitatively assess what effects the controlled flow releases by the nearby Flaming Gorge dam are having on the habitat of the endangered fish species.

4.3 The Logic Behind it All

Hopefully you had some fun looking at the functionality of this tool. It gives the user a lot of power to ask their own questions with remotely sensed data. As we start to dig into how this all happens you will notice a common theme: nearly every piece of code in RMET is a function. Functions are essential because they allow you to have flexible inputs. The necessity of this will become clear as we work through more examples below. We first covered functions in Module 9.

Rather than simply walking through the elements of the RMET code, we are going to adapt the existing code base to create our own GUI, “Delta Watch”. This GUI will be a simplified version of the RMET code and will use Landsat data to track changes in NDVI over time for three major river deltas around the world.

5Delta Watch”, an RMET Adaptation.

Much of the complexity of the RMET code comes from the fact that it does so much. By looking at multiple indices across multiple sensors you end up having to write out many unique functions. For our adaptation, we are going to simplify the process in the following ways:

The other elements of the RMET code will stay the same in this new addition.

It is essential to know what you want the GUI to do before you start creating it. As you will see, GUI development is an inherently interrelated process. This means the GUI and the geoprocessing components need to be developed at the same time in order to troubleshoot how they are working together. The more planning you do up front, the more time it will save you in the long term. Through the rest of this module, we will be describing each GUI element and its interrelated geoprocessing methods at the same time. This process reflects how you would build a GUI yourself: add an element, see it if works, move on to the next element.

5.1 Global Variables

It is a bit tricky to conceptualize the idea that elements of your script can exist exclusively in a function. Yet, because we will rely so heavily on functions, it becomes essential to understand when you need to connect elements that exist inside a function to other elements in your local environment. A way of building this relationship is by declaring variables in your script, then defining those variables in the functions. Below is an example of the variables that are defined within a function but are called upon by other elements in the script that exist in the local environment.

We will refer to these elements as global variables because they are present in both the server and client side environments.

Clear your script in the code editor so you are working with a clean slate.

If you were building this code from scratch, you would add these variable as needed. All these variables will be declared within functions at some point in the code.

5.2 Visualization Parameters

Much like the global variables, defining the visualizing parameters is something you will do as the code develops. That said, for clarity and organizational purposes we will declare all the visualization elements at the beginning of the script.

At this point, all the predefined variables that will be used in the code are present. We will now move on to developing the functionality of “Delta Watch”.

5.3 Visualizing the Graphic User Interface.

As part of the planning process, it is a good idea to draw out what you want the end product to look like in as much detail as possible. In this case, we can create a simplified diagram that shows the component parts of the GUI we will be creating.



Example conceptual diagram of what the “Delta Watch” GUI will do.

The first component ‘title’ and ‘description’ are not connected to any geoprocessing so we will start there.

The first step is creating the panel and adding it to the map. The ‘0’ in the ui.root.insert() function will place the panel on the left side of the map. A value ‘1’ will place on the right. We then define ui.Label elements that contain the text we want for the title and description of the project. The final line adds these elements to the panel and order matters. With this we can run the code and the title and description will appear on the new panel on the map.



This initial step of the GUI development. A panel with descriptive text about the function of the tool we are building.

5.4 Defining the Area of Interest

For this project we are examining three deltas around the world

Colorado River Delta, USA/Mexico The Colorado River is a heavily dammed and over appropriated water body. This means that more often than not the river does not make it to the delta. In recent years however, actions have been taken to ensure water from the Colorado is reaching the Sea of Cortez. With “Delta Watch” completed, we can check to see if these flows have been having an effect on vegetation in the delta.

Nile River Delta, Egypt One of the most consistently farmed agricultural areas in the world, the Nile River delta was unique for its consistency in water flow over time. However, the construction of the Awsan High Dam in the 1960s has dramatically altered the flow regimes of the river and it is possible that Landsat imagery will capture some geomorphic events that are still occurring as a result of the change in flow regimes over 50 years ago.

Betsiboka River Delta, Madagascar The Betsiboka River is an undammed waterway in north central Madagascar. It is known for its unique water color and very large delta. The color of the water and character of the delta are due to massive deforestation which began in the 1950s within the watershed. As a result of change in sediment load and natural flow regime, we can expect to see a very dynamic deltaic system at the mouth of the Betsiboka.

This code generates simple geometry features to define our areas of interest and generates labels that we will use in the drop down box from area of interest(AOI) selection.

In the script above, we use the ui.select() widget to generate a drop down box that lists the three variable representing our three study areas. We then create a label to identify what should be done at this step. After this, we add our widget and label to the existing panel feature. The order in which we add the elements to the panel determines the position where they are printed on the label.



The drop down element will allow users to define which delta they want to view.

At this point, the GUI is only referencing local variables; there is no real interaction between the elements yet. Therefore, if you run the script, you can select an AOI. But doing so does not trigger any events. In the next section we will cover the primary geoprocessing code that underlay this GUI.

5.5 Nested Functions

As we mentioned before, all the processing within the RMET tool requires some type of user input. This means that most of the processes need to be written as functions to be flexible in the information they accept. These functions are often nested within different functions. Understanding the structure of nested functions will require drafting a work flow to know what elements are required and when. Below is an example of the nested components of our first function which does most of the computation for the “Delta Watch” GUI. It is a big function with lots of moving parts, hence the diagram.



A conceptual diagram of the primary geoprocessing function in the “Delta Watch” code.

The end result of this process is a set of imagery that matches the year and region that the user selected. From that the other aspects of the code will take that those outputs and produce the added value products that we are interested in. A binary map of NDVI based on a user defined threshold value for a selected year and/or a change map of NDVI over time.

Here we begin to build up this function by adding component parts.

The first step is creating a wrapper. A wrapper is a term used to define a feature that will hold mutliple functions present in this step. By using the applyFilter() function as a wrapper we can nest all the geoprocessing steps within a single function. This will allow us to call a single element to execute a full list of unique functions and adds clarity to the code further on.

5.5.1 Defining the Region of Interest

The first element of our applyFilter function is defining the area of interest. As you add these code chucks to your script be sure they are contained within the curly brackets “{}” of the applyFilter function.

The setAreaOfInterest() function uses the getValue() function to declare one of the predefined locations as the aoi object. Then through a series of clause statements, the locally defined variable areaOfInterest is set equal to the geometry of the selected location. After running the function setAreaOfInterest() we define the geometry object to be equal to the areaOfInterest object. We will use the globally defined object geometry in more functions down the line.



The setAreaOfInterest() function is held within the applyFilter(). The next few sections of code will occur inside the applyFilter() function as well.

5.5.2 Defining the Years

In this case the variables selectYr1, selectYr2 and selectYr3 are defined in the GUI and are set by the user. We want to retrieve these values and declare them as one of the global variables we established early. This is relatively straightforward. The variables yr1, yr2 and yr3 were declared earlier using the var yr1 structure. Because of this we can set those elements equal to the value returned by the getValue() function.

5.5.3 Collecting the Imagery

Now that we have established the geometry and the year, we can compile our imagery. We will do this by constructing a dictionary that contains a single image for each year across the Landsat 5,7, and 8 collections. This is an adaptation of the dictionary we first used in Module 8.

/*
Creating image dictionaries.
*/

var constructLSDict = function(geometry)
  /*
  This function takes in a geometry feature from the GUI and uses it to
  generate a dictionary of median reduce images for the specified date
  range.
  inputs
  geometry = defined by the GUI
  Features to change
  y_list = set based on sensor type
  imagecollection = unique id for collection type
  filterDate = cat() this is used to refine the month and day of start and end time
  With adaptation this should be flexible across sensors and times
  */
  {
  var startMonth = "-04-05"
  var endMonth = "-09-30"
  // Construct a dictionary for landsat 8 imagery.
  var y_list = ee.List.sequence(2013, 2018)
  var ystr_list = y_list.map(function(y){return ee.Number(y).format('%1d')})
  var ls8 = y_list.map(function(y){return ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls8_dict = ee.Dictionary.fromLists(ystr_list, ls8);

  // Construct a dictionary for LS7 imagery
  var y_list7 = ee.List.sequence(2012, 2012)
  var ystr_list7 = y_list7.map(function(y){return ee.Number(y).format('%1d')})
  var ls7 = y_list7.map(function(y){return ee.ImageCollection('LANDSAT/LE07/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls7_dict = ee.Dictionary.fromLists(ystr_list7, ls7);

  // Combine the LS8 and LS7 dictionaries.
  var ls8_ls7_dict = ls8_dict.combine(ls7_dict)

  // Construct a dictionary for Landsat 5.
  var y_list5 = ee.List.sequence(1984,2011)
  var ystr_list5 = y_list5.map(function(y){return ee.Number(y).format('%1d')})
  var ls5 = y_list5.map(function(y){return ee.ImageCollection('LANDSAT/LT05/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls5_dict = ee.Dictionary.fromLists(ystr_list5, ls5);
  // Combine the LS5 with the LS8/7 dictionary
  var LS_Dict = ls8_ls7_dict.combine(ls5_dict)

  return(LS_Dict)
}

For each sensor we are generating a median reduced image for each year with imagery captured between April and September. This time frame represents the growing season along the Colorado River. At 32 degrees north it is the furthest site from the equator and is likely to have the most distinct growing season of our river delta ecosystems. That said, were working under the assumption that our data range will also capture the changes in vegetation for the Betsiboka River Delta in Madagascar, which is in the southern hemisphere.



Example print statement of the Landsat dictionary. There are 35 years of data available and each year has a single multiband image.

5.5.4 Defining the Indices

In the step above we declared a function. In the next step we will generate a few additioal functions that will be used to produce NDVI values based on the output of the constructLSDict function.

In this part of the script, we are attempting to utilize the full Landsat series from 5 to 8. This requires pulling data from multiple sensors which potentially have different band arrangements. Landsat 5 and 7 both have the same bands for NDVI so we can use a single function for them but will use a different function for Landsat 8.

5.5.5 Define the Image Collection to Use

We will now define a function that takes the year and geometry and return a single image.

This function is effectively a conditional statement that uses the input year to determine what year to use and therefore determine which NDVI algorithm to apply. In our case, we are using a single band, NDVI, so this is not the most efficient method but it does allow for the flexibility of being able to add more bands down the line.



An example from the RMET code. The selectYearAddBands() can easily be added if more indices are needed. Simply define the function to generate the indices of interest and add it using the addBands() function as we did with NDVI.

5.5.6 Set Images of Interest to Predefined Local Variables

Lastly, we call a final function to produce three image based of the three user defined years.

The result is three images saved as global variables that can be called upon by other functions. It is a long process to get three images but with GUI development it takes a lot of work to be comprehensive and flexible.

Important Note: The past 6 code chunks are all wrapped within the first function applyFilter(), be sure this is represented in your code editor. This is done so you can call a single function to return your three images.

If you were to run the code at this point nothing would happen. None of the processing will occur until the applyFilter function is called. Before that we need to ensure there is a place on the GUI where the user can define their years of interest.



You can ensure all elements are within the applyFilter() function by minimizing the function in the code editor by selecting the arrow next to it.

5.6 Generating the Binary NDVI Maps

The functions for generating the binary and difference maps are fairly straightforward. Still it is worth defining all the component parts and the connections in a conceptual diagram before moving forward.



Generalized workflow of the binary map production geoprocessing step.

5.6.1 Establish Binary Panel Element

Before creating the geoprocessing steps, we will generate the panel element that will hold them.

As with the previous examples, we are creating labels and adding them to the primary panel in the order we want them displayed.

We want users to be able to adjust what is considered to be “green” vegetation by altering where the line is drawn between vegetation or not within the GUI. This is an important step: to allow the user to adjust the presentation of the data based on the ecology of the selected river delta. NDVI values are based on a continuous scale and a slider bar is an appropriate widget for adjusting these type of values.

There is quite a bit of new functionality listed in the code block above. Creating the slider follows the form of creating the labels and text boxes. Using the setValue() function allows us to define the initial value that will be used if the user does not make a change. The onChange() function is the first example of an Event object. An onChange() event looks for a change in the value on the slider. If this change occurs it grabs that new value. The NDVIslider object is a map layer because it is positioned on the panel on the map. The onChange() function is called on the map object NDVIslider and returns the first and only value.

5.6.2 Generating the Binary Map

The last variable in the function creates the ui.Button. This widget function has a built in event object within it. The onClick parameter will run a function once the click occurs. Below we will define how the function the NDVIBIN() will run when clicked.

This function will create our first actual map output. When the user presses the “Landsat NDVI Map” button this function runs. If first calls the applyFilter() function returning a median reduced image from the Landsat collection based on the user defined year. From there we use an expression to determine all locations where NDVI is greater than the threshold value which is also defined by the user. After a quick mask to remove all the values that are lower than the threshold we add the image to map and center the map on the image so the user can view it immediately.

With both these elements in place we can see that our GUI is starting to grow.

GUI containing the binary map option.

At this point if you tried to produce the Landsat NDVI Map it would fail because variables that are present in the applyFilter() function are not defined. We will get to these last elements now.

5.7 Generating the NDVI Difference Maps

In this case the difference maps are created by subtraction two images. Our final code will add the difference image and the two original images to the map.

/*
Change maps
*/

// Generate a change map based on a user defined STARTYEAR, ENDYEAR, AND AOI.   
function GREENER() {
    applyFilter();
    var date1 = Fimageyr2.select('NDVI'); //Select NDVI Values for Start Year
    var date2 = Fimageyr3.select('NDVI'); //Select NDVI values for End Year
    var diff = date2.subtract(date1); //Subtract rasters to get magnitude of change
    var trend = date2.gt(date1); //Creates a binary where pixels that have a greater NDVI in year two are assigned a value of 1
    var final = trend.multiply(diff).clip(geometry); //Shows magnitude for greening trend

    Map.centerObject(geometry); // Center on AOI
    Map.addLayer(final, MG, 'Areas that have become more green(' + yr2 + ')-(' + yr3 + ')');
    Map.addLayer(date1, visNDVI, 'NDVI (' + yr2 + ')', false); //Creates a layer with the index in the Start Year.
    Map.addLayer(date2, visNDVI, 'NDVI(' + yr3 + ')', false); //Creates a layer with the index in the End Year.
  }
// Same process for areas that decrease in greenness
function LESSGREEN() {
      applyFilter();
    var date1 = Fimageyr2.select('NDVI'); //Select NDVI Values for Start Year
    var date2 = Fimageyr3.select('NDVI'); //Select NDVI values for End Year
    var diff = date2.subtract(date1); //Subtract rasters to get maginitude of change
    var trend = date2.lt(date1); //Creates a binary where pixels that have a lesser NDVI in year two are assigned a value of 1
    var final = trend.multiply(diff).clip(geometry); //Shows magnitude for less green trend

    Map.centerObject(geometry); // Center on AOI
    Map.addLayer(final, LG, 'Areas that have become less green(' + yr2 + ')-(' + yr3 + ')');
    Map.addLayer(date1, visNDVI, 'NDVI(' + yr2 + ')', false); //Creates a layer with the index in the Start Year.
    Map.addLayer(date2, visNDVI, 'NDVI(' + yr3 + ')', false); //Creates a layer with the index in the End Year.
  };

Much like with the binary map, we’re calling the applyFilter() function to gather our imagery. The gt and lt functions threshold the value of the image. This defines the locations that have experienced loss or gain of NDVI values. The binary output of this function is multiplied by the difference between the two values. This ensures that only values expressing the direction of change are found in the final image.

5.7.1 Develop the Panel Elements for the Change Maps

Next we will add the GUI element for the change maps.

While there are a lot of step in the code chunk there is only one new element and that comes in right at the end. Throughout this process, we have created labels and buttons that will call specific geoprocessing functions when clicked allow the user to determine what years they want to observe. For legibility purposes, rather than only adding the data range and select year boxes we create a new panel that places the two object next to one another horizontally. We then add that panel to the existing panel to create what is effectively nested panels. This does not change the look of the GUI at all because we did not add the new panel to the nap.



With the final elements added we can run our binary and change maps. Here we show areas that have greened between 1990 and 2018 in the Betsiboka Delta.

5.8 Resetting the map

The final function of the GUI is the rest button.

The same as all previous buttons, this element calls a reset() function when selected. The reset() function below allows the user to remove all the elements from the map giving them a clean slate to ask new questions.

There we have it, a complete set of code for a functional GUI that gives us some important information about the changes in vegetation growth in three of our world’s deltas.

6 Conclusion

At the end of this process you will have a GEE script that generates a GUI that allow users to ask questions about changes in NDVI within three major river deltas over time. This ability to allow your end users and partners ask their own questions with remotely sensed data is a critical step toward integrating their expert knowledge into your work. While the methods within this module are not designed to answer specific management questions, adapting this code will allow you to start developing your own GUI tailored to the research questions relevant to your project stakeholders . Because this code can be engaged view a web browser, the end user of the GUI will be working with an interactive web map. These are the communication tools people want due to ease of use. So, while it takes a lot of planning and effort to make it happen it is always worth considering if adding these dynamic elements will be worth it. This is a question that should be answered by your end user because if you can keep them engaged it is far more likely that your content will be used.

6.1 Complete Code for GUI

/*
Global Variables
*/
var aoi,
    areaOfInterest,
    Fimageyr1,Fimageyr2,Fimageyr3,
    geometry,
    thresh,
    yr1,yr2,yr3

/*
Visualization parameters
*/

var VEG = {"opacity":1,"palette":["2ddc0d"],"min":0,"max":1}
var MG = {"opacity":1, "palette":["ffffff", "085a16"], "min":0, "max":2}
var LG = {"opacity":1, "palette":["ff7c0b", "ffffff"], "min":-2, "max":0}
var visNDVI = {min: -1, max: 1, palette: ['blue', 'white', 'green']};
var FLG = {"opacity":1, "palette":[ "ffffff", "ff7c0b"], "min":0, "max":2}

/*
Create the panel
*/

// Generate main panel and add it to the map.
var panel = ui.Panel({style: {width:'33.333%'}});
ui.root.insert(0,panel);

// Define title and description.
var intro = ui.Label('Delta Watch: An Example GEE GUI ',
  {fontWeight: 'bold', fontSize: '24px', margin: '10px 5px'}
);
var subtitle = ui.Label('Use 30 m Landsat 5,7,8 imagery to'+
  ' visualize changes in delta vegetation patterns over time.'+
  ' Select from multiple area of interest and type of visualization; single year binary '+
  ' or change over time.', {});

// Add title and description to the panel  
panel.add(intro).add(subtitle);
/*
Define study areas.
*/
// Generate polygons for each study region.
var colo = ee.Geometry.Polygon([[-115.29,32.20], [-114.89,31.17],
   [-114.40,31.64], [-115.14,32.19], [-115.29,32.20]]),
    nile = ee.Geometry.Polygon([[28.76,31.49],[30.69,29.91],
      [31.52, 29.96],[32.89,31.43],[31.00,31.76],[28.76,31.49]]),
    bets = ee.Geometry.Polygon([[46.19,-15.66],[46.25,-16.04],
      [46.51,-16.15],[46.70,-16.06],[46.19,-15.66]])

// Define labels for each study region.
var COLO = 'Colorado River Delta',
    NILE = 'Nile River Delta',
    BETS = 'Betsiboka River Delta'

/*
Select Area of INTEREST
*/

// Define the select button for the AOI
var selectAoi = ui.Select({
  items:[COLO,NILE,BETS],
  placeholder:'Select area of interest',
  });

// Add a label.
var selectSIAOI = ui.Label({value:'Select area of interest',
style: {fontSize: '18px', fontWeight: 'bold'}});

// Add the select AOI panel to the map panel.
panel.add(selectSIAOI)
    .add(selectAoi);

//Function to create a binary NDVI map for a user selected year and AOI.
function NDVIBIN (){
    applyFilter();
    var Nimage = Fimageyr1.expression(
    "NDVI >= thresh1", {
      'NDVI': Fimageyr1.select('NDVI').clip(geometry),
      'thresh1' : NDVIslider.getValue()}); //Slider bar input

      var NDVIMimage = Nimage.updateMask(Nimage.gt(0)); //Mask 0 values
      Map.centerObject(geometry); //Center on AOI
      Map.addLayer(NDVIMimage, VEG, 'NDVI Landsat Binary Map(' + yr1+ ')');
}

// Container function to create Image for mapping.
function applyFilter(){
/*
Defining the area of interested
*/

function setAreaOfInterest(){
  aoi = selectAoi.getValue();
  if (aoi == COLO){
      areaOfInterest = colo;
  }//sets the area to nile river delta
  else if(aoi == NILE){
      areaOfInterest = nile;
  }//sets the area of interest to
  else if (aoi == BETS){
      areaOfInterest = bets;
  }
}

setAreaOfInterest();

geometry = areaOfInterest

/*
Defining years for image selection
*/
    yr1 = selectYr1.getValue(); //Input for Landsat Binary Map
    yr2 = selectYr2.getValue(); //Input for Landsat Change Map Start Year
    yr3 = selectYr3.getValue(); //Input for Landsat Change Map End Year

/*
Creating image dictionaries.
*/

var constructLSDict = function(geometry)
  /*
  This function takes in a geometry feature from the GUI and uses it to
  generate a dictionary of median reduce images for the specified date
  range
  inputs
  geometry = defined by the GUI
  Features to change
  y_list = set based on sensor type
  imagecollection = unique id for collection type
  filterDate = cat() this is used to refine the month and day of start and end time
  With adaptation this should be flexible across sensors and times
  */
  {
  var startMonth = "-04-05"
  var endMonth = "-09-30"
  // Construct a dictionary for landsat 8 imagery.
  var y_list = ee.List.sequence(2013, 2018)
  var ystr_list = y_list.map(function(y){return ee.Number(y).format('%1d')})
  var ls8 = y_list.map(function(y){return ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls8_dict = ee.Dictionary.fromLists(ystr_list, ls8);

  // Construct a dictionary for LS7 imagery
  var y_list7 = ee.List.sequence(2012, 2012)
  var ystr_list7 = y_list7.map(function(y){return ee.Number(y).format('%1d')})
  var ls7 = y_list7.map(function(y){return ee.ImageCollection('LANDSAT/LE07/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls7_dict = ee.Dictionary.fromLists(ystr_list7, ls7);

  // Combine the LS8 and LS7 dictionaries.
  var ls8_ls7_dict = ls8_dict.combine(ls7_dict)

  // Construct a dictionary for Landsat 5.
  var y_list5 = ee.List.sequence(1984,2011)
  var ystr_list5 = y_list5.map(function(y){return ee.Number(y).format('%1d')})
  var ls5 = y_list5.map(function(y){return ee.ImageCollection('LANDSAT/LT05/C01/T1_SR')
                                          .filterDate(ee.String(ee.Number(y).format('%1d')).cat(startMonth),
                                                      ee.String(ee.Number(y).format('%1d')).cat(endMonth))
                                          .filterBounds(geometry)
                                          .filterMetadata('CLOUD_COVER', 'less_than', 30)
                                          .median()})
  var ls5_dict = ee.Dictionary.fromLists(ystr_list5, ls5);
  // Combine the LS5 with the LS8/7 dictionary
  var LS_Dict = ls8_ls7_dict.combine(ls5_dict)

  return(LS_Dict)
}

/*
Generating indices
*/
// A function to compute NDVI for Landsat 5 and 7.
var NDVI57 = function(image) {
  return image.normalizedDifference(["B4","B3"]).rename("NDVI");
};

// A function to compute NDVI for Landsat 8.
var NDVI8 = function(image) {
  return image.normalizedDifference(["B5","B4"]).rename("NDVI");
};


/*
Selecting bands based on years
*/

var selectYearAddBands = function(dictionary, year, geometry)
{
    if (year >= 2012) {
      var imageYear = ee.Image(dictionary.get(year)).clip(geometry)
      var outImage = imageYear.addBands(NDVI8(imageYear))
    } else if (year == 2011) {
      var imageYear = ee.Image(dictionary.get(year)).clip(geometry)
      var outImage = imageYear.addBands(NDVI57(imageYear))
    } else {
      var imageYear = ee.Image(dictionary.get(year)).clip(geometry)
      var outImage = imageYear.addBands(NDVI57(imageYear))
    }
  return (outImage)
};


// Make LS Dictionary and Select years of interest.
var dict_ls = constructLSDict(geometry);
  Fimageyr1 = selectYearAddBands(dict_ls, yr1, geometry); //LS BINARY
  Fimageyr2 = selectYearAddBands(dict_ls, yr2, geometry); //LS Change Start Year
  Fimageyr3 = selectYearAddBands(dict_ls, yr3, geometry); //LS Change End Year

}


/*
Defining time frame for Binary MAP
*/

var durpanel = ui.Label({
  value:'Select Year for Landsat 30m Binary Map',
  style:{fontSize: '18px', fontWeight: 'bold'}});

var selectYr1 = ui.Textbox({placeholder: 'Year',  value: '2018',
  style: {width: '100px'}});

var datasetRange_label = ui.Label('Choose year from 1984 - 2018      ',
  {margin: '0 0 0 10px',fontSize: '12px',color: 'gray'});

panel.add(durpanel)
  .add(datasetRange_label)
  .add(selectYr1);

// Add a slider bar widget
var NDVIslider = ui.Slider();
// Set a default value.
NDVIslider.setValue(0.17);  
NDVIslider.onChange(function(value) {
  Map.layers().get(0);
});
// Create a button that will run the function that creates the NDVI binary object
var NDVIBINMAP = ui.Button({label:'Landsat NDVI Map' , onClick:NDVIBIN});
// Add slider and Button to the map.
panel.add(NDVIslider)
  .add(NDVIBINMAP)

/*
Change maps
*/

// Generate a change map based on a user defined STARTYEAR, ENDYEAR, AND AOI.   
function GREENER() {
    applyFilter();
    var date1 = Fimageyr2.select('NDVI'); //Select NDVI Values for Start Year
    var date2 = Fimageyr3.select('NDVI'); //Select NDVI values for End Year
    var diff = date2.subtract(date1); //Subtract rasters to get maginitude of change
    var trend = date2.gt(date1); //Creates a binary where pixels that have a greater NDVI in year two are assigned a value of 1
    var final = trend.multiply(diff).clip(geometry); //Shows magnitude for greening trend

    Map.centerObject(geometry); // Center on AOI
    Map.addLayer(final, MG, 'Areas that have become more green(' + yr2 + ')-(' + yr3 + ')');
    Map.addLayer(date1, visNDVI, 'NDVI (' + yr2 + ')', false); //Creates a layer with the index in the Start Year.
    Map.addLayer(date2, visNDVI, 'NDVI(' + yr3 + ')', false); //Creates a layer with the index in the End Year.
  }
// Same process for areas that decrease in greenness
function LESSGREEN() {
      applyFilter();
    var date1 = Fimageyr2.select('NDVI'); //Select NDVI Values for Start Year
    var date2 = Fimageyr3.select('NDVI'); //Select NDVI values for End Year
    var diff = date2.subtract(date1); //Subtract rasters to get magnitude of change
    var trend = date2.lt(date1); //Creates a binary where pixels that have a lesser NDVI in year two are assigned a value of 1
    var final = trend.multiply(diff).clip(geometry); //Shows magnitude for less green trend

    Map.centerObject(geometry); // Center on AOI
    Map.addLayer(final, LG, 'Areas that have become less green(' + yr2 + ')-(' + yr3 + ')');
    Map.addLayer(date1, visNDVI, 'NDVI(' + yr2 + ')', false); //Creates a layer with the index in the Start Year.
    Map.addLayer(date2, visNDVI, 'NDVI(' + yr3 + ')', false); //Creates a layer with the index in the End Year.
  }

// Define the textbox for the 1st and 2nd year
var selectYr2 = ui.Textbox({placeholder: 'Year',  value: '1985',
  style: {width: '100px'}});
var selectYr3 = ui.Textbox({placeholder: 'Year',  value: '2018',
  style: {width: '100px'}});
// define the labels needed for GUI
var changepanel = ui.Label({
  value:'Select Years for Landsat 30m Change Map',
  style:{fontSize: '18px', fontWeight: 'bold'}});
var datasetRange_label2 = ui.Label('Start Year (1984 - )          ',
  {margin: '0 0 0 10px',fontSize: '12px',color: 'gray'});
var datasetRange_label3 = ui.Label('End Year (-2018)     ',
  {margin: '0 0 0 10px',fontSize: '12px',color: 'gray'});
// Create two buttons that will add the greener or lessGreen images to the map
var GRMAP = ui.Button('More Green Trend Map', GREENER);
var LGMAP = ui.Button('Less Green Trend Map', LESSGREEN);
// Add all elements to the panel in the correct order.
panel.add(changepanel)
  .add(ui.Panel([datasetRange_label2, datasetRange_label3],ui.Panel.Layout.flow('horizontal')))
  .add(ui.Panel([selectYr2, selectYr3],ui.Panel.Layout.flow('horizontal')))
  .add(GRMAP)
  .add(LGMAP);

var resetButton = ui.Button('Reset Map', reset);
panel.add(resetButton);

/*
Reset Map
*/
function reset(){
  Map.clear();
}