#############################################################################
#
#	File:     carport.icn
#
#	Subject:  Program to create numerical carpets
#
#	Author:   Ralph E. Griswold
#
#	Date:     September 17, 1998
#
############################################################################
#
#  This is a program for specifying "numerical carpets".  It writes a $include
#  file and compiles and executes carplay.icn to produce the actual carpet.
#
############################################################################
#
#  For the basic idea that motivated this program, see "Carpets and Rugs: An
#  Exercise in Numbers", Dann E. Passoja and Akhlesh Lakhtakia, in The
#  Visual Mind:  Art and Mathematics, Michele Emmer, ed., The MIT Press,
#  1993, pp. 121-123.
#
#  The concepts and general operation of this application are described in
#  Issue 45 of The Icon Analyst (December, 1997).  For on-line documentation
#  on using this program, see
#
#	http://www.cs.arizona.edu/icon/analyst/iasub/ia45/programs/doc.htm
#
############################################################################
#
#  Requires:  Version 9 graphics, system(), and carplay.icn.
#
############################################################################
#
#  Links:  carputil, interact, io, tables, vsetup, xcode
#
############################################################################

link carputil
link carprec
link interact
link io
link tables
link vsetup
link xcode

global db_entries			# list of specifications in database
global db_file				# name of database file
global spec				# current carpet specification
global database				# database of specifications
global def_entries			# list of definitions
global dopt_list			# list of display options
global dset_list			# list of display option states
global fopt_list			# list of generation options
global fset_list			# list of generation option states
global touched				# database changed switch
global vidgets				# table of interface tools

$define NameDefault	  "default"
$define TopDefault	  "1"
$define LeftDefault	  "Top"
$define WidthDefault	  "128"
$define HeightDefault	  "Width"
$define ModulusDefault	  "5"
$define NeighborsDefault  "n + nw + w"
$define LinksDefault	  ["seqfncs"]
$define ColorsDefault	  image("c2")

$define	SymWidth	  15		# width of definition name field
$define DefWidth	  80		# width of definition field
$define ExprWidth	  80		# width of expression field
$define NameWidth	  40		# width of name field

procedure main()

   carprec
   
   init()

   GetEvents(vidgets["root"], , shortcuts)

end

#  Add (or overwrite) definition.

procedure add_def()

   if TextDialog("Add definition:", ["name", "definition"], , 
      [SymWidth, ExprWidth]) == "Cancel" then fail

   spec.Defns[dialog_value[1]] :=  dialog_value[2]
   refresh_defs()

   return

end

#  Add link

procedure add_link()

   if OpenDialog("Add link:", , , , 20) == "Cancel" then fail

   put(spec.Links, dialog_value)
   refresh_links()

   return

end

#  Clear the database of specifications (a default one is then added).

procedure clear_db()

   case TextDialog("Are you sure you want to clear the current database?",
      , , , ["Yes", "No"]) of {
         "No" :  fail
         "Yes":  {
            database := table()
            new_spec()
            database[spec.Name] := spec
            refresh_db()
            return
            }
         }

end

#  Clear the table of definitions.

procedure clear_defs()

   if TextDialog("Do you really want to clear the definition table?") ==
      "Cancel" then fail

   spec.Defns := table()
   refresh_defs()

   return

end

#  Clear all the links.

procedure clear_links()

   if TextDialog("Do you really want to clear all links?") ==
      "Cancel" then fail

   spec.Links := []
   refresh_links()

   return

end

#  Edit specification comments.

procedure comments()

   repeat {
      case TextDialog("Comments:", , spec.Comments, ExprWidth,
         ["Default", "Okay", "Cancel"], 2) of {
            "Cancel" :  fail
            "Default":  {
               spec.Comments := &dateline		# default comments
               next
               }
            "Okay"   :  {
               spec.Comments := dialog_value[1]
               break
               }
            }
      }

   return

end

#  Create a carpet from the current specification.

procedure create_cb()
   local path, output, i

   WAttrib("pointer=watch")

   output := open("carpincl.icn", "w") | {
      Notice("Cannot open include file for writing.")
      fail
      }

   every i := 1 to *dopt_list do
      if \dset_list[i] then
         write(output, "$define ", map(dopt_list[i][1], &lcase, &ucase),
            map(dopt_list[i][2:0], " ", "_"))

   every i := 1 to *fopt_list do
      if \fset_list[i] then
         write(output, "$define ", map(fopt_list[i][1], &lcase, &ucase),
            fopt_list[i][2:0])

   close(output)

   write_spec("carpincl.icn", spec) | {
      Notice("Cannot open include file for writing.")
      fail
      }

   path := dpath("carplay.icn") | {
      Notice("Fatal error; cannot find carpet generation program.")
      exit()
      }

   system("icont -s " || path || "  -x")

   WAttrib("pointer=arrow")

   return

end

#  Items for Database menu.

procedure database_cb(vidget, value)

   case value[1] of {
      "load     ^@L":  load_db()
      "merge    ^@M":  load_db(1)	# argument indicates merger
      "revert   ^@R":  load_db(2)	# argument indicates reversion
      "save     ^@S":  save_db()
      "save as  ^@T":  save_as_db()
      "clear    ^@Z":  clear_db()
      }

end

#  Callback for item selected from database list.

procedure db_cb(vidget, value)
   local state
   static db, sw

   initial db := vidgets["db"]

   if /value then return		# deselected item

   if \sw then {			# prevent loop from internal call
      sw := &null
      return
      }

   state := VGetState(db)		# save state to restore position

   repeat {
      case TextDialog("Specification " || value, , , ,
         ["Delete", "Display", "Okay", "Cancel"], 3) of {
            "Cancel":  fail
            "Okay"  :  {
               spec.Name := value
               spec := database[spec.Name]
               refresh_defs()
               refresh_db()
               sw := 1
               VSetState(db, state)
               refresh_links()
               return
               }
            "Delete":  {
               if value == spec.Name then {
                  Notice("You cannot delete the current specification.")
                  next
                  }
               delete(database, value)
               refresh_db()
               return
               } 
            "Display":  {
               display_spec(database[value])
               next
               }
            }
      }

end

#  Make the expression in the current dialog into a definition.

procedure define(s)

   if TextDialog("Add definition:", ["name", "definition"], [, s],
      [SymWidth, ExprWidth]) == "Cancel" then fail

   spec.Defns[dialog_value[1]] :=  dialog_value[2]
   refresh_defs()

   return

end

#  Items for the Definitions menu.

procedure definitions_cb(vidget, value)

   case value[1] of {
      "add       @A":  add_def()
      "clear     @Z":  clear_defs()
      "load      @F":  load_defs()
      "merge     @J":  load_defs(1)	# nonnull argument indicates merger
      "save      @S":  save_defs()
      }

   return

end

#  Callback for selection from the definitions text-list.

procedure defs_cb(vidget, value)

   if /value then fail

   case TextDialog("Name: " || value, "definition", spec.Defns[value],
      ExprWidth , ["Remove", "Okay", "Cancel"], 2) of {
         "Remove":  {
            delete(spec.Defns, value)
            refresh_defs()
            }
         "Okay"  :  spec.Defns[value] := dialog_value[1]
         "Cancel":  fail
         }

   return

end

#  Display all the current definitions.

procedure display_defs()
   local definition, lines, i

   if *def_entries = 0 then {
      Notice("The definition table is empty.")
      fail
      }

   lines := []

   every definition := !def_entries do
      put(lines, left(definition, 12) ||
         left(spec.Defns[definition], ExprWidth))

   push(lines, "", "name             definition ")

   Notice ! lines

   return

end

#  Display a carpet specification.

$define FieldWidth (SymWidth + 1)

procedure display_spec(dspec)
   local lines, s, lst

   /dspec := spec

   lines := [
      "Specifications:",
      "",
      left("Name", FieldWidth) || dspec.Name,
      left("Modulus", FieldWidth) || dspec.Modulus,
      left("Width", FieldWidth) || dspec.Width,
      left("Height", FieldWidth) || dspec.Height,
      left("Top Row", FieldWidth) || dspec.Top,
      left("Left Column", FieldWidth) || dspec.Left,
      left("Neighbors", FieldWidth) || dspec.Neighbors,
      left("Colors", FieldWidth) || dspec.Colors,
      left("Comments", FieldWidth) || (\dspec.Comments | "")
      ]

   if *dspec.Defns > 0 then {
      put(lines, "", "Definitions:", "")
      every put(lines, left(s := !keylist(dspec.Defns), FieldWidth) ||
         (\dspec.Defns[s] | "") \ 1)
      }

   if *dspec.Links > 0 then {
      put(lines, "",  "Links:", "")
      every put(lines, !dspec.Links)
      }

   Notice ! lines

   return

end

#  Write all specifications in include form

procedure dump_all()
   local spec
   static dump_file

   repeat {
      case OpenDialog("Save database as text:", dump_file) of {
         "Okay" :  {
            every spec := database[!db_entries] do
               write_spec(dialog_value, spec)
            dump_file := dialog_value
            return
            }
         "Cancel":  fail
         }
      }

end

#  Duplicate the current specification and make it current.

procedure dupl_spec()

   spec := copy(spec)
   spec.Defns := copy(spec.Defns)
   refresh_defs()
   name_spec(1)			# nonnull means don't delete the old one
   refresh_db()

   return

end

#  Items for the File menu.

procedure file_cb(vidgets, value)

   case value[1] of {
      "generate  @G":  create_cb()
      "display   @D":  doptions()
      "options   @O":  foptions()
      "quit      @Q":  quit()
      }

   return

end

#  Display options.

procedure doptions()

   if ToggleDialog("Specify display options:", dopt_list, dset_list) ==
      "Cancel" then fail
   else {
      dset_list := dialog_value
      return
      }

end

#  Display options.

procedure foptions()

   if ToggleDialog("Specify generation options:", fopt_list, fset_list) ==
      "Cancel" then fail
   else {
      fset_list := dialog_value
      return
      }

end

#  Set the carpet height.

procedure height()

   repeat {
      case TextDialog("Height:", , spec.Height, NameWidth,
         ["Default", "Okay", "Cancel"], 2) of {
            "Cancel" :  fail
            "Default":  {
               spec.Height := HeightDefault
               next
               }
            "Okay"   :  {
               spec.Height := dialog_value[1]
               break
               }
            }
      }

   return

end

#  Initialize the application.

procedure init()
   local atts

   atts := ui_atts()
   push(atts, "posx=10", "posy=10")

   (WOpen ! atts) | ExitNotice("Cannot open interface window.")

   vidgets := ui()

   database := table()
   new_spec()

   db_file := &null
   touched := &null

   dopt_list := [		# list of display options
      "mirror",			# show mirror image
      "hidden",			# hide images
      "save carpet",		# save carpet image automatically
      "save mirror",		# save mirror image automatically
      "dialogs",		# provide dialogs
      "background"		# run in background
      ]
   dset_list := list(*dopt_list)	# choices
   dset_list[1] := 1		# initially only enable mirroring

   fopt_list := [		# list of generation options
      "wrap",			# wrap edges
      "randomize",		# randomize
      "two pass"		# two-pass generation
      ]
   fset_list := list(*fopt_list)	# choices

   return

end

#  Edit the left-side expression.

procedure left_expr()

   repeat {
      case TextDialog("Left:", , spec.Left, ExprWidth,
         ["Define", "Default", "Okay", "Cancel"], 3) of {
            "Define" :  {
               define(dialog_value[1])
               break
               }
            "Cancel" :  fail
            "Default":  {
               spec.Left := LeftDefault
               next
               }
            "Okay"   :  {
               spec.Left := dialog_value[1]
               break
               }
            }
      }

   return

end

#  Items for the Link menu.

procedure link_cb(vidget, value)

   case value[1] of {
      "add      ^@A":  add_link()
      "clear    ^@C":  clear_links()
      }

   return

end

#  Callback for selection of an item from the links text-list.

procedure links_cb(vidget, value)
   local i, j, tmp

   if /value then return		# deselected item

   case TextDialog("Link: " || value, , , , ["Remove", "Cancel"], 1) of {
         "Remove":  {
            i := VGetState(vidgets["links"])[2]	# second element is line number
            tmp := []
            every (j := 1 to i - 1) | (j := i + 1 to *spec.Links) do
               put(tmp, spec.Links[j])
            spec.Links := tmp
            refresh_links()
            }
         "Cancel":  fail
         }

   return

end

#  Load a carpet database.  If sw is null, it replaces the current database.
#  If sw is one, it is merged with the current database.  If sw is 2, the
#  database reverts to the last one loaded.

procedure load_db(sw)
   local input, tbl, caption

   caption := if sw === 2 then {
      if \touched & \db_file then "Revert to last saved database?"
      else {
         Notice("Revert not possible or not necessary.")
         fail
         }
      }
   else "Load database:"

   repeat {
      if OpenDialog(caption, db_file) == "Cancel" then fail
      input := open(dialog_value) | {
         Notice("Cannot open database.")
         next
         }
      tbl := xdecode(input) | {
         Notice("Cannot decode carpet database.")
         next
         }
      db_file := dialog_value
      close(input)
      database := if sw === 1 then tblunion(database, tbl) else tbl
      refresh_db(1)
      spec := database[db_entries[1]]
      return
      }

end

#  Load definitions file.

procedure load_defs(sw)
   local input, tbl

   repeat {
      if OpenDialog("Specify definition file:") == "Cancel" then fail
      input := open(dialog_value) | {
         Notice("Cannot open definitions file.")
         next
         }
      tbl := xdecode(input) | {
         Notice("Cannot decode definitions.")
         next
         }
      spec.Defns := if /sw then tbl else tblunion(spec.Defns, tbl)
      close(input)
      refresh_defs()
      return
      }

end

#  Edit the modulus.

procedure modulus()

   repeat {
      case TextDialog("Modulus:", , spec.Modulus, NameWidth,
         ["Default", "Okay", "Cancel"], 2) of {
            "Cancel" :  fail
            "Default":  {
               spec.Modulus := ModulusDefault
               next
               }
            "Okay"   :  {
               spec.Modulus := dialog_value[1]
               break
               }
            }
      }

   return

end

procedure colors()

   repeat {
      case TextDialog("Colors:", , spec.Colors, ExprWidth,
         ["Default", "Okay", "Cancel"], 2) of {
            "Cancel" :  fail
            "Default":  {
               spec.Colors := ColorsDefault
               next
               }
            "Okay"   :  {
               spec.Colors := dialog_value[1]
               break
               }
            }
      }

   return

end

#  Edit the specification name.

procedure name_spec(sw)
   local old_name

   old_name := spec.Name

   if OpenDialog("Name:", spec.Name) == "Cancel" then fail
   else {
       spec.Name := dialog_value
       database[dialog_value] := spec
       if /sw then delete(database, old_name)
       refresh_db()
       }

   return

end

#  Edit the neighbors expression.

procedure neighbors()

   repeat {
      case TextDialog("Neighborhood:", , spec.Neighbors, ExprWidth,
         ["Define", "Default", "Okay", "Cancel"], 3) of {
            "Define" :  {
               define(dialog_value[1])
               break
               }
            "Cancel" :  fail
            "Default":  {
               spec.Neighbors := NeighborsDefault
               next
               }
            "Okay"   :  {
               spec.Neighbors := dialog_value[1]
               break
               }
            }
      }

   return

end

#  Create a fresh, empty definitions table.

procedure new_defs()

   spec.Defns := table()
   refresh_defs()

   return

end

#  Create a fresh, empty links list.  ??? what about clear_links()?

procedure new_links()

   spec.Links := LinksDefault
   refresh_links()

   return

end

#  Create a new carpet specification from the default.

procedure new_spec()

   spec := carpet()
   spec.Name := NameDefault
   spec.Width := WidthDefault
   spec.Height := HeightDefault
   spec.Modulus := ModulusDefault
   spec.Top := TopDefault
   spec.Left := LeftDefault
   spec.Neighbors := NeighborsDefault
   spec.Colors := ColorsDefault
   spec.Comments := &dateline

   new_defs()
   new_links()

   database[spec.Name] := spec
   refresh_db()

   return

end

#  Items for the Parameters menu.

procedure edit_cb(vidget, value)

   case value[1] of {
      "modulus   @M":  modulus()  
      "width     @W":  width()
      "height    @H":  height()
      "top       @T":  top_expr()
      "left      @L":  left_expr()
      "neighbors @N":  neighbors()
      "colors    @C":  colors()
      "name      @I":  name_spec()
      "comments  @K":  comments()
      }

   return

end

#  Quit the application.

procedure quit()

   if /touched then exit()

   case SaveDialog("Save database?", db_file) of {
      "Cancel":  fail
      "No"    :  exit()
      "Yes"   :  {
         save_db()
         exit()
         }
      }

   return

end

#  Refresh the carpet database.

procedure refresh_db(sw)

   VSetItems(vidgets["db"], db_entries := keylist(database))

   if sw === 1 then spec := database[db_entries[1]]

   update()

   if /sw then touched := 1

   return

end

#  Refresh the table of definitions.

procedure refresh_defs()

   VSetItems(vidgets["defs"], def_entries := keylist(spec.Defns))

   touched := 1

   return

end

#  Refresh the list of links.

procedure refresh_links()

   VSetItems(vidgets["links"], sort(spec.Links))

   touched := 1

   return

end

#  Save the current database to a specified file.

procedure save_as_db()
   local output, file

   repeat {
      if OpenDialog("Save database:", db_file) == "Cancel" then fail
      file := dialog_value
      if exists(file) then {
         if TextDialog("Overwrite existing file?") == "Cancel" then next
         }
      output := open(file, "w") | {
         Notice("Cannot open database file for writing.")
         next
         }
      db_file := file
      xencode(database, output)
      close(output)
      touched := &null
      return
      }

end

#  Save the current database

procedure save_db()
   local output

   if /db_file then return save_as_db()

   output := open(db_file, "w") | {
      Notice("Cannot write database file.")
      fail
      }

   xencode(database, output)

   close(output)

   touched := &null

   return

end

#  Save the current table of definitions to a file.

procedure save_defs()
   local output, file

   repeat {
      if OpenDialog("Defns file:") == "Cancel" then fail
      file := dialog_value
      if exists(file) then {
         if TextDialog("Overwrite existing file?") == "Cancel" then next
         }
      output := open(file, "w") | {
         Notice("Cannot open definitions file for writing.")
         next
         }
      xencode(spec.Defns, output)
      close(output)
      return
      }

end

#  Save the current specification as an include file.

procedure save_spec()
   static file

   initial file := "untitled.cpt"

   repeat {
      if TextDialog("Save specifications:", ["name", "comments", "file"],
         [spec.Name, spec.Comments, file], NameWidth) == "Cancel" then fail
      spec.Name := dialog_value[1]
      spec.Comments := dialog_value[2]
      write_spec(dialog_value[3], spec) | {
         Notice("Cannot write specification.")
         next
         }
      file := dialog_value[3]
      return
      }

end

#  Keyboard shortcuts.

procedure shortcuts(e)

   if e === "\r" then create_cb()	# quick generation initiation
   else if &meta then case map(e, &lcase, &ucase) of {
      "A"  :  add_def()
      "C"  :  colors()
      "D"  :  doptions()
      "F"  :  load_defs()
      "G"  :  create_cb()
      "H"  :  height()
      "I"  :  name_spec()
      "J"  :  load_defs(1)
      "K"  :  comments()
      "L"  :  left_expr()
      "M"  :  modulus()
      "N"  :  neighbors()
      "O"  :  foptions()
      "Q"  :  quit()
      "R"  :  show_colors()
      "S"  :  save_defs()
      "T"  :  top_expr()
      "W"  :  width()
      "X"  :  create_cb()
      "Y"  :  display_defs()
      "Z"  :  clear_defs()
      "\^A":  add_link()
      "\^C":  clear_links()
      "\^D":  dupl_spec()
      "\^L":  load_db()
      "\^M":  load_db(1)
      "\^N":  new_spec()
      "\^R":  load_db(2)
      "\^S":  save_db()
      "\^T":  save_as_db()
      "\^W":  save_spec()
      "\^X":  dump_all()
      "\^Y":  display_spec()
      "\^Z":  clear_db()
      }

   return

end

procedure show_colors()
   local colors

   colors := draw_colors(carpcolr(spec.Colors)) | {
      Notice("Invalid color specification.")
      fail
      }

   WAttrib(colors, "label=" || spec.Colors)

   Event(colors)

   WClose(colors)

   Raise()

   return

end

#  Items for the Specification menu.

procedure specification_cb(vidget, value)

   case value[1] of {
      "new      ^@N":  new_spec()
      "copy     ^@D":  dupl_spec()
      "display  ^@Y":  display_spec()
      "write    ^@W":  save_spec()
      }

   return

end

#  Edit the top-row specification.

procedure top_expr()

   repeat {
      case TextDialog("Top:", , spec.Top, ExprWidth,
         ["Define", "Default", "Okay", "Cancel"], 3) of {
            "Define" :  {
               define(dialog_value[1])
               break
               }
            "Cancel" :  fail
            "Default":  {
               spec.Top := TopDefault
               next
               }
            "Okay"   :  {
               spec.Top := dialog_value[1]
               break
               }
            }
     }

   return

end

#  Update the name of the current specification on the interface.

procedure update()
   static previous_name, sx, sy

   initial {
      sx := vidgets["placeholder"].ax
      sy := vidgets["placeholder"].ay
      }

   #  Update selection information on interface.

   WAttrib("drawop=reverse")

   DrawString(sx, sy, \previous_name)
   DrawString(sx, sy, spec.Name)

   WAttrib("drawop=copy")

   previous_name := spec.Name

   return

end

#  Edit the width of the carpet.

procedure width()

   repeat {
      case TextDialog("Width:", , spec.Width, NameWidth,
         ["Default", "Okay", "Cancel"], 2) of {
            "Cancel" :  fail
            "Default":  {
               spec.Width := WidthDefault
               next
               }
            "Okay"   :  {
               spec.Width := dialog_value[1]
               break
               }
            }
      }

   return

end

#===<<vib:begin>>===	modify using vib; do not remove this marker line
procedure ui_atts()
   return ["size=457,276", "bg=gray-white", "label=carpets"]
end

procedure ui(win, cbk)
return vsetup(win, cbk,
   [":Sizer:::0,0,457,276:carpets",],
   ["current label:Label:::15,253,161,13:current specification: ",],
   ["database:Menu:pull::35,0,64,21:Database",database_cb,
      ["load     ^@L","merge    ^@J","save     ^@S","save as  ^@T","clear    ^@Z",
      "revert   ^@R"]],
   ["db:List:w::15,41,125,160:",db_cb],
   ["definitions:Menu:pull::234,0,85,21:Definitions",definitions_cb,
      ["add       @A","load      @F","merge     @J","save      @S","clear     @Z"]],
   ["definitions:Label:::166,209,98,13: definitions  ",],
   ["defs:List:w::160,41,125,160:",defs_cb],
   ["edit:Menu:pull::99,0,36,21:Edit",edit_cb,
      ["modulus   @M","width     @W","height    @H","top       @T","left      @L",
      "neighbors @N","colors    @C","name      @I","comments  @K"]],
   ["file:Menu:pull::0,0,36,21:File",file_cb,
      ["generate  @G","display   @D","options   @O","quit      @Q"]],
   ["line1:Line:::0,21,457,21:",],
   ["line2:Line:::0,238,458,238:",],
   ["link:Menu:pull::320,0,43,21:Links",link_cb,
      ["add      ^@A","clear    ^@C"]],
   ["link:Label:::313,209,98,13:     links    ",],
   ["links:List:w::308,41,125,160:",links_cb],
   ["placeholder:Label:::180,264,35,13:     ",],
   ["specification:Menu:pull::135,0,99,21:Specification",specification_cb,
      ["new      ^@N","copy     ^@D","display  ^@Y","write    ^@W"]],
   ["specifications:Label:::21,209,98,13:specifications",],
   )
end
#===<<vib:end>>===	end of section maintained by vib
