############################################################################ # # File: tgrlink.icn # # Subject: Program to combine TIGER line chains # # Authors: Gregg M. Townsend and William S. Evans # # Date: June 23, 2000 # ############################################################################ # # Tgrlink connects records from a line chain file to produce a more # compact file composed of fewer, longer chains. Chains having common # endpoints and somewhat similar orientations are joined together. # Then, wherever three consecutive points are collinear, or nearly so, # the middle point is removed. # # Usage: tgrlink [-e maxerror] [-a maxangle] [file.lch] # # The maxerror parameter, measured in latitude units, sets the maximum # distance the middle of three points can deviate from the line connecting # its neighbors and still be considered "collinear". The default value # is 4, which is generally large enough to cover quantization errors # without introducing visible artifacts. # # The maxangle parameter, defaulting to 30 degrees, limits the change in # angle of the chain path due to the removal of a middle point. This # prevents narrow rectangles from turning into pointed triangles. # # The input file must be randomly seekable (a disk file, not a pipe). # ############################################################################ # # The algorithm is effective but not perfect. It is designed to # minimize memory to allow the handling of large input files. # Processing the output data a second time may give a little more # improvement. # # First, the input file is scanned and each chain is entered in a table. # Chains are segregated by feature and boundary code (chains with # different codes cannot be combined) and grouped by orientation. # # A table key is formed by concatenating latitude+longitude with # latitude (only), using whichever endpoint gives a smaller sum. The # table value for a chain is the chain's offset in the input file. # If multiple chains share the same key, a list of offsets is entered # in the table. # # Output is generated by iterating through all the codes from the # "least important" to "most important" (so that those end up on top # when the map is drawn). Within codes, vertically oriented lines # come first, then horizontally oriented lines, followed by others. # Within an orientation group, chains are sorted by key, with the # effect that they are produced from upper left to lower right # along a diagonally oriented wavefront. # # For each generated key, output proceeds as follows, given the file # offset o associated with the key. If offset o has already been # processed, as noted in the set "done", then do nothing further. # Otherwise, add o to the set and continue. Seek the input file to # offset o and read the chain data into memory. Calculate the far # endpoint of the chain and the key associated with that. Check the # tables for another unprocessed chain of similar orientation beginning # there; if successful, append the path and mark that chain as processed. # Repeat this as long as a successor chain can be found. # # Now go through the chain in memory and collapse collinear points within # the limits permitted by the command options. Finally, calculate the # maximum range of the chain from its starting point, and write it out. # # This all seems to work well in practice. One possible drawback, at # in theory, is that chains heading slightly more north than east will # not be connected to chains heading slightly more east than north. # The sorting of keys by latitude+longitude means that no matter which # chain is processed first, the wrong endpoint of the other one is in # the key table and no connection will be seen. # ############################################################################ # # Links: options # ############################################################################ link options $define DefaultError 4 # default max error for removing point $define DefaultAngle 30 # default max angle for removing point $define SECTORS 5 # number of different orientations global ifile # input file global maxerr, maxangle # point removal parameters global latsin # scaling factor: sin(latitude) global chtab # master chain table (keyed by code) global done # set of offsets already output global xoff, yoff # lists of deltas in current chain record crec(code, key, x1, x2, y1, y2, rev, aindex) # chain record data # main procedure procedure main(args) local opts, w, hdr1, hdr2, e, k, l, latmin, latmax opts := options(args, "a.e.") # process command options maxangle := \opts["a"] | DefaultAngle maxerr := \opts["e"] | DefaultError if *args > 1 then stop("usage: ", &progname, " file") else if *args = 1 then ifile := open(args[1]) | stop(&progname, ": can't open ", args[1]) else ifile := &input hdr1 := read(ifile) | stop(&progname, ": empty file") hdr2 := read(ifile) | stop(&progname, ": file truncated") latmin := hdr1[16+:7] latmax := hdr2[16+:7] latsin := sin(((latmax + latmin) / 2.0) * (&pi / 9999999)) loadfile() # load table keys write(hdr1) write(hdr2) every dumpcode(kgen(chtab)) # dump chains in code order end # loadfile() -- load input file keys into tables procedure loadfile() local w, line, alist, t, l, r chtab := table() repeat { w := where(ifile) | stop(&progname, ": input file is not seekable") line := read(ifile) | break r := crack(line) if /(alist := chtab[r.code]) then { # first time for this code; make new tables. alist := chtab[r.code] := list(SECTORS) every !alist := table() } t := alist[r.aindex] ((/t[r.key]) := w) | { if type(l := t[r.key]) ~== "list" then l := t[r.key] := [t[r.key]] put(l, w) } } return end # kgen(t) -- generate keys of t in better order, as in the "tgrsort" script procedure kgen(t) local l, k l := list() every k := key(t) do put(l, map(k[1], "FHEABCDX", "ZYXWVUTS") || k) l := sort(l) while k := pull(l) do suspend k[2:0] fail end # dumpcode(code) -- output all chains having a particular code procedure dumpcode(code) local h, v, i, l, k, o, alist alist := chtab[code] done := set() every l := sort(alist[aseq()], 3) do while k := get(l) do { o := get(l) if type(o) == "list" then every putchain(code, k, !o) else putchain(code, k, o) } return end # aseq() -- generate the orientation table subscripts in proper order procedure aseq() local v, h h := 1 + integer(0.25 * SECTORS) v := 1 + integer(0.75 * SECTORS) suspend h # sector that includes horizontal lines suspend v # sector that includes vertical lines suspend h+1 to v-1 # NW to SE quadrant suspend 1 to h-1 # ENE to WSW suspend v+1 to SECTORS # SSW to NNE fail end # putchain(code, k, o) -- output chain of given code, key, and offset procedure putchain(code, k, o) local t, r, x, y, x1, y1, xmin, xmax, ymin, ymax, d, w if member(done, o) then # if already processed return insert(done, o) # mark as done k ? { # extract (x1, y1) from key t := move(8) x1 := integer(move(7)) y1 := t - x1 } xoff := [] # init list of deltas yoff := [] r := putdel(o) # add this chain's deltas while o := successor(r) do { # while a successor can be found insert(done, o) # mark it as processed r := putdel(o) # append its deltas } collapse() # collapse collinear points x := xmin := xmax := x1 # find min/max x/y values y := ymin := ymax := y1 every x +:= !xoff do { xmin >:= x xmax <:= x } every y +:= !yoff do { ymin >:= y ymax <:= y } d := x - xmin # find max deviation from x1 | y1 d <:= xmax - x d <:= y - ymin d <:= ymax - y d >:= 9999 # limit to four digits # output the resulting chain writes(code, right(d, 4), right(x1, 7), right(y1, 7)) while x := get(xoff) & y := get(yoff) do if x ~= 0 | y ~= 0 then w := writes(right(5000 + x, 4), right(5000 + y, 4)) if /w then writes("50005000") # line had degenerated to a point write() return end # putdel(o) -- record deltas (only) for chain at offset o in input file. procedure putdel(o) local line, r, dy, mark # read the line located at offset o seek(ifile, o) | stop(&progname, ": can't reposition input file") line := read(ifile) | stop(&progname, ": input file changed during processing") # crack its data r := crack(line) # append deltas line ? { move(4) if ="|" then tab(upto('|') + 1) # skip feature name move(18) if /r.rev then # if endpoints were not reversed while put(xoff, move(4) - 5000) do put(yoff, move(4) - 5000) else { mark := &pos tab(0) # if must start at far end while (mark < &pos) & (put(yoff, 5000 - move(-4))) do { put(xoff, 5000 - move(-4)) } } } return r # return cracked data end # collapse() -- collapse collinear points in global xoff/yoff lists procedure collapse() local maxsq, maxa, i, x1, y1, a1, x2, y2, a2, da, d, dx, dy if maxerr <= 0 then # if no collapsing allowed return maxsq := maxerr * maxerr # square of error (avoid sqrt later) maxa := maxangle * &pi / 180 maxa >:= &pi # max angle in radians x2 := latsin * xoff[1] y2 := yoff[1] a2 := atan(y2, x2) every i := 2 to *xoff do { x1 := x2 y1 := y2 a1 := a2 x2 := latsin * xoff[i] y2 := yoff[i] a2 := atan(y2, x2) da := abs(a2 - a1) # change in angle if removed if da > maxa then # if too big, forget it next d := abs((x1 * x1 + y1 * y1) * sin(da)) # deviation from straight line if d <= maxsq then { # if close enough dx := xoff[i] + xoff[i-1] dy := yoff[i] + yoff[i-1] if abs(dx) < 5000 & abs(dy) < 5000 then { # if no overflow xoff[i] := dx # set in curr deltas yoff[i] := dy xoff[i-1] := yoff[i-1] := 0 # zero previous deltas } } } return end # successor(r) -- return offset of successor to chain given by crec record r procedure successor(r) local k, alist, t, i, o, e alist := chtab[r.code] # list, by orientation, for code k := right(r.x2 + r.y2, 8) || right(r.x2, 7) # successor's key would be this every i := 0 | 1 | -1 do { # try same orientation first t := alist[r.aindex + i] | next # table of offsets if o := \t[k] then { # entry can be int or list if type(o) ~== "list" then { if not member(done, o) then { return o } } else if (e := !o) & not member(done, e) then return e } } fail end # crack(line) -- return crec record giving data about chain procedure crack(line) local angle, x1, y1, x2, y2, a static o initial o := crec() line ? { o.code := move(4) if o.code ||:= ="|" then # if feature name present o.code ||:= tab(upto('|') + 1) move(4) # skip old dimension measurement x1 := x2 := integer(move(7)) y1 := y2 := integer(move(7)) while x2 +:= move(4) - 5000 do y2 +:= move(4) - 5000 if x1 + y1 > x2 + y2 then { # if far endpoint has smaller sum o.rev := 1 # chain needs to be reversed x1 :=: x2 y1 :=: y2 } else o.rev := &null o.key := right(x1 + y1, 8) || right(x1, 7) o.x1 := x1 o.y1 := y1 o.x2 := x2 o.y2 := y2 a := atan(y2 - y1, latsin * (x2 - x1)) o.aindex := 1 + integer(SECTORS * ((a / &pi) + 2.25)) % SECTORS } return o end