X
10333 Rate this article:
No rating

User-Defined ENVITasks with Progress and Abort

Anonym

ENVI 5.3 will be released in the very near future. While we have added another 55 tasks to have a total of 137, I think the more exciting addition is the new ENVIBroadcastChannel and ENVIAbortableTaskFromProcedure classes. The former allows your user-defined ENVITasks to display and update a progress dialog, and the latter provides a mechanism for your task to be notified if the user clicks on the Cancel button thereon. This is an extension to my previous blog post about user-defined ENVITasks that were introduced in ENVI 5.2 SP1.

The ENVIBroadcastChannel is the bases for a new publish and subscribe framework. Objects can subscribe to a channel, and then be notified of all ENVIMessage objects that are broadcast by any other part of the system. You can create your own broadcast channels, but for the purposes of progress dialogs we added one as a member of the ENVI object.  You access it by calling the ENVI::GetBroadcastChannel() function, and then broadcasting ENVIStartMessage, ENVIProgressMessage, and ENVIFinishMessage objects on this channel will show,update, or hide the progress dialog, respectively.

The message classes all use an object reference as a correlating identifier, so you need to use the same object when you construct each of these in your procedure. In the case where we don’t support task cancellation, then we can use any object reference, as long as we use it consistently.  If you skip this argument to the message constructors, they will throw a “You must provide a valid source object” message. For my example here I used the ENVI object, but the choice is yours.

We must first broadcast an ENVIStartMessage, passing in a message string and the source object reference. The message string used here will be the title of the progress dialog. As the procedure makes progress through the data, you can broadcast as many ENVIProgressMessage events as you want. You can create a new message object for each broadcast, or you can just update its PERCENT property and broadcast the same instance each time. The ENVIProgressMessage class takes in a message string as its first constructor parameter, which is displayed in the progress dialog just above the progress bar. This argument can be accessed as the MESSAGE property, but is immutable once the object Is created. So if you want to change your message to indicate to the user different phases of the task, then you will need to create separate progress message instances. The PERCENT property is meant to be an integer in the [0, 100] range, which tells the progress bar what percentage to fill with the blue color. When the procedure is finished, it must broadcast an ENVIFinishMessage in order to dismiss the progress dialog.  Failure to do so will result in a progress dialog that can’t be dismissed.

Here is an updated version of BandMathExample that displays a progress dialog. First the task template, BandMathProgressExample.task:

{
  "name": "BandMathProgressExample",
  "baseClass": "ENVITaskFromProcedure",
  "routine": "bandmathprogressexample",
  "displayName": "ENVI Band Math Progress Example",
  "description": "This is an example of a custom task that performs band math with progress on two rasters.",
  "version": "5.3",
  "parameters":[
    {
      "name": "INPUT_RASTER1",
      "displayName": "Input Raster 1",
      "dataType": "ENVIRASTER",
      "direction": "input",
      "parameterType": "required",
      "description": "Specify the first raster on which to perform band math. This raster is used to determine data type on output."
    },
    {
      "name": "INPUT_RASTER2",
      "displayName": "Input Raster 2",
      "dataType": "ENVIRASTER",
      "direction": "input",
      "parameterType": "required",
      "description": "Specify a second raster on which to perform band math."
    },
    {
      "name": "OUTPUT_RASTER",
      "displayName": "Output Raster",
      "dataType": "ENVIRASTER",
      "direction": "output",
      "parameterType": "required",
      "description": "This is a reference to the output raster of filetype ENVI."
    }
  ]
}

Then the procedure:

pro BandMathProgressExample, INPUT_RASTER1=raster1, $
                             INPUT_RASTER2=raster2, $
                             OUTPUT_RASTER_URI=outputURI
  compile_opt idl2

  e = ENVI(/CURRENT)

  ;Get the Broadcast Channel
  oChannel = e.GetBroadcastChannel()

  outputRaster = ENVIRaster(URI=outputURI, INHERITS_FROM=raster1)

  ; Create Iterator
  rasterTileIterator1 = raster1.CreateTileIterator()

  ; Determine number of steps for progress
  ; number of tiles plus one for saving
  nSteps = rasterTileIterator1.NTILES + 2
  oStartMessage = ENVIStartMessage('Band Math', e)
  oChannel.Broadcast, oStartMessage
  oProgressMessage = ENVIProgressMessage('Band Math Executing', $
                     0, e)
  oChannel.Broadcast, oProgressMessage

  ;Iterate through the data
  foreach rasterTile1, rasterTileIterator1, stepIndex do begin
    ; Broadcast progress
    oProgressMessage.Percent = stepIndex*100.0/nSteps
    oChannel.Broadcast, oProgressMessage

    ; Retrieve tile from second raster, first raster handled by foreach
    rasterTile2 = raster2.GetData(BANDS=rasterTileIterator1.CURRENT_BAND, $
                  SUB_RECT=rasterTileIterator1.CURRENT_SUBRECT)

    ; Set the output tile to the subtraction of raster2 tile from raster1 tile
    outputRaster.SetTile, rasterTile1 - rasterTile2, rasterTileIterator1
  endforeach

  ; Finalize the data
  outputRaster.Save

  oProgressMessage.Percent = 100
  oChannel.Broadcast, oProgressMessage

  ; Broadcast finish
  oFinishMessage = ENVIFinishMessage(e)
  oChannel.Broadcast, oFinishMessage
end

This example will display a progress dialog, but since I didn’t create the message objects using an ENVIAbortable object there was noCancel button on the dialog. In order toget the Cancel button, we need to use the ENVIAbortableTaskFromProcedure. The ENVIAbortableTaskFromProcedure class inherits from ENVITaskFromProcedure, so all the rules still apply when you write your procedure. But now there is a new rule – you must add the ABORTABLE keyword to your procedure, otherwise an error will be thrown when you try to Execute the task. This keyword does not get defined in your task template, it will be assigned with automatically by the task object, passing in an ENVIAbortable object that you can query. If the user clicks on the Cancel button onthe progress dialog, then this ENVIAbortable object will have its ABORT_REQUESTED property set to true. The Cancel button will not generate an interrupt event, since we can’t be sure that your procedure is in a state that can be easily cleaned up, so the onus is on you the procedure implementer to query the abortable object when appropriate to see if you should clean up and return instead of continuing to process.

You’ll notice in the example below that the test of theabortable’s ABORT_REQUESTED state happens after the progress broadcast, instead of before. This is because that property is updated inside the handling of that progress message, so if we checked the property before broadcasting progress we’d end up doing an extra iteration of the for loop before realizing the Cancel button was pressed.

Here is the enhanced version that supports cancellation. First the task template, BandMathProgressAbortExample.task:

{
  "name": "BandMathProgressAbortExample",
  "baseClass": "ENVIAbortableTaskFromProcedure",
  "routine": "bandmathprogressabortexample",
  "displayName": "ENVI Band Math Progress Example",
  "description": "This is an example of a custom task that performs band math with progress on two rasters.",
  "version": "5.3",
  "parameters":[
    {
      "name": "INPUT_RASTER1",
      "displayName": "Input Raster 1",
      "dataType": "ENVIRASTER",
      "direction": "input",
      "parameterType": "required",
      "description": "Specify the first raster on which to perform band math. This raster is used to determine data type on output."
    },
    {
      "name": "INPUT_RASTER2",
      "displayName": "Input Raster 2",
      "dataType": "ENVIRASTER",
      "direction": "input",
      "parameterType": "required",
      "description": "Specify a second raster on which to perform band math."
    },
    {
      "name": "OUTPUT_RASTER",
      "displayName": "Output Raster",
      "dataType": "ENVIRASTER",
      "direction": "output",
      "parameterType": "required",
      "description": "This is a reference to the output raster of filetype ENVI."
    }
  ]
}

Then the procedure:

pro BandMathProgressAbortExample, INPUT_RASTER1=raster1, $
                                  INPUT_RASTER2=raster2, $
                                  ABORTABLE=abortable, $
                                  OUTPUT_RASTER_URI=outputURI
                                    
  compile_opt idl2
 
  e = ENVI(/CURRENT)
 
  ;Get the Broadcast Channel
  oChannel = e.GetBroadcastChannel()
 
  outputRaster = ENVIRaster(URI=outputURI, INHERITS_FROM=raster1)
 
  ; Create Iterator
  rasterTileIterator1 = raster1.CreateTileIterator()
 
  ; Determine number of steps for progress
  ; number of tiles plus one for saving
  nSteps = rasterTileIterator1.NTILES + 2
  oStartMessage = ENVIStartMessage('Band Math', abortable)
  oChannel.Broadcast, oStartMessage
  oProgressMessage = ENVIProgressMessage('Band Math Executing', $
                                            0, abortable)
  oChannel.Broadcast, oProgressMessage
 
  ;Iterate through the data
  foreach rasterTile1, rasterTileIterator1, stepIndex do begin
    ; Broadcast progress
    oProgressMessage.Percent = stepIndex*100.0/nSteps
    oChannel.Broadcast, oProgressMessage
 
    ;Check if aborted after sending progress to see if Abort_Requested was set
    ;by any listeners
    if (abortable.ABORT_REQUESTED) then begin
      outputRaster.close
      return
    endif
 
    ; Retrieve tile from second raster, first raster handled by foreach
    rasterTile2 = raster2.GetData(BANDS=rasterTileIterator1.CURRENT_BAND, $
                  SUB_RECT=rasterTileIterator1.CURRENT_SUBRECT)
 
    ; Set the output tile to the subtraction of raster2 tile from raster1 tile
    outputRaster.SetTile, rasterTile1 - rasterTile2, rasterTileIterator1
  endforeach
 
  ; Finalize the data
  outputRaster.Save
 
  oProgressMessage.percent = 100
  oChannel.Broadcast, oProgressMessage
 
  ; Broadcast finish
  oFinishMessage = ENVIFinishMessage(abortable)
  oChannel.Broadcast, oFinishMessage
end