################################################################################
#
#  kPerl Sequence Phylogeny Laboratory
#  Library for I/O of Sequence Phylogeny Tree Formats
#
#  copyright (c)
#    Institute of Molecular Biotechnology Jena, Dept. Genome Analysis, 2001
#  author
#    Karol Szafranski, karol.szafranski@leibniz-fli.de
#
################################################################################
#
#  DESCRIPTION
#
# - individual description of functions at the beginning of the code blocks
#
################################################################################
#
#  FUNCTIONS, DATA
#
#   @EXPORT
#
# - basics
#   description of tree data structure
#
# - I/O of Newick format
#   description of Newick tree file format
#   &TreeFromNewick
#   &TreeNewickParseNode  (not exported)
#   &TreeToNewick
#
# - housekeeping and analysis of tree data structure
#   &TreeDownlink  (not exported)
#   &TreeDestroy
#   &TreeLeafCross
#   &TreeLeafDist
#
# - draw picture from tree data structure
#   &TreeRootedImg
#
################################################################################
#
#  DEBUG, CHANGES, ADDITIONS
#
# - look also for notes in the header of each function block
#
################################################################################

package SeqPhylog::TreeFormat;

# includes
#use strict; use warnings;  # OK 2003xxxx
use GD;  # this is not part of standard Perl distribution
  if ($GD::VERSION < 1.20 and ! $main::GlobStore{GdWarn}) {
    printf STDERR "WARNING: GD v%s doesn't support png\n", $GD::VERSION;
    $main::GlobStore{GdWarn} = 1;
  }
use MainLib::File qw(&ReadFile);
use MainLib::Data;
use MainLib::Misc qw(&MySub);
use MainLib::Graphics;
use Math::kCalc;
use Math::Geomet2dPoint;
use Math::Geomet2dTrform;

# symbol export
use Exporter qw(import);
our @EXPORT = qw (
  &TreeFromNewick &TreeToNewick
  &TreeDestroy &TreeLeafCross
  &TreeRootedImg
  );


################################################################################
# basics
################################################################################


# description of tree data structure
#
# DESCRIPTION
# - xxx
#
# %tree
# ( branch        reference to array of branch data structures
#                 calculated by &TreeDownlink, included in &TreeFromXY
# ( BrpointNode   reference to array of branchpoint node data structures
#                 calculated by &TreeDownlink, included in &TreeFromXY
# ( LeafNode      reference to array of leaf node data structures
#                 calculated by &TreeDownlink, included in &TreeFromXY
#   RootNode      reference to root branchpoint node data structure
#   SrcFmt        file format of the source tree file
#   SrcPath       file path of the source or destination sequence file.
#
# %BrpointNode
#   branch        reference to array of branches
#   is            'brpoint'
# ( PosDist       position of node in distance scale
#                 calculated by &TreeRootedImg
# ( PosDichot     position of node in dichtomous dimension
#                 scaled as spanned number of leafs (floating point)
#                 calculated by &TreeRootedImg
#   RefBranch     reference to referring branch data structure
#                 calculated by &TreeDownlink
#   RefNode       reference to referring node data structure
#                 calculated by &TreeDownlink
#   text          text label on branchpoint node
#
# %LeafNode
#   id            label of leaf
#   is            'leaf'
# ( PosDist       position of node in distance scale
#                 calculated by &TreeRootedImg
# ( PosDichot     position of node in dichtomous dimension
#                 scaled as spanned number of leafs (floating point)
#                 calculated by &TreeRootedImg
#   RefBranch     reference to referring branch data structure
#                 calculated by &TreeDownlink
#   RefNode       reference to referring node data structure
#                 calculated by &TreeDownlink
#
# %branch
# ( EndPosDichot  position of node/leaf at end of branch in dichtomous
#                 dimension, scaled as spanned number of leafs (floating
#                 point)
#                 calculated by &TreeRootedImg
#   length        branch length in distance scale
#   node          either:
#                 - reference to branchpoint node data structure
#                 - leaf ID
#   RefNode       reference to referring node data structure
#                 calculated by &TreeDownlink
#


################################################################################
# I/O of Newick format
################################################################################


# description of Newick tree file format
#
# DESCRIPTION
# - Newick format tree files are Paup and PHYLIP compatible
# - references:
#   - http://evolution.genetics.washington.edu/phylip/newicktree.html
#
# - the branch unit is denoted as:
#   (Node1:BranchLength,Node2:BranchLength)BranchnodeLabel
#
#   NodeX         may be either leaf ID (optionally written in quotes) or
#                 next higher order branch unit
#   BranchLength  floating point value
#
# - extensions:
#   multi-branch  (B1,B2,B3,...)
#   AddInfo       in square brackets following data field
#


# read Newick tree file into tree data structure
#
# INTERFACE
# - argument 1: - reference to plain text to be parsed
#               - file argument (may be system call)
#               - filehandle reference
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: - reference to sequence tree structure
#               - undef if an error occurred
#
sub TreeFromNewick {
  my ($InputArg, %opt) = @_;
  my $debug = $opt{-debug};

  # read file
  my $input;
  if (ref($InputArg) eq 'SCALAR') {
    $debug and printf STDERR "%s. got SCALAR reference\n", &MySub;
    $input = $$InputArg;
  } else {
    $debug and printf STDERR "%s. reading file %s\n", &MySub, $InputArg;
    $input = &ReadFile ($InputArg);
    $$pTree{SrcPath} = $InputArg;
  }
  unless ($input) {
    $debug and printf STDERR "%s. ERROR: no input from %s\n", &MySub, $InputArg;
    return undef;
  }

  # parse plain text
  my $pTree = {};
  ($$pTree{RootNode}, $input) = &TreeNewickParseNode ($input, %opt);
  unless (%{$$pTree{RootNode}}) {
    printf STDERR "%s. ERROR: empty tree, remaining input:\n%s\n", &MySub, $input;
    return undef;
  }
  ($input =~ s/[\s\n\r]*\;[\s\n\r]*$//) or
    printf STDERR "%s. WARNING: no final semicolon in remaining input:\n%s\n", &MySub,
      $input;

  # debug whole data structure (last chance here)
  $debug and printf STDERR "%s. primary tree data structure:\n", &MySub;
  $debug and &DataPrint ($pTree, -handle=>\*STDERR);

  # expand data
  $$pTree{SrcFmt} = 'Newick';
  unless (&TreeDownlink ($pTree)) {
    printf STDERR "%s. ERROR in \&TreeDownlink\n", &MySub;
    return undef;
  }
  if ($debug) {
    printf STDERR "%s. root node in final tree data structure:\n%s", &MySub,
      join ('', map { "  $_  $$pTree{RootNode}{$_}\n" } keys %{$$pTree{RootNode}});
    printf STDERR "%s. %d branchpoint nodes, %d leafs, %d branches\n", &MySub,
      int @{$$pTree{BrpointNode}}, int @{$$pTree{LeafNode}}, int @{$$pTree{branch}};
  }

  # return
  return $pTree;
}


# parse node from Newick tree format
#
# INTERFACE
# - argument 1: string to be parsed
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: - leaf ID
#               - reference to node data structure
#               - undef if an error occurred
#
# DESCRIPTION
# - note: there are two basic node types (cmp. 'description of tree data structure')
#   leaf         consisting of ID string only
#   branchpoint  consisting of array of branches
#
# DEBUG, CHANGES, ADDITIONS
# - neglect final comma at end of branching point. Do not expect next
#   leaf entry as you do now.
#
sub TreeNewickParseNode {
  my ($input, %opt) = @_;
  my $debug = $opt{-debug};

  # this node is a branchpoint => parse array of nodes
  if ($input =~ s/^[\s\n\r]*\(//) {
    $debug and printf STDERR "%s. node is a branchpoint\n", &MySub;
    my $pNode = { 'is'=>'brpoint' };
    while (! ($input =~ s/^[\s\n\r]*\)//)) {

      # enter new branch
      my $pBranch = {};
      push @{$$pNode{branch}}, $pBranch;

      # parse node component
      ($$pBranch{node}, $input) = &TreeNewickParseNode ($input, %opt);
      $$pBranch{node} or return undef;

      # parse length component
      my $bMatch = ($input =~ s/^[\s\n\r]*:[\s\n\r]*([\d.]*)[\s\n\r]*//);
      unless ($bMatch) {
        printf STDERR "%s. WARNING: no branch length in %s\n", &MySub, $input;
      }
      $$pBranch{length} = $1;
      $debug and printf STDERR "%s. branch length %s\n", &MySub, $$pBranch{length}||"''";

      # branch delimiter
      if ($input =~ s/^[\s\n\r]*,[\s\n\r]*//) {
        $debug and printf STDERR "%s. passed branch delimiter\n", &MySub;
        redo; 
      }
    }

    # branchpoint label?
    if ($input =~ s/^[\s\n\r]*(['"])?//) {
      my $quote = $1;
      my $text;
      if ($quote) {
        $input =~ s/^(.*?)$quote[\s\n\r]*//;
        $text = $1;
      } else {
        $input =~ s/^([^\n\r,;:)(\[\]]+)//;
        $text = $1;
      }
      if ($text) {
        $$pNode{text} = $text;
        $debug and printf STDERR "%s. found branchpoint label $text\n", &MySub;
      }
    }

    # skip rich data
    $input =~ s/^(\[[^\[\]]+\])*//;

    # check completeness of branchpoint
    if (@{$$pNode{branch}} < 2) {
      printf STDERR "%s. ERROR: only %d branches in branchpoint\n", &MySub;
      return undef;
    }

    # return branchpoint node
    return ($pNode, $input);
  }

  # this node is a leaf => parse Label
  elsif ($input =~ s/^[\s\n\r]*(['"])?//) {
    $debug and printf STDERR "%s. node is a leaf\n", &MySub;
    my $quote = $1;
    my $text;
    if ($quote) {
      $input =~ s/^(.*?)$quote[\s\n\r]*//;
      $text = $1;
    } else {
      $input =~ s/^([^\n\r,;:)(\[\]]+)//;
      $text = $1;
    }
    if ($text) {
      $debug and printf STDERR "  leaf Label %s\n", $text||"''";
    } else {
      printf STDERR "%s. ERROR: no leaf Label in %s\n", &MySub, $input;
      return undef;
    }

    # skip rich data
    $input =~ s/^(\[[^\[\]]+\])*//;

    # return leaf Label
    return ({ 'is'=>'leaf', 'id'=>$text }, $input);
  }

  # startup parsing ERROR
  else {
    printf STDERR "%s. startup parsing ERROR on %s\n", &MySub, $input;
    return undef;
  }
}


# read Newick tree file into tree data structure
#
# INTERFACE
# - argument 1: reference to sequence tree sub-structure
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: - reference to sequence tree sub-structure
#               - undef if an error occurred
#
sub TreeToNewick {
  my ($pTree, %opt) = @_;
  my $debug = $opt{-debug};

  # derive plain text of full tree sub-structure
  my $TreePlain;
  printf STDERR "%s. ERROR: no implementation\n", &MySub;
  exit 1;

  # return
  return $TreePlain;
}


################################################################################
# housekeeping and analysis of tree data structure
################################################################################


# install node-to-node double links
#
# INTERFACE
# - argument 1: reference to tree data structure
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: success status (boolean)
#
sub TreeDownlink {
  my ($pTree, %opt) = @_;
  my $debug = $opt{-debug};

  # basic index
  $$pTree{branch} = [];
  $$pTree{BrpointNode} = [];
  $$pTree{LeafNode} = [];

  # do recursion over tree nodes
  my $pActNode = sub {
    my ($pNode, $pNodeRef, $pBranchRef) = @_;
    my ($pBranch);
    push @{$$pTree{($$pNode{is} eq 'brpoint') ? 'BrpointNode':'LeafNode'}}, $pNode;
    $$pNode{RefNode} = $pNodeRef;
    $$pNode{RefBranch} = $pBranchRef;
    if (exists $$pNode{branch} and @{$$pNode{branch}}) {
      foreach $pBranch (@{$$pNode{branch}}) {
        push @{$$pTree{branch}}, $pBranch;
        &$pActNode ($$pBranch{node}, $pNode, $pBranch);
        $$pBranch{RefNode} = $pNode;
      }
    }
  };
  &$pActNode ($$pTree{RootNode}, undef, undef);

  # return
  return 1;
}


# destroy tree data structure
#
# INTERFACE
# - argument 1: reference to tree data structure
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: success status (boolean)
#
sub TreeDestroy {
  my ($pTree, %opt) = @_;
  my $debug = $opt{-debug};

  # delete basic index
  delete $$pTree{branch};
  delete $$pTree{BrpointNode};
  delete $$pTree{LeafNode};

  # do recursion over tree nodes
  my $pActNode = sub {
    my ($pNode) = @_;
    my ($pBranch);
    if (exists($$pNode{branch}) and @{$$pNode{branch}}) {
      foreach $pBranch (@{$$pNode{branch}}) {
        &$pActNode ($$pBranch{node});
        %$pBranch = ();
      }
    }
    %$pNode = ();
  };
  &$pActNode ($$pTree{RootNode});

  # return
  return 1;
}


# find node which is the crossing between two leafs
#
# INTERFACE
# - argument 1: reference to tree data structure
# - argument 2: reference to leaf data structure / leaf ID 1
# - argument 3: reference to leaf data structure / leaf ID 2
#
# - options:
#   -debug      print debug protocol to STDERR
#
# - return val: - wantscalar
#                 - reference to branchpoint node data structure
#               - wantarray
#                 - reference to branchpoint node data structure
#                 - distance between leaf nodes
#               - undef if an error occurred
#
sub TreeLeafCross {
  my ($pTree, @LeafArg, %opt) = @_;
  my $debug = $opt{-debug};

  # identify leafs
  my ($CtLeaf, @leaf);
  foreach $CtLeaf (0, 1) {
    unless (ref ($LeafArg[$CtLeaf])) {
      unless ($leaf[$CtLeaf] = (grep { $_->{id} eq $LeafArg[$CtLeaf] } @{$$pTree{LeafNode}})[0]) {
        $debug and printf STDERR "%s. ERROR: unable to find leaf %s\n", &MySub, $LeafArg[$CtLeaf];
        return undef;
      }
    }
  }
  $debug and printf STDERR "%s. looking for first shared node of leafs: %s, %s\n", &MySub,
    map { $_->{id} } @leaf;

  # need node-to-node double links
  unless (&TreeDownlink ($pTree)) {
    $debug and printf STDERR "%s. ERROR in \&TreeDownlink\n", &MySub;
    return undef;
  }

  # records paths from leaf to tree root
  my (@NodePath,$pNode,$pNodeCross);
  foreach $CtLeaf (0, 1) {
    $pNode = $leaf[$CtLeaf];
    push @{$NodePath[$CtLeaf]}, $pNode;
    while (($pNode = $$pNode{RefNode}) and $pNode ne $pTree) {
      push @{$NodePath[$CtLeaf]}, $pNode;
    }
    @{$NodePath[$CtLeaf]} = reverse @{$NodePath[$CtLeaf]};
  }
  $debug and printf STDERR "%s. path of nodes:\n  %s\n  %s\n", &MySub,
    join (', ', @{$NodePath[0]}), join (', ', @{$NodePath[1]});

  # find first shared node
  my $CtNode = 0;
  while ($NodePath[0][$CtNode] and $NodePath[0][$CtNode] eq $NodePath[1][$CtNode]) {
    $CtNode ++;
  }
  $pNodeCross = $NodePath[0][--$CtNode];
  $debug and printf STDERR "%s. first shared node of leafs: %s, %s\n", &MySub,
    $pNodeCross, Math::Geomet2dPoint->new($$pNodeCross{PosDist},$$pNodeCross{PosDichot});

  # measure distance between leafs
  if (wantarray) {
    my $dist = 0;
    foreach $CtLeaf (0, 1) {
      $pNode = $leaf[$CtLeaf];
      while ($pNode and $pNode ne $pNodeCross) {
        $dist += $$pNode{RefBranch}{length};
        $pNode = $$pNode{RefNode};
      }
    }
    $debug and printf STDERR "%s. distance: %s\n", &MySub, $dist;
  }

  # return
  return wantarray ? ($pNodeCross,$dist) : $pNodeCross;
}


################################################################################
# draw picture from tree data structure
################################################################################


# draw picture from tree data structure
#
# INTERFACE
# - argument 1: reference to tree data structure
#
# - options:
#   -debug      print debug protocol to STDERR
#   -ImgWidth   image width
#   -rotate     rotate tree object by specified radian angle (this works
#               only in steps 1/2 pi)
#
# - return val: - plain image string
#               - undef if an error occurred
#
sub TreeRootedImg {
  my ($pTree, %opt) = @_;
  my $debug = $opt{-debug};
  my $dbg2  = $debug ? $debug-1 : undef;
  if (&ModuloExt ($opt{-rotate}, 0.5 * $const{PI})) {
    $debug and printf STDERR "%s. ERROR: rotation angle must be N-multiple of 1/2 pi\n", &MySub;
    return undef;
  }
  $debug and printf STDERR "%s. entered SUB\n", &MySub;

  # drawing constants
  my %img;
  $img{xDim}{Font1} = 7;
  $img{yDim}{Font1} = 10;
  $img{yDim}{Font2} = 6;
  $img{yDim}{Grid} = 5;
  $img{dim}{space} = 10;
  $img{dim}{DichotSpace} = 1.5 * $img{yDim}{Font1};

  #############################################################################
  # pre-work:
  # - derive tree dimensions from tree data

  my ($pActNode, @PosDistPoint, $PosDichot, $pNode, $pBranch);
  my ($pPt, $pTl);

  # find defaults for branch lengths in tree
  unless (@{ &DataTreeSlc ($$pTree{RootNode}{branch}, [[undef,'all'], ['length']]) }) {
    printf STDERR "%s. ERROR: default branch length calculation not implemented\n", &MySub;
    return undef;
  }

  # distance dimensioning, do recursion over tree nodes:
  # - enter node positions in distance scale
  # - find maximum sum of branch lengths
  $pActNode = sub {
    my ($pNode, $PosDist) = @_;
    my ($pBranch);
    $$pNode{PosDist} = $PosDist;
    if (exists $$pNode{branch} and @{$$pNode{branch}}) {
      foreach $pBranch (@{$$pNode{branch}}) {
        &$pActNode ($$pBranch{node}, $PosDist + $$pBranch{length});
      }
    }
  };
  &$pActNode ($$pTree{RootNode}, 0);
  $img{TreeDim}{dist} = &Max (map { $_->{PosDist} } @{$$pTree{LeafNode}});
  if ($debug||1) {
    printf STDERR "%s. leaf distance positions:\n%s", &MySub,
      join ('', map { "  $_\n" } @PosDistPoint);
    printf STDERR "%s. max root-to-leaf distance: %s\n", &MySub, $img{TreeDim}{dist};
    printf STDERR "%s. node distance positions:\n%s", &MySub,
      join ('', map { "  $_->{PosDist}\n" } @{$$pTree{node}});
  }

  # find dichotomous positions for nodes, do recursion over tree nodes:
  # - enter node dichotomous position
  $pActNode = sub {
    my ($pNode) = @_;
    my ($pBranch);
    foreach $pBranch (@{$$pNode{branch}}) {
      if ($$pBranch{node}{is} eq 'brpoint') {
        &$pActNode ($$pBranch{node});
        $$pBranch{EndPosDichot} = $$pBranch{node}{PosDichot};
      } else {
        $$pBranch{EndPosDichot} = $PosDichot ++;
      }
      $$pNode{PosDichot} = &Mean ($$pNode{branch}[0]{EndPosDichot},
        $$pNode{branch}[$#{$$pNode{branch}}]{EndPosDichot});
    }
  };
  $PosDichot = 0;
  &$pActNode ($$pTree{RootNode});
  $img{TreeDim}{dichot} = -- $PosDichot;
  if ($debug) {
    printf STDERR "%s. dichotomous position range 0..%d:\n", &MySub,
      $img{TreeDim}{dichot};
    printf STDERR "%s. node dichotomous positions:\n%s", &MySub,
      join ('', map { "  $_->{PosDichot}\n" } @{$$pTree{BrpointNode}});
    printf STDERR "%s. branch end dichotomous positions:\n%s", &MySub,
      join ('', map { "  $_->{EndPosDichot}\n" } @{$$pTree{branch}});
  }

  #############################################################################
  # translations:
  # - define translations
  # - derive key image dimensions and positions
  # - create image
  $Math::Geomet2dPoint::LibGlob{switch}{-debug} = $dbg2;
  $pPt = Math::Geomet2dPoint->new();
  $Math::Geomet2dTrform::LibGlob{switch}{-debug} = $dbg2;
  $pTl = Math::Geomet2dTrform->new();
  $debug and printf STDERR "%s. debug point initialization: %s\n", &MySub, $pPt->string();

  # orientation of tree
  $img{trafo}{Rotate} = $pTl->new_rotate($opt{-rotate}||0);

  # image X/Y dimensions
  $img{dim}{dist} = $opt{-ImgWidth} || 640;
  $img{dim}{dichot} = ($img{TreeDim}{dichot}) * ($img{yDim}{Font1} + $img{dim}{DichotSpace})
    + $img{yDim}{Font1} + 2 * $img{dim}{space};
  $img{dim}{sum} = $pPt->new($img{dim}{dist},$img{dim}{dichot})->transform
    ($img{trafo}{Rotate});
  $debug and printf STDERR "%s. image dimensions: %s\n", &MySub, $img{dim}{sum}->string();
  $img{pos}{'0'} = $pPt->new($img{dim}{space},$img{dim}{space});

  # tree-to-image transformation
  $img{trafo}{TreeStretch} = $pTl->new( [
    [($img{dim}{dist}-0.0001-2*$img{dim}{space}) / $img{TreeDim}{dist}, 0],
    [0, ($img{dim}{dichot}-0.0001-2*$img{dim}{space}) / $img{TreeDim}{dichot}],
    [0, 0], ] );
  $debug and printf STDERR "%s. tree-to-pixel transformation 1:\n  %s\n", &MySub,
    $img{trafo}{TreeStretch}->string();
  $img{pos}{middle} = $pPt->new(0.5*$img{dim}{dist}, 0.5*$img{dim}{dichot});
  $img{trafo}{TreeToPixel} = $img{trafo}{TreeStretch}
    x $pTl->new_move (-$img{pos}{middle})
    x $img{trafo}{Rotate}
    x $pTl->new_move ($img{pos}{middle})
    x $pTl->new_move ($img{pos}{'0'});
  $debug and printf STDERR "%s. tree-to-pixel transformation 2:\n  %s\n", &MySub,
    $img{trafo}{TreeToPixel}->string();
#  # NOTE: y dimension starts on top edge of image and points downward
#  # we do not transform this to standard, because we want the first branches on
#  #   top of the image
#  $img{trafo}{yMinus} = $pTl->new([[1,0],[0,-1],[0,$img{dim}{sum}[1]]]);

  # start creating the image
  $img{obj} = GD::Image->new(@{$img{dim}{sum}});
  $img{obj}->interlaced('true');

  #############################################################################
  # draw image

  # allocate colors
  foreach (qw(white black)) {
    $img{pal}{$_} = $img{obj}->colorAllocate(@{$ColorLib{$_}});
  }

  # loop over tree nodes and draw lines in dichotomous dimension
  foreach $pNode (@{$$pTree{BrpointNode}}) {
    $img{pos}{Pen0} = $pPt->new($$pNode{PosDist}, $$pNode{branch}[0]{EndPosDichot});
    $img{pos}{Pen1} = $pPt->new($$pNode{PosDist}, $$pNode{branch}[-1]{EndPosDichot});
    $img{obj}->line (
      @{$img{pos}{Pen0}->clone()->transform ($img{trafo}{TreeToPixel})},
      @{$img{pos}{Pen1}->clone()->transform ($img{trafo}{TreeToPixel})}, 
      $img{pal}{black});
  }

  # loop over branches and draw lines in distance dimension
  foreach $pBranch (@{$$pTree{branch}}) {
    $img{pos}{Pen0} = $pPt->new($$pBranch{RefNode}{PosDist}, $$pBranch{EndPosDichot});
    $img{pos}{Pen1} = $pPt->new($$pBranch{node}{PosDist}, $$pBranch{EndPosDichot});
    $img{obj}->line (
      @{$img{pos}{Pen0}->clone()->transform ($img{trafo}{TreeToPixel})},
      @{$img{pos}{Pen1}->clone()->transform ($img{trafo}{TreeToPixel})}, 
      $img{pal}{black});
  }

  # create image
  return $img{img} = $img{obj}->png();
}


1;
# $Id: TreeFormat.pm,v 1.16 2018/06/05 18:02:56 szafrans Exp $
