############################################################################
#
#	File:     tgrmap.icn
#
#	Subject:  Program to generate map from TIGER files
#
#	Authors:  Gregg M. Townsend and William S. Evans
#
#	Date:     July 29, 2000
#
############################################################################
#
#  Tgrmap draws maps based on TIGER data files from the Census Bureau.
#  Data files must be in "line chain" (.lch) format as written by the
#  associated "tgrprep" program.
#
#  Usage:  tgrmap [file.lch ...]
#	Input is zero or more files of chains created by "tgrprep.icn".
#
#  All manipulation is done by mouse actions, keyboard shortcuts, or
#  window resizing.  There are no menus (although they would be nice
#  to have.)
#
#  Mouse actions:
#	Sweeping an area with the left mouse button zooms the image
#	to display that area better.  To cancel a sweep, just reduce
#	the swept height or width to less than 10 pixels.
#
#	Clicking the center mouse button pops up the Layers dialog,
#	which selects the categories of data to be shown or hidden.
#	No other actions are accepted while the dialog box is up.
#	(This only works on 8-bit displays, a vanishing breed.)
#
#	Clicking the right mouse button when the cursor is on a line
#	brings up a subwindow that shows the name and type of the
#	line.
#
#  Keyboard shortcuts:
#	F	find a feature by name
#	L	bring up the layers dialog
#	M	create PPM image
#	O	open a new file
#	P	create PostScript file for printing
#	Q	quit
#	R	refresh the display
#	S	save the displayed data to file
#	+	zoom in by a factor of 2
#	-	zoom out by a factor of 2
#	2-9	zoom to factor given
#	1	reset original map (centered and unzoomed)
#	arrow	arrow keys shift the center of the displayed area
#
#  Window resizing is allowed.
#
############################################################################
#
#  Requires:  Version 9 graphics
#
############################################################################
#
#  Links:  clipping, graphics, numbers, pscript, strings
#
############################################################################

#  Ideas for future changes:
#	Add menu alternatives to keyboard shortcuts
#	Write *color* PostScript, at least as an option
#	   (the programming is easy; tuning the colors is the hard part)

link clipping
link graphics
link numbers
link pscript
link strings

$include "keysyms.icn"

$ifndef _X_WINDOW_SYSTEM
   $define Key_KP_Up Key_Up
   $define Key_KP_Down Key_Down
   $define Key_KP_Left Key_Left
   $define Key_KP_Right Key_Right
$endif

$define MARGIN 5			# margin around full-sized map
$define CLIP 2				# clipping margin, allowing for linewdth
$define SHIFTBY 32			# number of pixels to shift at once
$define PSSCALE 100			# scaling from pixels to PostScript
$define MAXDRAW 4000			# maximum (even) args to avoid error 301
$define EPSILON 2.5			# how close is enough when clicking

# file values
global ifileList, fnameList		# input files and their names
global lonmin, lonmax, latmin, latmax 	# input range

# windows
global wintbl				# table of GCs by type
global msgwin				# base window for notices
global title				# window title
global tmpwin				# temp window for PPM snapshots

# window parameters
global dx, dy				# current translation values
global fullx, fully			# scaling for zoom-1 display

# display parameters
global ctrlon, ctrlat			# longitude/latitude of center
global curzoom, xscale, yscale		# current zoom factor and scaling
global lonrange, latrange		# distance from center to edge

# inquiry parameters
global litName				# string to match against feature name


# the classification list finds things based on line type
record class(				# classification record
   prefix,				#    CFCC prefix
   psgray,				#    PostScript graylevel
   pswidth,				#    PostScript linewidth
   label,				#    label
   color,				#    color
   width,				#    line width
   index,				#    mutable color index
   vispfx)				#    prefix code, but only if visible
global clist				# list of classification records
global ctable				# table keyed by two-char prefix



procedure main(args)
   local e, xywh, lon1, lat1, lon2, lat2, hilightOnly

   Window("size=870,870", "bg=pale moderate brown", args)

   &error := 1
   WAttrib("resize=on")
   &error := 0

   Font(("Helvetica" | "Univers" | "LucidaSans") || ",bold,12")	# may fail
   WAttrib("pointer=cross" | "pointer=crosshair")		# may fail
   msgwin := Clone(&window)		# save shaded window for notices
   setclasses()

   if *args > 0 then
      setfiles(args) | exit()
   else
      setfiles() | exit()

   setwindow()
   setcenter()
   setzoom()
   drawmap()

   repeat case e := Event() of {

      !"01": {
         setregion(lonmin, lonmax, latmin, latmax)
         drawmap()
         }

      !"23456789":	{ setzoom(e); drawmap() }
      !"+=":		{ setzoom(2.0 * curzoom); drawmap() }
      "-":		{ setzoom(0.5 * curzoom); drawmap() }

      !"Oo": {
         if setfiles() then {
            setwindow()
            setcenter()
            setzoom()
            drawmap()
            }
         }

      !"Ll":	setlayers()
      !"Rr":	drawmap()

      !"Mm": {
         tmpwin := WOpen("canvas=hidden",
            "width=" || WAttrib("width"), "height=" || WAttrib("height"))
         if /tmpwin then {
            Notice("can't open temporary canvas", "for PPM snapshot")
            break
            }
         CopyArea(&window, tmpwin)
         writefile(writeppm, "Write PPM file:", "Writing PPM file...")
         WClose(tmpwin)
         tmpwin := &null
         }

      !"Pp":	
         writefile(writeps, "Write PostScript file:", "Writing PostScript...")

      !"Ss":	
         writefile(writelch, "Save displayed portion as:", "Saving...")

      !"Ff": {
         if /litName then {
            hilightOnly := 1
            litName := ""
            }
         else {
            hilightOnly := &null
         }
         if TextDialog("Find features named:", , litName) == "Okay" then {
            litName := map(dialog_value[1])
            if litName == "" then litName := &null
            drawmap(hilightOnly)
         }
      }

      QuitEvents():	break

      Key_Left | Key_KP_Left:	shift(e, +SHIFTBY, 0)
      Key_Right | Key_KP_Right:	shift(e, -SHIFTBY, 0)
      Key_Up | Key_KP_Up:	shift(e, 0, +SHIFTBY)
      Key_Down | Key_KP_Down:	shift(e, 0, -SHIFTBY)

      &lpress: {
         xywh := Sweep()
         if xywh[3|4] < 10 then
            next
         lon1 := ctrlon + (get(xywh) - 0.5) / xscale
         lat1 := ctrlat + (get(xywh) - 0.5) / yscale
         lon2 := lon1 + (get(xywh) + 0.5) / xscale
         lat2 := lat1 + (get(xywh) + 0.5) / yscale
         setregion(lon1, lon2, lat1, lat2)
         drawmap()
         }

      &mrelease: {
         setlayers()
         }

      &rrelease: {
         identify(&x, &y)
         }

      &resize: {
         resize()
         }
   }
end


procedure writefile(proc, caption, message)
   local oname, ofile

   repeat case OpenDialog(msgwin, caption) of {
      "Okay": {
         if *dialog_value = 0 then
            next
         if close(open(oname := dialog_value)) then
            case TextDialog(msgwin, "Overwrite existing file?", , , ,
               ["Yes", "No", "Cancel"]) of {
                  "Yes":	&null
                  "No":		next
                  "Cancel":	fail
               }
         if ofile := open(oname, "w") then
            break
         case TextDialog(msgwin, "Cannot open " || oname) of {
            "Okay":	next
            "Cancel":	fail
            }
         }
      "Cancel":
         fail
      }

   Popup(msgwin, , , 32 + TextWidth(msgwin, message), 32,
      popmsg, message, proc, ofile)
   close(ofile)
   return
end

procedure popmsg(message, proc, ofile)
   CenterString(WAttrib("clipw") / 2, WAttrib("cliph") / 2, message)
   return proc(ofile)
end


procedure setfiles(L)
   local f, fname

   /L := list()
   fnameList := list()
   every close(!(\ifileList))
   ifileList := list()
   prescan()			# reset lonmin,lonmax,latmin,latmax
   until *fnameList > 0 do {
      until *L > 0 do {
         case OpenDialog(msgwin, "Input file(s):") of {
            "Okay":    put(L, words(dialog_value))
            "Cancel":  fail
         }
      }
      while fname := get(L) do {
         if not (f := open(fname)) then {
            Notice(msgwin, "Cannot open " || fname)
            next
         }
         if not (prescan(f)) then {
            Notice(msgwin, "Invalid format: " || fname)
            close(f)
            next
         }
         put(fnameList, fname)
         put(ifileList, f)
      }
   }
   return
end



#  prescan(f) -- verify that f is a valid file, setting globals if so

procedure prescan(f)
   local line, alon, alat, blon, blat
   if /f then {
      lonmin := latmin := 9999999
      lonmax := latmax := 0
      return
   }
   line := read(f)		| fail
   line ? {
      ="        "		| fail
      alon := move(7)		| fail
      alat := move(7)		| fail
   }
   line := read(f)		| fail
   line ? {
      ="        "		| fail
      blon := move(7)		| fail
      blat := move(7)		| fail
   }
   if alon > blon then {
      alon :=: blon
      alat :=: blat
   }
   lonmin >:= alon
   latmin >:= alat
   lonmax <:= blon
   latmax <:= blat
   return
end



procedure setwindow()
   local ww, wh, xstr, ystr, latsin, raspr, waspr

   ww := WAttrib("width")
   wh := WAttrib("height")
   dx := ww / 2
   dy := wh / 2

   xstr := "dx=" || (dx := WAttrib("width") / 2)
   ystr := "dy=" || (dy := WAttrib("height") / 2)
   every WAttrib(&window | !wintbl, xstr, ystr)

   # calculate aspect ratio of file region
   latsin := sin(((latmax + latmin) / 2.0) * (&pi / 9999999))
   raspr := real(lonmax - lonmin) / real(latmax - latmin) * latsin * (360 / 180)

   # calculate aspect ratio of window
   waspr := real(ww - 2 * MARGIN) / real(wh - 2 * MARGIN)

   # calculate scaling for zoom factor of 1.0
   if waspr > raspr then {
      # window is too wide
      fully := real(wh - 2 * MARGIN) / (latmax - latmin)
      fullx := fully * latsin * (360 / 180)
      }
   else {
      # window is too tall
      fullx := real(ww - 2 * MARGIN) / (lonmax - lonmin)
      fully := fullx / latsin / (360 / 180)
      }
   return
end



procedure setcenter(lon, lat)
   ctrlon := round(\lon | (lonmin + lonmax) / 2.0)
   ctrlat := round(\lat | (latmin + latmax) / 2.0)
   return
end



procedure setzoom(n)
   local x1, y1, x2, y2

   curzoom := \n | 1.0
   xscale := curzoom * fullx
   yscale := curzoom * fully
   lonrange := integer(dx / xscale + 0.5)
   latrange := integer(dy / yscale + 0.5)

   # clip out-of-bounds data because it's probably incomplete
   x1 := integer((lonmin - ctrlon) * xscale - 0.5) - CLIP
   x2 := integer((lonmax - ctrlon) * xscale + 0.5) + CLIP
   y1 := integer((latmin - ctrlat) * yscale - 0.5) - CLIP
   y2 := integer((latmax - ctrlat) * yscale + 0.5) + CLIP

   # limit clipping bounds to sensible values, else X gets confused
   x1 <:= -dx
   x2 >:= dx
   y1 <:= -dy
   y2 >:= dy

   # clip only drawing windows; NOT &window, used for copying and erasing!
   every Clip(!wintbl, x1, y1, x2 - x1, y2 - y1)
   return
end



procedure resize()
   local dxold, dyold, xshift, yshift

   dxold := dx			# save old translation values
   dyold := dy

   setwindow()			# set window parameters for new size

   xshift := dx - dxold
   yshift := dy - dyold

   # move to realign existing map with new window center
   CopyArea(-dx - xshift, -dy - yshift, 2 * dx, 2 * dy, -dx, -dy)
   if xshift > 0 then EraseArea(dx - xshift, -dy)
   if yshift > 0 then EraseArea(-dx, dy - yshift)

   # restore scaling and clipping
   setzoom(xscale / fullx)	# don't change zoom, but reset other globals

   return
end



procedure shift(e, nx, ny)

   while Pending()[1] === e do
      Event()				# consume duplicate shift events

   setcenter(ctrlon - nx / xscale, ctrlat - ny / yscale)
   CopyArea(-dx, -dy, 2 * dx, 2 * dy, -dx + nx, -dy + ny)
   if (nx > 0) then EraseArea(-dx, -dy, nx, 2 * dy)
   if (ny > 0) then EraseArea(-dx, -dy, 2 * dx, ny)
   if (nx < 0) then EraseArea(dx + nx, -dy)
   if (ny < 0) then EraseArea(-dx, dy + ny)
   settitle()			# reset center coords in title
   setzoom(curzoom)		# reset clipping

   drawmap()

   return
end



procedure drawmap(hilightOnly)
   local line, w, worig, lon, lat, dlon, dlat, a, bdy, dim, class, fename, f
   local drawProc, litFeature

   WAttrib("pointer=wait" | "pointer=watch")

   if /hilightOnly then {
      EraseArea()
      settitle()
   }
   litFeature := list()

   every f := !ifileList do {
      seek(f, 1)
      read(f)		# skip minima line
      read(f)		# skip maxima line

      while line := read(f) do line ? {

         if *Pending() > 0 then {
	    WAttrib("pointer=cross" | "pointer=crosshair")
	    return
	 }

         w := \wintbl[class := move(2)] | next
         move(1)
         bdy := move(1)
         if ="|" then {
            fename := tab(upto('|'))
            move(1)
         }
         else {
            fename := &null
         }
         dim := integer(move(4))
         lon := move(7) - ctrlon
         lat := move(7) - ctrlat

# quick clip
         if dim < 9999 & 
            (lon - dim > lonrange | lon + dim < -lonrange |
             lat - dim > latrange | lat + dim < -latrange) then
            next

         a := [xscale * lon, yscale * lat]
         while (dlon := move(4) - 5000) & (dlat := move(4) - 5000) do
            put(a, xscale * (lon +:= dlon), yscale * (lat +:= dlat))

# if beyond valid X range (with dx/dy margin), use library clipper
         if (!a > 32000) | (!a < -32000) then
            drawProc := DrawClipped
         else
            drawProc := DrawLine

         push(a, w)	# add graphics context

         if find(\litName, map(\fename)) then {
            put(litFeature, drawProc, a)
         }

         if /hilightOnly then {
            if any('57', bdy) & (a[1] := \wintbl["Y" || bdy]) then {
               drawProc ! a		# draw boundary indicator
               if any('F', class) then
                  next			# chain is ONLY a boundary
               a[1] := w
            }
            drawProc ! a			# draw line itself
         }
      }
   }
   if w := \wintbl["LL"] then {
      repeat {
         drawProc := get(litFeature) | break
         a := get(litFeature) | break
         a[1] := w	# replace graphics context
         drawProc ! a
      }
   }

   WAttrib("pointer=cross" | "pointer=crosshair")
   return
end



procedure identify(x, y)
   local line, lon, lat, dlon, dlat, dim, s, f
   local fename, cfcc, bndry, x0, y0, w, h
   local features

   WAttrib("pointer=wait" | "pointer=watch")

   # calculate region of interest in lat/lon coordinates
   x := (x - EPSILON) / xscale
   y := (y - EPSILON) / yscale
   w := (1 + 2 * EPSILON) / xscale
   h := (1 + 2 * EPSILON) / yscale

   features := set()
   every f := !ifileList do {
      seek(f, 1)
      read(f)		# skip minima line
      read(f)		# skip maxima line

      while line := read(f) do line ? {

         if *Pending() > 0 then {
	    WAttrib("pointer=cross" | "pointer=crosshair")
	    return
	 }

         cfcc := move(3)
         bndry := move(1)
         if ="|" then {		# get feature name
            fename := tab(upto('|'))
            move(1)
         }
         else {
            fename := ""
         }
         dim := integer(move(4))
         lon := move(7) - ctrlon
         lat := move(7) - ctrlat
         if dim < 9999 &
            (lon - dim > lonrange | lon + dim < -lonrange |
             lat - dim > latrange | lat + dim < -latrange) then
            next

         x0 := lon
         y0 := lat
         while (dlon := move(4) - 5000) & (dlat := move(4) - 5000) do {
            lon +:= dlon
            lat +:= dlat
            if ClipLine([x0, y0, lon, lat], x, y, w, h) then {
               s := case bndry of {
                  "9":" (national boundary) "
                  "8":" (state boundary) "
                  "7":" (county boundary) "
                  "5":" (city limit) "
                  "0":" "
               }
               insert(features, cfcc || s || fename)
            }
            x0 := lon
            y0 := lat
         }
      }
   }
   WAttrib("pointer=cross" | "pointer=crosshair")
   Popup(, , , WAttrib("leading") * (0 ~= *features), popList, sort(features))
   return
end



procedure popList(l)
   WAttrib("row=1", "col=1")
   every WWrite(!l)
   until Active()
end



procedure settitle()
   local lon, lat

   lon := ctrlon * (360.0 / 9999999)
   if lon > 180.0 then
      lon -:= 360.0
   lat := 90.0 - ctrlat * (180.0 / 9999999)
   title := fnameList[1]
   if *fnameList > 1 then
      title ||:= "..."
   title ||:= ":  " || dms(lon, "W", "E") || "  " || dms(lat, "S", "N")
   WAttrib("label=" || title)
   return
end



procedure dms(n, s1, s2)
   local deg, min, sec

   if n < 0 then
      n := -n
   else
      s1 := s2

   deg := integer(n)
   n := (n - deg) * 60
   min := integer(n)
   n := (n - min) * 60
   sec := integer(n + 0.5)

   return deg || "\260" || right(min, 2, "0") || "'" ||
      right(sec, 2, "0") || "\"" || s1
end



procedure setregion(lomin, lomax, ltmin, ltmax)
   local xzoom, yzoom

   setcenter((lomin + lomax + 1) / 2, (ltmin + ltmax + 1) / 2)
   xzoom := ((dx - MARGIN) * 2 / fullx) / (lomax - lomin)
   yzoom := ((dy - MARGIN) * 2 / fully) / (ltmax - ltmin)
   if xzoom < yzoom then
      setzoom(xzoom)
   else
      setzoom(yzoom)
   return
end



#  setclasses() -- initialize table of classifications
#
#  The order used here is reflected in the Layers dialog box.

procedure setclasses()
   local c, w, mcolors, m

   clist := [				# classification list
      # prefix, psgray&w, label,   color,  width
      class("A1", .0, 4, "roads", "black", 3),		# freeway/tollway
      class("A2", .0, 2, "roads", "black", 2),		# primary road
      class("A3", .0, 1, "roads", "black"),		# secondary road
      class("A4", .0, 0, "roads", "white"),		# local road
      class("A",  .0, 0, "roads", "white"),		# other road
      class("B1", .4, 2, "railroads", "deep green", 2),	# railroad line
      class("B",  .4, 1, "railroads", "deep green"),	# r.r. spur, yard, etc.
      class("H",  .7, 1, "water", "dark cyanish blue"),	# water
      class("Y7", .9, 5, "major boundaries", "orange", 3),	# county
      class("Y5", .9, 3, "major boundaries", "orange", 2),	# city
      class("E",  .9, 1, "minor boundaries", "light orange"),	# visible
      class("F",  .9, 1, "minor boundaries", "light orange"),	# invisible
      class("D",  .0, 1, "landmarks", "dark red"),		# landmark
      class("C",  .5, 1, "piplines & power", "purple"),		# pipe, power
      class("LL", .2, 2, "highlighted feature", "yellow", 10),	# hilit feature
      class("T0", .8, 3, "GPS track", "dark greenish cyan", 2),	# Track data
      class("X",  .8, 1, "unclassified", "purple")]		# unclassified

   every c := !clist do
      c.vispfx := c.prefix		# initially, all layers visible

   ctable := table()
   every c := !clist do
      if *c.prefix = 1 then
         every /ctable[c.prefix || !"0123456789"] := c
      else
         ctable[c.prefix] := c

   wintbl := table()			# global window table
   mcolors := table()			# local table of mutable colors

   every c := !clist do {

      w := Clone() | stop("can't clone window for ", c.label)
      /mcolors[c.color] := NewColor(w, c.color)		# may fail
      c.index := mcolors[c.color]
      Fg(w, \c.index | c.color) | stop("can't set color for ", c.label)

      WAttrib(w, "linewidth=" || \c.width)
      wintbl[c.prefix] := w
      if *c.prefix = 1 then
         every /wintbl[c.prefix || (0 to 9)] := w
      }
   return
end



#  setlayers() -- bring up layers dialog

procedure setlayers()
   local c, i, defaults, buttons, choice, lset
   static labels, values

   initial {
      lset := set()
      labels := list()
      values := list()
      every c := !clist do
         if \c.index & not member(lset, c.label) then {
            insert(lset, c.label)
            put(labels, c.label)
            put(values, 1)
            }
      }

   if *labels = 0 then {
      Notice("No layer control available")
      fail
      }

   while choice ~=== "Okay" do {	# loop when "Apply" selected

      defaults := values
      buttons := ["Okay", "Apply", "Cancel"]
      choice := ToggleDialog(msgwin, "Layers:", labels, defaults, buttons)
      if choice == "Cancel" then
         fail
      values := dialog_value

      #  change mutable color for every item that changed in the dialog
      every i := 1 to *values do
         if values[i] ~=== defaults[i] then
            every c := !clist do
               if c.label == labels[i] then {
                  if \values[i] then {
                     Color(\c.index, c.color)
                     c.vispfx := c.prefix
                     }
                  else {
                     Color(\c.index, Bg())
                     c.vispfx := &null
                     }
                  }
      }
   return
end



procedure writelch(ofile)
   local line, dim, lon, lat, f, a, b, x, y, dlon, dlat, nlon, nlat, w, head
   local startlon, startlat, minlon, minlat, maxlon, maxlat, deltas, class

   write(ofile, "        ",
      right(ctrlon - lonrange, 7), right(ctrlat - latrange, 7))
   write(ofile, "        ",
      right(ctrlon + lonrange, 7), right(ctrlat + latrange, 7))

   every f := !ifileList do {
      seek(f, 1)
      read(f)			# skip minima line
      read(f)			# skip maxima line

      while line := read(f) do line ? {
         w := \wintbl[class := move(2)] | next
         head := class || move(2)
         if ="|" then {
            head ||:= "|" || tab(upto('|') + 1)
         }
         dim := integer(move(4))
         lon := move(7) - ctrlon
         lat := move(7) - ctrlat
# quick clip
         if dim < 9999 & 
            (lon - dim > lonrange | lon + dim < -lonrange |
             lat - dim > latrange | lat + dim < -latrange) then
            next

         a := [xscale * lon, yscale * lat]
         while (dlon := move(4) - 5000) & (dlat := move(4) - 5000) do
            put(a, xscale * (lon +:= dlon), yscale * (lat +:= dlat))
         a := Coalesce(ClipLine(w, a)) | next
         every b := !a do {
            deltas := ""
            startlon := minlon := maxlon := lon :=
               round(get(b) / xscale) + ctrlon
            startlat := minlat := maxlat := lat :=
               round(get(b) / yscale) + ctrlat
            while nlon := round(get(b) / xscale) + ctrlon do {
               nlat := round(get(b) / yscale) + ctrlat
               deltas ||:= right(nlon - lon + 5000, 4, "0")
               deltas ||:= right(nlat - lat + 5000, 4, "0")
               lon := nlon
               lat := nlat
               maxlon <:= lon
               minlon >:= lon
               maxlat <:= lat
               minlat >:= lat
            }
            dim := startlon - minlon
            dim <:= maxlon - startlon
            dim <:= startlat - minlat
            dim <:= maxlat - startlat
            dim >:= 9999

            write(ofile, head, right(dim, 4), right(startlon, 7, "0"),
                  right(startlat, 7, "0"), deltas)
         }
      }
   }
   return
end

#  writeppm(ofile) -- write PPM image to ofile
#
#  comments note latitude and longitude bounds in arc-seconds

procedure writeppm(ofile)
   local w, h, rw, rh, s, lon, lat, dlon, dlat

   w := WAttrib("width")
   h := WAttrib("height")
   rw := real(w)
   rh := real(h)

   lon := ctrlon * (360.0 / 9999999)
   if lon > 180.0 then
      lon -:= 360.0
   lat := 90.0 - ctrlat * (180.0 / 9999999)

   dlon := lonrange * (360.0 / 9999999)
   dlat := latrange * (180.0 / 9999999)

   write(ofile, "P6")
   write(ofile, "#RTIN")
   write(ofile, "#lon,lat:",
      arcs(lon - dlon), arcs(lat - dlat), arcs(lon - dlon), arcs(lat + dlat),
      arcs(lon + dlon), arcs(lat + dlat), arcs(lon + dlon), arcs(lat - dlat))
   write(ofile, "#x,y: 0.0 0.0 0.0 ", rh, " ", rw, " ", rh, " ", rw, " 0.0")
   write(ofile, w, " ", h)
   write(ofile, 255)

   every writes(ofile, rgb24(Pixel(tmpwin)))

   return
end



#  arcs(n) -- format latitude or longitude in arc-seconds with leading space

procedure arcs(n)
   return " " || (n * 3600.0)
end



#  rgb24(k) -- return 24-bit (3-byte) r-g-b value for k

procedure rgb24(k)
   local s, r, g, b
   static t
   initial t := table()

   if s := \t[k] then
      return s

   (ColorValue(k | Color(k)) | fail) ? {
      s := char(tab(upto(',')) / 256)
      move(1)
      s ||:= char(tab(upto(',')) / 256)
      move(1)
      s ||:= char(tab(0) / 256)
      }
   t[k] := s
   return s
end



#  writeps(ofile) -- write Encapsulated PostScript to ofile

procedure writeps(ofile)
   local x1, x2, y1, y2, line, prevcode, code, dim, lon, lat, a, n, c, f

   x1 := integer((lonmin - ctrlon) * xscale - 0.5) - CLIP
   y1 := integer((latmin - ctrlat) * yscale - 0.5) - CLIP
   x2 := integer((lonmax - ctrlon) * xscale + 0.5) + CLIP
   y2 := integer((latmax - ctrlat) * yscale + 0.5) + CLIP
   x1 <:= -dx
   y1 <:= -dy
   x2 >:= dx
   y2 >:= dy
   epsheader(ofile, (dx + x1) * PSSCALE, (dy - y2) * PSSCALE,
      (x2 - x1) * PSSCALE, (y2 - y1) * PSSCALE, "r")

   every write(ofile, ![
      "/m { moveto } bind def",
      "/r { rlineto } bind def",
      "/s { stroke } bind def",
      "/w { .00666667 mul inch setlinewidth setgray stroke } bind def"])

   every c := !clist do
      write(ofile, "/", left(\c.vispfx, 2),
         " { ", c.psgray, " ", c.pswidth, " w } bind def")

   every f := !ifileList do {
      seek(f, 1)
      read(f)			# skip minima line
      read(f)			# skip maxima line

      while line := read(f) do line ? {
         code := \ctable[move(2)].vispfx | next
         move(2)
# skip feature name
         if ="|" then
            tab(upto('|') + 1)
         dim := integer(move(4))
         lon := xscale * (move(7) - ctrlon)
         lat := yscale * (move(7) - ctrlat)
         if dim < 9999 &
            (lon - dim > lonrange | lon + dim < -lonrange |
             lat - dim > latrange | lat + dim < -latrange) then
            next
         else {
            writes(ofile, integer(PSSCALE * (dx + lon)), " ",
               integer(PSSCALE * (dy - lat)), " m")
            while lon := move(4) - 5000 & lat := move(4) - 5000 do
               writes(ofile, "\n", integer(PSSCALE * xscale * lon), " ",
                  integer(-PSSCALE * yscale * lat), " r")
            write(ofile, " ", (prevcode ~===:= code) | "s")
         }
      }
   }
   write(ofile, "showpage")
   return
end
