;  $Id: satplot.pro,v 1.3 2024/10/08 19:35:48 nathan Exp $
;  $Log: satplot.pro,v $
;  Revision 1.3  2024/10/08 19:35:48  nathan
;  contents of SATPLOT_2.3.tar.gz
;
;  Revision 2.3 2024/09/09 00:00:00 penteado
;  Updating satplot package to V2.3, added documentation
;  
; "Copyright 2012, by the California Institute of Technology.
; ALL RIGHTS RESERVED. United States Government Sponsorship
; acknowledged. Any commercial use must be negotiated with the
; Office of Technology Transfer at the California Institute of
; Technology.
;
; This software may be subject to U.S. export control laws.
; By accepting this software, the user agrees to comply with
; all applicable U.S. export laws and regulations. User has
; the responsibility to obtain export licenses, or other
; export authority as may be required before exporting such
; information to foreign countries or providing access to
; foreign persons."

;+
; satplot Version 2.3
;
; Example:
;
;  satplot,'jplot_20070831_000000__20070902_233000.png'
;               file name breakdown:
;                'jplot__' with double underscore
;                yyyymmdd date of interval start
;                single underscore
;                hhmmss time of interval start
;                double underscore
;                yyyymmdd date of interval end
;                single underscore
;                hhmmss time of interval end
;
; Notes:
;
;  Satplot (Solar Angle-Time plot) is one tool in a package designed to construct and analyze J-plots.
;   Other tools include (in order of execution):
;
;    Data pre-processing:
;
;     jdiff.pro      Create difference maps for COR2.
;     jsrem.pro      Create difference maps for HI_1 and HI_2.
;     jmap.pro       Create Platte Carre (simple cylindrical) maps from the difference images.
;                     These jmaps have twice latitudinal resolution, representing angle from sun center.
;                     These jmaps are composites of COR2, HI_1 and HI_2.  It is from these jmaps that a strip 
;                     (of Delta width, starting a Position Angle) is compressed into a column for the jplot.
;     jcount.pro     Count all of above elements for entire STEREO mission since launch.
;                     These counts help determine if sufficient data is available to create the desired jplot.
;
;    Data analysis:
;
;     jpredict.pro   Highlight a strip in a jmap for given datetime, Position Angle and Delta.
;     jplot.pro      Create a jplot (columns) for given datetime, Position Angle and Delta.
;                     A jplot may be of any time span, longer time spans just result in wider jplots.
;     satplot.pro    View and analyze a jplot, select tiepoints, save picture, save H-T plot and save report.
;     anaht5.pro     (AN EXAMPLE, NOT INCLUDED IN PACKAGE) analyse height-time plots, estimate t-0.
;
;    Additional required utilities:
;
;     get_closest_jmap_filename.pro  (required by:  jplot.pro jpredict.pro satplot.pro)
;     get_closest_fits_filename.pro  (required by:  jmap.pro)
;     get_closest_srem_filename.pro  (required by:  jmap.pro satplot.pro)
;     get_closest_tiff_filename.pro  (required by:  jmap.pro)
;     rotate_vector.pro  (required by:  jmap.pro)
;     chain_link_utilities.pro  (required by:  satplot.pro)
;
; Terminology:
;
;  jdiff - Difference images for COR2
;  jsrem - Difference images for HI_1 and HI_2.
;  jmap - Image that is a cylindrical map, composited from COR2, HI_1 and HI_2.
;  jplot - Image where rows represent angle from sun center, and columns represent time steps (cadence).
;  satplot - Tool for analyzing a jplot.  Also prefix for saved picture, plot and report that the tool can generate.
;  saved picture - PNG picture of main satplot window.
;  saved plot - PNG picture of Angle-Time plot (similar to a Height-Time plot.)
;  saved report - Text file of tiepoints and meta data (similar to Height-Time file, and is called *.ht)
;  curvefit - Curve fit program by Chris Mostl, Mar 2012, produces two fit methods:  Fixed Phi and Harmonic Mean.
;-
@chain_link_utilities.pro
FUNCTION satplot::ANGLE2TEL,angle
 CASE 1 OF
  (FLOAT(angle) GT SELF.hi_2_min_angle) : tel='hi_2'
  (FLOAT(angle) GT SELF.hi_1_min_angle) AND (angle LE SELF.hi_2_min_angle) : tel='hi_1'
  (FLOAT(angle) GT SELF.cor2_min_angle) AND (angle LE SELF.hi_1_min_angle) : tel='cor2'
  (FLOAT(angle) LE SELF.cor2_min_angle) : tel='cor1'
 ENDCASE
 RETURN,STRLOWCASE(tel)
END
FUNCTION satplot::get_color,x,y
 curwin=SELF.draw_window_id
 WSET,SELF.draw_window_id
 ;-------------------------------------------------------
 ; Make color black or white depending on the image value
 ; of an averaged area centered at the tiepoint location.
 ;-------------------------------------------------------
; buffer=0
; xsub=(x*(x LE buffer))+(buffer*(x GT buffer))
; ysub=(y*(y LE buffer))+(buffer*(y GT buffer))
; xtmp=(SELF.x_dataelements-1)-x
; xadd=(xtmp*(xtmp LE buffer))+(buffer*(xtmp GT buffer))
; ytmp=(SELF.y_dataelements-1)-y
; yadd=(ytmp*(ytmp LE buffer))+(buffer*(ytmp GT buffer))
; chip=(*SELF.jplot_image_ptr)[x-xsub:x+xadd,y-ysub:y+yadd]
; color=(!D.N_COLORS-1)*(MEAN(chip) LT 128)
 color=(!D.N_COLORS-1)*((*SELF.jplot_image_ptr)[x,y] LT 128)
 WSET,curwin
 RETURN,color
END
PRO satplot::plot_all_tiepoints,UPDATE_PLOT=update_plot
 IF (SIZE(update_plot,/TYPE) EQ 0) THEN BEGIN
  update_plot=0
 ENDIF
 IF ((SELF.actual_time_plot EQ 1) AND (SELF.plot_window_is_onscreen EQ 1) AND (update_plot EQ 1) AND (CHAIN_LINK_COUNTER(SELF.chain) EQ 0)) THEN BEGIN
  ; No tiepoints, update plot with this message.
  curwin=!D.WINDOW
  ; Use WINDOW instead of WSET so that RETAIN=2 can be invoked, which is critical for plotting.
  WINDOW,SELF.plot_window_id,RETAIN=2
  ERASE,!D.N_COLORS-1
  XYOUTS,0.5,0.5,'NO TIEPOINTS',ALIGN=0.5,CHARSIZE=5,COLOR=0,/NORMAL
  WSET,curwin
 ENDIF
 ;-------------------------------------------------------
 ; Plot all the tiepoints.
 ;-------------------------------------------------------
 SELF.chain=CHAIN_START_FINDER(SELF.chain)
 n_chain_links=CHAIN_LINK_COUNTER(SELF.chain)
 IF (n_chain_links EQ 0) THEN BEGIN
  RETURN
 ENDIF
 column=LONARR(n_chain_links)
 rowrow=LONARR(n_chain_links)
 color=LONARR(n_chain_links)
 actual_time_values=STRARR(n_chain_links)
 actual_jd_values=DBLARR(n_chain_links)
 observed_angle=DBLARR(n_chain_links)
 x_points=LONARR(n_chain_links)
 y_points=LONARR(n_chain_links)
 colors=BYTARR(n_chain_links)
 FOR i=0,n_chain_links-1 DO BEGIN
  actual_time_values[i]=(*(*SELF.chain).data)[1]
  data_string=(*(*SELF.chain).data)[0]
  data_string_elements=STRSPLIT(data_string,' ',/EXTRACT)
  column[i]=data_string_elements[N_ELEMENTS(data_string_elements)-2]
  rowrow[i]=data_string_elements[N_ELEMENTS(data_string_elements)-1]
  colors[i]=SELF->get_color(column[i],rowrow[i])
  x_points[i]=SELF.x1_margin+((column[i]-SELF.x_start_value)*SELF.zoom_value)+(SELF.zoom_value/2)
  y_points[i]=SELF.y1_margin+((rowrow[i]-SELF.y_start_value)*SELF.zoom_value)+(SELF.zoom_value/2)
  IF PTR_VALID((*SELF.chain).next) THEN BEGIN
   SELF.chain=(*SELF.chain).next
  ENDIF
 ENDFOR
 valid_ndx=WHERE((x_points GE SELF.x1_margin) AND (x_points LE (SELF.plot_x_pixels+SELF.x1_margin)) $
             AND (y_points GE SELF.y1_margin) AND (y_points LE (SELF.plot_y_pixels+SELF.y1_margin)),valid_count)
 IF (valid_count GT 0) THEN BEGIN
  x_points=x_points[valid_ndx]
  y_points=y_points[valid_ndx]
  colors=colors[valid_ndx]
  curwin=!D.WINDOW
  WSET,SELF.draw_window_id
  ; Plot tiepoints over jplot.
  PLOTS,x_points,y_points,PSYM=7,COLOR=colors,/DEVICE

  IF ((SELF.actual_time_plot EQ 1) AND (SELF.plot_window_is_onscreen EQ 1) AND (update_plot EQ 1)) THEN BEGIN
   ; In a separate plotting window, plot the actual FITS times from which each jpixel was derived.
   FOR which_point=0,n_chain_links-1 DO BEGIN
    observed_angle[which_point]=(*SELF.observed_angle_master_ptr)[rowrow[which_point]]
    actual_time_jd=ANYTIM2JD(actual_time_values[which_point])
    actual_jd_values[which_point]=actual_time_jd.(0)+actual_time_jd.(1)
   ENDFOR

   ;-------------------------------------------------------------------------------------------------------
   ; Kluge to force major tick marks to midnight instead of noon as per julian date convention.
   ; Artificially set time back by 12 hours.
   actual_jd_values=actual_jd_values-0.5d
   ;-------------------------------------------------------------------------------------------------------

   xmin=MIN(actual_jd_values)-((MAX(actual_jd_values)-MIN(actual_jd_values))*0.1)
   xmax=MAX(actual_jd_values)+((MAX(actual_jd_values)-MIN(actual_jd_values))*0.1)
   ymin=MIN(observed_angle)-((MAX(observed_angle)-MIN(observed_angle))*0.1)
   ymax=MAX(observed_angle)+((MAX(observed_angle)-MIN(observed_angle))*0.1)
   ; Format the first time for plot title.
;   first_actual_time=(actual_time_values[WHERE(actual_jd_values EQ MIN(actual_jd_values))])[0]
;   first_actual_year=STRMID(first_actual_time,0,4)
;   first_actual_month=STRMID(first_actual_time,5,2)
;   first_actual_day=STRMID(first_actual_time,8,2)
;   first_actual_hour=STRMID(first_actual_time,11,2)
;   first_actual_minute=STRMID(first_actual_time,14,2)
;   first_actual_formatted_date=first_actual_month+'/'+first_actual_day+'/'+first_actual_year
;   first_actual_formatted_time=first_actual_hour+':'+first_actual_minute
;   first_actual_formatted_datetime=first_actual_formatted_date+' '+first_actual_formatted_time

   ;-------------------------------------------------------------------------------------------------------
   ; Kluge to force major tick marks to midnight instead of noon as per julian date convention.
   ; Re-correct time forward by 12 hours.
   title_jd=xmin+0.5d
   ;-------------------------------------------------------------------------------------------------------

   CALDAT,title_jd,title_month,title_day,title_year,title_hour,title_minute
   IF title_hour LT 10 THEN title_hour='0'+STRTRIM(title_hour,2) ELSE title_hour=STRTRIM(title_hour,2)
   IF title_minute LT 10 THEN title_minute='0'+STRTRIM(title_minute,2) ELSE title_minute=STRTRIM(title_minute,2)
   title_datetime=STRTRIM(title_month,2)+'/'+STRTRIM(title_day,2)+'/'+STRTRIM(title_year,2)+' '+title_hour+':'+title_minute
   curwin=!D.WINDOW
   ; Use WINDOW instead of WSET so that RETAIN=2 can be invoked, which is critical for plotting.
   WINDOW,SELF.plot_window_id,RETAIN=2
   !P.MULTI=0
   ; default window is wider (x > y), use full window.  curvefit window is taller (x < y), use upper half of window.
   CASE !D.X_SIZE GT !D.Y_SIZE OF
    0 : plot_position=[SELF.x1_position,SELF.y2_position/2,SELF.x2_position,SELF.y2_position]
    1 : plot_position=[SELF.x1_position,SELF.y1_position,SELF.x2_position,SELF.y2_position]
   ENDCASE
;   PLOT,[actual_jd_values],[observed_angle],TITLE=first_actual_formatted_datetime+$
   PLOT,[actual_jd_values],[observed_angle],TITLE=title_datetime+$
    ', PA '+STRTRIM(SELF.position_angle,2)+', D '+STRTRIM(SELF.delta,2)+', '+SELF.spacecraft,$
    XTITLE='Date_Obs',$
    YTITLE='Elongation (degrees)',CHARSIZE=1.5,PSYM=7,$
    XTICKFORMAT='tick_string_function',XRANGE=[xmin,xmax],YRANGE=[ymin,ymax],XSTYLE=1,YSTYLE=1,$
    COLOR=0,BACKGROUND=!D.N_COLORS-1,POSITION=plot_position
   ; Draw the least squares line (only if 2 or more points).
   IF (CHAIN_LINK_COUNTER(SELF.chain) GT 1) THEN BEGIN
    time_sorted_ndx=SORT(actual_jd_values)
    time=actual_jd_values[time_sorted_ndx]
    height=observed_angle[time_sorted_ndx]
    nfeatures=CHAIN_LINK_COUNTER(SELF.chain)
    slope = (REGRESS( (time - time[0]), height, CONST=offset, /DOUBLE ))[0]
;help,time,nfeatures,(time[nfeatures-1])[0]
    OPLOT, [time[0],time[nfeatures-1]], [offset,(time[nfeatures-1]-time[0])*slope+offset],LINESTYLE=1,COLOR=0
   ENDIF
   ; Enable the curvefit button (only if 2 or more points).
   IF (CHAIN_LINK_COUNTER(SELF.chain) GE SELF.min_tiepoints_for_curvefit) THEN BEGIN
    WIDGET_CONTROL,SELF.curvefit_button,SENSITIVE=1
    WIDGET_CONTROL,SELF.curvefit_label,SENSITIVE=0
   ENDIF ELSE BEGIN
    WIDGET_CONTROL,SELF.curvefit_button,SENSITIVE=0
    WIDGET_CONTROL,SELF.curvefit_label,SENSITIVE=1
   ENDELSE
   WSET,curwin
   ; Update point counter.
   point_count=STRMID(SELF.point_count_string, 0, STRLEN(SELF.point_count_string)-STRLEN(STRTRIM(CHAIN_LINK_COUNTER(SELF.chain),2))) + STRTRIM(CHAIN_LINK_COUNTER(SELF.chain),2)
   WIDGET_CONTROL,SELF.tiepoint_count_box,SET_VALUE=point_count
  ENDIF
 ENDIF
END

PRO satplot::calculate_everything
 curwin=SELF.draw_window_id
 WSET,SELF.draw_window_id
 ; Normal coordinates: x1 y1 x2 y2.  Corners of the plot space where the axis start/end.
 SELF.x1_position=SELF.x1_margin/!D.X_SIZE
 SELF.y1_position=SELF.y1_margin/!D.Y_SIZE
 SELF.x2_position=(!D.X_SIZE-SELF.x2_margin)/!D.X_SIZE
 SELF.y2_position=(!D.Y_SIZE-SELF.y2_margin)/!D.Y_SIZE
 ; Pixel coordinates for TV command X,Y image placement.  Aligns with plot space.
 SELF.x_plotspace=!D.X_SIZE-(SELF.x1_margin+SELF.x2_margin)
 SELF.y_plotspace=!D.Y_SIZE-(SELF.y1_margin+SELF.y2_margin)
 ; Calculate pixels inside plot axes, or size of (zoomed) image, whichever is lesser.
 SELF.plot_x_pixels=(SELF.x_plotspace) < ROUND(SELF.x_dataelements*SELF.zoom_value)
 SELF.plot_y_pixels=(SELF.y_plotspace) < ROUND(SELF.y_dataelements*SELF.zoom_value)
 ; This fixes the special case of enlarging the window when x_start_slider is all the way to the right.
 IF (WIDGET_INFO(SELF.x_start_slider,/VALID_ID) EQ 1) THEN SELF->set_zoom_value,SELF.zoom_value
 ; This fixes the special case of enlarging the window when y_start_slider is all the way to the top.
 IF (WIDGET_INFO(SELF.y_start_slider,/VALID_ID) EQ 1) THEN SELF->set_zoom_value,SELF.zoom_value
 ; Subsetting values for trimming image input to CONGRID command.  Subsetting speeds up redraw.
 SELF.subset_xs=SELF.x_start_value
; SELF.subset_xe=(SELF.subset_xs+(SELF.x_plotspace-1)/SELF.zoom_value+SELF.zoom_value) < (SELF.x_dataelements-1)
 SELF.subset_xe=(SELF.subset_xs+(SELF.x_plotspace-1)/SELF.zoom_value) < (SELF.x_dataelements-1)
 SELF.subset_ys=SELF.y_start_value
; SELF.subset_ye=(SELF.subset_ys+(SELF.y_plotspace-1)/SELF.zoom_value+SELF.zoom_value) < (SELF.y_dataelements-1)
 SELF.subset_ye=(SELF.subset_ys+(SELF.y_plotspace-1)/SELF.zoom_value) < (SELF.y_dataelements-1)
 ; Y axis.
 IF (PTR_VALID(SELF.observed_angle_ptr)) THEN BEGIN
  PTR_FREE,SELF.observed_angle_ptr
 ENDIF
 SELF.y_end_value=SELF.y_start_value+(SELF.plot_y_pixels/SELF.zoom_value)-1
 SELF.observed_angle_ptr=PTR_NEW((*SELF.observed_angle_master_ptr)[SELF.y_start_value:SELF.y_end_value])
 ; X axis.
 IF (PTR_VALID(SELF.time_ptr)) THEN PTR_FREE,SELF.time_ptr
 current_start_time=(*SELF.time_master_ptr)[SELF.subset_xs]
 current_time_span=(*SELF.time_master_ptr)[SELF.subset_xe]-current_start_time
 time_axis_length=ROUND(SELF.plot_x_pixels/SELF.zoom_value)
 SELF.time_ptr=PTR_NEW(((DINDGEN(time_axis_length)/(time_axis_length-1))*current_time_span)+current_start_time)
 ; Format the date for plot title.
 CALDAT,(*SELF.time_ptr)[0],month,day,year,hour,minute,sec
 IF month LT 10 THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
 IF day LT 10 THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
 IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
 IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
 SELF.formatted_datetime=STRTRIM(year,2)+'-'+STRTRIM(month,2)+'-'+STRTRIM(day,2)+' '+hour+':'+minute

 ;-------------------------------------------------------------------------------------------------------
 ; Kluge to force major tick marks to midnight instead of noon as per julian date convention.
 ; See matching (*SELF.time_ptr)+0.5d in tick_string_function and readout functions.
 ; Artificially set time back by 12 hours.
 *SELF.time_ptr=(*SELF.time_ptr)-0.5d
 ;-------------------------------------------------------------------------------------------------------

 new_xsize=ROUND((SELF.subset_xe-SELF.subset_xs+1)*SELF.zoom_value)
 xmin_ndx=0
 xmax_ndx=(N_ELEMENTS(*SELF.time_ptr)-1) < ((SELF.plot_x_pixels-1) < (new_xsize-1))
 SELF.xmin=(*SELF.time_ptr)[xmin_ndx]
 SELF.xmax=(*SELF.time_ptr)[xmax_ndx]
 IF (SELF.x_plotspace GT SELF.plot_x_pixels) THEN BEGIN
  ; Extend xmax to end of x-axis on plot because time_ptr only goes to end of jplot image data.
  SELF.xmax=SELF.xmin+((*SELF.time_ptr)[N_ELEMENTS(*SELF.time_ptr)-1]-SELF.xmin)*(SELF.x_plotspace/DOUBLE(SELF.plot_x_pixels))
 ENDIF
 SELF.ymin=(*SELF.observed_angle_ptr)[0]
 SELF.ymax=(*SELF.observed_angle_ptr)[N_ELEMENTS(*SELF.observed_angle_ptr)-1]
 WSET,curwin
END
PRO satplot::set_x_start_value,value
 ; Convert slider value into pixels, where maximum value is image size minus plot size accounting for zoom.
 SELF.x_start_value=ROUND((value/DOUBLE(SELF.x_start_slider_max))*(SELF.x_dataelements-(SELF.plot_x_pixels/SELF.zoom_value)))
END
PRO satplot::set_y_start_value,value,THIS_IS_A_SLIDER_EVENT=this_is_a_slider_event
 ; Convert slider value into pixels, where maximum value is image size minus plot size accounting for zoom.
 SELF.y_start_value=ROUND((value/DOUBLE(SELF.y_start_slider_max))*(SELF.y_dataelements-(SELF.plot_y_pixels/SELF.zoom_value)))
 IF (KEYWORD_SET(this_is_a_slider_event)) THEN BEGIN
  SELF->calculate_everything
  WIDGET_CONTROL,SELF.y_start_label,SET_VALUE=STRMID(STRTRIM((*SELF.observed_angle_ptr)[0],2),0,5)
 ENDIF
END
FUNCTION satplot::get_zoom_value
 RETURN,SELF.zoom_value
END
FUNCTION satplot::get_zoom_scale
 RETURN,SELF.zoom_scale
END
FUNCTION satplot::get_zoom_label
 RETURN,SELF.zoom_label
END
PRO satplot::set_zoom_value,value
; SELF.zoom_value=ROUND(value*10.0)/10.0
 SELF.zoom_value=value
 IF ((SELF.x_dataelements-ROUND(SELF.plot_x_pixels/SELF.zoom_value)) GT 0) THEN BEGIN
  ; Adjust the x_start slider position as zoom changes.
  new_setting=FLOAT(SELF.x_start_value)
  new_setting=new_setting/(SELF.x_dataelements-ROUND(SELF.plot_x_pixels/SELF.zoom_value))
  new_setting=(new_setting*SELF.x_start_slider_max) < SELF.x_start_slider_max
  WIDGET_CONTROL,SELF.x_start_slider,SET_VALUE=new_setting
  ; Adjust the x_start value.
  SELF->set_x_start_value,new_setting
 ENDIF ELSE BEGIN
  WIDGET_CONTROL,SELF.x_start_slider,SET_VALUE=0
  SELF->set_x_start_value,0
 ENDELSE
 IF ((SELF.y_dataelements-ROUND(SELF.plot_y_pixels/SELF.zoom_value)) GT 0) THEN BEGIN
  ; Adjust the y_start slider position as zoom changes.
  new_setting=FLOAT(SELF.y_start_value)
  new_setting=new_setting/(SELF.y_dataelements-ROUND(SELF.plot_y_pixels/SELF.zoom_value))
  new_setting=(new_setting*SELF.y_start_slider_max) < SELF.y_start_slider_max
  WIDGET_CONTROL,SELF.y_start_slider,SET_VALUE=new_setting
  ; Adjust the y_start value.
  SELF->set_y_start_value,new_setting
 ENDIF ELSE BEGIN
  WIDGET_CONTROL,SELF.y_start_slider,SET_VALUE=0
  SELF->set_y_start_value,0
 ENDELSE
END
FUNCTION satplot::get_jd_first
 RETURN,SELF.jd_first
END
PRO zoom_slider_eh,event
 ; IDL object code does not allow an event handler to be a method.  Work-around:  get the object ID and call methods.
 IF (event.DRAG EQ 0) THEN RETURN
 WIDGET_CONTROL,event.TOP,GET_UVALUE=self_obj
 self_obj->set_zoom_value,event.VALUE*self_obj->get_zoom_scale()
 WIDGET_CONTROL,self_obj->get_zoom_label(),SET_VALUE='Zoom   '+STRMID(STRTRIM(self_obj->get_zoom_value(),2),0,3)
 self_obj->redraw
END
PRO x_start_slider_eh,event
 ; IDL object code does not allow an event handler to be a method.  Work-around:  get the object ID and call methods.
 IF (event.DRAG EQ 0) THEN RETURN
 WIDGET_CONTROL,event.TOP,GET_UVALUE=self_obj
 self_obj->set_x_start_value,event.VALUE
 self_obj->redraw
END
PRO y_start_slider_eh,event
 ; IDL object code does not allow an event handler to be a method.  Work-around:  get the object ID and call methods.
 IF (event.DRAG EQ 0) THEN RETURN
 WIDGET_CONTROL,event.TOP,GET_UVALUE=self_obj
 self_obj->set_y_start_value,event.VALUE,/this_is_a_slider_event
 self_obj->redraw
END
FUNCTION tick_string_function,axis,index,value
 ; Plotter tick function to return correct string per tickmark.

 ;-------------------------------------------------------------------------------------------------------
 ; Kluge to force major tick marks to midnight instead of noon as per julian date convention.
 ; See matching (*SELF.time_ptr)-0.5d in redraw routine.
 ; Re-correct time forward by 12 hours.
 value=value+0.5d
 ;-------------------------------------------------------------------------------------------------------

 CALDAT,value,month,day,year,hour,minute,sec
 ; Fix an odd problem where sometimes time is one minute early.
 minute=CEIL(minute)
 IF (minute GE 59) THEN BEGIN
  minute=0
  hour=hour+1
 ENDIF
 ; Pad the minute string with a zero, if necessary.
 IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
 IF ((hour EQ 0 OR hour EQ 24) AND minute EQ 0) THEN BEGIN
  ; Date.
  tick_string=STRTRIM(month,2)+'/'+STRTRIM(day,2)+'/'+STRMID(STRTRIM(year,2),2,2)
 ENDIF ELSE BEGIN
  ; Time.
  tick_string=STRTRIM(hour,2)+':'+minute
 ENDELSE
 RETURN,tick_string
END
PRO satplot::redraw,UPDATE_PLOT=update_plot
 curwin=SELF.draw_window_id
 WSET,SELF.draw_window_id
 SELF->calculate_everything
 ERASE,!D.N_COLORS-1
 new_xsize=ROUND((SELF.subset_xe-SELF.subset_xs+1)*SELF.zoom_value)
 new_ysize=ROUND((SELF.subset_ye-SELF.subset_ys+1)*SELF.zoom_value)
 img=CONGRID((*SELF.jplot_image_ptr)[SELF.subset_xs:SELF.subset_xe,SELF.subset_ys:SELF.subset_ye],new_xsize,new_ysize)
 xe=(SELF.plot_x_pixels-1) < (new_xsize-1)
 ye=(SELF.plot_y_pixels-1) < (new_ysize-1)
 TV,img[0:xe,0:ye],SELF.x1_margin,SELF.y1_margin
 XYOUTS,0.5,1.0-SELF.y2_margin/!D.Y_SIZE*0.4,'Angle vs. Time',CHARSIZE=3,COLOR=0,/NORMAL,ALIGN=0.5
 PLOT,*SELF.time_ptr,*SELF.observed_angle_ptr,TITLE=SELF.formatted_datetime+ $
  ', PA '+STRTRIM(SELF.position_angle,2)+', D '+STRTRIM(SELF.delta,2)+', '+SELF.spacecraft,$
  XTITLE='Time ('+STRTRIM(ROUND(60*24*SELF.jd_cadence),2)+' Minutes per Pixel Column)',$
  YTITLE='Elongation (degrees)',CHARSIZE=2,$
  XTICKFORMAT='tick_string_function',XRANGE=[SELF.xmin,SELF.xmax],YRANGE=[SELF.ymin,SELF.ymax],XSTYLE=1,YSTYLE=1,$
  POSITION=[SELF.x1_position,SELF.y1_position,SELF.x2_position,SELF.y2_position],$
  COLOR=0,BACKGROUND=!D.N_COLORS-1,/NODATA,/NOERASE
 SELF->plot_all_tiepoints,UPDATE_PLOT=update_plot
 ; When image data exceeds plot space, activate x-start and/or y-start sliders.
 WIDGET_CONTROL,SELF.x_start_slider,SENSITIVE=(SELF.x_dataelements GT ROUND(SELF.x_plotspace/SELF.zoom_value))
 WIDGET_CONTROL,SELF.y_start_slider,SENSITIVE=(SELF.y_dataelements GT ROUND(SELF.y_plotspace/SELF.zoom_value))
 ; Activate or de-activate depending on if any tiepoints exist in the chain.
 WIDGET_CONTROL,SELF.save_plot_button,SENSITIVE=PTR_VALID(SELF.chain)
 WIDGET_CONTROL,SELF.save_report_button,SENSITIVE=PTR_VALID(SELF.chain)
 WSET,curwin
END
FUNCTION satplot::get_output_filename
 ;               file name breakdown:
 ;                'satplot__' with double underscore
 ;                yyyymmdd date of interval start
 ;                single underscore
 ;                hhmmss time of interval start
 ;                double underscore
 ;                yyyymmdd date of interval end
 ;                single underscore
 ;                hhmmss time of interval end
 ;                double underscore
 ;                'pa' stands for position angle
 ;                value of position angle can be 1-3 digits
 ;                single underscore
 ;                'd' stands for delta or width, can by 1-3 digits
 ;                single underscore
 ;                'A' or 'B' stands for STEREO-A or STEREO-B
 ;                '.ht' file name extension stands for height time.
 CALDAT,(*SELF.time_master_ptr)[0],month,day,year,hour,minute,sec
print,month,day,year,hour,minute,sec
 IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
 IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
 IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
 IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
 IF (sec LT 10) THEN sec='0'+STRTRIM(FIX(sec),2) ELSE sec=STRTRIM(FIX(sec),2)
 begin_date=STRTRIM(year,2)+month+day+'_'+hour+minute+sec
 CALDAT,(*SELF.time_master_ptr)[N_ELEMENTS(*SELF.time_master_ptr)-1],month,day,year,hour,minute,sec
print,month,day,year,hour,minute,sec
 IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
 IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
 IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
 IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
 IF (sec LT 10) THEN sec='0'+STRTRIM(FIX(sec),2) ELSE sec=STRTRIM(FIX(sec),2)
 end_date=STRTRIM(year,2)+month+day+'_'+hour+minute+sec
 out_filename='satplot__'+begin_date+'__'+end_date
help,begin_date
 out_filename=out_filename+'__pa'+STRTRIM(SELF.position_angle,2)+'_d'+STRTRIM(SELF.delta,2)+'_'+STRMID(SELF.spacecraft,0,1,/REVERSE)
print,out_filename
 RETURN,out_filename
END
FUNCTION tick_string, axis, index, value
        ;-------------------------------------------------------
        ; Plotter tick function to return correct string per tickmark.
        ;-------------------------------------------------------
        CALDAT,value,month,day,year,hour,minute,second
        ; Fix an odd problem where sometimes time is one minute early.
        minute = CEIL( minute )
        IF ( minute GE 59 ) THEN BEGIN
                minute = 0
                hour = hour + 1
        ENDIF
        ; Pad the minute string with a zero, if necessary.
        IF ( minute LT 10 ) THEN minute = '0'+STRTRIM(minute,2) ELSE minute = STRTRIM(minute,2)
        IF ( hour EQ 0 ) THEN BEGIN
                ; Date.
                RETURN,STRTRIM(month,2)+'/'+STRTRIM(day,2)+'/'+STRTRIM(year,2)
        ENDIF ELSE BEGIN
                ; Time.
                RETURN,STRTRIM(hour,2)+':'+minute
        ENDELSE
END
PRO satplot::manage_widgets,event
 CASE event.ID OF
  SELF.tlb : BEGIN
   ;-------------------------------------------------------
   ; Resize by user.
   ;-------------------------------------------------------
   tlb_geom=WIDGET_INFO(SELF.tlb,/GEOMETRY)
   button_base0_geom=WIDGET_INFO(SELF.button_base0,/GEOMETRY)
   new_xsize=event.X-(tlb_geom.XPAD*2)
   new_ysize=event.Y-(tlb_geom.YPAD*2)-(button_base0_geom.YPAD)-(button_base0_geom.YSIZE)
   ; xsize minimum is 600 plus margins to allow for plot title.  ysize minimum is 300 plus margins.
   new_xsize=new_xsize > (600+SELF.x1_margin+SELF.x2_margin)
   new_ysize=new_ysize > (300+SELF.y1_margin+SELF.y2_margin)
   WIDGET_CONTROL,SELF.widgetdraw,XSIZE=new_xsize,YSIZE=new_ysize
   SELF->redraw
   END
  SELF.widgetdraw : BEGIN
   WIDGET_CONTROL,/HOURGLASS
   ; Update the cursor readouts.
;   x_ndx=ROUND((event.X-SELF.x1_margin-1)/SELF.zoom_value)
;   y_ndx=ROUND((event.Y-SELF.y1_margin-1)/SELF.zoom_value)
   x_ndx=(event.X-SELF.x1_margin)/SELF.zoom_value
   y_ndx=(event.Y-SELF.y1_margin)/SELF.zoom_value
   IF (x_ndx LT 0) OR (x_ndx GE N_ELEMENTS(*SELF.time_ptr)) $
   OR (y_ndx LT 0) OR (y_ndx GE N_ELEMENTS(*SELF.observed_angle_ptr)) THEN BEGIN
    ; Update elongation angle value.
    elongation_angle='n/a'
    elongation_angle=STRMID(SELF.elongation_angle_string, 0, STRLEN(SELF.elongation_angle_string)-STRLEN(elongation_angle)) + elongation_angle
    WIDGET_CONTROL,SELF.elongation_value_readout,SET_VALUE=elongation_angle
    ; Update time axis value.
    new_time='n/a'
    new_time=STRMID(SELF.time_axis_string, 0, STRLEN(SELF.time_axis_string)-STRLEN(new_time)) + new_time
    WIDGET_CONTROL,SELF.time_axis_value_readout,SET_VALUE=new_time
;    ; Update date_obs value (only if srem files available, normally only at JPL).
;    new_time='n/a'
;    new_time=STRMID(SELF.date_obs_string, 0, STRLEN(SELF.date_obs_string)-STRLEN(new_time)) + new_time
;    WIDGET_CONTROL,SELF.date_obs_value_readout,SET_VALUE=new_time
    ; Update detector value.
    detector='n/a'
    detector=STRMID(SELF.detector_string, 0, STRLEN(SELF.detector_string)-STRLEN(detector)) + detector
    WIDGET_CONTROL,SELF.detector_value_readout,SET_VALUE=detector
    RETURN
   ENDIF

   ;-------------------------------------------------------------------------------------------------------
   ; Kluge to force major tick marks to midnight instead of noon as per julian date convention.
   ; See matching (*SELF.time_ptr)-0.5d in redraw routine.
   ; Re-correct time forward by 12 hours.
   time=(*SELF.time_ptr)+0.5d
   ;-------------------------------------------------------------------------------------------------------

   ; Do a reverse-lookup to get the actual time for the jplot pixel (instead of just the 30-min cadence time.)
   CALDAT,time[x_ndx],month,day,year,hour,minute,sec
   year=STRTRIM(year,2)
   IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
   IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
   IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
   IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
   IF (sec LT 10) THEN sec='0'+STRTRIM(FIX(sec),2) ELSE sec=STRTRIM(FIX(sec),2)
   ; Spacecraft is 0 or 1 for GET_CLOSEST_SREM_FILENAME.
   sc=(STRMID(SELF.spacecraft,0,1,/REVERSE) EQ 'B')
   observed_angle=(*SELF.observed_angle_ptr)[y_ndx]
   tel=SELF->ANGLE2TEL(observed_angle)
   IF (STRLOWCASE(tel) EQ STRLOWCASE('cor1')) THEN BEGIN
     ; Update elongation angle value.
     elongation_angle='n/a'
     elongation_angle=STRMID(SELF.elongation_angle_string, 0, STRLEN(SELF.elongation_angle_string)-STRLEN(elongation_angle)) + elongation_angle
     WIDGET_CONTROL,SELF.elongation_value_readout,SET_VALUE=elongation_angle
     ; Update time axis value.
     new_time='n/a'
     new_time=STRMID(SELF.time_axis_string, 0, STRLEN(SELF.time_axis_string)-STRLEN(new_time)) + new_time
     WIDGET_CONTROL,SELF.time_axis_value_readout,SET_VALUE=new_time
;     ; Update date_obs value (only if srem files available, normally only at JPL).
;     date_obs='n/a'
;     date_obs=STRMID(SELF.date_obs_string, 0, STRLEN(SELF.date_obs_string)-STRLEN(date_obs)) + date_obs
;     WIDGET_CONTROL,SELF.date_obs_value_readout,SET_VALUE=date_obs
     ; Update detector value.
     detector='n/a'
     detector=STRMID(SELF.detector_string, 0, STRLEN(SELF.detector_string)-STRLEN(detector)) + detector
     WIDGET_CONTROL,SELF.detector_value_readout,SET_VALUE=detector
     RETURN
   ENDIF
;   actual_filename=GET_CLOSEST_SREM_FILENAME(year+month+day,hour+minute+sec,sc,tel)
;   actual_time=STRMID(FILE_BASENAME(actual_filename),0,15)
;   actual_year=STRMID(actual_time,0,4)
;   actual_month=STRMID(actual_time,4,2)
;   actual_day=STRMID(actual_time,6,2)
;   actual_hour=STRMID(actual_time,9,2)
;   actual_minute=STRMID(actual_time,11,2)
;   actual_sec=STRMID(actual_time,13,2)
;   formatted_datetime=actual_year+'-'+actual_month+'-'+actual_day+'T'+actual_hour+':'+actual_minute+':'+actual_sec
   ; Update elongation angle.
   elongation_angle=STRMID(STRTRIM((*SELF.observed_angle_ptr)[y_ndx],2),0,5)
   elongation_angle=STRMID(SELF.elongation_angle_string, 0, STRLEN(SELF.elongation_angle_string)-STRLEN(elongation_angle)) + elongation_angle
   WIDGET_CONTROL,SELF.elongation_value_readout,SET_VALUE=elongation_angle
   ; Update time axis value.
   time_axis_ndx=FLOOR((event.X-SELF.x1_margin)/SELF.zoom_value)
   time_axis_value=time[time_axis_ndx]
   CALDAT,time_axis_value,month,day,year,hour,minute,sec
   IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
   IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
   IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
   IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
   IF (sec LT 10) THEN sec='0'+STRMID(STRTRIM(sec,2),0,5) ELSE sec=STRMID(STRTRIM(sec,2),0,6)
   datetime=STRTRIM(year,2)+'-'+month+'-'+day+'T'+hour+':'+minute+':'+sec
   time_axis_value=STRMID(datetime,0,19)
   time_axis_value=STRTRIM(time_axis_value,2)
   time_axis_value=STRMID(SELF.time_axis_string, 0, STRLEN(SELF.time_axis_string)-STRLEN(time_axis_value)) + time_axis_value
   WIDGET_CONTROL,SELF.time_axis_value_readout,SET_VALUE=time_axis_value
;   ; Update date_obs value (only if srem files available, normally only at JPL).
;   date_obs=formatted_datetime
;   date_obs=STRMID(SELF.date_obs_string, 0, STRLEN(SELF.date_obs_string)-STRLEN(date_obs)) + date_obs
;   WIDGET_CONTROL,SELF.date_obs_value_readout,SET_VALUE=date_obs
   ; Update detector.
   detector=tel
   detector=STRMID(SELF.detector_string, 0, STRLEN(SELF.detector_string)-STRLEN(detector)) + detector
   WIDGET_CONTROL,SELF.detector_value_readout,SET_VALUE=detector

   ;-------------------------------------------------------
   ; Remember the mouse button status.
   ;-------------------------------------------------------
   IF (event.TYPE EQ 0) AND (event.PRESS GE 1) THEN BEGIN
    SELF.mouse_button_is_down=event.PRESS
    ;-------------------------------------------------------
    ; Middle button is being pressed, switch TYPE to 2 so
    ; that it thinks motion is occuring
    ;-------------------------------------------------------
    IF (event.PRESS EQ 2) THEN event.TYPE=2
   ENDIF
   IF (event.TYPE EQ 1) AND (event.RELEASE GE 2) THEN BEGIN
    SELF.mouse_button_is_down=0
; Do redraw only on mouse release to speed up moving tiepoints.
;SELF->redraw
   ENDIF
   CASE 1 OF
    ;-------------------------------------------------------
    ; Add tiepoint on left mouse button press.
    ;-------------------------------------------------------
    (event.TYPE EQ 0 AND event.PRESS EQ 1 AND event.RELEASE EQ 0) : BEGIN
     WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
     add_x_ndx=FLOOR((event.X-SELF.x1_margin)/SELF.zoom_value)
     add_y_ndx=FLOOR((event.Y-SELF.y1_margin)/SELF.zoom_value)
;add_x_ndx=x_ndx
;add_y_ndx=y_ndx
     ; This "time" variable has 0.5d added back in, see above.
     add_x_value=time[add_x_ndx]
     add_y_value=(*SELF.observed_angle_ptr)[add_y_ndx]
     add_x_pixel=add_x_ndx+SELF.x_start_value
     add_y_pixel=add_y_ndx+SELF.y_start_value

     ; Check to see if tiepoint already exists.
     SELF.chain=CHAIN_START_FINDER(SELF.chain)
     n_chain_links=CHAIN_LINK_COUNTER(SELF.chain)
     FOR link=0,n_chain_links-1 DO BEGIN
      data_string=(*(*SELF.chain).data)[0]
      data_string_elements=STRSPLIT(data_string,' ',/EXTRACT)
      tp_x_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-2]
      tp_y_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-1]
      ; Do nothing if tiepoint already exists for this pixel.
      IF ((tp_x_pixel EQ add_x_pixel) AND (tp_y_pixel EQ add_y_pixel)) THEN BEGIN
       WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
       RETURN
      ENDIF
      IF PTR_VALID((*SELF.chain).next) THEN BEGIN
       SELF.chain=(*SELF.chain).next
      ENDIF
     ENDFOR
     ; Add tiepoint.

     ; Format the date value.
     ; Angle is used in the height column for this satplot application.
     observed_angle_degrees=STRMID(add_y_value,4,8)
     CALDAT,add_x_value,month,day,year,hour,minute,sec
     IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
     IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
     IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
     IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
     IF (sec LT 10) THEN sec='0'+STRMID(STRTRIM(sec,2),0,5) ELSE sec=STRMID(STRTRIM(sec,2),0,6)
     datetime=STRTRIM(year,2)+'-'+month+'-'+day+'T'+hour+':'+minute+':'+sec
     date=STRMID(datetime,0,19)
     ; Angle appears again but is formatted with one decimal.
     observed_angle_one_decimal=STRMID(ROUND(FLOAT(observed_angle_degrees)*10)/10.0,4,6)
     CASE (SELF->ANGLE2TEL(FLOAT(observed_angle_degrees))) OF
      'hi_2' : tel='  HI2'
      'hi_1' : tel='  HI1'
      'cor2' : tel=' COR2'
      'cor1' : tel=' COR1'
     ENDCASE
     fc='  0'
     col=STRMID(FIX(add_x_pixel),3,5)
     row=STRMID(FIX(add_y_pixel),3,5)

     ; Do a reverse-lookup to get the actual time for the jplot pixel (instead of just the 30-min cadence time.)
     year=STRMID(date,0,4)
     month=STRMID(date,5,2)
     day=STRMID(date,8,2)
     hour=STRMID(date,11,2)
     minute=STRMID(date,14,2)
     sec=STRMID(date,17,2)
;     ; Spacecraft is 0 or 1 for GET_CLOSEST_SREM_FILENAME.
;     sc=([0,1])[(STRUPCASE(STRMID(SELF.spacecraft,0,1,/REVERSE)) EQ 'B')]
;     tel=SELF->ANGLE2TEL(observed_angle_degrees)
;     actual_filename=GET_CLOSEST_SREM_FILENAME(year+month+day,hour+minute+sec,sc,tel)
;     actual_time_value=STRMID(FILE_BASENAME(actual_filename),0,15)
;     year=STRMID(actual_time_value,0,4)
;     month=STRMID(actual_time_value,4,2)
;     day=STRMID(actual_time_value,6,2)
;     hour=STRMID(actual_time_value,9,2)
;     minute=STRMID(actual_time_value,11,2)
;     sec=STRMID(actual_time_value,13,2)
;     actual_time_string=year+'-'+month+'-'+day+'T'+hour+':'+minute+':'+sec
;DO TIME AXIS INSTEAD OF REVERSE-LOOUP.  THIS ALLOWS FUNCTIONALITY AWAY FROM JPL ENVIRONMENT.
     time_axis_ndx=FLOOR((event.X-SELF.x1_margin)/SELF.zoom_value)
     time_axis_value=time[time_axis_ndx]
     CALDAT,time_axis_value,month,day,year,hour,minute,sec
     IF (month LT 10) THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
     IF (day LT 10) THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
     IF (hour LT 10) THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
     IF (minute LT 10) THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
     IF (sec LT 10) THEN sec='0'+STRMID(STRTRIM(sec,2),0,5) ELSE sec=STRMID(STRTRIM(sec,2),0,6)
     datetime=STRTRIM(year,2)+'-'+month+'-'+day+'T'+hour+':'+minute+':'+sec
     time_axis_value=STRMID(datetime,0,19)
     time_axis_value=STRTRIM(time_axis_value,2)

     data_string=observed_angle_degrees+' '+date+observed_angle_one_decimal+' '+tel+fc+col+row
     ; Add "actual time" as second element in data string array.
;     data_string_array=[data_string,actual_time_string]
     data_string_array=[data_string,time_axis_value]
     SELF.chain=CHAIN_LINK_ADD(SELF.chain,data_string_array)
     SELF->redraw,/UPDATE_PLOT
     WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
     END
    ;-------------------------------------------------------
    ; Delete tiepoint on right mouse button press.
    ; Remove it from the screen.
    ;-------------------------------------------------------
    ((event.TYPE EQ 0 AND event.PRESS EQ 4) AND PTR_VALID(SELF.chain)) : BEGIN
     del_x_ndx=FLOOR((event.X-SELF.x1_margin)/SELF.zoom_value)
     del_y_ndx=FLOOR((event.Y-SELF.y1_margin)/SELF.zoom_value)
;del_x_ndx=x_ndx
;del_y_ndx=y_ndx
     del_x_pixel=del_x_ndx+SELF.x_start_value
     del_y_pixel=del_y_ndx+SELF.y_start_value
     SELF.chain=CHAIN_START_FINDER(SELF.chain)
     n_chain_links=CHAIN_LINK_COUNTER(SELF.chain)
     ; First, check all points for for direct hit.
     FOR link=0,n_chain_links-1 DO BEGIN
      data_string=(*(*SELF.chain).data)[0]
      data_string_elements=STRSPLIT(data_string,' ',/EXTRACT)
      tp_x_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-2]
      tp_y_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-1]
      ok_to_delete_tp=0
      CASE 1 OF
       ; If direct hit then delete point.
       ((tp_x_pixel EQ del_x_pixel) AND (tp_y_pixel EQ del_y_pixel)) : BEGIN
        ok_to_delete_tp=1
        END
       ; Else do nothing.
       ELSE : BEGIN
        END
      ENDCASE
      IF (ok_to_delete_tp EQ 1) THEN BEGIN
       SELF.chain=CHAIN_LINK_DELETE(SELF.chain)
       WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
       SELF->redraw,/UPDATE_PLOT
       WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
       RETURN
      ENDIF
      IF PTR_VALID((*SELF.chain).next) THEN BEGIN
       SELF.chain=(*SELF.chain).next
      ENDIF
     ENDFOR
     SELF.chain=CHAIN_START_FINDER(SELF.chain)
     ; Secondly, check for nearness.
     FOR link=0,n_chain_links-1 DO BEGIN
      data_string=(*(*SELF.chain).data)[0]
      data_string_elements=STRSPLIT(data_string,' ',/EXTRACT)
      tp_x_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-2]
      tp_y_pixel=data_string_elements[N_ELEMENTS(data_string_elements)-1]
      ok_to_delete_tp=0
      ; Escalating reach, so as to avoid multiple unwanted deletions.
      CASE 1 OF
       ; If neighbor on same line then delete point.
       ((ABS(tp_x_pixel-del_x_pixel) EQ 1) AND (tp_y_pixel EQ del_y_pixel)) : BEGIN
        ok_to_delete_tp=1
        END
       ; If neighbor within one pixel in any direction then delete point.
       (SQRT((tp_x_pixel-del_x_pixel)^2+(tp_y_pixel-del_y_pixel)^2) LE SQRT(2.0d)) : BEGIN
        ok_to_delete_tp=1
        END
       ; Else do nothing.
       ELSE : BEGIN
        END
      ENDCASE
      IF (ok_to_delete_tp EQ 1) THEN BEGIN
       SELF.chain=CHAIN_LINK_DELETE(SELF.chain)
       WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
       SELF->redraw,/UPDATE_PLOT
       WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
       RETURN
      ENDIF
      IF PTR_VALID((*SELF.chain).next) THEN BEGIN
       SELF.chain=(*SELF.chain).next
      ENDIF
     ENDFOR
     END
    ELSE : BEGIN
     END
   ENDCASE

   END
  SELF.save_picture_button : BEGIN
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
   curwin=!D.WINDOW
   WSET,SELF.draw_window_id
   img2sav=TVRD()
   png_filename=SELF->get_output_filename()+'_picture.png'
   WRITE_PNG,png_filename,img2sav
   PRINT,'Picture saved as:  ',png_filename
   tv,img2sav*0.75
   FOR thick=10,1,-1 DO XYOUTS,0.5,0.6,'picture',CHARSIZE=10,CHARTHICK=thick,COLOR=255/thick,/NORMAL,ALIGN=0.5
   FOR thick=10,1,-1 DO XYOUTS,0.5,0.3,'saved',CHARSIZE=10,CHARTHICK=thick,COLOR=255/thick,/NORMAL,ALIGN=0.5
   wait,1
   SELF->redraw
   WSET,curwin
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
   END
  SELF.save_plot_button : BEGIN
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
   curwin=!D.WINDOW
   ; Use WINDOW instead of WSET so that RETAIN=2 can be invoked, which is critical for plotting.
   WINDOW,SELF.plot_window_id,RETAIN=2
   SELF->redraw,/UPDATE_PLOT
   WSET,SELF.plot_window_id
   img2sav=TVRD()
   png_filename=SELF->get_output_filename()+'_plot.png'
   WRITE_PNG,png_filename,img2sav
   PRINT,'Plot saved as:  ',png_filename
   tv,img2sav*0.75
   FOR thick=10,1,-1 DO XYOUTS,0.5,0.6,'plot',CHARSIZE=10,CHARTHICK=thick,COLOR=255/thick,/NORMAL,ALIGN=0.5
   FOR thick=10,1,-1 DO XYOUTS,0.5,0.3,'saved',CHARSIZE=10,CHARTHICK=thick,COLOR=255/thick,/NORMAL,ALIGN=0.5
   wait,1
   SELF->redraw,/UPDATE_PLOT
   WSET,curwin
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
   END
  SELF.save_report_button : BEGIN
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
   SELF.chain=CHAIN_START_FINDER(SELF.chain)
   n_chain_links=CHAIN_LINK_COUNTER(SELF.chain)
   all_data=STRARR(n_chain_links)
   ; Extract all data from chain.
   FOR link=0,n_chain_links-1 DO BEGIN
    actual_time_string=(*(*SELF.chain).data)[1]
    data_string=(*(*SELF.chain).data)[0]
    all_data[link]=data_string
    IF (PTR_VALID((*SELF.chain).next) EQ 1) THEN BEGIN
     SELF.chain=(*SELF.chain).next
    ENDIF
    data=all_data[link]

    ; Skip over all leading whitespaces in the data string.
    position=0
    WHILE (STRMID(data,position,1) EQ ' ') DO BEGIN
     position=position+1
    ENDWHILE
    ; Locate the whitespaces surrounding the time string.
    first_whitespace=STRPOS(data,' ',position+1)
    second_whitespace=STRPOS(data,' ',first_whitespace+1)
    all_data[link]=STRMID(data,0,first_whitespace+1)+actual_time_string+STRMID(data,second_whitespace,STRLEN(data))

   ENDFOR

   ; Sort data by observed angle.
   all_angles=FLOAT(STRMID(all_data,0,8))
   sort_ndx=SORT(all_angles)
   all_data=all_data[sort_ndx]
   all_angles=all_angles[sort_ndx]
   ; Sub-sort by time, if necessary.
   IF (N_ELEMENTS(UNIQ(all_angles)) LT N_ELEMENTS(all_angles)) THEN BEGIN
    FOR angle=0,N_ELEMENTS(all_angles)-1 DO BEGIN
     sort_completed=0
     duplicate_angle_ndx=WHERE(all_angles EQ all_angles[angle],duplicate_angle_count)
     IF ((duplicate_angle_count GT 1) AND (sort_completed EQ 0)) THEN BEGIN
      ; Collect up all the julian times from the duplicate angle data strings.
      time2sort=DBLARR(duplicate_angle_count)
      FOR dup_ang=0,duplicate_angle_count-1 DO BEGIN      
       ; Skip over all leading whitespaces in the data string.
       time_position=0
       WHILE (STRMID(all_data[duplicate_angle_ndx[dup_ang]],time_position,1) EQ ' ') DO BEGIN
        time_position=time_position+1
       ENDWHILE
       time_position=STRPOS(data,' ',time_position+1)+1
       ; Convert time tag string to julian.
       time_tag_string=STRMID(all_data[duplicate_angle_ndx[dup_ang]],time_position,STRLEN('yyyy-mm-ddThh:mm:ss'))
       jd_struct=ANYTIM2JD(time_tag_string)
       time2sort[dup_ang]=jd_struct.(0)+jd_struct.(1)
      ENDFOR
      all_data[duplicate_angle_ndx]=all_data[duplicate_angle_ndx[SORT(time2sort)]]
     ENDIF
    ENDFOR
   ENDIF

   OPENW,lun,SELF->get_output_filename()+'.ht',/GET_LUN
   PRINTF,lun,'#VERSION=5'
   PRINTF,lun,'#DATE-OBS: '+STRMID((STRSPLIT(all_data[0],' ',/EXTRACT))[1],0,10)
   PRINTF,lun,'#TIME-OBS: '+STRMID((STRSPLIT(all_data[0],' ',/EXTRACT))[1],11,12)
   PRINTF,lun,'#SPACECRAFT: '+STRUPCASE(STRMID(STRTRIM(SELF.spacecraft,2),0,1,/REVERSE))
   PRINTF,lun,'#DETECTOR: JMAP'
   PRINTF,lun,'#FILTER:'
   PRINTF,lun,'#POLAR:'
   PRINTF,lun,'#OBSERVER: pliewer'
   PRINTF,lun,'#IMAGE_TYPE:'
   PRINTF,lun,'#UNITS: Degrees'
   PRINTF,lun,'#PLATESCALE:'
   PRINTF,lun,'#SUBFIELD[0,0]:'
   PRINTF,lun,'#COMMENT:'
   PRINTF,lun,'#FEAT_CODE:'
   PRINTF,lun,'# HEIGHT    DATE     TIME   ANGLE  TEL  FC  COL  ROW'
   FOR data=0,n_chain_links-1 DO BEGIN
    PRINTF,lun,all_data[data]
   ENDFOR
   FREE_LUN,lun
help,SELF->get_output_filename()
   PRINT,'Report saved as:  ',SELF->get_output_filename()+'.ht'
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
   END
  SELF.plot_button_on : BEGIN
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
   SELF.actual_time_plot=1
   ; Use WINDOW instead of WSET so that RETAIN=2 can be invoked, which is critical for plotting.
   WINDOW,SELF.plot_window_id,RETAIN=2
   ERASE,!D.N_COLORS-1
   SELF.plot_window_is_onscreen=1
   IF (CHAIN_LINK_COUNTER(SELF.chain) GT 0) THEN BEGIN
    WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
    SELF->plot_all_tiepoints,/UPDATE_PLOT
    WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
    WIDGET_CONTROL,SELF.save_plot_button,SENSITIVE=1
   ENDIF ELSE BEGIN
    WIDGET_CONTROL,SELF.save_plot_button,SENSITIVE=0
    XYOUTS,0.5,0.5,'NO TIEPOINTS',ALIGN=0.5,CHARSIZE=5,COLOR=0,/NORMAL
   ENDELSE
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
   END
  SELF.plot_button_off : BEGIN
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
   IF (SELF.plot_window_id GE 0) THEN BEGIN
    IF (SELF.plot_window_is_onscreen EQ 1) THEN BEGIN
     WDELETE,SELF.plot_window_id
     WIDGET_CONTROL,SELF.save_plot_button,SENSITIVE=0
     SELF.plot_window_is_onscreen=0
    ENDIF
   ENDIF
   SELF.actual_time_plot=0
   WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
   END
  SELF.read_report_button : BEGIN
   ; Read existing points file that was previously saved using this program.
   IF CHAIN_LINK_COUNTER(SELF.chain) GT 0 THEN BEGIN
    answer=DIALOG_MESSAGE('Importing points from HT file will replace existing tiepoints.  Continue Reading?',/QUESTION)
    If answer EQ 'No' THEN BEGIN
     RETURN
    ENDIF
   ENDIF
   ht_filename=DIALOG_PICKFILE()
   IF ht_filename EQ '' THEN BEGIN
    ; Indicates "Cancel" was clicked in the DIALOG_PICKFILE dialog.
    RETURN
   ENDIF
   
   ;--------------------------------------------------------------------------------------------------------------------------------------
   ;  ********** BEGIN ERROR CHECKS **********
   ;
   ; Error check the filename convention.
   IF STRLEN(FILE_BASENAME(ht_filename)) LT 4 THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Filename must conform to this convention.', $
                          'Example:', $
                          'satplot__20110607_000000__20110615_000000__pa252_d5_B.ht'])
    RETURN
   ENDIF
   IF N_ELEMENTS(STRSPLIT(FILE_BASENAME(ht_filename),'_')) NE 8 THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Filename must conform to this convention.', $
                          'Example:', $
                          'satplot__20110607_000000__20110615_000000__pa252_d5_B.ht'])
    RETURN
   ENDIF
   IF STRMID(FILE_BASENAME(ht_filename),2,3,/REVERSE) NE '.ht' THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Filename must have .ht extension.', $
                          'Example:', $
                          'satplot__20110607_000000__20110615_000000__pa252_d5_B.ht'])
    RETURN
   ENDIF
   ; Error check the start time.  Must conform to previously initialized value.
   datetime_first=(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[1]+'_'+(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[2]
help,datetime_first
   jd_structure_first=ANYTIM2JD( datetime_first )
   jd_first=jd_structure_first.(0) + jd_structure_first.(1)
   IF (jd_first NE SELF.jd_first) THEN BEGIN
    CALDAT,SELF.jd_first,month,day,year,hour,minute,sec
    year=STRTRIM(year,2)
    IF month LT 10 THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
    IF day LT 10 THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
    IF hour LT 10 THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
    IF minute LT 10 THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
    IF sec LT 10 THEN sec='0'+STRTRIM(sec,2) ELSE sec=STRTRIM(sec,2)
    datetime=STRTRIM(year,2)+'-'+month+'-'+day+'_'+hour+':'+minute+':'+sec
    ddddd=DIALOG_MESSAGE(['Incompatable HT file.', $
                          'Start Time must be the same as initialized.', $
                          'Initialized value:  '+datetime, $
                          'Incompatable value: '+datetime_first, $
                          '', $
                          'Failed to import:  '+ht_filename],/INFORMATION)
    RETURN
   ENDIF
   ; Error check the end time.  Must conform to previously initialized value.
   datetime_last=(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[3]+'_'+(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[4]
   jd_structure_last=ANYTIM2JD( datetime_last )
   jd_last=jd_structure_last.(0) + jd_structure_last.(1)
   IF (jd_last NE SELF.jd_last) THEN BEGIN
    CALDAT,SELF.jd_last,month,day,year,hour,minute,sec
    year=STRTRIM(year,2)
    IF month LT 10 THEN month='0'+STRTRIM(month,2) ELSE month=STRTRIM(month,2)
    IF day LT 10 THEN day='0'+STRTRIM(day,2) ELSE day=STRTRIM(day,2)
    IF hour LT 10 THEN hour='0'+STRTRIM(hour,2) ELSE hour=STRTRIM(hour,2)
    IF minute LT 10 THEN minute='0'+STRTRIM(minute,2) ELSE minute=STRTRIM(minute,2)
    IF sec LT 10 THEN sec='0'+STRTRIM(sec,2) ELSE sec=STRTRIM(sec,2)
    datetime=STRTRIM(year,2)+'-'+month+'-'+day+'_'+hour+':'+minute+':'+sec
    ddddd=DIALOG_MESSAGE(['Incompatable HT file.', $
                          'Ending Time must be the same as initialized.', $
                          'Initialized value:  '+datetime, $
                          'Incompatable value: '+datetime_last, $
                          '', $
                          'Failed to import:  '+ht_filename],/INFORMATION)
    RETURN
   ENDIF
   ; Error check the position angle.  Must conform to previously initialized value.
   position_angle=(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[5]
   position_angle=FLOAT(STRMID(position_angle,2,STRLEN(position_angle)-2))
   IF (position_angle NE SELF.position_angle) THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Incompatable HT file.', $
                          'Position Angle must be the same as initialized.', $
                          'Initialized value:  '+STRTRIM(SELF.position_angle,2), $
                          'Incompatable value: '+STRTRIM(position_angle,2), $
                          '', $
                          'Failed to import:  '+ht_filename],/INFORMATION)
    RETURN
   ENDIF
   ; Error check the delta.  Must conform to previously initialized value.
   delta=(STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[6]
   delta=FLOAT(STRMID(delta,1,STRLEN(delta)-1))
   IF (delta NE SELF.delta) THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Incompatable HT file.', $
                          'Delta value must be the same as initialized.', $
                          'Initialized value:  '+STRTRIM(SELF.delta,2), $
                          'Incompatable value: '+STRTRIM(delta,2), $
                          '', $
                          'Failed to import:  '+ht_filename],/INFORMATION)
    RETURN
   ENDIF
   ; Error check the spacecraft.  Must conform to previously initialized value.
   spacecraft='STEREO-'+STRUPCASE((STRSPLIT((STRSPLIT(FILE_BASENAME(ht_filename),'_',/EXTRACT))[7],'.',/EXTRACT))[0])
   IF (spacecraft NE SELF.spacecraft) THEN BEGIN
    ddddd=DIALOG_MESSAGE(['Incompatable HT file.', $
                          'Spacecraft must be the same as initialized.', $
                          'Initialized value:  '+SELF.spacecraft, $
                          'Incompatable value: '+spacecraft, $
                          '', $
                          'Failed to import:  '+ht_filename], $
                          /INFORMATION)
    RETURN
   ENDIF
   ;
   ;  ********** FINISHED ERROR CHECKS **********
   ;--------------------------------------------------------------------------------------------------------------------------------------

   ; Eliminate any existing tiepoints.
   IF PTR_VALID(SELF.chain) THEN BEGIN
    result=CHAIN_DEATH(SELF.chain)
   ENDIF
   ; Read the selected file.
   ascii=READ_ASCII(ht_filename,COUNT=count)
   ascii=STRARR(count)
   OPENR,lun,ht_filename,/GET_LUN
   READF,lun,ascii
   FREE_LUN,lun
   ; Isolate data from metadata.
   ndx=WHERE(STRMID(ascii,0,1) NE '#',count)
   all_data=ascii[ndx]
   ; Populate the chain with tiepoints from the file.
   FOR t=0,count-1 DO BEGIN
    data=all_data[t]
    ; Skip over all leading whitespaces in the data string.
    position=0
    WHILE (STRMID(data,position,1) EQ ' ') DO BEGIN
     position=position+1
    ENDWHILE
    ; Locate the whitespaces surrounding the time string.
    first_whitespace=STRPOS(data,' ',position+1)
    second_whitespace=STRPOS(data,' ',first_whitespace+1)
    actual_time_value=STRMID(data,first_whitespace+1,second_whitespace-first_whitespace)
    ; Create one link with one tiepoint.
    SELF.chain=CHAIN_LINK_ADD(SELF.chain,[data,actual_time_value])
   ENDFOR
   ; Redraw using new tiepoints.
   SELF->redraw,/UPDATE_PLOT
   END
  SELF.curvefit_button : BEGIN
   SELF->manage_widgets,{id:SELF.save_report_button,top:SELF.tlb,handler:SELF.tlb,select:1}
   curwin=!D.WINDOW
   ; Curve fit program by Chris Mostl, Mar 2012, produces two fit methods:  Fixed Phi and Harmonic Mean.
   WIDGET_CONTROL,/HOURGLASS
   results=fitall(SELF->get_output_filename()+'.ht')
   WSET,curwin
   END
  ELSE:
 ENDCASE
END
PRO manage_widgets,event
 ;-------------------------------------------------------
 ; This routine is necessary because widget event handler 
 ; routines cannot be object methods.  This routine just 
 ; passes the event to an object method.
 ;-------------------------------------------------------
 WIDGET_CONTROL,event.TOP,GET_UVALUE=satplot_obj
 IF OBJ_VALID(satplot_obj) THEN satplot_obj->manage_widgets,event
END
PRO satplot::create_widgets
 SELF.tlb=WIDGET_BASE(/TLB_SIZE_EVENTS,TITLE='Solar Angle-Time Plot',UVALUE=SELF,KILL_NOTIFY='cleanup_pro',/COLUMN)
 ; xsize and ysize defaults are lesser of image size or 80% screen size.
 draw_xsize=((GET_SCREEN_SIZE())[0]*.8) < (SELF.x_dataelements+SELF.x1_margin+SELF.x2_margin)
 draw_ysize=((GET_SCREEN_SIZE())[1]*.8) < (SELF.y_dataelements+SELF.y1_margin+SELF.y2_margin)
 ; xsize minimum is 600 plus margins to allow for plot title.  ysize minimum is 300 plus margins.
 draw_xsize=(600+SELF.x1_margin+SELF.x2_margin) > draw_xsize
 draw_ysize=(300+SELF.y1_margin+SELF.y2_margin) > draw_ysize
 draw_xsize=ROUND(draw_xsize)
 draw_ysize=ROUND(draw_ysize)
 ; ysize default is lesser of image height or 90% screen height.
 SELF.button_base0=WIDGET_BASE(SELF.tlb,/ROW)
 button_base2=WIDGET_BASE(SELF.button_base0,/COLUMN)
; SELF.up_button=WIDGET_BUTTON(SELF.button_base1,VALUE='^')
; SELF.down_button=WIDGET_BUTTON(SELF.button_base1,VALUE='v')
 x_start_label=WIDGET_LABEL(button_base2,VALUE='Time Start')
 SELF.x_start_slider=WIDGET_SLIDER(button_base2,/SUPPRESS_VALUE,EVENT_PRO='x_start_slider_eh',$
  MIN=0,MAX=SELF.x_start_slider_max,/DRAG)
 SELF.zoom_label=WIDGET_LABEL(button_base2,VALUE='Zoom   '+STRMID(STRTRIM(SELF.zoom_value,2),0,3))
 SELF.zoom_slider=WIDGET_SLIDER(button_base2,/SUPPRESS_VALUE,EVENT_PRO='zoom_slider_eh',$
  MIN=ROUND(1/SELF.zoom_scale),MAX=SELF.zoom_max,/DRAG)
 button_base3=WIDGET_BASE(SELF.button_base0,/COLUMN)
 y_start_label=WIDGET_LABEL(button_base3,VALUE='Angle Start')
 SELF.y_start_label=WIDGET_LABEL(button_base3,VALUE=' 0.000 ')
 SELF.y_start_slider=WIDGET_SLIDER(SELF.button_base0,/SUPPRESS_VALUE,EVENT_PRO='y_start_slider_eh',$
  MIN=0,MAX=SELF.y_start_slider_max,/DRAG,/VERTICAL)
 button_base4=WIDGET_BASE(SELF.button_base0,/COLUMN)
 button_base4a=WIDGET_BASE(button_base4,/COLUMN,/FRAME)
 SELF.point_count_string=     'Points     =                   0'
 SELF.elongation_angle_string='Elongation =                 n/a'
; SELF.date_obs_string=        'Date_Obs   =                 n/a'
 SELF.time_axis_string=       'Time Axis  =                 n/a'
 SELF.detector_string=        'Detector   =                 n/a'
 SELF.tiepoint_count_box=WIDGET_LABEL(button_base4a,VALUE=SELF.point_count_string)
 button_base4b=WIDGET_BASE(button_base4,/COLUMN,/FRAME)
 label=WIDGET_LABEL(button_base4b,VALUE='Cursor Coordinates')
 SELF.elongation_value_readout=WIDGET_LABEL(button_base4b,VALUE=SELF.elongation_angle_string)
 SELF.time_axis_value_readout=WIDGET_LABEL(button_base4b,VALUE=SELF.time_axis_string)
; SELF.date_obs_value_readout=WIDGET_LABEL(button_base4b,VALUE=SELF.date_obs_string)
 SELF.detector_value_readout=WIDGET_LABEL(button_base4b,VALUE=SELF.detector_string)
 button_base5=WIDGET_BASE(SELF.button_base0,/COLUMN,/FRAME)
 save_label=WIDGET_LABEL(button_base5,VALUE='Export')
 SELF.save_report_button=WIDGET_BUTTON(button_base5,VALUE='Points')
 SELF.save_picture_button=WIDGET_BUTTON(button_base5,VALUE='Picture')
 SELF.save_plot_button=WIDGET_BUTTON(button_base5,VALUE='Plot')
 button_base6=WIDGET_BASE(SELF.button_base0,/COLUMN)
 button_base6a=WIDGET_BASE(button_base6,/COLUMN,/FRAME)
 save_label=WIDGET_LABEL(button_base6a,VALUE='Import')
 SELF.read_report_button=WIDGET_BUTTON(button_base6a,VALUE='Points (.ht)')
 button_base6b=WIDGET_BASE(button_base6,/COLUMN,/FRAME)
; plot_label=WIDGET_LABEL(button_base6b,VALUE='Plot')
 button_base7=WIDGET_BASE(button_base6b,/COLUMN,/EXCLUSIVE)
 SELF.plot_button_on=WIDGET_BUTTON(button_base7,VALUE='Plot On')
 SELF.plot_button_off=WIDGET_BUTTON(button_base7,VALUE='Plot Off')
 button_base8=WIDGET_BASE(SELF.button_base0,/COLUMN,/FRAME)
 SELF.min_tiepoints_for_curvefit=4
 SELF.curvefit_label=WIDGET_LABEL(button_base8,VALUE='('+STRTRIM(SELF.min_tiepoints_for_curvefit,2)+' pt min)')
 SELF.curvefit_button=WIDGET_BUTTON(button_base8,VALUE='Curve Fit',SENSITIVE=0)
 curvefit_label=WIDGET_LABEL(button_base8,VALUE='Points')
 curvefit_label=WIDGET_LABEL(button_base8,VALUE='will be')
 curvefit_label=WIDGET_LABEL(button_base8,VALUE='exported')
 curvefit_label=WIDGET_LABEL(button_base8,VALUE='to file.')
 SELF.widgetdraw=WIDGET_DRAW(SELF.tlb,XSIZE=draw_xsize,YSIZE=draw_ysize,RETAIN=2,/BUTTON_EVENTS,/MOTION_EVENTS)
 WIDGET_CONTROL,SELF.tlb,/REALIZE
 WIDGET_CONTROL,SELF.widgetdraw,GET_VALUE=window_id
 SELF.draw_window_id=window_id
 curwin=!D.WINDOW
 WINDOW
 ERASE,!D.N_COLORS-1
 SELF.plot_window_id=!D.WINDOW
 WSET,curwin
END
PRO satplot::cleanup
 PTR_FREE,SELF.jplot_image_ptr
 PTR_FREE,SELF.observed_angle_master_ptr
 PTR_FREE,SELF.observed_angle_ptr
 PTR_FREE,SELF.time_master_ptr
 PTR_FREE,SELF.time_ptr
 IF PTR_VALID(SELF.chain) THEN BEGIN
  result=CHAIN_DEATH(SELF.chain)
 ENDIF
 IF (SELF.plot_window_is_onscreen EQ 1 AND !D.WINDOW GE 0) THEN BEGIN
;print,'SELF.plot_window_id = ',SELF.plot_window_id
;print,'SELF.plot_window_is_onscreen = ',SELF.plot_window_is_onscreen
;print,'!d.window = ',!d.window
  WDELETE,SELF.plot_window_id
 ENDIF
END
PRO cleanup_pro,tlb
 WIDGET_CONTROL,tlb,GET_UVALUE=satplot
 OBJ_DESTROY,satplot
END
FUNCTION satplot::init,jplot_image,datetime_first,datetime_last,position_angle,delta,spacecraft
 SELF.jplot_image_ptr=PTR_NEW(jplot_image)
 SELF.x_dataelements=N_ELEMENTS((*SELF.jplot_image_ptr)[*,0])
 SELF.y_dataelements=N_ELEMENTS((*SELF.jplot_image_ptr)[0,*])
 jd_structure_first=ANYTIM2JD( datetime_first )
 jd_structure_last=ANYTIM2JD( datetime_last )
 SELF.jd_first=jd_structure_first.(0) + jd_structure_first.(1)
 SELF.jd_last=jd_structure_last.(0) + jd_structure_last.(1)
 time_master=(DINDGEN(SELF.x_dataelements)/(SELF.x_dataelements-1))*(SELF.jd_last-SELF.jd_first)+SELF.jd_first
 SELF.time_master_ptr=PTR_NEW(time_master)
 ; Build observed angle.
 SELF.observed_angle_cadence=(90.0)/(SELF.y_dataelements-1)
 SELF.observed_angle_master_ptr=PTR_NEW(DBLARR((SELF.y_dataelements)))
 FOR t=1,SELF.y_dataelements-1 DO BEGIN
  (*SELF.observed_angle_master_ptr)[t]=(*SELF.observed_angle_master_ptr)[t-1]+SELF.observed_angle_cadence
 ENDFOR
 ; Calculate cadence (fraction of angle degree per row and fraction of julian day per image column).
 ; (The jmaps are constant degrees per pixel [Plate Carree], thus per-pixel-cadence is a fixed value.)
 SELF.jd_cadence=(SELF.jd_last-SELF.jd_first)/(SELF.x_dataelements-1)
 SELF.position_angle=position_angle
 SELF.delta=delta
 SELF.spacecraft=spacecraft
 SELF.x_start_slider_max=100
 SELF.y_start_slider_max=80
 SELF.zoom_scale=0.1d
 SELF.zoom_value=1.0d
 SELF.zoom_max=100
 SELF.x1_margin=80
 SELF.y1_margin=80
 SELF.x2_margin=80
 SELF.y2_margin=80
 CASE STRMID(STRUPCASE(SELF.spacecraft),0,1,/REVERSE) OF
  'A' : BEGIN
   SELF.cor2_min_angle=0.5	;0.83
   SELF.hi_1_min_angle=3.75
   SELF.hi_2_min_angle=18.1
   END
  'B' : BEGIN
   SELF.cor2_min_angle=0.5	;0.95
   SELF.hi_1_min_angle=4.10
   SELF.hi_2_min_angle=19.0
   END
 ENDCASE
 SELF->create_widgets
 WIDGET_CONTROL,SELF.plot_button_on,/SET_BUTTON
 SELF.actual_time_plot=1
 SELF.plot_window_is_onscreen=1
 SELF->calculate_everything
 XMANAGER,'satplot',SELF.tlb,EVENT_HANDLER='manage_widgets',NO_BLOCK=1
 WIDGET_CONTROL,SELF.tlb,SENSITIVE=0
 SELF->redraw,/UPDATE_PLOT
 WIDGET_CONTROL,SELF.tlb,SENSITIVE=1
 RETURN,1
END
PRO satplot__define
 satplot={ $
  satplot,$
  tlb:0L,$
  button_base0:0L,$
;  up_button:0L,$
  button_base1:0L,$
  point_count_string:'',$
  elongation_angle_string:'',$
  time_axis_string:'',$
;  date_obs_string:'',$
  detector_string:'',$
  tiepoint_count_box:0L,$
;  down_button:0L,$
  widgetdraw:0L,$
  draw_window_id:0L,$
  plot_window_id:0L,$
  plot_window_is_onscreen:0,$
  x1_position:0.0d,$
  y1_position:0.0d,$
  x2_position:0.0d,$
  y2_position:0.0d,$
  x1_margin:0.0d,$
  y1_margin:0.0d,$
  x2_margin:0.0d,$
  y2_margin:0.0d,$
  x_plotspace:0L,$
  y_plotspace:0L,$
  plot_x_pixels:0L,$
  plot_y_pixels:0L,$
  formatted_datetime:'',$
  jd_first:0.0d,$
  jd_last:0.0d,$
  jd_cadence:0.0d,$
  observed_angle_cadence:0.0d,$
  position_angle:'',$
  delta:'',$
  spacecraft:'',$
  zoom_value:0.0d,$
  zoom_slider:0L,$
  zoom_label:0L,$
  zoom_max:0.0d,$
  zoom_scale:0.0d,$
  x_start_slider_max:0L,$
  x_start_value:0L,$
  x_start_slider:0L,$
  y_start_slider_max:0L,$
  y_start_value:0L,$
  y_end_value:0L,$
  y_start_slider:0L,$
  y_start_label:0L,$
  time_axis_value_readout:0L,$
;  date_obs_value_readout:0L,$
  elongation_value_readout:0L,$
  detector_value_readout:0L,$
  save_picture_button:0L,$
  save_plot_button:0L,$
  save_report_button:0L,$
  read_report_button:0L,$
  actual_time_plot_button:0L,$
  plot_button_on:0L,$
  plot_button_off:0L,$
  min_tiepoints_for_curvefit:0L,$
  curvefit_button:0L,$
  curvefit_label:0L,$
  actual_time_plot:0L,$
  subset_xs:0L,$
  subset_xe:0L,$
  subset_ys:0L,$
  subset_ye:0L,$
  xmin:0.0d,$
  xmax:0.0d,$
  ymin:0.0d,$
  ymax:0.0d,$
  x_dataelements:0L,$
  y_dataelements:0L,$
  cor2_min_angle:0.0,$
  hi_1_min_angle:0.0,$
  hi_2_min_angle:0.0,$
  mouse_button_is_down:0L,$
  chain:PTR_NEW(),$
  time_master_ptr:PTR_NEW(),$
  time_ptr:PTR_NEW(),$
  observed_angle_master_ptr:PTR_NEW(),$
  observed_angle_ptr:PTR_NEW(),$
  jplot_image_ptr:PTR_NEW()}
END
PRO satplot,jplot_image_filename
 datetime_first=(STRSPLIT(jplot_image_filename,'_',/EXTRACT))[1]+'_'+(STRSPLIT(jplot_image_filename,'_',/EXTRACT))[2]
help,datetime_first
 datetime_last=(STRSPLIT(jplot_image_filename,'_',/EXTRACT))[3]+'_'+(STRSPLIT(jplot_image_filename,'_',/EXTRACT))[4]
 position_angle=FLOAT((STRSPLIT(jplot_image_filename,'_',/EXTRACT))[5])
 delta=FLOAT((STRSPLIT(jplot_image_filename,'_',/EXTRACT))[6])
 WHILE (STRMID(position_angle,0,1,/REVERSE) EQ '0') DO position_angle=STRMID(position_angle,0,STRLEN(position_angle)-1)
 IF (STRMID(position_angle,0,1,/REVERSE) EQ '.') THEN position_angle=STRMID(position_angle,0,STRLEN(position_angle)-1)
 WHILE (STRMID(delta,0,1,/REVERSE) EQ '0') DO delta=STRMID(delta,0,STRLEN(delta)-1)
 IF (STRMID(delta,0,1,/REVERSE) EQ '.') THEN delta=STRMID(delta,0,STRLEN(delta)-1)
 spacecraft='STEREO-'+STRUPCASE((STRSPLIT((STRSPLIT(jplot_image_filename,'_',/EXTRACT))[7],'.',/EXTRACT))[0])
satplot_obj=OBJ_NEW('satplot',READ_IMAGE(jplot_image_filename),datetime_first,datetime_last,position_angle,delta,spacecraft)
END
