summaryrefslogtreecommitdiff
path: root/ipl/gprogs/travels.icn
diff options
context:
space:
mode:
Diffstat (limited to 'ipl/gprogs/travels.icn')
-rw-r--r--ipl/gprogs/travels.icn1121
1 files changed, 1121 insertions, 0 deletions
diff --git a/ipl/gprogs/travels.icn b/ipl/gprogs/travels.icn
new file mode 100644
index 0000000..e268405
--- /dev/null
+++ b/ipl/gprogs/travels.icn
@@ -0,0 +1,1121 @@
+############################################################################
+#
+# File: travels.icn
+#
+# Subject: Program to animate the traveling salesman problem
+#
+# Author: Gregg M. Townsend
+#
+# Date: September 17, 1997
+#
+############################################################################
+#
+# This file is in the public domain.
+#
+############################################################################
+#
+# Usage: travels [window options] [-q] [npoints]
+#
+# -q (quiet) suppresses commentary normally written to stdout
+#
+# npoints seeds the field with that many initial cities
+# and sets the count for the "reseed" button
+#
+#
+# travels illustrates several heuristic algorithms for obtaining
+# approximate solutions to the traveling salesman problem. Cities may
+# be seeded randomly or entered with the mouse. Speed may be controlled
+# using a slider. The CPU time, number of cities, and path length are
+# displayed on a status line and written to standard output after every
+# major action.
+#
+############################################################################
+#
+# Several types of controls are provided. New cities may be added
+# at any time, invalidating any current path. At least two cities must
+# be seeded before a path can be constructed. A path must be constructed
+# before any of the optimization algorithms can be applied.
+#
+# For a description on of the algorithms used, see:
+# David S. Johnson
+# Local Optimization and the Traveling Salesman Problem
+# Proc. 17th Colloquium on Automata, Languages, & Programming
+# Springer-Verlag (1990), pp. 446-461
+#
+#
+# Mouse Actions:
+#
+# Clicking the left mouse button adds a new point.
+#
+#
+# Keyboard Actions:
+#
+# The digit 0 clears all points.
+# The digits 1 through 9 seed 1 to 81 (n ^ 2) new points.
+#
+# Each of the pushbuttons below also has a keyboard equivalent
+# which is indicated on the pushbutton.
+#
+#
+# Pushbuttons:
+#
+# Removing and adding points:
+# Clear Remove all points
+# Reseed Add n random points (a command option, default 20)
+#
+# Path construction:
+# Initial Connect points in order of initialization
+# Random Random path
+# Strip Strip-wise construction
+# NearNbr Nearest-neighbor algorithm
+# NearIns Nearest-insertion algorithm
+# FarIns Farthest-insertion algorithm
+# Greedy Greedy algorithm
+#
+# Optimizations:
+# 2-Adj Swap pairs of adjacent points
+# Uncross Swap pairs of intersecting segments
+# 2-Opt Swap all segment pairs that shorten the path
+#
+# Control:
+# List List coordinates of points on standard output
+# Refresh Redraw the screen
+# Quit Exit the program
+#
+#
+# Delay Slider:
+#
+# The delay slider can be used to slow down the action. It specifies a
+# number of milliseconds to pause before visiting a new point or drawing
+# a new path segment. Its response is nonlinear in order to allow finer
+# control of short delays. Delays are inexact due to system granularity
+# and other problems.
+#
+# Unfortunately, the delay slider can only be changed between actions,
+# not during construction or optimization.
+#
+############################################################################
+#
+# Requires: Version 9 graphics
+#
+############################################################################
+#
+# Links: options, optwindw, button, slider, evmux, random, graphics
+#
+############################################################################
+
+link options
+link optwindw
+link button
+link slider
+link evmux
+link random
+link graphics
+
+$define EColor "dark blue" # emphasis color
+
+
+
+global ptlist # list of point records (permanent id order, not route)
+record point(
+ id, # permanent id
+ x, y, # location
+ nxt, prv, # forward and backward links for route
+ t1, t2) # scratch cells for traversal algorithms
+
+
+
+global distlist # list of distance recs (linearized triangular matrix)
+global distsrt # sorted distance list (created when needed)
+record dstrec(
+ d, # distance between two points (x1000, stored as int)
+ p, q) # the two points
+
+
+
+global newpts # non-null if points are new since last report
+
+global havepath # non-null if we have a valid path
+ # (start from any point and follow links)
+
+global lastclk # value of &time before last computation
+global delaytime # delay time between steps, in msec
+
+global opts # command line options
+global nseed # number of points to seed
+
+global win # main window
+global fgwin # binding for drawing in foreground color
+global emwin # binding for drawing in emphasis color
+global bgwin # binding for erasing in background color
+
+global m, w, h, bw, bh, fh # screen layout parameters
+global ax, ay, aw, ah # corners and size of arena
+
+
+
+
+######################### main program #########################
+
+
+
+procedure main(args)
+ local base, pt, sg, hl
+
+ # get options and open a window
+ opts := options(args, "qE:" || winoptions()) # get options
+
+ /opts["W"] := 700 # default width
+ /opts["H"] := 500 # default height
+ /opts["E"] := EColor # default emphasis
+ /opts["T"] := "sans,bold,12" # default font
+ /opts["M"] := -1 # use standard margin
+ win := optwindow(opts, "linewidth=2") # open window
+ m := opts["M"] # save specified margin
+ h := opts["H"] # save usable height
+ w := opts["W"] # save usable width
+
+ bw := 100 # button width
+ bh := 18 # button height
+ fh := 20 # footer height
+
+ ax := m + bw + m # arena bounds and size
+ ay := m
+ aw := w - bw - m
+ ah := h - fh - m
+
+ fgwin := Clone(win)
+ emwin := Clone(win, "fg=" || (opts["E"] | EColor | "black"), "linewidth=4")
+ bgwin := Clone(win, "fg=" || Bg(win), "linewidth=4")
+
+ # set up sensor for adding points
+ sensor(win, &lrelease, addpt, &null, ax, ay, aw, ah)
+
+ # set up buttons
+ buttonrow(win, m, m, bw, bh, 0, bh + (2 > m | 2),
+ "seeding", &null, &null,
+ "Clear 0", argless, clrpts,
+ "Reseed D", argless, reseed,
+ &null, &null, &null, # spacing
+ "construction", &null, &null,
+ "Initial I", argless, initpath,
+ "Random R", argless, randpath,
+ "Strip S", argless, strippath,
+ "NearNbr B", argless, nearnbr,
+ "NearIns N", argless, nearins,
+ "FarIns F", argless, farins,
+ "Greedy G", argless, greedypath,
+ &null, &null, &null,
+ "optimization", &null, &null,
+ "2-Adj A", argless, twoadj,
+ "Uncross U", argless, uncross,
+ "2-Opt T", argless, twoopt,
+ &null, &null, &null,
+ "control", &null, &null,
+ "Refresh H", argless, refresh,
+ "List L", argless, listpath,
+ &null, &null, &null,
+ "Quit Q", argless, exit,
+ )
+
+ # set up corresponding keyboard handlers
+ quitsensor(win) # q and Q
+ sensor(win, 'Ii', argless, initpath)
+ sensor(win, 'Rr', argless, randpath)
+ sensor(win, 'Ss', argless, strippath)
+ sensor(win, 'Bb', argless, nearnbr)
+ sensor(win, 'Nn', argless, nearins)
+ sensor(win, 'Ff', argless, farins)
+ sensor(win, 'Gg', argless, greedypath)
+ sensor(win, 'Aa', argless, twoadj)
+ sensor(win, 'Uu', argless, uncross)
+ sensor(win, 'Tt', argless, twoopt)
+ sensor(win, 'Ll', argless, listpath)
+ sensor(win, 'Dd', argless, reseed)
+ sensor(win, 'Hh', argless, refresh)
+ sensor(win, '0', argless, clrpts)
+ sensor(win, '123456789', reseed)
+
+ # set up speed slider
+ slider(win, setdly, 0, m, m + h - bh, bw, bh, 0, 0, 1)
+ setdly(win, 0, 0)
+
+ # initialize
+ randomize()
+ clrpts()
+ lastclk := &time
+
+ if nseed := integer(args[1]) then
+ reseed()
+ else
+ nseed := 20
+
+ # process events
+ evmux(win)
+end
+
+
+
+# setdly(win, arg, value) -- set delay time
+
+procedure setdly(win, arg, value)
+ local s, l
+
+ value := integer(10001 ^ value + 0.5) - 1
+ delaytime := value
+ s := " delay " || value || " "
+ l := TextWidth(win, s)
+ GotoXY(win, m + (bw - l) / 2, m + h - bh - m / 2)
+ writes(win, s)
+ return
+end
+
+
+
+# pause() -- delay according to the current setting
+
+procedure pause()
+ if delaytime > 0 then
+ WDelay(win, delaytime)
+ return
+end
+
+
+
+######################### path constructions #########################
+
+
+
+# initpath() -- connect in initial placement order
+
+procedure initpath()
+ local i
+
+ bgnpath(0, "placement order...") | fail
+ ptlist[1].nxt := &null
+ every i := 2 to *ptlist do {
+ follow(ptlist[i-1], ptlist[i])
+ pause()
+ }
+ ptlist[-1].nxt := ptlist[1]
+ ptlist[1].prv := ptlist[-1]
+ drawpath(fgwin, ptlist[-1], ptlist[1])
+
+ havepath := 1
+ report("initial path")
+ return
+end
+
+
+
+# randpath() -- make random connections
+
+procedure randpath()
+ local l, i, p, q
+
+ bgnpath(0, "connecting randomly...") | fail
+
+ l := copy(ptlist) # get copy of point list
+ every i := 1 to *l do # shuffle it
+ l[i] :=: l[?i]
+
+ p := l[1]
+ q := l[-1]
+ p.nxt := &null
+ every i := 2 to *l do {
+ follow(l[i-1], l[i])
+ pause()
+ }
+ p.prv := q
+ q.nxt := p
+ drawpath(fgwin, q, p)
+
+ havepath := 1
+ report("random path")
+ return
+end
+
+
+
+# strippath() -- construct using strips
+
+procedure strippath()
+ local i, l, n, p, q, r
+
+ if *ptlist < 3 then
+ return
+ bgnpath(0, "stripwise algorithm")
+
+ n := integer(sqrt(*ptlist) + .5)
+ l := list(n)
+ every !l := list()
+
+ every p := !ptlist do {
+ i := integer(1 + n * (p.x - ax) / real(aw + 1))
+ put(l[i], p)
+ }
+
+ every i := 1 to n do
+ l[i] := sortf(l[i], 3)
+ every i := 2 to n by 2 do {
+ r := []
+ every push(r, !l[i])
+ l[i] := r
+ }
+
+ q := !!l # get first point from first non-empty bin
+ every p := !!l do {
+ q.nxt := p
+ p.prv := q
+ drawpath(fgwin, q, p)
+ q := p
+ pause()
+ }
+ q := !!l
+ p.nxt := q
+ q.prv := p
+ drawpath(fgwin, p, q)
+
+ havepath := 1
+ report("stripwise algorithm")
+ return
+end
+
+
+
+# nearnbr() -- nearest neighbor
+
+procedure nearnbr()
+ local f, p, q, s, d
+
+ bgnpath(1, "nearest neighbor...") | fail
+
+ f := p := ?ptlist
+ p.nxt := p.prv := &null
+ s := set([p])
+ while *s < *ptlist do {
+ every d := !distsrt do {
+ if d.p === p then
+ q := d.q
+ else if d.q === p then
+ q := d.p
+ else
+ next
+ if member(s, q) then
+ next
+ insert(s, q)
+ p := follow(p, q)
+ p.nxt := &null
+ pause()
+ break
+ }
+ }
+ p.nxt := f
+ f.prv := p
+ drawpath(fgwin, p, f)
+
+ havepath := 1
+ report("nearest neighbor")
+ return
+end
+
+
+
+# nearins() -- make path using nearest-insertion algorithm
+
+procedure nearins()
+ local d, p, q, t, todo, mind
+
+ bgnpath(0, "nearest insertion...") | fail
+
+ # init path with the two closest points
+ mind := 1000000000
+ every d := !distlist do
+ if mind >:= d.d then {
+ p := d.p
+ q := d.q
+ }
+ p.nxt := p.prv := q
+ q.nxt := q.prv := p
+ drawpath(fgwin, p, q)
+ pause()
+
+ todo := set(ptlist) # set of points not yet on path
+ every delete(todo, p | q)
+
+ every t := !todo do
+ t.t1 := dist(t, q) # point.t1 = distance to nearest point on path
+
+ while *todo > 0 do { # repeat for each new point added to path
+ mind := 1000000000 # mind = minimum distance this pass
+ every t := !todo do {
+ t.t1 >:= dist(t, p) # update pt's dist to path if latest pt closer
+ if mind >:= t.t1 then # check for better (smaller) min d this pass
+ q := t # if nearest so far
+ }
+ # point q is the remaining point nearest from any point on the path
+ joinpath(p, q)
+ delete(todo, q)
+ pause()
+ p := q
+ }
+
+ havepath := 1
+ redraw()
+ report("nearest insertion")
+ return
+end
+
+
+
+# farins() -- make path using farthest-insertion algorithm
+
+procedure farins()
+ local d, p, q, t, todo, maxd
+
+ bgnpath(0, "farthest insertion...") | fail
+
+ # init path with the two most distant points
+ maxd := -1
+ every d := !distlist do
+ if maxd <:= d.d then {
+ p := d.p
+ q := d.q
+ }
+ p.nxt := p.prv := q
+ q.nxt := q.prv := p
+ drawpath(fgwin, p, q)
+ pause()
+
+ todo := set(ptlist) # set of points not yet on path
+ every delete(todo, p | q)
+
+ every t := !todo do
+ t.t1 := dist(t, q) # point.t1 = distance to nearest point on path
+
+ while *todo > 0 do { # repeat for each new point added to path
+ maxd := -1 # maxd = furthest distance this pass
+ every t := !todo do {
+ t.t1 >:= dist(t, p) # update pt's dist to path if latest pt closer
+ if maxd <:= t.t1 then # check for better (larger) maxd this pass
+ q := t # if farthest so far
+ }
+ # point q is the remaining point farthest from any point on the path
+ joinpath(p, q)
+ delete(todo, q)
+ pause()
+ p := q
+ }
+
+ havepath := 1
+ redraw()
+ report("farthest insertion")
+ return
+end
+
+
+
+# joinpath(p, q) -- add q at best place in path beginning at p
+
+procedure joinpath(p, q)
+ local start, best, d
+
+ d := dist(p, q) + dist(q, p.nxt) - dist(p, p.nxt)
+ start := best := p
+ while (p := p.nxt) ~=== start do
+ if d >:= dist(p, q) + dist(q, p.nxt) - dist(p, p.nxt) then
+ best := p
+
+ follow(best, q)
+ return
+end
+
+
+
+# greedypath() -- make path using greedy algorithm
+
+procedure greedypath()
+ local p, q, d, g, need
+
+ bgnpath(1, "greedy algorithm...") | fail
+
+ every p := !ptlist do {
+ p.nxt := p.prv := &null
+ p.t1 := p.id # point.t1 = group membership
+ p.t2 := 0 # point.t2 = degree of node
+ }
+
+ need := *ptlist # number of edges we still need
+
+ every d := |!distsrt do { # |! is to handle 2-pt case
+ p := d.p
+ q := d.q
+ if p.t2 > 1 | q.t2 > 1 then # if either is fully connected
+ next
+ if p.t1 = q.t1 & need > 1 then # if would be cycle & not done
+ next
+
+ # now we are committed to adding the point
+ pause()
+ DrawLine(fgwin, p.x, p.y, q.x, q.y) # draw new edge
+ p.t2 +:= 1 # increase degree counts
+ q.t2 +:= 1
+
+ if /p.nxt <- q & /q.prv := p then { # if q can follow p easily
+ g := q.t1 ~=:= p.t1 | break # break if the final connection
+ while q := \q.nxt do
+ q.t1 := g
+ }
+ else if /q.nxt <- p & /p.prv := q then { # if p can follow q easily
+ g := p.t1 ~=:= q.t1 | break # break if the final connection
+ while p := \p.nxt do
+ p.t1 := g
+ }
+ else if /p.nxt := q then { # implies /q.nxt -- both are chain tails
+ g := p.t1
+ repeat {
+ q.t1 := g
+ q.nxt := q.prv
+ q.prv := p
+ p := q
+ q := \q.nxt | break
+ }
+ }
+ else { # /p.prv & /q.prv -- both are chain heads
+ p.prv := q
+ g := p.t1
+ repeat {
+ q.t1 := g
+ q.prv := q.nxt
+ q.nxt := p
+ p := q
+ q := \q.prv | break
+ }
+ }
+
+ if (need -:= 1) = 0 then # quit when have all edges
+ break
+ }
+
+ havepath := 1
+ report("greedy algorithm")
+ return
+end
+
+
+
+
+# bgnpath(i, msg) -- common setup for path construction
+#
+# i > 0 if *sorted* distance table will be needed
+# msg is status message
+
+procedure bgnpath(i, msg)
+ if *ptlist < 2 then
+ fail
+ prepdist(i)
+ status(msg)
+ if \havepath then
+ erasepath()
+ havepath := &null
+ lastclk := &time
+ return
+end
+
+
+
+######################### optimizations #########################
+
+
+
+# twoadj() -- swap pairs of adjacent points
+
+procedure twoadj()
+ local lastchg, nflips, p, q
+
+ if /havepath then
+ return
+ status("2-adj...")
+ lastclk := &time
+ nflips := 0
+
+ lastchg := p := ?ptlist # pick random starting point
+
+ repeat {
+
+ q := p.nxt.nxt
+ repeat {
+ DrawLine(emwin, p.x, p.y, p.nxt.x, p.nxt.y) # mark current spot
+ if not pairtest(p, q) then # if swap doesn't help
+ break
+ flip(p, q) # do the swap
+ nflips +:= 1 # count it
+ lastchg := p # update point of last change
+ }
+
+ pause()
+ p := p.nxt
+ if p === lastchg then
+ break # have made complete circuit without changes
+ }
+
+ report("2-adj (" || nflips || " flips)")
+ refresh()
+ return
+end
+
+procedure adjtest(p, q)
+ return ((p.nxt.nxt === q) | (q.nxt.nxt === p)) & pairtest(p, q)
+end
+
+
+
+# twoopt() -- swap segments if total path shortens
+
+procedure twoopt()
+ pairdriver("2-opt", pairtest)
+ return
+end
+
+# pairtest(p, q) -- succeed if swapping out-segments from p and q shortens path
+
+procedure pairtest(p, q)
+ return (dist(p,q) + dist(p.nxt,q.nxt)) < (dist(p,p.nxt) + dist(q,q.nxt)) &
+ (not (p === (q.prv | q | q.nxt)))
+end
+
+
+
+# uncross() -- swap intersecting segments
+
+procedure uncross()
+ pairdriver("uncross", intersect)
+ return
+end
+
+# intersect(p, q) -- succeed if outward segments from p and q intersect
+#
+# from comp.graphics.algorithms FAQ, by O'Rourke
+
+procedure intersect(p, q)
+ local a, b, c, d
+ local xac, xdc, xba, yac, ydc, yba
+ local n1, n2, d12, r, s
+
+ a := p
+ b := p.nxt
+ c := q
+ d := q.nxt
+ xac := a.x - c.x
+ xdc := d.x - c.x
+ xba := b.x - a.x
+ yac := a.y - c.y
+ ydc := d.y - c.y
+ yba := b.y - a.y
+
+ n1 := yac * xdc - xac * ydc
+ n2 := yac * xba - xac * yba
+ d12 := real(xba * ydc - yba * xdc)
+
+ if d12 = 0.0 then
+ fail # lines are parallel or coincident
+
+ r := n1 / d12
+ s := n2 / d12
+
+ # intersection point is: (a.x + r * xba, a.y + r * yba)
+
+ if 0.0 < r < 1.0 & 0.0 < s < 1.0 then
+ return # segments AB and CD do intersect
+ else
+ fail # segments do not intersect (though extensions do)
+end
+
+
+
+
+# pairdriver(label, tproc) -- driver for "uncross" and "2-opt"
+
+procedure pairdriver(label, tproc)
+ local slist, todo, nflips, a, p, q
+
+ if /havepath then
+ return
+ status(label || "...")
+ lastclk := &time
+ nflips := 0
+
+ slist := list() # initial list of segments
+ every put(slist, path())
+ todo := set() # segments to reconsider
+
+ while p := get(slist) | ?todo do { # pick candidate segment
+
+ delete(todo, p)
+ pause()
+
+ # restart search every time p's outgoing edge changes
+ repeat {
+
+ DrawLine(emwin, p.x, p.y, p.nxt.x, p.nxt.y) # mark segment in progress
+
+ # check for swap with every other edge
+ every q := !ptlist do {
+
+ if tproc(p, q) then { # if test procedure succeeds,
+ # a swap is worthwhile
+
+ # the path from p.nxt through q will reverse direction;
+ # this will change segment labelings; so fix up "todo" set
+ a := q.prv
+ while a ~=== p do {
+ if member(todo, a) then { # if segment is on list
+ delete(todo, a) # remove under old name
+ insert(todo, a.nxt) # add under new name
+ }
+ a := a.prv
+ }
+
+ # new segment from p will be done when we loop again
+ # other new segment to list
+ insert(todo, p.nxt) # add to list
+
+ # now flip the edges
+ flip(p, q) # flip the edges
+ nflips +:= 1 # count the flip
+
+ break next # restart search loop using new edge
+ }
+ }
+
+ break # if no improvement for one full loop
+ }
+
+ }
+
+ report(label || " (" || nflips || " flips)")
+ refresh()
+ return
+end
+
+
+
+######################### point maintenance #########################
+
+
+
+# clrpts() -- remove all points
+
+procedure clrpts()
+ ptlist := []
+ distlist := []
+ distsrt := []
+ havepath := &null
+ refresh()
+ fillrect(bgwin)
+ status("0 points")
+ return
+end
+
+
+
+# reseed() -- add random points to the list
+
+procedure reseed(win, dummy, x, y, event)
+ local p, v, n
+
+ n := integer(\event)^2 | nseed
+ every 1 to n do
+ addpt(win, &null, ax + ?aw, ay + ?ah)
+ return
+end
+
+
+
+# addpt(win, dummy, x, y) -- add one point to the list
+
+procedure addpt(win, dummy, x, y)
+ local n, p, q
+
+ if \havepath then {
+ erasepath()
+ havepath := &null
+ }
+ n := *ptlist
+ p := point(n + 1, x, y)
+ every q := !ptlist do
+ put(distlist, dstrec(integer(1000 * sqrt((q.x-x)^2 + (q.y-y)^2)), p, q))
+ put(ptlist, p)
+ drawpt(p)
+ status(*ptlist || " points")
+ newpts := 1
+ return p
+end
+
+
+
+# prepdist(i) -- prepare distance data for path construction
+#
+# copy the distance list, if not already done, so it can be indexed quickly.
+# also create the sorted list if i > 0.
+
+procedure prepdist(i)
+ static c, n
+
+ if c ~=== distlist | n ~= *distlist then {
+ c := distlist := copy(distlist)
+ n := *distlist
+ }
+ if \i > 0 & *distsrt < *distlist then {
+ status("sorting distances... ")
+ lastclk := &time
+ WFlush(win)
+ distsrt := sortf(distlist, 1)
+ report("distance sort")
+ }
+ return
+end
+
+
+
+# dist(p, q) -- return distance between p and q assuming p ~=== q
+
+procedure dist(p, q)
+ local m, n
+ m := p.id
+ n := q.id
+ if m < n then
+ m :=: n
+ return distlist[((m - 1) * (m - 2)) / 2 + n].d
+end
+
+
+
+# path() -- generate current path, even if it changes during generation
+
+procedure path()
+ local l, p, q
+ p := q := ptlist[1] | fail
+ l := [p]
+ while (p := p.nxt) ~=== q do
+ put(l, p)
+ suspend !l
+end
+
+
+# follow(p, q) -- insert q to follow p (erases old path from p, draws new)
+
+procedure follow(p, q)
+ DrawLine(bgwin, p.x, p.y, (p.prv~===\p.nxt).x, p.nxt.y)
+ every drawpt(p | \p.nxt)
+ q.nxt := p.nxt
+ q.prv := p
+ (\p.nxt).prv := q
+ p.nxt := q
+ DrawLine(fgwin, p.x, p.y, q.x, q.y)
+ DrawLine(fgwin, q.x, q.y, (\q.nxt).x, q.nxt.y)
+ return q
+end
+
+
+
+# flip(p, q) -- link p to q, and their successors to each other
+
+procedure flip(p, q)
+ local a, b
+
+ DrawLine(bgwin, p.x, p.y, p.nxt.x, p.nxt.y)
+ DrawLine(bgwin, q.x, q.y, q.nxt.x, q.nxt.y)
+ # relink half of the chain backwards
+ a := q
+ while a ~=== p do {
+ a.prv :=: a.nxt
+ a := a.nxt
+ }
+ a := p.nxt
+ b := q.prv
+ p.nxt := q
+ q.prv := p
+ a.nxt := b
+ b.prv := a
+ DrawLine(fgwin, p.x, p.y, q.x, q.y)
+ DrawLine(fgwin, a.x, a.y, b.x, b.y)
+ every drawpt(p | q | a | b)
+ return
+end
+
+
+
+# linkpath(p, q, ...) -- link points p, q, ... in order
+
+procedure linkpath(l[])
+ local i, p, q, v
+ i := p := get(l)
+ v := [fgwin, p.x, p.y]
+ every q := !l do {
+ p.nxt := q
+ q.prv := p
+ p := q
+ put(v, p.x, p.y)
+ }
+ DrawLine ! v
+ every drawpt(i | !l)
+ return
+end
+
+
+
+
+######################### drawing #########################
+
+
+
+# refresh() -- redraw screen to repair segments and points
+
+procedure refresh()
+ fillrect(bgwin) # erase segs
+ redraw()
+ return
+end
+
+
+
+# redraw() -- redraw path without erasing
+
+procedure redraw()
+ local p
+
+ every drawpt(!ptlist)
+ every p := !ptlist do
+ DrawLine(fgwin, p.x, p.y, (\p.nxt).x, p.nxt.y)
+ return
+end
+
+
+
+# erasepath() -- erase path, redraw points if necessary
+
+procedure erasepath()
+ local l, p, v
+
+ v := [bgwin]
+ every p := ptlist[1].prv | path() do
+ put(v, p.x, p.y)
+ DrawLine ! v
+ every drawpt(!ptlist)
+ return
+end
+
+
+
+# drawpath(win, p, q) -- draw the path from p to q
+#
+# (of course, depending on the foreground color, this can hide a path, too.)
+
+procedure drawpath(win, p, q)
+ local v
+
+ v := [win, p.x, p.y]
+ while p ~=== q do {
+ p := p.nxt
+ put(v, p.x)
+ put(v, p.y)
+ }
+ DrawLine ! v
+ return
+end
+
+
+
+# drawpt(p) -- draw the single point p
+
+procedure drawpt(p)
+ FillRectangle(fgwin, p.x - 2, p.y - 2, 5, 5)
+ return
+end
+
+
+
+# fillrect(win) -- fill the working area
+
+procedure fillrect(win)
+ FillRectangle(win, ax - m + 1, ay - m + 1, aw + 2 * m - 1, ah + 2 * m - 1)
+ return
+end
+
+
+
+######################### reporting #########################
+
+
+
+# listpath() -- list the coordinates of each point on standard output
+
+procedure listpath()
+ local p
+
+ if \havepath then {
+ write("\point list in order of traversal:")
+ every listpt(path())
+ }
+ else {
+ write("\point list (no path established):")
+ every listpt(!ptlist)
+ }
+ return
+end
+
+# listpt(p) - list one point
+
+procedure listpt(p)
+ write(right(p.id, 3), ".", right(p.x, 5), right(p.y, 5),
+ right((\p.prv).id | "", 6), right((\p.nxt).id | "", 6))
+ return
+end
+
+
+
+# report(text) -- display statistics on screen and stdout
+#
+# The statistics include the delta time since lastclk was last set.
+#
+# Output to stdout is suppressed if the "-q" option was given.
+# Output to stdout is double spaced if the set of points has changed.
+
+procedure report(text)
+ local p, n, d, s, dt
+
+ dt := ((((&time - lastclk) / 1000.0) || "000") ? (tab(upto(".")) || move(3)))
+ s := right(*ptlist, 4) || " pts "
+
+ if \havepath then {
+ d := 0
+ every p := !ptlist do
+ d +:= dist(p, p.nxt)
+ d := (d + 500) / 1000
+ s ||:= right("d = " || d, 10)
+ }
+ else
+ s ||:= " "
+
+ s ||:= right(dt , 8) || " sec " || text
+
+ status(s)
+ if /opts["q"] then {
+ if \newpts then
+ write()
+ write(s)
+ }
+ newpts := &null
+ return
+end
+
+
+# status(s) -- write s as a status message
+
+procedure status(s)
+ EraseArea(win, m + bw + m, m + h - fh)
+ GotoXY(win, m + bw + m, m + h - (fh / 4))
+ writes(win, s)
+ return
+end