import add itcl

class ZoomStackGraph {

    # The name of graph (nee the namespace path)
    variable graph ""

    # Indicates which corner of the rectangular zoom region 
    # is currently being choosen.  
    variable corner "first"

    # Coordinates of the current zoom region. They represent the
    # two corners of a rectangular area.  The two points are order 
    # independent.
    variable x1
    variable y1
    variable x2
    variable y2

    # A list of axis configuration commmands.  Acts as a stack to
    # unzoom the graph back to previous axis limits.
    variable stack {}

    constructor { args } {
	# This will need to change when we start using inheritance.
	set graph [info namespace tail $this]

	# What about collisions between the blt::graph instance
	# command and the ZoomStackGraph instance command?
	blt::graph $graph

	if { [llength $args] > 0 } {
	    $graph configure $args
	}
	# Set up the bindings to select/deselect the zoom region
	bind $graph <1> [code $this SelectPoint %x %y]
	bind $graph <3> [code $this ClearZoom]
	# The particular mouse buttons should be configurable.
    }
    destructor {
	if { [winfo exists $graph] } {
	    destroy $graph
	}
    }

    # These methods are used internally, within this class, to manage the
    # zoom stack.
    private method SaveCoords { x y } 
    private method Zoom {}
    private method Unzoom {}
    private method Push { cmd }
    private method Pop {}
    private method MarkPoint { x y }
    private method SetTitle { title }
    private method DrawBox { } 

    # These methods are called by "bind" and "after" from the Tk 
    # event loop.  Is there any way of hiding them, so that it
    # doesn't look to the user as part of the public interface?
    method ClearZoom {}
    method ClearTitle {}
    method UpdateOutline { x y } 
    method SelectPoint { x y }
}

# ----------------------------------------------------------------------
# 
# SaveCoords --
#
#	Given a point on the screen, transforms the point into graph 
#	coordinates and saves it as one of the points representing a 
#	corner of the zoom region. 
#
# ----------------------------------------------------------------------
body ZoomStackGraph::SaveCoords { x y } {

    set coords [$graph invtransform $x $y]
    set x [lindex $coords 0]
    set y [lindex $coords 1]

    scan [$graph xaxis limits] "%s %s" min max
    if { $x > $max } { 
	set x $max 
    } elseif { $x < $min } { 
	set x $min 
    }

    scan [$graph yaxis limits] "%s %s" min max
    if { $y > $max } { 
	set y $max 
    } elseif { $y < $min } { 
	set y $min 
    }

    if { $corner == "first" } {
	set x1 $x ; set y1 $y 
    } else {
	set x2 $x ; set y2 $y
    }
}

# ----------------------------------------------------------------------
# 
# MarkPoint --
#
#	Adds text around one of the corners of the zoom region.
#	The text consists of the x,y graph coordinates of the 
#	corner.
#
# ----------------------------------------------------------------------
body ZoomStackGraph::MarkPoint { x y } {

    set marker "bltZoom_text_$corner"
    set text [format "x=%.4g\ny=%.4g" $x $y] 

    if [$graph marker exists $marker] {
     	$graph marker configure $marker -coords { $x $y } -text $text 
    } else {
    	$graph marker create text -coords { $x $y } -name $marker \
   	    -font *lucida*-r-*-10-* \
	    -text $text -anchor center -bg {} -justify left
    }
}

# ----------------------------------------------------------------------
# 
# Empty --
#
#	Indicates if the stack of axis configuration commands is
#	empty.
#
# ----------------------------------------------------------------------
body ZoomStackGraph::Empty { } {
    return [llength $stack]
}


# ----------------------------------------------------------------------
# 
# Push --
#
#	Appends a command on the list "stack" which can be used
#	to return to previous graph x and y axis ranges.  
#
# ----------------------------------------------------------------------
body ZoomStackGraph::Push { cmd } {
    lappend stack $cmd
}

# ----------------------------------------------------------------------
# 
# Pop --
#
#	Remove the last item pushed onto the stack and returns it.
#
# ----------------------------------------------------------------------
body ZoomStackGraph::Pop { } {
    set cmd [lindex $stack end]
    set stack [lreplace $stack end end]
    return $cmd
}

# ----------------------------------------------------------------------
# 
# ClearTitle --
#
#	Clears the zoom title (displayed in the upper left corner
#	of the graph).  This routine is called from the event queue 
#	using "after".
#
# ----------------------------------------------------------------------
body ZoomStackGraph::ClearTitle {} {
    $graph marker delete "bltZoom_title"
}

# ----------------------------------------------------------------------
# 
# Unzoom --
#
#	Reverts to a previous zoom.  Resets the x and y axis limits
#	back to a previous setting.  First checks if there's anything
#	to pop back to.  In addition, displays a title in the upper
#	left corner showing the current zoom level.
#
# ----------------------------------------------------------------------
body ZoomStackGraph::Unzoom { } {

    if ![Empty] {

	# Reset the x and y axis limits, by invoking the saved graph
	# command.
	eval [Pop]

	# Cheat:  Using "Empty" to get the number of entries on the stack.
	set level [Empty]
	if { $level > 0 } {
	    SetTitle "Zoom #$level"
	}

	blt::busy hold $graph
	update
	if { $corner == "first" } {
	    # Remember to remove the zoom title in a couple of seconds
	    after 2000 [code $this ClearTitle]
	}
	blt::busy release $graph
    } else {
	$graph marker delete "bltZoom_title"
    }
}

# ----------------------------------------------------------------------
#
# Zoom --
# 
#	Push the old axis limits on the stack and set them to the
#	zoom region.
#
# ----------------------------------------------------------------------
body ZoomStackGraph::Zoom { } {
    $graph marker delete "bltZoom_*" 

    if { ($x1 == $x2) && ($y1 == $y2) } { 
	# The first and last points of the zoom region are the same.
	# Revert back to the start.
	return
    }

    # Put a command on the stack that lets us revert back to the current 
    # axis limits.
    set cmd [format {
	%s xaxis configure -min "%s" -max "%s"
	%s yaxis configure -min "%s" -max "%s"
    } $graph [$graph xaxis cget -min] [$graph xaxis cget -max] \
		 $graph [$graph yaxis cget -min] [$graph yaxis cget -max] ]
    Push $cmd

    # The first and last corners of the zoom region don't have to be
    # selected in ascending order.  So consider their relative positions
    # when setting min and max axis limits.

    if { $x1 > $x2 } { 
	$graph xaxis configure -min $x2 -max $x1 
    } elseif { $x1 < $x2 } {
	$graph xaxis configure -min $x1 -max $x2
    } 
    if { $y1 > $y2 } { 
	$graph yaxis configure -min $y2 -max $y1
    } elseif { $y1 < $y2 } {
	$graph yaxis configure -min $y1 -max $y2
    } 

    # Call "update" explicitly here after the graph is made busy.
    # This prevents the user from inadvertantly selecting another zoom
    # region when the graph is recalculating and redrawing itself.

    blt::busy hold $graph
    update
    blt::busy release $graph
}

# ----------------------------------------------------------------------
# 
# ClearZoom --
#
# ----------------------------------------------------------------------
body ZoomStackGraph::ClearZoom { } {

    $graph marker delete "bltZoom_*" 
    if { $corner == "first" } {
	# We're haven't started to select a zoom region, so assume
	# that we want to revert back to a previous zoom level.
	Unzoom
    } else {
	# Let the user re-pick the first corner again. So reset the
	# indicator "corner" and turn off the <Motion> binding.
	set corner "first"
	bind $graph <Motion> {}
    }
}

# ----------------------------------------------------------------------
# 
# SetTitle --
#
# ----------------------------------------------------------------------
body ZoomStackGraph::SetTitle { title } {

    $graph marker create text -name "bltZoom_title" -text $title \
	    -coords {-Inf Inf} -anchor nw -bg {} 
}

# ----------------------------------------------------------------------
# 
# UpdateOutline --
#
# ----------------------------------------------------------------------
body ZoomStackGraph::UpdateOutline { x y } {
    SaveCoords $x $y
    MarkPoint $x2 $y2
    DrawBox
}

# ----------------------------------------------------------------------
# 
# SelectPoint --
#
#	Invoked from the binding to ButtonPress-1 events.  Saves
#	a corner of zoom region.
#
#
# ----------------------------------------------------------------------
body ZoomStackGraph::SelectPoint { x y } {

    SaveCoords $x $y 
    if { $corner == "first" } {
	MarkPoint $x1 $y1

	# Display a new title indicating zoom pick is active
	set level [expr [llength $stack] + 1]
	SetTitle "Zoom #$level"

	# Start watching now for motion events, drawing an outline
	bind $graph <Any-Motion> [code $this UpdateOutline %x %y]

	# Indicate the next corner is the last 
	set corner last
    } else {

	# Stop watching motion events
	bind $graph <Any-Motion> {}

	# Zoom into the new region defined by the outline
	Zoom

	# Reset to select the first corner, again
	set corner first
    }
}

# ----------------------------------------------------------------------
# 
# DrawBox --
#
# ----------------------------------------------------------------------
body ZoomStackGraph::DrawBox { } {

    set coords {
	$x1 $y1 $x2 $y1 $x2 $y2 $x1 $y2 $x1 $y1
    }
    if [$graph marker exists "bltZoom_outline"] {
	$graph marker configure "bltZoom_outline" -coords $coords
    } else {
	$graph marker create line -coords $coords -name "bltZoom_outline" \
	    -dashes { 4 2 }
    }
    $graph marker before "bltZoom_outline"
}

