eval 'exec perl -S $0 "$@"'
    if $runnning_under_some_shell
;

#-----------------------------------------------------------------------

=head1 NAME

sitemapper - script for generating site maps

=head1 SYNOPSIS

    sitemapper [ -verbose ] [ -help ] [-doc ] [ -depth <depth> ] [ -proxy <proxy URL> ] [ -format <html|js> ] [ -title <page title> ] -site <base URL>

=cut

#-----------------------------------------------------------------------

require 5.003;
use strict;

#-----------------------------------------------------------------------

=head1 DESCRIPTION

B<sitemapper> generates site maps for a given site. It traverses a site from
the base URL given as the L<OPTIONS/-site> option and generates an HTML page
consisting of a bulleted list which reflects the structure of the site. 

The structure reflects the distance from the home page of the pages listed;
i.e.  the first level bullets are pages accessible directly from the home page,
the next level, pages accessible from those pages, etc. Obviously, pages that
are linked from "higher" up pages may appear in the "wrong place" in the tree,
than they "belong".

The L<OPTIONS/-format> option can be used to specify alternative options for
formating the site map. Currently the options are html (as described above -
the default) and js, which uses Jef Pearlman's (jef@mit.edu) javascript Tree
class to display the site map as a collapsable tree.

=head1 OPTIONS

=over 4

=item -depth

Option to specify the depth of the site map generated. If no specified, 
generates a sitemap of unlimited depth.

=item -site

Option to specify a base URL to generate a site map for.

=item -proxy

Specify an HTTP proxy to use.

=item -format

Format for the site map. Current options are html and js. html is
the default.

=item -title

Option to specify a page title for the site map.

=item -help

Display a short help message to standard output, with a brief
description of purpose, and supported command-line switches.

=item -doc

Display the full documentation for the script,
generated from the embedded pod format doc.

=item -verbose

Enable verbose reporting as the script runs.

=item -verbose

Print out the current version number.

=back

=head1 ENVIRONMENT

B<sitemapper> makes use of the C<$http_proxy> environment variable, if it is
set.

=head1 SEE ALSO

Getopt::Long (L<Getopt::Long>)
IO::File (L<IO::File>)
LWP::UserAgent (L<LWP::UserAgent>)
HTML::LinkExtor (L<HTML::LinkExtor>)
URI::URL (L<URI::URL>)
Pod::Usage (L<Pod::Usage>)
MD5 (L<MD5>)
Date::Format (L<Date::Format>)
Jef Pearlman's javascript Tree class 
(http://developer.netscape.com/docs/examples/dynhtml/tree.html)

=cut

#-----------------------------------------------------------------------

use Getopt::Long;
use IO::File;
use LWP::UserAgent;
use HTML::LinkExtor;
use URI::URL;
use MD5;
use Date::Format;

#------------------------------------------------------------------------------
#
# Public global variables
#
#------------------------------------------------------------------------------

use vars qw( 
    $NAME 
    $VERSION 
    $CONTACT 
    $HELP 
    $DOC 
    $USAGE
    $VERBOSE 
    $WHEN 
    $HEADER 
    $FOOTER 
);

# command line options - see pod

use vars qw (
    $opt_verbose
    $opt_version
    $opt_help
    $opt_doc
    $opt_depth
    $opt_title
    $opt_format
    $opt_site
    $opt_proxy
    $opt_output
);


#------------------------------------------------------------------------------
#
# Private global variables
#
#------------------------------------------------------------------------------

my (

    %MD5Hash,           # MD5 hash to identify identical pages
    %UrlSeen,           # hash of URLs already seen
    %Parent,            # hash of the parents of each node in the tree
                        # - used when deleting duplicate pages (spotted by MD5
                        # hash) or pages that can't be got by the robot
);

#-----------------------------------------------------------------------

=head1 BUGS

Should use WWW::Robot (L<WWW::Robot>) to do the site traversal.

The javascript sitemap has only been tested on Netscape 4.05.

=head1 AUTHOR

Ave Wrigley E<lt>wrigley@cre.canon.co.ukE<gt>
Web Group, Canon Research Centre Europe

=head1 COPYRIGHT

Copyright (c) 1998 Canon Research Centre Europe. All rights reserved.

This script is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

#------------------------------------------------------------------------------
#
# Initialize global variables
#
#------------------------------------------------------------------------------

( $NAME ) = $0 =~ m{([^/]+)$};

$CONTACT = 'wrigley@cre.canon.co.uk';

$VERSION = '1.004';

$VERBOSE = sub {
    print STDERR shift, "\n" if $opt_verbose;
};

$USAGE = sub {
    require 'Pod/Usage.pm';
    import Pod::Usage;
    pod2usage( 
        message => shift,
        verbose => 0,
        exitval => 1,
    );
};

$DOC = sub {
    require 'Pod/Usage.pm';
    import Pod::Usage;
    pod2usage( 
        verbose => 2,
        exitval => 0,
    );
};

$HELP = sub {
    require 'Pod/Usage.pm';
    import Pod::Usage;
    pod2usage(
        message => shift,
        exitval => 0,
        verbose => 1,
    );
};

$WHEN = time2str( "on %A the %o of %B %Y at %r", time );

$HEADER = sub {
    my $title = shift;

    return <<HTML_HEADER;
<HTML>
<HEAD>
    <TITLE>$title</TITLE>
</HEAD>
<BODY BGCOLOR = "#FFFFFF">
    <H1>$title</H1>
    <HR NOSHADE>
HTML_HEADER
};

$FOOTER = <<FOOTER;
<HR NOSHADE>
<TABLE WIDTH = "100%">
    <TR>
        <TD VALIGN = "TOP" ALIGN = "LEFT">
            $NAME version $VERSION
        <TD VALIGN = "TOP" ALIGN = "RIGHT">
            <A HREF = "mailto:$CONTACT">$CONTACT</A>
    <TR>
        <TD COLSPAN = 2 VALIGN = "TOP" ALIGN = "LEFT">
            Generated $WHEN
</TABLE>
FOOTER

&$HELP unless GetOptions qw( 
    help 
    doc 
    verbose 
    version 
    depth=i
    site=s
    output=s
    proxy=s
    title=s
    format=s
);

&$HELP if $opt_help;
&$DOC if $opt_doc;
print "$VERSION\n" and exit( 0 ) if $opt_version;
&$USAGE( '-sites argument is required' ) unless $opt_site;
$opt_format ||= 'html';
&$USAGE( '-format option must be one of (js|html)' )
    unless $opt_format =~ /^(html|js)$/i
;
$opt_format = lc( $opt_format );

#==============================================================================
#
# Start of main
#
#==============================================================================

#------------------------------------------------------------------------------
#
# Display hashes - these hashes are used to print out sitemap, using
# $opt_format as a key
#
#------------------------------------------------------------------------------

my %print_start_all_lists = (
    'js'        => sub { print { shift } "\t\"[\" +\n" },
    'html'      => sub { print { shift } '<UL>' },
);

my %print_end_all_lists = (
    'js'        => sub { print { shift } "\t\"]\" +\n" },
    'html'      => sub { print { shift } '</UL>' },
);

my %print_start_list = (
    'js'        => sub { print { shift } "\t\"[\" +\n" },
    'html'      => sub { print { shift } '<UL>' },
);

my %print_end_list = (
    'js'        => sub { print { shift } "\t\"],\" +\n" },
    'html'      => sub { print { shift } '</UL>' },
);

my %print_node = (
    'js'        => sub {
        my $fh          = shift;
        my $url         = shift;
        my $title       = shift;

        &entify( '\'"', $title );
        print $fh "\t\"'<A HREF = $url>$title</A>',\" +\n";
    },
    'html'      => sub {
        my $fh          = shift;
        my $url         = shift;
        my $title       = shift;

        my ( $path );
        ( $path = $url ) =~ s!^$opt_site!!;

        print $fh <<HTML_NODE;
<LI>
    <DL>
        <DT><A HREF = "$url"><B>$title</B></A>
        <DD><A HREF = "$url">$path</A>
    </DL>
HTML_NODE
    },
);

my %print_page_start = (
    'js'        => sub {
        my $fh      = shift;
        my $title   = shift;

        print $fh $HEADER->( $title );
        print $fh join( '', <DATA> );
        print $fh <<JS;
<SCRIPT LANGUAGE = "JavaScript">
    firstTree = new Tree ( 
        { 
            id:
                "sitemap", 
            items:
JS
        ;
    },
    'html'      => sub {
        my $fh      = shift;
        my $title   = shift;

        print $fh $HEADER->( $title );
    },
);

my %print_page_end = (
    'js'        => sub {
        my $fh = shift;

        print $fh <<JAVASCRIPT_FOOTER;
    ""
});
</SCRIPT>
<LAYER>$FOOTER</LAYER>
<SCRIPT LANGUAGE = "JavaScript">
    var layer = document.layers[ document.layers.length - 1 ]; 
    layer.moveTo( 0, firstTree.getY() + firstTree.getHeight() );
</SCRIPT>
</BODY>
</HTML>
JAVASCRIPT_FOOTER
    },
    'html'      => sub {
        my $fh = shift;

        print $fh "$FOOTER</BODY></HTML>";
    },
);

#------------------------------------------------------------------------------
#
# Turn on autoflushing
#
#------------------------------------------------------------------------------

$|++;

#------------------------------------------------------------------------------
#
# Create MD5 object & LWP::UserAgent object
#
#------------------------------------------------------------------------------

my $MD5_Obj = new MD5;
my $Robot = new LWP::UserAgent;

#------------------------------------------------------------------------------
#
# Set the proxy from the environment
#
#------------------------------------------------------------------------------

if ( defined( $opt_proxy ) )
{
    &$VERBOSE( "proxy = $opt_proxy ..." );
    $Robot->proxy( [ 'http' ], $opt_proxy );
}
elsif ( defined( $ENV{ http_proxy } ) )
{
    &$VERBOSE( "getting proxy from environment ..." );
    &$VERBOSE( "proxy = $ENV{ http_proxy } ..." );
    $Robot->env_proxy;
}

#------------------------------------------------------------------------------
#
# Initialize the %UrlSeen hash (used to detect urls already seen)
#
#------------------------------------------------------------------------------

%UrlSeen = ();

#------------------------------------------------------------------------------
#
# Initialize the MD5Hash (used to detect different URLs which have the same
# content)
#
#------------------------------------------------------------------------------

%MD5Hash = ();

#------------------------------------------------------------------------------
#
# Create the root node
#
#------------------------------------------------------------------------------

my $root;

$root->{ 'url' } = $opt_site;

#------------------------------------------------------------------------------
#
# create site tree from the root node, recursively
#
#------------------------------------------------------------------------------

&create_site_tree( $root );

#------------------------------------------------------------------------------
#
# create the output file handle - either file sceified by -output, or STDOUT
#
#------------------------------------------------------------------------------

my $fh = new IO::File defined( $opt_output ) ? ">$opt_output" : ">&STDOUT" ;
die "$opt_output : $!\n" unless defined $fh;

#------------------------------------------------------------------------------
#
# print the header
#
#------------------------------------------------------------------------------

$print_page_start{ $opt_format }->( 
    $fh, 
    defined( $opt_title ) ? $opt_title : "Site Map for $opt_site" 
);

#------------------------------------------------------------------------------
#
# print site tree (recursively)
#
#------------------------------------------------------------------------------

$print_start_all_lists{ $opt_format }->( $fh );
print_site_tree( $fh, $root );
$print_end_all_lists{ $opt_format }->( $fh );

#------------------------------------------------------------------------------
#
# print the footer
#
#------------------------------------------------------------------------------

$print_page_end{ $opt_format }->( $fh );

#==============================================================================
#
# End of main
#
#==============================================================================

#==============================================================================
#
# Subroutines
#
#==============================================================================

#------------------------------------------------------------------------------
#
# hexhash - get an MD5 hex hash value for the contents of this URL
#
#------------------------------------------------------------------------------

sub hexhash
{
    my $html = shift;

    $MD5_Obj->reset;
    return $MD5_Obj->hexhash( $html );
}

#------------------------------------------------------------------------------
#
# get_links - returns a list of the links for a given HTML string
#
#------------------------------------------------------------------------------

sub get_links
{
    my $url     = shift;
    my $html    = shift;
    my $base    = shift;

    my ( @links );

    &$VERBOSE( "extracting links from $url ..." );

    my @frame_links = &expand_frameset( $url, $html, $base );
    return @frame_links if @frame_links;

    my $link_extor = new HTML::LinkExtor( 

        # anonymous callback function for HTML::LinkExtor

        sub {

            my ( $tag, %attr ) = @_; 
            my ( $link );

            # grab anchor / area links

            if( lc( $tag ) =~ /^a(?:rea)?$/ )
            {
                return unless defined( $link = $attr{ 'href' } );
            }
            else
            {
                return;
            }

            # ignore off site links

            unless ( $link =~ m!^$opt_site! )
            {
                return;
            }

            # strip hashes (i.e. ignore / don't distinguish page internal links)

            $link =~ s!#.*!!;

            # only follow html links (.html or .htm or no extension)

            if ( $link =~ m{\.([^./]+)$} )
            {
                return unless $1 =~ /^html?$/;
            }

            # only follow links we haven't seen yet ...

            return if $UrlSeen{ $link };
            $UrlSeen{ $link }++;

            &$VERBOSE( "adding $link ..." );

            push( @links, $link );
        },
        $base
    );

    # do the business ...

    $link_extor->parse( $html );

    # ... and return the links created in the callback

    return( @links );
}

#------------------------------------------------------------------------------
#
# expand_frameset - if this URL is a frameset, extract the links from the
# frames in that frameset
#
#------------------------------------------------------------------------------

sub expand_frameset
{
    my $url     = shift;
    my $html    = shift;
    my $base    = shift;

    # try extracting any frames ...

    my ( @frames, @links );

    my $frame_extor = new HTML::LinkExtor( 
        sub {

            my ( $tag, %attr ) = @_;
            my ( $frame );

            push( @frames, $frame )
                if ( lc( $tag ) eq 'frame' )
                and defined( $frame = $attr{ 'src' } )
            ;
        },
        $base
    );

    # do the business ...

    $frame_extor->parse( $html );

    if ( @frames )
    {
        foreach my $frame ( @frames )
        {
            my ( $content, $base ) = &get_content_and_base( $frame );
            push( @links, &get_links( $frame, $content, $base ) )
                if defined $content
            ;
        }
        return @links;
    }

    # no frames!

    return ();
}

#------------------------------------------------------------------------------
#
# get_response - get HTTP::Response object for a given URL. Returns undef on
# failure
#
#------------------------------------------------------------------------------

sub get_response
{
    my $url = shift;

    &$VERBOSE( "getting $url ..." );

    my $response = $Robot->request( new HTTP::Request( 'GET', $url ) );

    if ( $response->is_success )
    {
        return $response;
    }
    else
    {
        &$VERBOSE( "failed to get $url ..." );
        return undef;
    }
}

#------------------------------------------------------------------------------
#
# get_title - get the title from an HTML string
#
#------------------------------------------------------------------------------

sub get_title
{
    my $html = shift;

    # get title from page - if no title is specified, get the contents of the
    # first H1, H2, H3, H4, H5, or H6 tag

    my ( $title ) = 
        $html =~ m!<TITLE[^>]*>(.*?)</TITLE>!i or
        $html =~ m!<H([1-6])[^>]*>(?:.*?)</H\1>!i
    ;

    return $title;

}

#------------------------------------------------------------------------------
#
# create_site_tree - create the site tree data structure (recursively)
#
#------------------------------------------------------------------------------

sub create_site_tree
{
    my $node = shift;
    my $depth = shift || 0;

    my $url = $node->{ 'url' };
    my ( $content, $base ) = &get_content_and_base( $url );
    return unless defined( $content );
    $node->{ 'title' } = &get_title( $content ) || $url;

    my ( @links ) = &get_links( $url, $content, $base )
        if ( ! defined( $opt_depth ) ) or $depth < $opt_depth
    ;

    for ( @links )
    {
        my $child_node;
        $child_node->{ 'url' } = $_;
        $Parent{ $_ } = $node;
        $node->{ 'children' }{ $_ } = $child_node;
        &create_site_tree( $child_node, $depth + 1 );
    }
}

#------------------------------------------------------------------------------
#
# get_content_and_base - get the HTML content, and the base URL for a given
# URL. Return undef if GET fails, or if have seen that page before
#
#------------------------------------------------------------------------------

sub get_content_and_base
{
    my $url = shift;

    my $response = &get_response( $url );
    unless ( defined $response )
    {
        &$VERBOSE( "failed to get $url ..." );
        &delete_node( $url );
        return ( undef, undef ) 
    }
    my $base = $response->base;
    my $content = $response->content;
    my $hash = &hexhash( $content );
    if ( $MD5Hash{ $hash } )
    {
        &$VERBOSE( "$url is identical to $MD5Hash{ $hash } ..." );
        &delete_node( $url );
        return ( undef, undef );
    }
    $MD5Hash{ $hash } = $url;
    return ( $content, $base );
}

#------------------------------------------------------------------------------
#
# delete_node - delete a node from the sitemap tree
#
#------------------------------------------------------------------------------

sub delete_node
{
    my $url = shift;

    my $parent = $Parent{ $url };

    return unless defined( $parent );

    &$VERBOSE( "deleting $url for its parent, $parent->{ url } ..." );
    delete( $parent->{ 'children' }{ $url } );
}

#------------------------------------------------------------------------------
#
# print_site_tree - print the code for each of the "children" of the root
# URL (recursively), based on the site tree structure
#
#------------------------------------------------------------------------------

sub print_site_tree
{
    my $fh      = shift;
    my $node    = shift;

    my $title           = $node->{ 'title' };
    my $url             = $node->{ 'url' };
    my $children        = $node->{ 'children' };

    $title =~ s!\r!!g; # some wierd Windows pages have \r characters

    $print_node{ $opt_format }->( $fh, $url, $title );

    if ( ref( $children ) eq 'HASH' and keys %{ $children } )
    {
        $print_start_list{ $opt_format }->( $fh );
        for ( keys %{ $children } )
        {
            &print_site_tree( $fh, $children->{ $_ } );
        }
        $print_end_list{ $opt_format }->( $fh );
    }
}

#------------------------------------------------------------------------------
#
# entify - convert all non-alphnumerics to HTML entities
#
#------------------------------------------------------------------------------

sub entify
{
    my $chars = shift;
    $_[ 0 ] =~ s/([$chars])/sprintf "&#%lx;", ord( $1 )/ge;
}

#==============================================================================
#
# End of subroutines
#
#==============================================================================


#==============================================================================
#
# JavaScript Code - Jef Pearlman's (jef@mit.edu) Tree class
# http://developer.netscape.com/docs/examples/dynhtml/tree.html
#
#==============================================================================

__END__

<SCRIPT LANGUAGE = "JavaScript">

// Tree.js
//
// Javascript expandable/collapsable tree class.
// Written by Jef Pearlman (jef@mit.edu)
// 
///////////////////////////////////////////////////////////////////////////////

// class Tree 
// {
//   public: 
//       // These functions can be used to interface with a tree. 
//     void TreeView(params);
//       // Constructs a TreeView. Params must be an object containing the
//       // following properties:
//       // id: UNIQUE id for the tree
//       // items: Nested array of strings and arrays determining the tree 
//       //        structure and content.
//       // x: Optional x position for tree.
//       // y: Optional y position for tree.
//     int getHeight();
//       // Returns the height of the tree, fully expanded.
//     int getWidth();
//       // Returns the width of the widest section of the tree, 
//       // fully expanded.
//     int getVisibleHeight();
//       // Returns the height of the visible tree.
//     int getVisibleWidth();
//       // Returns the width of the widest visible section of the tree. 
//     int getX();
//       // Returns the x position of the tree. 
//     int getY();
//       // Returns the y position of the tree.
//     Object getLayer();
//       // Returns the layer object enclosing the entire tree.
// }

function TreeNode(content, enclosing, id, depth, y)
     // Constructor for a TreeNode object, creates the appropriate layers
     // and sets the required properties.
{
  this.id = id;
  this.enclosing = enclosing;
  this.children = new Array;
  this.maxChild = 0;
  this.expanded = false;
  this.getWidth = TreeNode_getWidth;
  this.getVisibleWidth = TreeNode_getVisibleWidth;
  this.getHeight = TreeNode_getHeight;
  this.getVisibleHeight = TreeNode_getVisibleHeight;
  this.layout = TreeNode_layout;
  this.relayout = TreeNode_relayout;
  this.childLayer = null;
  this.parent = this.enclosing.node;
  this.tree = this.parent.tree;
  this.depth = depth;

  // Write out the content for this item.
  document.write("<LAYER TOP="+y+" LEFT="+(this.depth*10)+" ID=Item"+this.id+">");
  document.write("<LAYER ID=Buttons WIDTH=9 HEIGHT=9>");
  document.write("<LAYER ID=Minus VISIBILITY=HIDE WIDTH=9 HEIGHT=9><IMG SRC=Tree_minus.gif WIDTH=9 HEIGHT=9></LAYER>");
  document.write("<LAYER ID=Plus WIDTH=9 VISIBILITY=HIDE HEIGHT=9><IMG SRC=Tree_plus.gif WIDTH=9 HEIGHT=9></LAYER>");
  document.write("<LAYER ID=Disabled VISIBILITY=INHERIT WIDTH=9 HEIGHT=9><IMG SRC=Tree_disabled.gif WIDTH=9 HEIGHT=9></LAYER>");
  document.write("</LAYER>"); // Buttons
  this.layer = this.enclosing.layers['Item'+this.id];
  this.layers = this.layer.layers;
  document.write("<LAYER ID=Content LEFT="+(this.layers['Buttons'].x+10)+">"+content+"</LAYER>");
  document.write("</LAYER>"); // Item

  // Move the buttons to the right position (centered vertically) and
  // capture the appropriate events.
  this.layers['Buttons'].moveTo(this.layers['Buttons'].x, this.layers['Content'].y+((this.layers['Content'].document.height-9)/2));
  this.layers['Buttons'].layers['Plus'].captureEvents(Event.MOUSEDOWN);
  this.layers['Buttons'].layers['Plus'].onmousedown=TreeNode_onmousedown_Plus;
  this.layers['Buttons'].layers['Plus'].node=this;
  this.layers['Buttons'].layers['Minus'].captureEvents(Event.MOUSEDOWN);
  this.layers['Buttons'].layers['Minus'].onmousedown=TreeNode_onmousedown_Minus;
  this.layers['Buttons'].layers['Minus'].node=this;

  // Note the height and width;
  this.height=this.layers['Content'].document.height;
  this.width=this.layers['Content'].document.width + 10 + (depth*10);
}

function Tree_build(node, items, depth, nexty)
     // Recursive function builds a tree, starting at the current node
     // using the items in items, starting at depth depth, where nexty
     // is where to locate the new layer to be placed correctly.
{
  var i;
  var nextyChild=0;

  if (node.tree.version >= 4)
    {
      // Create the layer for all the children.
      document.write("<LAYER TOP="+nexty+" VISIBILITY=HIDE ID=Children>");
      node.childLayer = node.enclosing.layers['Children'];
      node.childLayer.node = node;
    }
  else
    {
      // For Navigator 3.0, create a nested unordered list.
      document.write("<UL>");
    }

  for (i=0; i<items.length; i++)
    {
      if(typeof(items[i]) == "string")
	{
	  if (node.tree.version >= 4)
	    {
	      // Create a new node as the next child.
	      node.children[node.maxChild] = new TreeNode(items[i], node.childLayer, node.maxChild, depth, nextyChild);
	      nextyChild+=node.children[node.maxChild].height;
	    }
	  else
	    {
	      // Create a new item.
	      document.write("<LI>"+items[i]);
	    }
	  node.maxChild++;
	}
      else
	if (node.maxChild > 0)
	  {
	    // Build a new tree using the nested items array, placing it
	    // under the last child created.
	    if (node.tree.version >= 4)
	      {
		Tree_build(node.children[node.maxChild-1], items[i], depth+1, nextyChild);    
		nextyChild+=node.children[node.maxChild-1].getHeight()-node.children[node.maxChild-1].height;
		node.children[node.maxChild-1].layer.layers['Buttons'].layers['Disabled'].visibility="hide";
		node.children[node.maxChild-1].layer.layers['Buttons'].layers['Plus'].visibility="inherit";
	      }
	    else
	      Tree_build(node, items[i], depth+1, nextyChild);    
	  }
    }
  
  // End the layer or nested unordered list.
  if (node.tree.version >= 4)
    document.write("</LAYER>"); // childLayer
  else
    {
      document.write("</UL>");
    }

}

function TreeNode_onmousedown_Plus(e)
     // Handle a mouse down on a plus (expand).
{
  var node=this.node;
  var oldHeight=node.getVisibleHeight();
  // Switch the buttons, set the current node expanded, and
  // relayout everything below it before before displaying the node.
  node.layers['Buttons'].layers['Minus'].visibility="inherit";
  node.layers['Buttons'].layers['Plus'].visibility="hide";
  node.expanded=true;
  node.parent.relayout(node.id,node.getVisibleHeight()-oldHeight);
  node.childLayer.visibility='inherit';
  return false;
}

function TreeNode_onmousedown_Minus(e)
     // Handle a mouse down on a minus (collapse).
{
  var node=this.node;
  var oldHeight=node.getVisibleHeight();
  // Switch the buttons, set the current node collapsed, and
  // hide the node before relaying out everything below it.
  node.layers['Buttons'].layers['Plus'].visibility="inherit";
  node.layers['Buttons'].layers['Minus'].visibility="hide";
  node.expanded=false;
  node.childLayer.visibility='hide';
  node.parent.relayout(node.id,node.getVisibleHeight()-oldHeight);  
  return false;
}

function TreeNode_getHeight()
     // Get the Height of the current node and it's children.
{
  // Recursively add heights.
  var h=0, i;
  for (i = 0; i < this.maxChild; i++)
    h += this.children[i].getHeight();
  h += this.height;
  return h;
}

function TreeNode_getVisibleHeight()
     // Get the Height of the current node and it's visible children.
{
  // Recursively add heights. Only recurse if expanded.
  var h=0, i;
  if (this.expanded)
    for (i = 0; i < this.maxChild; i++)
      h += this.children[i].getVisibleHeight();
  h += this.height;
  return h;
}

function TreeNode_getWidth()
     // Get the max Width of the current node and it's children.
{
  // Find the max width by recursively comparing.
  var w=0, i;
  for (i=0; i<this.maxChild; i++)
    if (this.children[i].getWidth() > w)
      w = this.children[i].getWidth();
  if (this.width > w)
    return this.width;
  return w;
}

function TreeNode_getVisibleWidth()
     // Get the max Width of the current node and it's visible children.
{
  // Find the max width by recursively comparing. Only recurse if expanded.
  var w=0, i;
  if (this.expanded)
    for (i=0; i<this.maxChild; i++)
      if (this.children[i].getVisibleWidth() > w)
	w = this.children[i].getVisibleWidth();
  if (this.width > w)
    return this.width;
  return w;
}

function TreeView_getX()
     // Get the x location of the main tree layer.
{
  // Return the x property of the main layer.
  return document.layers[this.id+"Tree"].x;
}

function TreeView_getY()
     // Get the y location of the main tree layer.
{
  // Return the y property of the main layer.
  return document.layers[this.id+"Tree"].y;
}

function getLayer()
     // Get the main layer object.
{
  // Returnt he main layer.
  return document.layers[this.id+"Tree"];
}

function TreeNode_layout()
     // Layout the entire tree from scratch, recursively.
{
  var nexty=0, i;
  // Set the layer visible if expanded, hidden if not.
  if (this.expanded)
    this.childLayer.visibility="inherit";
  else
    if (this.childLayer != null)
      this.childLayer.visibility="hide";
  // If there is a child layer, move it to the appropriate position, and
  // move the children, laying them each out in turn.
  if (this.childLayer != null)
    {
      this.childLayer.moveTo(0, this.layer.y+this.height);
      for (i=0; i<this.maxChild; i++)
	{
	  this.children[i].layer.moveTo((this.depth+1)*10, nexty);
	  this.children[i].layout();
	  nexty+=this.children[i].height;
	}
    }
}

function TreeNode_relayout(id, movey)
{
  // Move all children physically below the current child number id of
  // the current node. Much faster than doing a layout() each time.

  // Move all children _after_ this child.
  for (id++;id<this.maxChild; id++)
    {
      this.children[id].layer.moveBy(0, movey);
      if (this.children[id].childLayer != null)
	this.children[id].childLayer.moveBy(0, movey);
    }
  // If there is a parent, move all of its children below this node,
  // recursively.
  if (this.parent != null)
    this.parent.relayout(this.id, movey);
}

function Tree(param)
     // Instantiates a tree and displays it, using the items, id, and optional
     // x and y in param.
{
  // Set up member variables and functions. Also duplicate important TreeNode
  // member variables so this can serve as a TreeNode (vaguely like 
  // subclassing)
  this.version=eval(navigator.appVersion.charAt(0));
  this.id = param.id;
  this.children = new Array;
  this.maxChild = 0;
  this.expanded = true;
  this.layout = TreeNode_layout;
  this.relayout = TreeNode_relayout;
  this.getX = TreeView_getX;
  this.getY = TreeView_getY;
  this.getWidth = TreeNode_getWidth;
  this.getVisibleWidth = TreeNode_getVisibleWidth;
  this.getHeight = TreeNode_getHeight;
  this.getVisibleHeight = TreeNode_getVisibleHeight;
  this.depth = -1;
  this.height = 0;
  this.width = 0;
  this.tree = this;
  var items = eval(param.items);

  var left = "";
  var top = "";
  if (param.x != null && param.x != "")
    left += " LEFT="+param.x;
  if (param.y != null && param.y != "")
    top += " TOP="+param.y;


  if (this.version >= 4)
    {
      // Create a surrounding layer to guage size and control the entire tree.
      // Also create a secondary internal layer so that the code can treat
      // the tree itself correctly as a node (must have an enclosing layer
      // and a children layer).
      document.write("<LAYER VISIBILITY=HIDE ID="+this.id+"Tree"+left+top+">");
      document.write("<LAYER ID=mainLayer>");
      this.enclosing = document.layers[this.id+"Tree"].layers['mainLayer'];
      this.layers = this.enclosing.layers;
      this.layer = this.enclosing;
      this.enclosing.node = this;
    } 

  Tree_build(this, items, 0, 0); // Build the tree.
  
  if (this.version >= 4)
    {
      // Finish output, record size;
      document.write("</LAYER></LAYER>");
      this.layout();
      document.layers[this.id+"Tree"].visibility="inherit";
    }
}
</SCRIPT>
