#!/usr/bin/perl -w # filename: makebbox.pl # author: Marvin Simkin # date: 2002-10-07 # purpose: read drawing instructions make bounding box # syntax: makebbox.pl [-Btickinfo] < pic.draw > box.draw # -B: flag indicating the presence of tickinfo # tickinfo: limited implementation of psbasemap tickinfo, see NOTE # pic.draw: drawing instructions of some data area # box.draw: drawing instructions to make a box surrounding above # example: makebbox.pl < pic.draw > box.draw # cat pic.draw red.draw box.draw | makeiv.pl > pic.iv # pic.draw: instructions to make a picture of some object # box.draw: a box around that object # red.draw: contains "color=1 0 0" to make the box red # pic.iv: a complete VRML 1.0 scene # NOTE: # This makes a box around your data. # It does NOT echo the input to output; this gives you the option # of creating a box around something without actually showing that # something. In the normal case when you DO want to see the thing, # you must separately include it in the files input to makeiv.pl. # # This does not set the color or line width of the box. # You do that by inserting those instructions ahead of the # instructions this produces, in the stream that goes to makeiv.pl. # Otherwise, the box will assume the attributes of the last thing drawn. # So if your entire scene is one color you don't have to bother with # a redundant color instruction. # # If you want multiple colors or line widths for, say, various sides # of the box or for tick marks vs. box lines, try running makebbox.pl # several times, once for each attribute combination, with only those # features you want in that color turned on. # # If you want the box to start and end on even numbers # you could put a few such points or lines in your data file, # or feed them directly to this program as standard input. # # TICK MARKS # # This can also (and will by default) make tickmarks on every axis. # You can control how often the tickmarks occur and how big they are. # If you don't set anything it tries to make a decent guess, # but your mileage may vary. If you care, set them explicitly. # # The basic options are # TickFreq - how often (in whatever units you use) do you want a tick? # TickLen - how long (in whatever units you use) should the ticks be? # Generally these will be the same for X, Y and Z # but you can set the overall default and then override for Y and Z. # # Tickmarks can be suppressed by setting TickLen(X/Y/Z) to zero # (using the notation -0 or +0, see t+l and t-l below). # # For those familiar with GMT, the "tickinfo" argument uses a LIMITED # implementation of the same syntax, with some ENHANCEMENTS to specify # tick length. This means your tickinfo strings from other scripts MAY # have a similar effect here. # # However, the normal GMT plot (like many graphs) is 2D so there are # only 4 bounding lines to consider, and text is all flat with respect # to the paper or screen. A 3D cube however has 12 bounding lines, # and tick marks and text could extend in the Z dimension as well. # This makes for many more permutations of tick placement info. # # GMT names the 4 lines around a plot W S E and N for the compass # directions, and on the occasional 3D plot one of the vertical lines # is called Z. This program keeps those defined "line names" and names # the remaining lines according to a similar if mostly meaningless # pattern: # # +-------------------------+ # /| M .| # / | . | # I/ | H. | # / | . | # / | . | # + . . | . . . . . . . . . + | # | | N . | # | V| . D| # | | . | # | | . | # | | . | # | | . | # | | . | # W| | E. | # | | . | # | +-------------------------+ # | / R . / # | / . / # | Z/ . J/ # | / . / # |/ ./ # +-------------------------+ # S # # You use these "line names" when you want to turn tick marks, # annotations and grid lines on or off. By default W S and Z # get tick marks and annotations, and all 12 grid (box) lines are drawn. # # # Currently supported options are: # # -Bt # Sets TickFreq to the same t in X, Y and Z. # (In all cases Y and Z settings are copied from X by default.) # # -Bt/t/t # Sets individual TickFreq for X, Y, and Z. # If you only specify X/Y then Z will be the same as X. # To accept the default for X and y, but specify Z, # leave the X and Y spots null, as in //t. # # Similar X/Y/Z repetition can be used with the options below. # # -Bt+l # Sets TickFreq to the same t in X, Y and Z # and TickLen to the same l in X, Y and Z. # Ticks go OUTSIDE the bounding box. # This is the recommended and default tick direction. # When annotation is enabled, text appears at the end of the tick line. # # -Bt-l # Sets TickFreq to the same t in X, Y and Z # and TickLen to the same l in X, Y and Z. # Ticks go INSIDE the bounding box. # This may not be a good idea. Tick lines may intersect each other # and/or be invisible at corners. They may interfere with what you are # trying to see. # Input data format # The goal of the data format is to make it possible to describe an object # in a one-line instruction (e.g. draw a line from point A to point B) # and to set defaults that apply to all subsequent objects (e.g. color red). # You can have one "word" per line, or multiple "words" separated by ; # A word describing an object has a colon, such as "line:X1,Y1,Z1,X2,Y2,Z2". # This immediately causes the object to be created with existing colors etc. # Or a word can set a value with an equal sign, such as "color=R G B". # When a word changes a value it applies to all subsequent instructions. # color=1 0 0 # linewidth=3 # line:X1,Y1,Z1,X2,Y2,Z2 # line:X1,Y1,Z1,X2,Y2,Z2;line:X1,Y1,Z1,X2,Y2,Z2 # color=0 0 1 # line:X1,Y1,Z1,X2,Y2,Z2 # line:X1,Y1,Z1,X2,Y2,Z2 # linewidth=1;line:X1,Y1,Z1,X2,Y2,Z2 # color=0.2 0.2 0.2;line:X1,Y1,Z1,X2,Y2,Z2 # Output data format # Program will generate "line:" instructions in the same format # INITIALIZE: my $MaxX; my $MaxY; my $MaxZ; my $MinX; my $MinY; my $MinZ; # Tick mark, annotation, boxline On/Off flags and defaults my %TickFlag; my %NoteFlag; my %BBoxFlag; # The first few have everything switched on $BBoxFlag{'S'} = 1; # the South line $BBoxFlag{'W'} = 1; # the West line $BBoxFlag{'Z'} = 1; # the Z line %NoteFlag = %BBoxFlag; %TickFlag = %BBoxFlag; # Turn on the remaining sides only for bounding box lines $BBoxFlag{'N'} = 1; # the North line $BBoxFlag{'E'} = 1; # the East line $BBoxFlag{'R'} = 1; $BBoxFlag{'V'} = 1; $BBoxFlag{'M'} = 1; $BBoxFlag{'D'} = 1; $BBoxFlag{'I'} = 1; $BBoxFlag{'H'} = 1; $BBoxFlag{'J'} = 1; # SUBROUTINES: use strict; # this provides the floor() function use POSIX; sub Min { # find the smallest number in a list my $Min; $Min = 1e+308; # hope to never see a bigger number than that # print STDERR "Default Min value = $Min\n"; my $Value; while (defined ($Value = pop)) { # print STDERR "Checking $Value < $Min\n"; $Min = $Value if $Value < $Min; } die 'undefined Min' unless defined $Min; return $Min; } sub Max { # find the largest number in a list my $Max; $Max = -1e+308; # hope to never see a smaller number than that my $Value; while (defined ($Value = pop)) { # print STDERR "Checking $Value > $Max\n"; $Max = $Value if $Value > $Max; } return $Max; } # ARGUMENTS: my $TickInfo; $TickInfo = $ARGV[0]; # check to see if it starts with -B if (defined ($TickInfo)) { die "Unrecognized argument '$TickInfo'" unless $TickInfo =~ /^-B./; # strip the -B $TickInfo =~ s/^-B//; } # allow for potential user overrides my $UserFreqX; my $UserLenX; my $UserFreqY; my $UserLenY; my $UserFreqZ; my $UserLenZ; # check for TickInfo from command line if (defined ($TickInfo)) { my @TickInfo; # This won't work if we accept quoted text labels later; # there might be slashes within the quotes. @TickInfo = split (/\//, $TickInfo); if ($#TickInfo > 2) { warn 'More than 3 dimensions in TickInfo will be ignored'; } # can't really loop because of vars named X, Y, Z # might be nice to redesign this someday but... if ($TickInfo[0] =~ /^([\.0-9]*)([-+][\.0-9]*)$/) { $UserFreqX = $1; $UserLenX = $2; } elsif ($TickInfo[0] =~ /^([\.0-9]*)$/) { $UserFreqX = $1; } else { warn "Could not parse TickInfo '$TickInfo[0]'"; } if ($TickInfo[1] =~ /^([\.0-9]*)([-+][\.0-9]*)$/) { $UserFreqY = $1; $UserLenY = $2; } elsif ($TickInfo[1] =~ /^([\.0-9]*)$/) { $UserFreqY = $1; } else { warn "Could not parse TickInfo '$TickInfo[1]'"; } if ($TickInfo[2] =~ /^([\.0-9]*)([-+][\.0-9]*)$/) { $UserFreqZ = $1; $UserLenZ = $2; } elsif ($TickInfo[2] =~ /^([\.0-9]*)$/) { $UserFreqZ = $1; } else { warn "Could not parse TickInfo '$TickInfo[2]'"; } } # MAINLINE: my $Line; while ($Line = ) { chomp $Line; # a "word" is anything separated by semicolons my @Word; @Word = split (/;/, $Line); my $Word; foreach $Word (@Word) { # print STDERR "Processing '$Word'\n"; # some words may set variables if ($Word =~ /=/) { ## this program does not care (code cloned from makeiv.pl) ## # parse VAR=VALUE ## my $Var; ## my $Value; ## ($Var, $Value) = split (/=/, $Word, 2); ## ## if ($Var eq 'color') { ## $Color = $Value; ## } elsif ($Var eq 'linewidth') { ## $LineWidth = $Value; ## } else { ## print STDERR "Unrecognized variable assignment: '$Word'\n"; ## } ## # other words may specify objects } elsif ($Word =~ /:/) { # parse VAR=VALUE my $Var; my $Value; ($Var, $Value) = split (/:/, $Word, 2); if ($Var eq 'line') { my $X1; my $Y1; my $Z1; my $X2; my $Y2; my $Z2; ($X1, $Y1, $Z1, $X2, $Y2, $Z2) = split (/,/, $Value); # save bounding box $MaxX = &Max ($MaxX, $X1, $X2); $MinX = &Min ($MinX, $X1, $X2); $MaxY = &Max ($MaxY, $Y1, $Y2); $MinY = &Min ($MinY, $Y1, $Y2); $MaxZ = &Max ($MaxZ, $Z1, $Z2); $MinZ = &Min ($MinZ, $Z1, $Z2); ## &PlotLine ($Value); ## ## } elsif ($Var eq 'text') { ## ## ## } else { ## print STDERR "Unrecognized object name: '$Word'\n"; } ## ## ## } else { ## print STDERR "Unrecognized instruction: '$Word'\n"; } } } ### for now if no Z dimension assume a thickness of 1 unit, whatever that is ###$MinZ = $MaxZ - 1 if $MinZ == $MaxZ; # output bounding box print STDERR "Bounding box is $MinX,$MinY,$MinZ to $MaxX,$MaxY,$MaxZ\n"; # now that its size is known, compute TickFreq and TickLen defaults # see how much room we have to work with my $SpanX; $SpanX = $MaxX - $MinX; my $SpanY; $SpanY = $MaxY - $MinY; my $SpanZ; $SpanZ = $MaxZ - $MinZ; # if it is only a 2D structure assume the third dimension = the smaller of the first 2 if ($SpanX == 0) { $SpanX = &Min ($SpanY, $SpanZ); } if ($SpanY == 0) { $SpanY = &Min ($SpanX, $SpanZ); } if ($SpanZ == 0) { $SpanZ = &Min ($SpanX, $SpanY); } # there has to be something by now... right? die "Cannot put tickmarks with no span" if $SpanX == 0; # find the shortest and longest span my $MinSpan; $MinSpan = &Min ($SpanX, $SpanY, $SpanZ); my $MaxSpan; $MaxSpan = &Max ($SpanX, $SpanY, $SpanZ); # We NEVER want less than 3 ticks along that shortest axis # so in the perfect case TickFreq would be MinSpan / 2 # (ticks at 0, 0.5, 1) # But the edges might not be even: # 0.1 .. 1.3 is a span of 1.2, so / 2 would be 0.6 # but that would give us ticks at 0.6 and 1.2 only. # On the other hand 1.2 / 3 gives 0.4 # so ticks at 0.4 0.8 1.2, all of which are in range # Now consider a span of 1.25 / 3 gives 0.41666666... # which is a lousy tick increment. # So is 1.25 / 4, at 0.3125 # Probably the best generic solution is to use only # the topmost meaningful digit of that fraction, # e.g. 0.4 for 0.41666666... my $TickFreqX; $TickFreqX = $MinSpan / 3; # Now if that number is less than one, such as 0.000000123 # we want to throw away all digits but the first nonzero one, # leaving 0.0000001 if ($TickFreqX =~ /(^0\.0*[1-9])/) { $TickFreqX = $1; # But if greater than one, such as 1234.567, # all we want is the first nonzero digit (as before) # with zeros after: 1000.000 } else { my $FirstDigit; $FirstDigit = $TickFreqX; $FirstDigit =~ /^(.)/; $FirstDigit = $1; $TickFreqX =~ s/[1-9]/0/g; $TickFreqX =~ s/.//; $TickFreqX = $FirstDigit . $TickFreqX } # tick marks should be the smaller of: # 1/3 of the shortest span, or # 1/33 of the longest span my $TickLenX; $TickLenX = &Min ($MinSpan / 3, $MaxSpan / 33); $TickFreqX = $UserFreqX if defined ($UserFreqX) and $UserFreqX ne ''; $TickLenX = $UserLenX if defined ($UserLenX); # Y and Z default from X unless overridden in TickInfo my $TickFreqY; $TickFreqY = $TickFreqX; $TickFreqY = $UserFreqY if defined ($UserFreqY) and $UserFreqY ne ''; my $TickLenY; $TickLenY = $TickLenX; $TickLenY = $UserLenY if defined ($UserLenY); my $TickFreqZ; $TickFreqZ = $TickFreqX; $TickFreqZ = $UserFreqZ if defined ($UserFreqZ) and $UserFreqZ ne ''; my $TickLenZ; $TickLenZ = $TickLenX; $TickLenZ = $UserLenZ if defined ($UserLenZ); ############################################################################# # Here is where you would hard code an override if you want to # For example, negative TickLen makes the ticks go IN instead of OUT ##$TickLenX = -$TickLenX; ##$TickLenY = -$TickLenY; ##$TickLenZ = -$TickLenZ; ############################################################################# print STDERR "TickFreqX=$TickFreqX,TickLenX=$TickLenX\n"; print STDERR "TickFreqY=$TickFreqY,TickLenY=$TickLenY\n"; print STDERR "TickFreqZ=$TickFreqZ,TickLenZ=$TickLenZ\n"; # draw the 4 lines that run in the X direction # MinX to MaxX print "line:$MinX,$MinY,$MinZ,$MaxX,$MinY,$MinZ\n" if $BBoxFlag{'S'}; print "line:$MinX,$MinY,$MaxZ,$MaxX,$MinY,$MaxZ\n" if $BBoxFlag{'R'}; print "line:$MinX,$MaxY,$MinZ,$MaxX,$MaxY,$MinZ\n" if $BBoxFlag{'N'}; print "line:$MinX,$MaxY,$MaxZ,$MaxX,$MaxY,$MaxZ\n" if $BBoxFlag{'M'}; # put tickmarks along those 4 lines if ($TickLenX) { # see if we need a first tick at the lowest X my $TickPoint; $TickPoint = floor ($MinX / $TickFreqX) * $TickFreqX; # so we have the first POTENTIAL TickPoint # now loop TickPoint by TickFreqX until MaxX exceeded while ($TickPoint <= $MaxX) { # don't draw the first tick if out of range if ($TickPoint >= $MinX) { # first the Y ticks if ($TickLenY) { # OK, draw Y ticks on all 4 X lines my $TickY; $TickY = $MinY - $TickLenY; print "line:$TickPoint,$MinY,$MinZ,$TickPoint,$TickY,$MinZ\n" if $TickFlag{'S'}; print "line:$TickPoint,$MinY,$MaxZ,$TickPoint,$TickY,$MaxZ\n" if $TickFlag{'R'}; $TickY = $MaxY + $TickLenY; print "line:$TickPoint,$MaxY,$MinZ,$TickPoint,$TickY,$MinZ\n" if $TickFlag{'N'}; print "line:$TickPoint,$MaxY,$MaxZ,$TickPoint,$TickY,$MaxZ\n" if $TickFlag{'M'}; } # then the Z ticks if ($TickLenZ) { # OK, draw Z ticks on all 4 X lines my $TickZ; $TickZ = $MinZ - $TickLenZ; print "line:$TickPoint,$MinY,$MinZ,$TickPoint,$MinY,$TickZ\n" if $TickFlag{'S'}; print "line:$TickPoint,$MaxY,$MinZ,$TickPoint,$MaxY,$TickZ\n" if $TickFlag{'N'}; $TickZ = $MaxZ + $TickLenZ; print "line:$TickPoint,$MinY,$MaxZ,$TickPoint,$MinY,$TickZ\n" if $TickFlag{'R'}; print "line:$TickPoint,$MaxY,$MaxZ,$TickPoint,$MaxY,$TickZ\n" if $TickFlag{'M'}; } } # increment loop $TickPoint += $TickFreqX; } } # draw the 4 lines that run in the Y direction # MinY to MaxY print "line:$MinX,$MinY,$MinZ,$MinX,$MaxY,$MinZ\n" if $BBoxFlag{'W'}; print "line:$MinX,$MinY,$MaxZ,$MinX,$MaxY,$MaxZ\n" if $BBoxFlag{'V'}; print "line:$MaxX,$MinY,$MinZ,$MaxX,$MaxY,$MinZ\n" if $BBoxFlag{'E'}; print "line:$MaxX,$MinY,$MaxZ,$MaxX,$MaxY,$MaxZ\n" if $BBoxFlag{'D'}; # put tickmarks along those 4 lines if ($TickLenY) { # see if we need a first tick at the lowest Y my $TickPoint; $TickPoint = floor ($MinY / $TickFreqY) * $TickFreqY; # so we have the first POTENTIAL TickPoint # now loop TickPoint by TickFreqY until MaxY exceeded while ($TickPoint <= $MaxY) { # don't draw the first tick if out of range if ($TickPoint >= $MinY) { # first the X ticks if ($TickLenX) { # OK, draw X ticks on all 4 Y lines my $TickX; $TickX = $MinX - $TickLenX; print "line:$MinX,$TickPoint,$MinZ,$TickX,$TickPoint,$MinZ\n" if $TickFlag{'W'}; print "line:$MinX,$TickPoint,$MaxZ,$TickX,$TickPoint,$MaxZ\n" if $TickFlag{'V'}; $TickX = $MaxX + $TickLenX; print "line:$MaxX,$TickPoint,$MinZ,$TickX,$TickPoint,$MinZ\n" if $TickFlag{'E'}; print "line:$MaxX,$TickPoint,$MaxZ,$TickX,$TickPoint,$MaxZ\n" if $TickFlag{'D'}; } # then the Z ticks if ($TickLenZ) { # OK, draw Z ticks on all 4 Y lines my $TickZ; $TickZ = $MinZ - $TickLenZ; print "line:$MinX,$TickPoint,$MinZ,$MinX,$TickPoint,$TickZ\n" if $TickFlag{'W'}; print "line:$MaxX,$TickPoint,$MinZ,$MaxX,$TickPoint,$TickZ\n" if $TickFlag{'E'}; $TickZ = $MaxZ + $TickLenZ; print "line:$MinX,$TickPoint,$MaxZ,$MinX,$TickPoint,$TickZ\n" if $TickFlag{'V'}; print "line:$MaxX,$TickPoint,$MaxZ,$MaxX,$TickPoint,$TickZ\n" if $TickFlag{'D'}; } } # increment loop $TickPoint += $TickFreqY; } } # draw the 4 lines that run in the Z direction # MinZ to MaxZ print "line:$MinX,$MinY,$MinZ,$MinX,$MinY,$MaxZ\n" if $BBoxFlag{'Z'}; print "line:$MinX,$MaxY,$MinZ,$MinX,$MaxY,$MaxZ\n" if $BBoxFlag{'I'}; print "line:$MaxX,$MinY,$MinZ,$MaxX,$MinY,$MaxZ\n" if $BBoxFlag{'J'}; print "line:$MaxX,$MaxY,$MinZ,$MaxX,$MaxY,$MaxZ\n" if $BBoxFlag{'H'}; # put tickmarks along those 4 lines if ($TickLenZ) { # see if we need a first tick at the lowest Z my $TickPoint; $TickPoint = floor ($MinZ / $TickFreqZ) * $TickFreqZ; # so we have the first POTENTIAL TickPoint # now loop TickPoint by TickFreqZ until MaxZ exceeded while ($TickPoint <= $MaxZ) { # don't draw the first tick if out of range if ($TickPoint >= $MinZ) { # first the Y ticks if ($TickLenY) { # OK, draw Y ticks on all 4 Z lines my $TickY; $TickY = $MinY - $TickLenY; print "line:$MinX,$MinY,$TickPoint,$MinX,$TickY,$TickPoint\n" if $TickFlag{'Z'}; print "line:$MaxX,$MinY,$TickPoint,$MaxX,$TickY,$TickPoint\n" if $TickFlag{'J'}; $TickY = $MaxY + $TickLenY; print "line:$MinX,$MaxY,$TickPoint,$MinX,$TickY,$TickPoint\n" if $TickFlag{'I'}; print "line:$MaxX,$MaxY,$TickPoint,$MaxX,$TickY,$TickPoint\n" if $TickFlag{'H'}; } # then the X ticks if ($TickLenX) { # OK, draw X ticks on all 4 Z lines my $TickX; $TickX = $MinX - $TickLenX; print "line:$MinX,$MinY,$TickPoint,$TickX,$MinY,$TickPoint\n" if $TickFlag{'Z'}; print "line:$MinX,$MaxY,$TickPoint,$TickX,$MaxY,$TickPoint\n" if $TickFlag{'I'}; $TickX = $MaxX + $TickLenX; print "line:$MaxX,$MinY,$TickPoint,$TickX,$MinY,$TickPoint\n" if $TickFlag{'J'}; print "line:$MaxX,$MaxY,$TickPoint,$TickX,$MaxY,$TickPoint\n" if $TickFlag{'H'}; } } # increment loop $TickPoint += $TickFreqZ; } }