############################################################################
#
#	File:     mapnav.icn
#
#	Subject:  Procedures for navigating a map interactively
#
#	Authors:  Gregg M. Townsend
#
#	Date:     May 7, 2002
#
############################################################################
#
#   This file is in the public domain.
#
############################################################################
#
#	These procedures implement a user interface for browsing a map
#	that is drawn using a simple rectangular projection.  The
#	following interface actions are provided, if not overridden
#	by the calling program:
#	   The arrow keys pan the display.
#	   Shifted arrows pan by a smaller amount.
#	   The + and - keys zoom in and out.
#	   The 0, 1, or HOME key resets the original display.
#	   The q key causes an immediate exit.
#	   Sweeping a region with the mouse zooms the display.
#	   Resizing the window causes it to be redrawn.
#
#	The calling program provides the main loop and a drawing
#	procedure; this module handles the interface and computes
#	the output transformation.
#
#	mapinit(win, proc, arg, xleft, xright, ytop, ybottom, m) initializes.
#	mapgen(win, proc, arg) generates the map by invoking the callback.
#	mapevent(win, e) handles a window event, possibly invoking a callback.
#	mapproj(win) returns the projection used in the window.
#
#	The win argument is optional in all procedures but can be used
#	to supply the correct graphics context.  The window argument is
#	always supplied to the callback procedure.
#
############################################################################
#
#	Typical use is like this:
#
#	procedure main(...)
#	   ... initialize ...
#	   ... load data ...
#	   ... open window ...
#	   mapinit(draw, ...)
#	   mapgen()
#	   case e := Event() of {
#	      ... handle custom events ...
#	      default: mapevent(e)
#	      }
#	end
#
#	procedure draw(win, p, arg)
#	   ... create list of coordinates ...
#	   ... L2 := project(p, L1) ...
#	   ... draw map ...
#	end
#
############################################################################
#
#	mapinit(win, proc, arg, xleft, xright, ytop, ybottom, m) configures
#	the navigator.  proc is a drawing procedure to be called whenever
#	the window needs to be redrawn.  arg is an arbitrary value to be
#	passed to proc along with the transformation parameters.
#
#	xleft, xright, ytop, and ybottom specify the range of coordinates
#	for the data that is to be displayed.  For both the x and y pairs,
#	the values must differ but either can be the greater.
#
#	The value of m (default 1.0) specifies the aspect ratio of the
#	input units.  If the input data is in units of latitude and
#	longitude, choose a central latitude for projection and pass
#	the cosine of that latitude as m.
#
############################################################################
#
#	mapgen(win, proc, arg) calls the drawing procedure proc to draw a
#	map.  win is optional, and proc and arg default to the values
#	registered by the last mapinit() call.
#
#	The drawing procedure is called as
#	   proc(win, pj, arg)
#       where pj the projection returned by mapproj(win).
#
#	The drawing procedure should project and display its data.
#	It must ensure that the resulting coordinates lie inside
#	the range -32768 <= v <=32767 before passing them to Icon
#	drawing functions.  (See also clipping.icn.)
#
############################################################################
#
#	mapevent(win, e) handles a window event.  If e is recognized as
#	an interface action, the map parameters are altered and mapgen()
#	is called, resulting in a call to the drawing procedure.  For
#	a panning action, the window contents are first shifted;
#	otherwise, the window is first erased.  mapevent() fails if
#	e is not recognized.
#
#	The calling program can intercept and override any action it does
#	not want handled by the navigator.  This can be used to customize
#	the interface -- for example, to use "0" for something other than
#	"reset zooming".  However, any &resize event, even if handled by
#	the caller, should be passed to the navigator to allow it to
#	properly adjust its view of the world.
#
############################################################################
#
#	mapproj(win) returns a rectangular projection (see cartog.icn)
#	that maximizes the display of the currently selected data range
#	for viewing in the center of window win.  The "selected data range"
#	is that passed to mapinit() and subsequently modified by any
#	zooming or panning actions.
#
############################################################################
#
#   Links: graphics, cartog
#
############################################################################

$include "keysyms.icn"

link graphics
link cartog

$define MARGIN 16

global mnv_proc			# registered drawing procedure
global mnv_arg			# arbitrary argument for that procedure
global mnv_aspr			# coordinate system aspect ratio

global mnv_prjn			# current projection

# map limits
global mnv_mleft, mnv_mright, mnv_mtop, mnv_mbottom

# viewport configuration
global mnv_vleft, mnv_vright, mnv_vtop, mnv_vbottom

procedure mapinit(win,p,a,xleft,xright,ytop,ybottom,m)	#: initialize navigator

   if type(win) ~== "window" then	# handle missing optional win argument
      return mapinit((\&window | runerr(140)),
         win, p, a, xleft, xright, ytop, ybottom)

   mnv_proc := p
   mnv_arg := a
   mnv_aspr := \m | 1.0

   mnv_mleft := mnv_vleft := xleft
   mnv_mright := mnv_vright := xright
   mnv_mtop := mnv_vtop := ytop
   mnv_mbottom := mnv_vbottom := ybottom

   return
end

procedure mapgen(win, proc, arg)	#: invoke callback to redraw the map

   if type(win) ~== "window" then {	# handle missing optional win argument
      win :=: proc :=: arg
      win := \&window | runerr(140)
      }

   /proc := mnv_proc
   /arg := mnv_arg
   return proc(win, mapproj(win), arg)
end

procedure mapproj(win)			#: compute map projection
   local l, r, t, b, d, nx, ny, xmul, ymul

   /win := \&window | runerr(140)	# handle missing optional win argument

   l := \WAttrib(win, "clipx") | 0
   t := \WAttrib(win, "clipy") | 0
   r := l + \WAttrib(win, "clipw" | "width")
   b := t + \WAttrib(win, "cliph" | "height")
   nx := MARGIN
   ny := MARGIN

   xmul := real(r - l - 2 * nx) / (mnv_vright - mnv_vleft)
   ymul := real(b - t - 2 * ny) / (mnv_vbottom - mnv_vtop)

   d := abs(xmul / (ymul * mnv_aspr))
   if d > 1.0 then {
      xmul /:= d
      nx := (r - l - xmul * (mnv_vright - mnv_vleft)) / 2
      }
   else {
      ymul *:= d
      ny := (b - t - ymul * (mnv_vbottom - mnv_vtop)) / 2
      }

   mnv_prjn := rectp(mnv_vleft, mnv_vtop, l + nx, t + ny, xmul, ymul)
   return mnv_prjn
end

procedure mapevent(win, e)		#: navigate map as directed by action e
   local win2, xywh, ltrb

   if type(win) ~== "window" then {	# handle missing optional win argument
      e := win
      win := \&window | runerr(140)
      }

   case e of {

      &resize: {
         EraseArea(win)
         mapgen(win)
         }

      &lpress: {
         win2 := Clone(win, "linewidth=4", "linestyle=solid", "fg=orange")
         xywh := Sweep(win2)
         Uncouple(win2)
         if xywh[3|4] < 10 then
            return
         xywh[3] +:= xywh[1]
         xywh[4] +:= xywh[2]
         ltrb := project(invp(mnv_prjn), xywh)

         mnv_vleft := get(ltrb)
         mnv_vtop := get(ltrb)
         mnv_vright := get(ltrb)
         mnv_vbottom := get(ltrb)
         EraseArea(win)
         mapgen(win)
         }

      !"01" | Key_Home: {
         mnv_vleft := mnv_mleft
         mnv_vright := mnv_mright
         mnv_vtop := mnv_mtop
         mnv_vbottom := mnv_mbottom
         EraseArea(win)
         mapgen(win)
         }

      Key_Up:    mnv_pan(win,  0, -1)
      Key_Down:  mnv_pan(win,  0, +1)
      Key_Left:  mnv_pan(win, -1,  0)
      Key_Right: mnv_pan(win, +1,  0)

      !"+=":  mnv_inout(win, -0.20)
      !"-_":  mnv_inout(win, +0.25)

      !"Qq":  exit()

      default:	fail

      }

   return
end

procedure mnv_pan(win, px, py)		# process panning action
   local n, l, r, t, b, w, h, nx, ny, dx, dy, xyxy

   n := if &shift then 10 else 100
   nx := n * px
   ny := n * py

   l := \WAttrib(win, "clipx") | 0
   t := \WAttrib(win, "clipy") | 0
   r := l + \WAttrib(win, "clipw" | "width")
   b := t + \WAttrib(win, "cliph" | "height")
   w := r - l - abs(nx)
   h := b - t - abs(ny)

   if nx > 0 then {
      CopyArea(win, l + nx, t, w, h, l, t)
      EraseArea(win, r - nx, t, nx, h)
      }
   else if nx < 0 then {
      CopyArea(win, l, t, w, h, l - nx, t)
      EraseArea(win, l, t, -nx, h)
      }

   if ny > 0 then {
      CopyArea(win, l, t + ny, w, h, l, t)
      EraseArea(win, l, b - ny, w, ny)
      }
   else if ny < 0 then {
      CopyArea(win, l, t, w, h, l, t - ny)
      EraseArea(win, l, t, w, -ny)
      }

   xyxy := project(invp(mnv_prjn), [l, t, l + nx, t + ny])
   dx := xyxy[3] - xyxy[1]
   dy := xyxy[4] - xyxy[2]
   mnv_vleft +:= dx
   mnv_vright +:= dx
   mnv_vtop +:= dy
   mnv_vbottom +:= dy
   mapgen(win)

   return
end

procedure mnv_inout(win, f)		# process zooming action
   local xc, yc

   xc := (mnv_vleft + mnv_vright) / 2
   yc := (mnv_vtop + mnv_vbottom) / 2

   mnv_vleft +:= f * (mnv_vleft - xc)
   mnv_vright +:= f * (mnv_vright - xc)
   mnv_vtop +:= f * (mnv_vtop - yc)
   mnv_vbottom +:= f * (mnv_vbottom - yc)

   EraseArea(win)
   mapgen(win)
   return
end
