;+
; NAME:
;       CONTRASTZOOM
;
; PURPOSE:
;
;       The purpose of this program is to demonstrate how to
;       zoom an image "in place" and how to window and level
;       (set "contrast and brightness") an image using object
;       graphics functionality. The exercise involves using
;       multiple views in an object graphics scene, and being
;       able to interact with different views in different ways.
;
; AUTHOR:
;
;       FANNING SOFTWARE CONSULTING
;       David Fanning, Ph.D.
;       1645 Sheely Drive
;       Fort Collins, CO 80526 USA
;       Phone: 970-221-0438
;       E-mail: david@idlcoyote.com
;       Coyote's Guide to IDL Programming: http://www.idlcoyote.com
;
; CATEGORY:
;
;       Widgets, Object Graphics.
;
; CALLING SEQUENCE:
;
;       ContrastZoom, image
;
; REQUIRED INPUTS:
;
;       None. The image "mr_knee.dcm" from the examples/data directory
;       is used if no data is supplied in call.
;
; OPTIONAL INPUTS
;
;       image: A 2D image array of any data type.
;
; OPTIONAL KEYWORD PARAMETERS:
;
;       COLORTABLE: The number of a color table to use as the image palette.
;       Color table 0 (grayscale) is used as a default.

;       GROUP_LEADER: The group leader for this program. When the group leader
;       is destroyed, this program will be destroyed.
;
; COMMON BLOCKS:
;
;       None.
;
; SIDE EFFECTS:
;
;       None.
;
; RESTRICTIONS:
;
;       None. The Coyote Library program VCOLORBAR is included.
;
; EXAMPLE:
;
;       To use this program with your 8-bit image data and a red-temperature
;       color scale, type:
;
;          IDL> ContrastZoom, image, Colortable=3
;
; NOTES:
;
;       The left image is used to "zoom" into a portion of the image.
;       The aspect ratio of the sub-image is always preserved. To see
;       the entire image, click and release the mouse button in this
;       window.
;
;       The center image is used to adjust the contrast and brightness
;       (sometimes called the "window" and "level" of the image. Click and
;       drag the mouse vertically to set contrast. Click and drag the mouse
;       horizontally to set brightness. To return to original values (25%
;       contrast and 75% brightness), click and release in the center image.
;
;       The color bars shows the image values of the image.
;
; MODIFICATION HISTORY:
;
;       Written by David Fanning, 18 November 2001.
;       Added second colorbar to show the relationship of the clamped
;          colors to the overall image values. 19 November 2001. DWF.
;       Changed FSC_Normalize to cgNormalize to reflect new name. 6 Feb 2013. DWF.
;-
;
;******************************************************************************************;
;  Copyright (c) 2008, by Fanning Software Consulting, Inc.                                ;
;  All rights reserved.                                                                    ;
;                                                                                          ;
;  Redistribution and use in source and binary forms, with or without                      ;
;  modification, are permitted provided that the following conditions are met:             ;
;                                                                                          ;
;      * Redistributions of source code must retain the above copyright                    ;
;        notice, this list of conditions and the following disclaimer.                     ;
;      * Redistributions in binary form must reproduce the above copyright                 ;
;        notice, this list of conditions and the following disclaimer in the               ;
;        documentation and/or other materials provided with the distribution.              ;
;      * Neither the name of Fanning Software Consulting, Inc. nor the names of its        ;
;        contributors may be used to endorse or promote products derived from this         ;
;        software without specific prior written permission.                               ;
;                                                                                          ;
;  THIS SOFTWARE IS PROVIDED BY FANNING SOFTWARE CONSULTING, INC. ''AS IS'' AND ANY        ;
;  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES    ;
;  OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT     ;
;  SHALL FANNING SOFTWARE CONSULTING, INC. BE LIABLE FOR ANY DIRECT, INDIRECT,             ;
;  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED    ;
;  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;         ;
;  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND             ;
;  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT              ;
;  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS           ;
;  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.                            ;
;******************************************************************************************;
PRO ContrastZoom_VColorBar::Clamp, datarange

; This method clamps the data to a particular data range.

self->GetProperty, Range=currentRange

thisclamp = Bytscl(datarange, Max=currentRange[1], Min=currentRange[0])
bar = BytScl(Replicate(1B,10) # Bindgen(self.ncolors), Min=thisclamp[0], Max=thisclamp[1])
self.thisImage->SetProperty, Data=bar
END
;-------------------------------------------------------------------------



FUNCTION ContrastZoom_VColorBar::INIT, Position=position, $
    NColors=ncolors, Title=title, Palette=palette, $
    Major=major, Minor=minor, Range=range, Color=color, $
    _Extra=extra, Name=name

   ; Catch possible errors.

Catch, theError
IF theError NE 0 THEN BEGIN
   Catch, /Cancel
   ok = Dialog_Message(!Error_State.Msg)
   Message, !Error_State.Msg, /Informational
   RETURN, 0
ENDIF

   ; Initialize model superclass.

IF (self->IDLgrModel::Init(_EXTRA=extra) NE 1) THEN RETURN, 0

    ; Define default values for keywords, if necessary.

IF N_Elements(name) EQ 0 THEN name=''
IF N_Elements(color) EQ 0 THEN self.color = [255,255,255] $
   ELSE self.color = color
thisFont = Obj_New('IDLgrFont', 'Helvetica', Size=8.0)
self.thisFont = thisFont
IF N_Elements(title) EQ 0 THEN title=''

thisTitle = Obj_New('IDLgrText', title, Color=self.color, $
    Font=thisFont, Recompute_Dimensions=2, /Enable_Formatting)

IF N_Elements(ncolors) EQ 0 THEN self.ncolors = 256 $
   ELSE self.ncolors = ncolors
IF N_Elements(palette) EQ 0 THEN BEGIN
    red = (green = (blue = BIndGen(self.ncolors)))
    self.palette = Obj_New('IDLgrPalette', red, green, blue)
ENDIF ELSE self.palette = palette
IF N_Elements(range) EQ 0 THEN self.range = [0, self.ncolors] $
   ELSE self.range = range
IF N_Elements(major) EQ 0 THEN self.major = 5 $
   ELSE self.major = major
IF N_Elements(minor) EQ 0 THEN self.minor = 4 $
   ELSE self.minor = minor
IF N_Elements(position) EQ 0 THEN self.position = [0.90, 0.10, 0.95, 0.90] $
   ELSE self.position = position

    ; Create the colorbar image. Get its size.

bar = REPLICATE(1B,10) # BINDGEN(self.ncolors)
s = SIZE(bar, /Dimensions)
xsize = s[0]
ysize = s[1]

    ; Create the colorbar image object. Add palette to it.

thisImage = Obj_New('IDLgrImage', bar, Palette=self.palette)
xs = cgNormalize([0,xsize], Position=[0,1.])
ys = cgNormalize([0,ysize], Position=[0,1.])
thisImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

   ; Create a polygon object. Add the image as a texture map. We do
   ; this so the image can rotate in 3D space.

thisPolygon = Obj_New('IDLgrPolygon', [0, 1, 1, 0], [0, 0, 1, 1], [0,0,0,0], $
   Texture_Map=thisImage, Texture_Coord = [[0,0], [1,0], [1,1], [0,1]], color=[255,255,255])

    ; Scale the polygon into the correct position.

xs = cgNormalize([0,1], Position=[self.position(0), self.position(2)])
ys = cgNormalize([0,1], Position=[self.position(1), self.position(3)])
thispolygon->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

    ; Create scale factors to position the axes.

longScale = cgNormalize(self.range, Position=[self.position(1), self.position(3)])
shortScale = cgNormalize([0,1], Position=[self.position(0), self.position(2)])

    ; Create the colorbar axes. 1000 indicates this location ignored.

shortAxis1 = Obj_New("IDLgrAxis", 0, Color=self.color, Ticklen=0.025, $
    Major=1, Range=[0,1], /NoText, /Exact, XCoord_Conv=shortScale,  $
    Location=[1000, self.position(1), 0.001])
shortAxis2 = Obj_New("IDLgrAxis", 0, Color=self.color, Ticklen=0.025, $0.001
    Major=1, Range=[0,1], /NoText, /Exact, XCoord_Conv=shortScale,  $
    Location=[1000, self.position(3), 0.001], TickDir=1)

textAxis = Obj_New("IDLgrAxis", 1, Color=self.color, Ticklen=0.025, $
    Major=self.major, Minor=self.minor, Title=thisTitle, Range=self.range, /Exact, $
    YCoord_Conv=longScale, Location=[self.position(0), 1000, 0.001])
textAxis->GetProperty, TickText=thisText
thisText->SetProperty, Font=self.thisFont, Recompute_Dimensions=2

longAxis2 = Obj_New("IDLgrAxis", 1, Color=self.color, /NoText, Ticklen=0.025, $
    Major=self.major, Minor=self.minor, Range=self.range, TickDir=1, $
    YCoord_Conv=longScale, Location=[self.position(2), 1000, 0.001], /Exact)

    ; Add the parts to the colorbar model.

self->Add, shortAxis1
self->Add, shortAxis2
self->Add, textAxis
self->Add, longAxis2
self->Add, thisPolygon

   ; Assign the name.

self->IDLgrModel::SetProperty, Name=name, Select_Target=1

    ; Create a container object and put the objects into it.

thisContainer = Obj_New('IDL_Container')
thisContainer->Add, thisFont
thisContainer->Add, thisImage
thisContainer->Add, thisText
thisContainer->Add, thisTitle
thisContainer->Add, self.palette
thisContainer->Add, textAxis
thisContainer->Add, shortAxis1
thisContainer->Add, shortAxis2
thisContainer->Add, longAxis2

    ; Update the SELF structure.

self.thisImage = thisImage
self.thisFont = thisFont
self.thisText = thisText
self.textAxis = textAxis
self.shortAxis1 = shortAxis1
self.shortAxis2 = shortAxis2
self.longAxis2 = longAxis2
self.thisContainer = thisContainer
self.thisTitle = thisTitle

RETURN, 1
END
;-------------------------------------------------------------------------



PRO ContrastZoom_VColorBar::Cleanup

    ; Lifecycle method to clean itself up.

Obj_Destroy, self.thisContainer
self->IDLgrMODEL::Cleanup
END
;-------------------------------------------------------------------------



PRO ContrastZoom_VColorBar::GetProperty, Position=position, Text=text, $
    Title=title, Palette=palette, Major=major, Minor=minor, $
    Range=range, Color=color, Name=name, $
    Transform=transform, _Ref_Extra=extra

    ; Get the properties of the colorbar.

IF Arg_Present(position) THEN position = self.position
IF Arg_Present(text) THEN text = self.thisText
IF Arg_Present(title) THEN self.thisTitle->GetProperty, Strings=title
IF Arg_Present(palette) THEN palette = self.palette
IF Arg_Present(major) THEN major = self.major
IF Arg_Present(minor) THEN minor = self.minor
IF Arg_Present(range) THEN range = self.range
IF Arg_Present(color) THEN color = self.color
IF Arg_Present(name) THEN self->IDLgrMODEL::GetProperty, Name=name
IF Arg_Present(transform) THEN self->IDLgrMODEL::GetProperty, Transform=transform
IF Arg_Present(extra) THEN self->IDLgrMODEL::GetProperty, _Extra=extra

END
;-------------------------------------------------------------------------



PRO ContrastZoom_VColorBar::SetProperty, Position=position, $
    Title=title, Palette=palette, Major=major, Minor=minor, $
    Range=range, Color=color, Name=name, Transform=transform, _Extra=extra

    ; Set properties of the colorbar.

IF N_Elements(position) NE 0 THEN BEGIN
    self.position = position

        ; Find the size of the image.

    self.thisImage->GetProperty, Data=image
    s = Size(image)
    xsize = s(1)
    ysize = s(2)
    xs = cgNormalize([0,xsize], Position=[position(0), position(2)])
    ys = cgNormalize([0,ysize], Position=[position(1), position(3)])
    self.thisImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

        ; Create new scale factors to position the axes.

    longScale = cgNormalize(self.range, $
       Position=[self.position(1), self.position(3)])
    shortScale = cgNormalize([0,1], $
       Position=[self.position(0), self.position(2)])

        ; Position the axes. 1000 indicates this position ignored.

    self.textaxis->SetProperty, YCoord_Conv=longScale, $
       Location=[self.position(0), 1000, 0]
    self.longaxis2->SetProperty, YCoord_Conv=longScale, $
       Location=[self.position(2), 1000, 0]
    self.shortAxis1->SetProperty, XCoord_Conv=shortScale, $
       Location=[1000, self.position(1), 0]
    self.shortAxis2->SetProperty, XCoord_Conv=shortScale, $
       Location=[1000, self.position(3), 0]

ENDIF
IF N_Elements(title) NE 0 THEN self.thisTitle->SetProperty, Strings=title
IF N_Elements(transform) NE 0 THEN self->IDLgrMODEL::SetProperty, Transform=transform
IF N_Elements(palette) NE 0 THEN BEGIN
    self.palette = palette
    self.thisImage->SetProperty, Palette=palette
ENDIF
IF N_Elements(major) NE 0 THEN BEGIN
    self.major = major
    self.textAxis->SetProperty, Major=major
    self.longAxis2->SetProperty, Major=major
END
IF N_Elements(minor) NE 0 THEN BEGIN
    self.minor = minor
    self.textAxis->SetProperty, Minor=minor
    self.longAxis2->SetProperty, Minor=minor
END
IF N_Elements(range) NE 0 THEN BEGIN
    self.range = range
    longScale = cgNormalize(range, $
       Position=[self.position(1), self.position(3)])
    self.textAxis->SetProperty, Range=range, YCoord_Conv=longScale
    self.longAxis2->SetProperty, Range=range, YCoord_Conv=longScale
ENDIF
IF N_Elements(color) NE 0 THEN BEGIN
    self.color = color
    self.textAxis->SetProperty, Color=color
    self.longAxis2->SetProperty, Color=color
    self.shortAxis1->SetProperty, Color=color
    self.shortAxis2->SetProperty, Color=color
    self.thisText->SetProperty, Color=color
ENDIF
IF N_Elements(name) NE 0 THEN self->IDLgrMODEL::SetProperty, Name=name
IF N_Elements(extra) NE 0 THEN self->IDLgrMODEL::SetProperty, _Extra=extra
END
;-------------------------------------------------------------------------



PRO ContrastZoom_VColorBar__Define

; For details on how this colorbar object works, see the VCOLORBAR__DEFINE.PRO
; program in the Coyote Library.
;
;   http://www.idlcoyote.com/programs/vcolorbar__define.pro

colorbar = { ContrastZoom_VColorBar, $
             INHERITS IDLgrMODEL, $      ; Inherits the Model Object.
             Position:FltArr(4), $       ; The position of the colorbar.
             Palette:Obj_New(), $        ; The colorbar palette.
             thisImage:Obj_New(), $      ; The colorbar image.
             imageModel:Obj_New(), $     ; The colorbar image model.
             thisContainer:Obj_New(), $  ; Container for cleaning up.
             thisFont:Obj_New(), $       ; The annotation font object.
             thisText:Obj_New(), $       ; The bar annotation text object.
             thisTitle: Obj_New(), $     ; The title of the colorbar.
             textAxis:Obj_New(), $       ; The axis containing annotation.
             shortAxis1:Obj_New(), $     ; A short axis.
             shortAxis2:Obj_New(), $     ; A second short axis.
             longAxis2:Obj_New(), $      ; The other long axis.
             NColors:0, $                ; The number of colors in the bar.
             Major:0, $                  ; Number of major axis intervals.
             Minor:0, $                  ; Number of minor axis intervals.
             Color:BytArr(3), $          ; Color of axes and annotation.
             Range:FltArr(2) }           ; The range of the colorbar axis.

END
;-------------------------------------------------------------------------



FUNCTION ContrastZoom_Aspect, aspectRatio, MARGIN=margin, WindowAspect=wAspectRatio

; This function calculates the correct aspect ratios for positioning
; objects in windows.

ON_ERROR, 2

   ; Check for aspect ratio parameter and possibilities.

IF N_PARAMS() EQ 0 THEN aspectRatio = 1.0

IF aspectRatio EQ 0 THEN BEGIN
   MESSAGE, 'Aspect Ratio of 0. Changing to 1...', /Informational
   aspectRatio = 1.0
ENDIF

s = SIZE(aspectRatio)
IF s(s(0)+1) NE 4 THEN $
   MESSAGE, 'Aspect Ratio is not a FLOAT. Take care...', /Informational

   ; Check for margins.

IF N_ELEMENTS(margin) EQ 0 THEN margin = 0.15

   ; Error checking.

IF margin LT 0 OR margin GE 0.5 THEN $
   MESSAGE, 'The MARGIN keyword value must be between 0.0 and 0.5.'

   ; Calculate the aspect ratio of the current window.

IF N_Elements(wAspectRatio) EQ 0 THEN wAspectRatio = FLOAT(!D.Y_VSIZE) / !D.X_VSIZE

   ; Calculate normalized positions in window.

IF (aspectRatio LE wAspectRatio) THEN BEGIN
   xstart = margin
   ystart = 0.5 - (0.5 - margin) * (aspectRatio / wAspectRatio)
   xend = 1.0 - margin
   yend = 0.5 + (0.5 - margin) * (aspectRatio / wAspectRatio)
ENDIF ELSE BEGIN
   xstart = 0.5 - (0.5 - margin) * (wAspectRatio / aspectRatio)
   ystart = margin
   xend = 0.5 + (0.5 - margin) * (wAspectRatio / aspectRatio)
   yend = 1.0 - margin
ENDELSE

position = [xstart, ystart, xend, yend]

RETURN, position
END
;-------------------------------------------------------------------------



PRO ContrastZoom_Resize, event

; This event handler responds to resize events.

Widget_Control, event.top, Get_UValue=info, /No_Copy

    ; Resize the draw widget. This is the proper way to do this
    ; in object graphics, but it does not always work in UNIX
    ; versions of IDL. If it doesn't work for you, comment the
    ; first line out and try the second. The second line is more
    ; portable, but not exactly the proper "object" way. :-(

info.theWindow->SetProperty, Dimensions=[event.x, event.y-20]
;Widget_Control, info.drawID, Draw_XSize=event.x, Draw_YSize=event.y-20


   ; Update the aspect ratios and re-position the images
   ; in the window.

sz = Size(*info.subimage, /Dimensions)
imageAspect = Float(sz[1]) / sz[0]
info.theWindow->GetProperty, Dimensions=dims
windowAspect = (450./info.window_ysize * dims[1]) / (300./info.window_xsize * dims[0])
pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
info.zoomImage->GetProperty, XRange=xrange, YRange=yrange
xs = cgNormalize(xrange, Position=[pos(0), pos(2)])
ys = cgNormalize(yrange, Position=[pos(1), pos(3)])
info.zoomImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys
info.theBox->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

sc = Size(*info.image, /Dimensions)
imageAspect = Float(sc[1]) / sc[0]
info.theWindow->GetProperty, Dimensions=dims
windowAspect = (450./info.window_ysize * dims[1]) / (300./info.window_xsize * dims[0])
pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
info.contrastImage->GetProperty, XRange=xrange, YRange=yrange
xs = cgNormalize(xrange, Position=[pos(0), pos(2)])
ys = cgNormalize(yrange, Position=[pos(1), pos(3)])
info.contrastImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

   ; Draw the scene.

info.theWindow->Draw, info.theScene
Widget_Control, event.top, Set_UValue=info, /No_Copy
END ;-------------------------------------------------------------------------



PRO ContrastZoom_DistinguishEvents, event

; This event handler responds to all draw widget events.

   ; Error handling.

Catch, theError
IF theError NE 0 THEN BEGIN
   Catch, /Cancel
   ok = cgErrorMsg(/Traceback)
   Widget_Control, event.top, Set_UValue=info, /No_Copy
   RETURN
ENDIF

   ; Get the info structure.

Widget_Control, event.top, Get_UValue=info, /No_Copy

   ; Which view object are we dealing with. Find out by getting
   ; the UVALUE from the view obect?

thisView = info.theWindow->Select(info.theScene, [event.x, event.y], Dimensions=[1,1])
thisView = thisView[0]
IF Obj_Valid(thisView) EQ 0 THEN BEGIN
   possibleEventTypes = [ 'DOWN', 'UP', 'MOTION', 'SCROLL', 'EXPOSE' ]
   thisEvent = possibleEventTypes(event.type)
   IF thisEvent EQ 'EXPOSE' THEN info.theWindow->Draw, info.theScene
   Widget_Control, event.top, Set_UValue=info, /No_Copy
   RETURN
ENDIF
thisView->GetProperty, UValue=selectWindow

   ; What happens depends upon which view the event comes from.

CASE selectWindow OF

   'ZOOMWINDOW': BEGIN ; You are trying to zoom into a region in the left-hand image.

         ; Make sure ContrastWindow events don't show up here.

      IF info.currentMode EQ 'CONTRASTWINDOW' THEN BEGIN
         Widget_Control, event.top, Set_UValue=info, /No_Copy
         RETURN
      ENDIF

         ; Find the point in the coordinates of the image in the window.

      hit = info.theWindow->Pickdata(thisView, info.zoomImage, [event.x, event.y], xyz)
      xpt = Floor(xyz[0])
      ypt = Floor(xyz[1])

      possibleEventTypes = [ 'DOWN', 'UP', 'MOTION', 'SCROLL', 'EXPOSE' ]
      thisEvent = possibleEventTypes(event.type)

      CASE thisEvent OF

         'DOWN': BEGIN

               ; While the coordinates are in the image coordinate system, it is
               ; possible that they are *outside* the actual coordinates of the
               ; image. Make sure they don't exceed the size of the image.
               ; What you do with the point depends on what kind of event it is.

            IF xpt LT 0 OR xpt GT (info.zxsize-1) THEN BEGIN
               Widget_Control, event.top, Set_UValue=info, /No_Copy
               RETURN
            ENDIF
            IF ypt LT 0 OR ypt GT (info.zysize-1) THEN BEGIN
               Widget_Control, event.top, Set_UValue=info, /No_Copy
               RETURN
            ENDIF

               ; Set the static corners of the box to current
               ; cursor location. See the current mode so dragging
               ; outside the image can't cause problems.

            info.xs = xpt
            info.ys = ypt
            info.currentMode = "ZOOMWINDOW"

               ; Change the event handler for the draw widget and turn MOTION
               ; events ON.

            Widget_Control, event.id, Draw_Motion_Events=1

               ; Initialize and hide the polyline object.

            box = FltArr(2,5)
            box[0,*] = Replicate(info.xs, 5)
            box[1,*] = Replicate(info.ys, 5)
            info.theBox->SetProperty, Data=box, Hide=0

            END ; of DOWN event

        'UP': BEGIN

              ; It is possible to get an UP event without a previous DOWN event. (For
              ; example, the user starts the box outside the draw widget window.) If this
              ; occurs, info.xs and info.ys will be negative. Check for this and return,
              ; if necessary.

           IF info.xs EQ -1 OR info.ys EQ -1 THEN BEGIN
              Widget_Control, event.top, Set_UValue=info, /No_Copy
              RETURN
           ENDIF

              ; If this is an UP event, you need to erase the zoombox, turn motion events
              ; OFF, and draw the "zoomed" plot.

              ; Turn motion events off. Set the current mode to NULL.

           Widget_Control, event.id, Draw_Motion_Events=0
           Widget_Control, event.id, /Clear_Events
           info.currentMode = ""

              ; Draw the "zoomed" image. Start by getting the LAST zoom
              ; box outline. These are indices into image array.

              xpt = 0 > xpt < (info.zxsize)
              ypt = 0 > ypt < (info.zysize)

              x = [info.xs, xpt]
              y = [info.ys, ypt]

                 ; If the static point and the dynamic point are the same,
                 ; zoom all the way out.

           IF Abs(info.xs - xpt) LT 2 AND Abs(info.ys-ypt) LT 2 THEN BEGIN
              s = Size(*info.image, /Dimensions)
              info.zxsize = s[0]
              info.zysize = s[1]
              contrast = info.contrast
              brightness = info.brightness
              level = (1-brightness/100.)*(info.maxVal - info.minVal) + info.minVal
              width = (1-contrast/100.)*(info.maxVal - info.minVal)

                 ; Calculate display minimum and maximum.

              displayMax = (level + (width / 2))
              displayMin = (level - (width / 2))

              IF displayMax GT info.maxval THEN BEGIN
                 difference = Abs(displayMax - info.maxval)
                 displayMax = displayMax - difference
                 displayMin = displayMin - difference
              ENDIF
              IF displayMin LT info.minval THEN BEGIN
                 difference = Abs(info.minval - displayMin)
                 displayMin = displayMin + difference
                 displayMax = displayMax + difference
              ENDIF

                     ; Display the image.

              info.zoomImage->SetProperty, Data=BytScl(*info.image, Min=info.minval > $
                  displayMin, Max=displayMax < info.maxval), Dimensions=s
              *info.subimage = *info.image
              imageAspect = Float(s[1]) / s[0]
              info.theWindow->GetProperty, Dimensions=dims
              windowAspect = (450./info.window_ysize * dims[1]) / (300./info.window_xsize * dims[0])
              pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
              info.zoomImage->GetProperty, XRange=xrange, YRange=yrange
              xs = cgNormalize(xrange, Position=[pos(0), pos(2)])
              ys = cgNormalize(yrange, Position=[pos(1), pos(3)])

              info.zoomImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys
              info.theBox->SetProperty, Hide=1, XCoord_Conv=xs, YCoord_Conv=ys
              info.theWindow->Draw, info.theScene
              info.xs = 0
              info.ys = 0
              info.xd = s[0]
              info.yd = s[1]
              Widget_Control, event.top, Set_UValue=info, /No_Copy
              RETURN
           ENDIF

              ; Make sure the x and y values are ordered as [min, max].

           IF info.xs GT xpt THEN x = [xpt, info.xs]
           IF info.ys GT ypt THEN y = [ypt, info.ys]

              ; Subset the image.


           *info.subimage = (*info.subimage)(x[0]:x[1] < (info.zxsize-1), y[0]:y[1] < (info.zysize-1))
           zoomedImage = *info.subimage

              ; Update the zoomed image data and draw it.

           s = Size(zoomedImage, /Dimensions)
           info.zxsize = s[0]
           info.zysize = s[1]
           contrast = info.contrast
           brightness = info.brightness
           level = (1-brightness/100.)*(info.maxVal - info.minVal) + info.minVal
           width = (1-contrast/100.)*(info.maxVal - info.minVal)

              ; Calculate display minimum and maximum.

           displayMax = (level + (width / 2))
           displayMin = (level - (width / 2))

           IF displayMax GT info.maxval THEN BEGIN
              difference = Abs(displayMax - info.maxval)
              displayMax = displayMax - difference
              displayMin = displayMin - difference
           ENDIF
           IF displayMin LT info.minval THEN BEGIN
              difference = Abs(info.minval - displayMin)
              displayMin = displayMin + difference
              displayMax = displayMax + difference
           ENDIF

              ; Display the image after positioning it appropriately
              ; in the window so that it maintains the aspect ratio of the
              ; sub-sampled image. Apply the positioning to the image and
              ; to the zoom box.

           info.zoomImage->SetProperty, Data=BytScl(*info.subimage, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval), Dimensions=s
           imageAspect = Float(s[1]) / s[0]
           info.theWindow->GetProperty, Dimensions=dims
           windowAspect = (450./info.window_ysize * dims[1]) / (300./info.window_xsize * dims[0])
           pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
           info.zoomImage->GetProperty, XRange=xrange, YRange=yrange
           xs = cgNormalize(xrange, Position=[pos(0), pos(2)])
           ys = cgNormalize(yrange, Position=[pos(1), pos(3)])
           info.zoomImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys
           info.theBox->SetProperty, Hide=1, XCoord_Conv=xs, YCoord_Conv=ys

              ; Clear any motion events that may have occurred.

           Widget_Control, event.id, /Clear_Events

              ; Set the static values to negative values.

           info.xs = -1
           info.ys = -1

           END ; of UP event

         'MOTION': BEGIN

               ; Get the dynamic corner of the box.

             info.xd = 0 > xpt < (info.zxsize)
             info.yd = 0 > ypt < (info.zysize)

               ; Re-configure the box coordinates.

            box = FltArr(2,5)
            box[0,*] = [info.xs, info.xd, info.xd, info.xs, info.xs]
            box[1,*] = [info.ys, info.ys, info.yd, info.yd, info.ys]

               ; Draw the new zoom box.

           info.theBox->SetProperty, Data=box

            END ; of MOTION event.

         ELSE:

     ENDCASE

  END ; of ZOOMWINDOW processing.

  'CONTRASTWINDOW': BEGIN ; You are trying to window and level the image.

        ; Make sure no zooming event sneak in here by accident.

     IF info.currentMode EQ 'ZOOMWINDOW' THEN BEGIN
        Widget_Control, event.top, Set_UValue=info, /No_Copy
        RETURN
     ENDIF


        ; Find the point in the coordinates of the image in the window.

     hit = info.theWindow->Pickdata(thisView, info.zoomImage, [event.x, event.y], xyz)
     xpt = Floor(xyz[0])
     ypt = Floor(xyz[1])

        ; What happens next depends on the type of event this is.

     possibleEventTypes = [ 'DOWN', 'UP', 'MOTION', 'SCROLL', 'EXPOSE' ]
     thisEvent = possibleEventTypes(event.type)

     CASE thisEvent OF

     'DOWN': BEGIN

           ; Set the initial (x,y) point. Turn motion events on.

        info.x1 = xpt
        info.y1 = ypt
        Widget_Control, info.drawID, Draw_Motion_Events=1
        info.currentMode = "CONTRASTWINDOW"
        END

      'UP': BEGIN

            ; Turn motion events off. Clear any motion events that might have queued.
            ; Reset the current mode.

         Widget_Control, info.drawID, Draw_Motion_Events=0
         Widget_Control, info.drawID, /Clear_Events
         info.currentMode = ""

            ; If the static point and the dynamic point are the same,
            ; reset the level and width.

         IF Abs(info.x1 - xpt) LT 2 AND Abs(info.y1-ypt) LT 2 THEN BEGIN
            contrast = 0
            brightness = 100
            level = (1-brightness/100.)*(info.maxVal - info.minVal) + info.minVal
            width = (1-contrast/100.)*(info.maxVal - info.minVal)

             ; Calculate display minimum and maximum.

            displayMax = (level + (width / 2))
            displayMin = (level - (width / 2))

            IF displayMax GT info.maxval THEN BEGIN
               difference = Abs(displayMax - info.maxval)
               displayMax = displayMax - difference
               displayMin = displayMin - difference
            ENDIF
            IF displayMin LT info.minval THEN BEGIN
               difference = Abs(info.minval - displayMin)
               displayMin = displayMin + difference
              displayMax = displayMax + difference
             ENDIF

             ; Display the image.

            info.contrastImage->SetProperty, Data=BytScl(*info.image, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
            info.zoomImage->SetProperty, Data=BytScl(*info.subimage, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
            info.colorbar->Clamp, [info.minval > displayMin, displayMax < info.maxval]
            info.colorbar2->SetProperty, Range=[info.minval > displayMin, displayMax < info.maxval]

               ; Update the current contrast and brightness values.

            info.contrast = contrast
            info.brightness = brightness
            info.theWindow->Draw, info.theScene
            Widget_Control, event.top, Set_UValue=info, /No_Copy
            RETURN

           ENDIF

               ; Calculate new contrast, brightness, level, and width parameters.

           contrast = 0 > ((info.y1 - ypt) * info.cstep + info.contrast) < 99
           brightness = 0 > ((info.x1 - xpt) * info.bstep + info.brightness) < 100
           level = (1-brightness/100.)*(info.maxVal - info.minVal) + info.minVal
           width = (1-contrast/100.)*(info.maxVal - info.minVal)

               ; Calculate display minimum and maximum.

           displayMax = (level + (width / 2))
           displayMin = (level - (width / 2))

           IF displayMax GT info.maxval THEN BEGIN
              difference = Abs(displayMax - info.maxval)
              displayMax = displayMax - difference
              displayMin = displayMin - difference
           ENDIF
           IF displayMin LT info.minval THEN BEGIN
              difference = Abs(info.minval - displayMin)
              displayMin = displayMin + difference
              displayMax = displayMax + difference
           ENDIF

              ; Display the image.

           info.contrastImage->SetProperty, Data=BytScl(*info.image, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
           info.zoomImage->SetProperty, Data=BytScl(*info.subimage, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
           info.colorbar->Clamp, [info.minval > displayMin, displayMax < info.maxval]
           info.colorbar2->SetProperty, Range=[info.minval > displayMin, displayMax < info.maxval]

              ; Update the current contrast and brightness values.

           info.contrast = contrast
           info.brightness = brightness
           END

         'MOTION': BEGIN

               ; Calculate new contrast, brightness, level, and width parameters.
               ; Restrict the width to 5 percent of the image range.

            contrast = 0 > ((info.y1 - ypt) * info.cstep + info.contrast) < 99
            brightness = 0 > ((info.x1 - xpt) * info.bstep + info.brightness) < 100
            level = (1-brightness/100.)*(info.maxVal - info.minVal) + info.minVal
            width = (1-contrast/100.)*(info.maxVal - info.minVal)

               ; Calculate display minimum and maximum.

            displayMax = (level + (width / 2))
            displayMin = (level - (width / 2))

            IF displayMax GT info.maxval THEN BEGIN
               difference = Abs(displayMax - info.maxval)
               displayMax = displayMax - difference
               displayMin = displayMin - difference
            ENDIF
            IF displayMin LT info.minval THEN BEGIN
               difference = Abs(info.minval - displayMin)
               displayMin = displayMin + difference
               displayMax = displayMax + difference
            ENDIF

               ; Display the image.

            info.contrastImage->SetProperty, Data=BytScl(*info.image, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
            info.zoomImage->SetProperty, Data=BytScl(*info.subimage, Min=info.minval > $
               displayMin, Max=displayMax < info.maxval)
            info.colorbar->Clamp, [info.minval > displayMin, displayMax < info.maxval]
            info.colorbar2->SetProperty, Range=[info.minval > displayMin, displayMax < info.maxval]

            END

         ELSE:

         ENDCASE

      END ; of ContrastWindow processing.

   ELSE:


ENDCASE

   ; Draw the scene.

info.theWindow->Draw, info.theScene

   ; Store the info structure.

Widget_Control, event.top, Set_UValue=info, /No_Copy
END;-------------------------------------------------------------------------------------



PRO ContrastZoom_Cleanup, tlb

   ; This is the clean-up procedure for the program.

Widget_Control, tlb, Get_UValue=info, /No_Copy
IF N_Elements(info) EQ 0 THEN RETURN

Obj_Destroy, info.theContainer
Ptr_Free, info.image
Ptr_Free, info.subimage

END;-------------------------------------------------------------------------------------


PRO ContrastZoom, image, Colortable=colortable, Group_Leader=group_leader

On_Error, 2

   ; Find an image, if needed.

IF N_Elements(image) EQ 0 THEN BEGIN
   filename = Filepath(Subdir=['examples','data'], 'rbcells.jpg')
   Read_JPEG, filename, image
   image = Reverse(image,2) ; Image is upside down.
ENDIF

   ; Only 2D images can be used.

IF Size(image, /N_Dimensions) NE 2 THEN Message, 'Image must be 2D. Returning...'

   ; Get a colortable if needed.

IF N_Elements(colortable) EQ 0 THEN colortable = 0 ELSE colortable = 0 > colortable < 41

   ; Create a color palette and load the colortable.

thePalette = Obj_New('IDLgrPalette')
thePalette->LoadCT, colortable

   ; Create a zoom box for zoom rubberbanding.

theBox = Obj_New('IDLgrPolyline', Hide=1, Color=[255,255,255])

   ; Create contrast and zoom image objects.

dims = Size(image, /Dimensions)
zxsize = dims[0]
zysize = dims[1]
zoomImage = Obj_New('IDLgrImage', image, Palette=thePalette, Dimensions=dims)
contrastImage = Obj_New('IDLgrImage', BytScl(image), Palette=thePalette, Dimensions=dims)

   ; Create a color bar.

colorbar = Obj_New('ContrastZoom_VColorBar', Palette=thePalette, Range=[Min(image), Max(image)], $
   Position=[0.7, 0.1, 0.95, 0.95], Title='Image Values')
colorbar2 = Obj_New('ContrastZoom_VColorBar', Palette=thePalette, Range=[Min(image), Max(image)], $
   Position=[0.7, 0.1, 0.95, 0.95], Title='Displayed Values', Major=8)

   ; Create the scene, views, and models for the object heirarchy. The dimensions
   ; and location are in normalized units, but device units are used to calculate
   ; the values. The numbers window_xsize and window_ysize refer to the X size and Y
   ; size of the initial draw widget that will be created later. User values will be
   ; used to identify which events are associated with which view.

window_xsize = 925
window_ysize = 500

theScene = Obj_New('IDLgrScene', Color=[125, 125, 125])
zoomView = Obj_New('IDLgrView', Color=[125, 125, 125], Viewplane_Rect=[0,0,1,1], $
   Location=[25./window_xsize, 25./window_ysize], Dimensions=[300./window_xsize, 450./window_ysize], Units=3, UValue='ZOOMWINDOW')
contrastView = Obj_New('IDLgrView', Color=[125, 125, 125], Viewplane_Rect=[0,0,1,1], $
   Location=[350./window_xsize, 25./window_ysize], Dimensions=[300./window_xsize, 450./window_ysize], Units=3, UValue='CONTRASTWINDOW')
colorbarView = Obj_New('IDLgrView', Color=[125, 125, 125], Viewplane_Rect=[-0.2,0,1.2,1], $
   Location=[675./window_xsize, 25./window_ysize], Dimensions=[75./window_xsize, 450./window_ysize], Units=3, UValue='COLORBARWINDOW')
colorbar2View = Obj_New('IDLgrView', Color=[125, 125, 125], Viewplane_Rect=[-0.2,0,1.2,1], $
   Location=[775./window_xsize, 25./window_ysize], Dimensions=[75./window_xsize, 450./window_ysize], Units=3, UValue='COLORBAR2WINDOW')

theScene->Add, zoomView
theScene->Add, contrastView
theScene->Add, colorbarView
theScene->Add, colorbar2View

zoomModel = Obj_New('IDLgrModel')
contrastModel = Obj_New('IDLgrModel')
colorbarModel = Obj_New('IDLgrModel')
colorbar2Model = Obj_New('IDLgrModel')

zoomView->Add, zoomModel
contrastView->Add, contrastModel
colorbarView->Add, colorbarModel
colorbar2View->Add, colorbar2Model

zoomModel->Add, zoomImage
zoomModel->Add, theBox
contrastModel->Add, contrastImage
colorbarModel->Add, colorbar
colorbar2Model->Add, colorbar2

   ; We need to scale the image into the view. Start by
   ; getting the image range. This is the same for both
   ; the zoom and contrast image.

zoomImage->GetProperty, XRange=xrange, YRange=yrange

    ; Calculate the aspect ratios (height/width) for the image
    ; and for the display window. Scale the images and the zoom box.

s = Size(image, /Dimensions)
imageAspect = Float(s[1]) / s[0]
windowAspect = Float(450) / 300
pos = ContrastZoom_Aspect(imageAspect, WindowAspect=windowAspect, Margin=0)
xs = cgNormalize(xrange, Position=[pos(0), pos(2)])
ys = cgNormalize(yrange, Position=[pos(1), pos(3)])
zoomImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys
contrastImage->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys
theBox->SetProperty, XCoord_Conv=xs, YCoord_Conv=ys

   ; Set up initial parameters. Contrast and brightness values
   ; go from 0 to 100. Start with 25% contrast and 75% brightness.

maxval = Max(image, Min=minVal)
contrast = 25
brightness = 75

   ; Calculate window level and width from contrast/brightness values.

level = (1-brightness/100.)*(maxVal - minVal) + minVal
width = (1-contrast/100.)*(maxVal - minVal)

   ; Calculate display minimum and maximum.

displayMax = (level + (width / 2))
displayMin = (level - (width / 2))

IF displayMax GT maxval THEN BEGIN
   difference = Abs(displayMax - maxval)
   displayMax = displayMax - difference
   displayMin = displayMin - difference
ENDIF
IF displayMin LT minval THEN BEGIN
   difference = Abs(minval - displayMin)
   displayMin = displayMin + difference
   displayMax = displayMax + difference
ENDIF

   ; Update the contrast and zoom image after changes.

contrastImage->SetProperty, Data=BytScl(image, Min=displayMin, Max=displayMax)
zoomImage->SetProperty, Data=BytScl(image, Min=displayMin, Max=displayMax)
colorbar2->SetProperty, Range=[displayMin, displayMax]

   ; Create the widgets for the program. Set software rendering for the draw
   ; widget, or rubberbanding may be slow.

tlb = Widget_Base(Title='Contrast/Zoom Object Graphics Example', /Base_Align_Center, $
   Column=1, /TLB_Size_Events)
drawID = Widget_Draw(tlb, XSize=window_xsize, YSize=window_ysize, Button_Events=1, $
   Graphics_Level=2, Event_Pro='ContrastZoom_DistinguishEvents', Expose_Events=1, $
   Renderer=1)
label = Widget_Label(tlb, Value='Left image allows ZOOMING. Center image allows WINDOWING. ' + $
   'Click and drag. Click and release in window restores original view.')
Widget_Control, tlb, /Realize

   ; Get the window object and draw the scene.

Widget_Control, drawID, Get_Value=theWindow
theWindow->Draw, theScene

   ; Create a container object to aid in proper object clean-up.

theContainer = Obj_New('IDL_Container')
theContainer->Add, thePalette
theContainer->Add, zoomView
theContainer->Add, contrastView
theContainer->Add, colorbarView
theContainer->Add, colorbar2View
theContainer->Add, theScene

   ; Create the info structure.

info = { zoomImage:zoomImage, $            ; The zoom image object.
         contrastImage: contrastImage, $   ; The contrast image object.
         image:Ptr_New(image), $           ; A pointer to the original image.
         subimage: Ptr_New(image), $       ; A pointer to the current image subset.
         zoomView:zoomView, $              ; The zoom view object.
         contrastView:contrastView, $      ; The contrast view object.
         colorbar:colorbar, $              ; The color bar object.
         colorbar2:colorbar2, $            ; The second color bar.
         colorbarView:colorbarView, $      ; The color bar view.
         theContainer:theContainer, $      ; The container object.
         theWindow:theWindow, $            ; The window object.
         theScene:theScene, $              ; The scene to be displayed.
         drawID:drawID, $                  ; The draw widget identifier.
         thebox: thebox, $                 ; The zoom box object.
         currentMode: "", $                ; The current mode the window is operating in.
         x1:-1L, $                         ; Locations in the window for contrast/brightness operations.
         x2:-1L, $
         y1:-1L, $
         y2:-1L, $
         zxsize:zxsize, $                  ; The X size of the zoom image.
         zysize:zysize, $                  ; The Y size of the zoom image.
         contrast:contrast, $              ; The current contrast value.
         brightness:brightness, $          ; The current brightness value.
         bstep:zxsize/512., $              ; The amount of brighness change for one pixel movement.
         cstep:zysize/512., $              ; The amount of contrast change for one pixel movement.
         maxVal:maxVal, $
         minVal:minVal, $
         window_xsize:window_xsize, $      ; The original X size of the draw widget.
         window_ysize:window_ysize, $      ; The original Y size of the draw widget.
         xs:-1L, $                         ; Locations in the window for zooming operations.
         ys:-1L, $
         xd:zxsize-1, $
         yd:zysize-1 }

Widget_Control, tlb, Set_UValue=info, /No_Copy
XManager, 'contrastzoom', tlb, /No_Block, Cleanup='ContrastZoom_Cleanup', $
   Event_Handler='Contrastzoom_Resize', Group_Leader=group_leader

END
