#!/usr/bin/perl

# Copyright (C) 2004 Erik Zachte , email xxx\@chello.nl (nospam: xxx=epzachte)
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 2
# as published by the Free Software Foundation.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details, at
# http://www.fsf.org/licenses/gpl.html

# history:
# 1.5 May 27 2004 :
# - when a chart contains only one bar this bar was always centered in the image
#     now AlignBars works well in this case aslo ("justify" treated as "center")
# - interwiki links reinstalled e.g. [[de:Gorbachev]]
# - error msgs corrected
# - minimum image size fixed
# - line numbering adapted <timeline>spaces<br> does not count as line one in Wikipedia
# - line breaks in wiki links parsed correctly [[Vladimir~Ilyich~Lenin]]
# - partial url shown as hint for external link (in GIF/PNG)
# - BarData: no attribute 'text:..' supplied -> default to space = show no text on axis
# - PlotData: new attribute 'anchor:..'
# - revert html encoding of '<' & '>' by MediaWiki

# 1.6 May 28 2004 :
# - SVG decode special chars in SVG input fixed
# - BarData: new attributes 'barset:..' and 'barcount:..' # autoincrement bar id
# - PlotData: new attribute 'barset:..'
# - LineData: new attribute 'layer:..', draw lines to back or front of bars and texts

# 1.7
# - EscapeShellArg (Tim Starling)

# 1.8 June .. 2004 :
# - optional autosizing of image (implied when auto incrementing bar count (also new))
# - presentation left-right order of bars reversed on TimeAxis = orientation:vertical
# - TimeAxis option 'order:[normal|reverse]' added
# - BarData: option barcount replaced by auto incrementing bar count and 'break' and 'skip' attributes
# - DrawLines -> LineData (command renamed, but also restructured like PlotData, TextData)
# - new drawing options for LineData, now also lines parallel to time axis, or between arbitrary points
# - Preset command added (specify default settings with 'Preset =', two sets to start with)
# - 'text' attribute parsing bugs (# or : in text gave problems, spaces got lost)
# - PlotArea  new attributes 'top' and 'right' make it possible to define plot area margins only
#   so resizing image does not imply adjusting PlotArea 'width' and 'height'
# - PlotData option 'shift': only changing x or y value is now possible, e.g. shift=(,10)
# - command ScaleMajor: subs for time axis can now be specified verbatim in option 'text'
# - extra validation checks, defaults, etc
# - function PlotScale now provides workaround for Ploticus bug: auto incrementing dates failed

# 1.9 June 2004
# - stub display order fixed on non time axis

# 1.10 July 2004
# - tempory debug code (removed)

# 1.11 August 2004
# - dot in folder name in input path was misunderstood as start of file extension
# - utf-8 chars within 160-255 range are translated to extended ascii
#   however internal font used by Ploticus has strange mapping so some are replaced
#   by undercore or unaccented version of character
#   this is a make do solution until full unicode support with external fonts will be added

  $version = "1.9" ;

  use Time::Local ;
  use Getopt::Std ;
  use Cwd ;

  $| = 1; # flush screen output

  print "EasyTimeline version $version\n" .
        "Copyright (C) 2004 Erik Zachte\n" .
        "Email xxx\@chello.nl (nospam: xxx=epzachte)\n\n" .
        "This program is free software; you can redistribute it\n" .
        "and/or modify it under the terms of the \n" .
        "GNU General Public License version 2 as published by\n" .
        "the Free Software Foundation\n" .
        "------------------------------------------------------\n" ;

  &SetImageFormat ;
  &ParseArguments ;
  &InitFiles ;

  open "FILE_IN", "<", $file_in ;
  @lines = <FILE_IN> ;
  close "FILE_IN" ;

  &InitVars ;
  &ParseScript ;

  if ($CntErrors == 0)
  { &WritePlotFile ; }

  if ($CntErrors == 1)
  { &Abort ("1 error found") ; }
  elsif ($CntErrors > 1)
  { &Abort ("$CntErrors errors found") ; }
  else
  {
    if (defined @Info)
    {
      print "\nINFO\n" ;
      print @Info ;
      print "\n" ;
    }
    if (defined @Warnings)
    {
      print "\nWARNING(S)\n" ;
      print @Warnings ;
      print "\n" ;
    }

    if (! (-e $file_bitmap))
    {
      print "\nImage $file_bitmap not created.\n" ;
      if ((! (-e "pl.exe")) && (! (-e "pl")))
      { print "\nPloticus not found in local folder. Is it on your system path?\n" ; }
    }
    elsif (! (-e $file_vector))
    {
      print "\nImage $file_vector not created.\n" ;
    }
    else
    { print "\nREADY\nNo errors found.\n" ; }
  }

  exit ;

sub ParseArguments
{
  my $options ;
  getopt ("iTAPe", \%options) ;

  &Abort ("Specify input file as: -i filename") if (! defined (@options {"i"})) ;

  $file_in   = @options {"i"} ;
  $listinput = @options {"l"} ; # list all input lines (not recommended)
  $linkmap   = @options {"m"} ; # make clickmap for inclusion in html
  $makehtml  = @options {"h"} ; # make test html file with gif/png + svg output
  $bypass    = @options {"b"} ; # do not use in Wikipedia:bypass some checks
  $showmap   = @options {"d"} ; # debug: shows clickable areas in gif/png
                                  # The following parameters are used by MediaWiki
                                # to pass config settings from LocalSettings.php to
                                # the perl script
  $tmpdir    = @options {"T"} ; # For MediaWiki: temp directory to use
  $plcommand = @options {"P"} ; # For MediaWiki: full path of ploticus command
  $articlepath=@options {"A"} ; # For MediaWiki: Path of an article, relative to this servers root

  if (! defined @options {"A"} )
  { $articlepath="http://en.wikipedia.org/wiki/\$1"; }

  if (! -e $file_in)
  { &Abort ("Input file '" . $file_in . "' not found.") ; }
}

sub InitVars
{
  $true  = 1 ;
  $false = 0 ;
  $CntErrors = 0 ;
  $LinkColor = "brightblue" ;
  $MapPNG = $false ; # switched when link or hint found
  $MapSVG = $false ; # switched when link found
  $WarnTextOutsideArea = 0 ;
  $WarnOnRightAlignedText = 0 ;

  $hPerc    = &EncodeInput ("\%") ;
  $hAmp     = &EncodeInput ("\&") ;
  $hAt      = &EncodeInput ("\@") ;
  $hDollar  = &EncodeInput ("\$") ;
  $hBrO     = &EncodeInput ("\(") ;
  $hBrC     = &EncodeInput ("\)") ;
  $hSemi    = &EncodeInput ("\;") ;
  $hIs      = &EncodeInput ("\=") ;
  $hLt      = &EncodeInput ("\<") ;
  $hGt      = &EncodeInput ("\>") ;
}

sub InitFiles
{
  print "\nInput:  Script file $file_in\n" ;

  $file  = $file_in ;
# 1.10 dot ignore dots in folder names ->
  $file  =~ s/\.[^\\\/\.]*$// ; # remove extension
  $file_name    = $file ;
  $file_bitmap  = $file . "." . $fmt ;
  $file_vector  = $file . ".svg" ;
  $file_png     = $file . ".png" ;
  $file_htmlmap = $file . ".map" ;
  $file_html    = $file . ".html" ;
  $file_errors  = $file . ".err" ;
# $file_pl_info = $file . ".inf" ;
# $file_pl_err  = $file . ".err" ;
  print "Output: Image files $file_bitmap & $file_vector\n" ;

  if ($linkmap)
  { print "        Map file $file_htmlmap (add to html for clickable map)\n" ; }
  if ($makehtml)
  { print "        HTML test file $file_html\n" ; }

  # remove previous output
  if (-e $file_bitmap)   { unlink $file_bitmap ; }
  if (-e $file_vector)   { unlink $file_vector ; }
  if (-e $file_png)      { unlink $file_png ; }
  if (-e $file_htmlmap)  { unlink $file_htmlmap ; }
  if (-e $file_html)     { unlink $file_html ; }
  if (-e $file_errors)   { unlink $file_errors ; }
}

sub SetImageFormat
{
  $env = "" ;
# $dir = cwd() ; # is there a better way to detect OS?
# if ($dir =~ /\//) { $env = "Linux" ;   $fmt = "png" ; $pathseparator = "/";}
# if ($dir =~ /\\/) { $env = "Windows" ; $fmt = "gif" ; $pathseparator = "\\";}
# cwd always to returns '/'s ? ->
  $OS = $^O ;
  if ($OS =~ /darwin/i)
  { $env = "Linux";    $fmt = "png" ; $pathseparator = "/";}
  elsif ($OS =~ /win/i)
  { $env = "Windows" ; $fmt = "gif" ; $pathseparator = "\\";}
  else
  { $env = "Linux" ;   $fmt = "png" ; $pathseparator = "/";}

  if ($env ne "")
  { print "\nOS $env detected -> create image in $fmt format.\n" ; }
  else
  {
    print "\nOS not detected. Assuming Windows -> create image in $fmt format.\n" ;
    $env = "Windows" ;
  }
}
sub ParseScript
{
  my $command ; # local version, $Command = global
  $LineNo = 0 ;
  $InputParsed = $false ;
  $CommandNext = "" ;
  $DateFormat = "x.y" ;

  $firstcmd = $true ;
  &GetCommand ;

  &StoreColor ("white",         &EncodeInput ("gray(0.999)"),  "") ;
  &StoreColor ("barcoldefault", &EncodeInput ("rgb(0,0.6,0)"), "") ;

  while (! $InputParsed)
  {
    if ($Command =~ /^\s*$/)
    { &GetCommand ; next ; }

    if (! ($Command =~ /$hIs/))
    { &Error ("Invalid statement. No '=' found.") ;
      &GetCommand ; next ; }

    if ($Command =~ /$hIs.*$hIs/)
    { &Error ("Invalid statement. Multiple '=' found.") ;
      &GetCommand ; next ; }

    my ($name, $value) = split ($hIs, $Command) ;
    $name  =~ s/^\s*(.*?)\s*$/$1/ ;

    if ($name =~ /PlotDividers/i)
    { &Error ("Command 'PlotDividers' has been renamed to 'LineData', please adjust.") ;
      &GetCommand ; next ; }
    if ($name =~ /DrawLines/i)
    { &Error ("Command 'DrawLines' has been renamed to 'LineData', please adjust.\n" .
              "  Reason for change is consistency: LineData now follows the same syntax rules as PlotData and TextData.") ;
      &GetCommand ; next ; }

    if ((! ($name =~ /^(?:Define)\s/)) &&
        (! ($name =~ /^(?:AlignBars|BarData|
                          BackgroundColors|Colors|DateFormat|LineData|
                          ScaleMajor|ScaleMinor|
                          LegendLeft|LegendTop|
                          ImageSize|PlotArea|Legend|
                          Period|PlotData|Preset|
                          TextData|TimeAxis)$/xi)))
    { &ParseUnknownCommand ;
      &GetCommand ; next ; }

    $value =~ s/^\s*(.*?)\s*// ;
    if (! ($name =~ /^(?:BarData|Colors|LineData|PlotData|TextData)$/i))
    {
      if ((! (defined ($value))) || ($value eq ""))
      {
        if ($name =~ /Preset/i)
        {
          &Error ("$name definition incomplete. No value specified\n" .
                  "  At the moment only one preset exists: 'TimeVertical_OneBar_UnitYear'.\n" .
                  "  See also meta.wikipedia.org/wiki/EasyTimeline/Presets") ;
        }
        else
        { &Error ("$name definition incomplete. No attributes specified") ; }
        &GetCommand ; next ; }
    }

    if ($name =~ /^(?:BackgroundColors|Colors|Period|ScaleMajor|ScaleMinor|TimeAxis)$/i)
    {
      my @attributes = split (" ", $value) ;
      foreach $attribute (@attributes)
      {
        my ($attrname, $attrvalue) = split ("\:", $attribute) ;
        if (! ($name."-".$attrname =~ /^(?:Colors-Value|Colors-Legend|
                                        Period-From|Period-Till|
                                        ScaleMajor-Color|ScaleMajor-Unit|ScaleMajor-Increment|ScaleMajor-Start|
                                        ScaleMinor-Color|ScaleMinor-Unit|ScaleMinor-Increment|ScaleMinor-Start|
                                        BackgroundColors-Canvas|BackgroundColors-Bars|
                                        TimeAxis-Orientation|TimeAxis-Format)$/xi))
        { &Error ("$name definition invalid. Unknown attribute '$attrname'.") ;
          &GetCommand ; next ; }

        if ((! defined ($attrvalue)) || ($attrvalue eq ""))
        { &Error ("$name definition incomplete. No value specified for attribute '$attrname'.") ;
          &GetCommand ; next ; }
      }
    }

       if ($Command =~ /^AlignBars/i)        { &ParseAlignBars ; }
    elsif ($Command =~ /^BackgroundColors/i) { &ParseBackgroundColors ; }
    elsif ($Command =~ /^BarData/i)          { &ParseBarData ; }
    elsif ($Command =~ /^Colors/i)           { &ParseColors ; }
    elsif ($Command =~ /^DateFormat/i)       { &ParseDateFormat ; }
    elsif ($Command =~ /^Define/i)           { &ParseDefine ; }
    elsif ($Command =~ /^ImageSize/i)        { &ParseImageSize ; }
    elsif ($Command =~ /^Legend/i)           { &ParseLegend ; }
    elsif ($Command =~ /^LineData/i)         { &ParseLineData ; }
    elsif ($Command =~ /^Period/i)           { &ParsePeriod ; }
    elsif ($Command =~ /^PlotArea/i)         { &ParsePlotArea ; }
    elsif ($Command =~ /^PlotData/i)         { &ParsePlotData ; }
    elsif ($Command =~ /^Preset/i)           { &ParsePreset ; }
    elsif ($Command =~ /^Scale/i)            { &ParseScale ; }
    elsif ($Command =~ /^TextData/i)         { &ParseTextData ; }
    elsif ($Command =~ /^TimeAxis/i)         { &ParseTimeAxis ; }

    &GetCommand ;
    $firstcmd = $false ;
  }

  if ($CntErrors == 0)
  { &DetectMissingCommands ; }

  if ($CntErrors == 0)
  { &ValidateAndNormalizeDimensions ; }
}


sub GetLine
{
  if ($#lines < 0)
  { $InputParsed = $true ; return ("") ; }

  # running in Wikipedia context and first line empty ?
  # skip first line without incrementing line count
  # this is part behind <timeline> and will not be thought of as line 1
  if (defined @options {"A"})
  {
    if (($#lines >= 0) && (@lines [0] =~ /^\s*$/))
    { $Line = shift (@lines) ; }
  }

  $Line = "" ;
  while (($#lines >= 0) && ($Line =~ /^\s*$/))
  {
    $LineNo ++ ;
    $Line = shift (@lines) ;
    chomp ($Line) ;

    if ($listinput)
    { print "$LineNo: " . &DecodeInput ($Line) . "\n" ; }

    # preserve '#' within double quotes
    $Line =~ s/(\"[^\"]*\")/$a=$1,$a=~s^\#^\%\?\+^g,$a/ge ;

    $Line =~ s/#>.*?<#//g ;
    if ($Line =~ /#>/)
    {
      $commentstart = $LineNo ;
      $Line =~ s/#>.*?$// ;
    }
    elsif ($Line =~ /<#/)
    {
      undef $commentstart ;
      $Line =~ s/^.*?<#//x ;
    }
    elsif (defined ($commentstart))
    { $Line = "" ; next ; }

    # remove single line comments (keep html char tags, like &#32;)
    $Line =~ s/\&\#/\&\$\%/g ;
    $Line =~ s/\#.*$// ;
    $Line =~ s/\&\$\%/\&\#/g ;
    $Line =~ s/\%\?\+/\#/g ;
    $Line =~ s/\s*$//g ;
    $Line =~ s/\t/ /g ;
  }

  if ($Line !~ /^\s*$/)
  {
    $Line = &EncodeInput ($Line) ;

    if (! ($Line =~ /^\s*Define/i))
    { $Line =~ s/($hDollar[a-zA-Z0-9]+)/&GetDefine($Line,$1)/ge ; }
  }

  if (($#lines < 0) && (defined ($commentstart)))
  { &Error2 ("No matching end of comment found for comment block starting at line $commentstart.\n" .
             "Text between \#> and <\# (multiple lines) or following \# (single line) will be treated as comment.") ; }
  return ($Line) ;
}

sub GetCommand
{
  undef (%Attributes) ;
  $Command = "" ;

  if ($CommandNext ne "")
  {
    $Command = $CommandNext ;
    $CommandNext = "" ;
  }
  else
  { $Command = &GetLine ; }

  if ($Command =~ /^\s/)
  {
    &Error ("New command expected instead of data line (= line starting with spaces). Data line(s) ignored.\n") ;
    $Command = &GetLine ;
    while (($#lines >= 0) && ($Command =~ /^\s/))
    { $Command = &GetLine ; }
  }

  if ($Command =~ /^[^\s]/)
  {
    $line = $Command ;
    $line =~ s/^.*$hIs\s*// ;
    &CollectAttributes ($line) ;
  }
}

sub GetData
{
  undef (%Attributes) ;
  $Command = "" ;
  $NoData = $false ;
  my $line = &GetLine ;

  if ($line =~ /^[^\s]/)
  {
    $CommandNext = $line ;
    $NoData = $true ;
    return ("") ;
  }

  if ($line =~ /^\s*$/)
  {
    $NoData = $true ;
    return ("") ;
  }

  $line =~ s/^\s*//g ;
  &CollectAttributes ($line) ;
}

sub CollectAttributes
{
  my $line = shift ;

  $line =~ s/(\slink\:[^\s\:]*)\:/$1'colon'/i ; # replace colon (:), would conflict with syntax
  $line =~ s/(\stext\:[^\s\:]*)\:/$1'colon'/i ; # replace colon (:), would conflict with syntax
  $line =~ s/(https?)\:/$1'colon'/i ;             # replace colon (:), would conflict with syntax

  my $text ;
  ($line, $text) = &ExtractText ($line) ;
  $text =~ s/'colon'/:/ ;

  $line =~ s/( $hBrO .+? $hBrC )/&RemoveSpaces($1)/gxe ;
  $line =~ s/\s*\:\s*/:/g ;
  $line =~ s/([a-zA-Z0-9\_]+)\:/lc($1) . ":"/gxe ;
  @Fields = split (" ", $line) ;

  $name = "" ;
  foreach $field (@Fields)
  {
    if ($field =~ /\:/)
    {
      ($name, $value) = split (":", $field) ;
      $name  =~ s/^\s*(.*)\s*$/lc($1)/gxe ;
      $value =~ s/^\s*(.*)\s*$/$1/gxe ;
      if (($name ne "bar") && ($name ne "text") && ($name ne "link") && ($name ne "legend")) #  && ($name ne "hint")
      { $value = lc ($value) ; }

      if ($name eq "link") # restore colon
      { $value =~ s/'colon'/:/ ; }

      if ($value eq "")
      {
        if ($name =~ /Text/i)
        { $value = " " ; }
        else
        { &Error ("No value specified for attribute '$name'. Attribute ignored.") ; }
      }
      else
      { @Attributes {$name} = $value ; }
    }
    else
    {
      if (defined (@Attributes {"single"}))
      { &Error ("Invalid attribute '$field' ignored.\nSpecify attributes as 'name:value' pair(s).") ; }
      else
      {
        $field  =~ s/^\s*(.*)\s*$/$1/gxe ;
        @Attributes {"single"} = $field ;
      }
    }
  }
  if (($name ne "") && (@Attributes {"single"} ne ""))
  {
    &Error ("Invalid attribute '" . @Attributes {"single"} . "' ignored.\nSpecify attributes as 'name:value' pairs.") ;
    delete (@Attributes {"single"}) ;
  }

  if ((defined ($text)) && ($text ne ""))
  { @Attributes {"text"} = &ParseText ($text) ; }
}

sub GetDefine
{
  my $command = shift ;
  my $const = shift ;
  $const = lc ($const) ;
  my $value = @Consts {lc ($const)} ;
  if (! defined ($value))
  {
    &Error ("Unknown constant. 'Define $const = ... ' expected.") ;
    return ($const);
  }
  return ($value) ;
}

sub ParseAlignBars
{
  &CheckPreset ("AlignBars") ;

  $align = @Attributes {"single"} ;
  if (! ($align =~ /^(?:justify|early|late)$/i))
  { &Error ("AlignBars value '$align' invalid. Specify 'justify', 'early' or 'late'.") ; return ; }

  $AlignBars = lc ($align) ;
}

sub ParseBackgroundColors
{
  if (! &ValidAttributes ("BackgroundColors"))
  { &GetData ; next ;}

  &CheckPreset ("BackGroundColors") ;

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;

    if ($attribute =~ /Canvas/i)
    {
      if (! &ColorPredefined ($attrvalue))
      {
        if (! defined (@Colors {lc ($attrvalue)}))
        { &Error ("BackgroundColors definition invalid. Attribute '$attribute': unknown color '$attrvalue'.\n" .
                  "  Specify command 'Color' before this command.") ;  return ; }
      }
      if (defined (@Colors {lc ($attrvalue)}))
      { @Attributes {"canvas"} = @Colors { lc ($attrvalue) } ; }
      else
      { @Attributes {"canvas"} = lc ($attrvalue) ; }
    }
    elsif ($attribute =~ /Bars/i)
    {
      if (! defined (@Colors {lc ($attrvalue)}))
      { &Error ("BackgroundColors definition invalid. Attribute '$attribute' unknown color '$attrvalue'.\n" .
                "  Specify command 'Color' before this command.") ;  return ; }

      @Attributes {"bars"} = lc ($attrvalue) ;
    }
  }

  %BackgroundColors = %Attributes ;
}

sub ParseBarData
{
  &GetData ;
  if ($NoData)
  { &Error ("Data expected for command 'BarData', but line is not indented.\n") ; return ; }

  my ($bar, $text, $link, $hint, $barset) ; # , $barcount) ;

  BarData:
  while ((! $InputParsed) && (! $NoData))
  {
    if (! &ValidAttributes ("BarData"))
    { &GetData ; next ;}

    $bar = "" ; $link = "" ; $hint = "" ; $barset = "" ; # $barcount = "" ;

    my $data2 = $data ;
    ($data2, $text) = &ExtractText ($data2) ;
    @Attributes = split (" ", $data2) ;

    foreach $attribute (keys %Attributes)
    {
      my $attrvalue = @Attributes {$attribute} ;

      if ($attribute =~ /^Bar$/i)
      {
        $bar = $attrvalue ;
      }
      elsif ($attribute =~ /^BarSet$/i)
      {
        $barset = $attrvalue ;
      }
      # elsif ($attribute =~ /^BarCount$/i)
      # {
      #   $barcount = $attrvalue ;
      # if (($barcount !~ /^\d?\d?\d$/) || ($barcount < 2) || ($barcount > 200))
      # { &Error ("BarData attribute 'barcount' invalid. Specify a number between 2 and 200\n") ;
      #   &GetData ; next BarData ; }
      # }
      elsif ($attribute =~ /^Text$/i)
      {
        $text = $attrvalue ;
        $text =~ s/\\n/~/gs ;
        if ($text =~ /\~/)
        { &Warning ("BarData attribute 'text' contains ~ (tilde).\n" .
                    "Tilde will not be translated into newline character (only in PlotData)") ; }
        if ($text =~ /\^/)
        { &Warning ("BarData attribute 'text' contains ^ (caret).\n" .
                    "Caret will not be translated into tab character (only in PlotData)") ; }
      }
      elsif ($attribute =~ /^Link$/i)
      {
        $link = &ParseText ($attrvalue) ;

        if ($link =~ /\[.*\]/)
        { &Error ("BarData attribute 'link' contains implicit (wiki style) link.\n" .
                  "Use implicit link style with attribute 'text' only.\n") ;
          &GetData ; next BarData ; }

        $link = &EncodeURL (&NormalizeURL ($link)) ;

        $MapPNG = $true ;
      }
    }

    if (($bar eq "") && ($barset eq ""))
    { &Error ("BarData attribute missing. Specify either 'bar' of 'barset'.\n") ;
      &GetData ; next BarData ; }

    if (($bar ne "") && ($barset ne ""))
    { &Error ("BarData attributes 'bar' and 'barset' are mutually exclusive.\nSpecify one of these per data line\n") ;
      &GetData ; next BarData ; }

    # if (($barset ne "") && ($barcount eq ""))
    # { &Error ("BarData attribute 'barset' specified without attribute 'barcount'.\n") ;
    #   &GetData ; next BarData ; }

    # if (($barset eq "") && ($barcount ne ""))
    # { &Error ("BarData attribute 'barcount' specified without attribute 'barset'.\n") ;
    #   &GetData ; next BarData ; }

    if (($barset ne "") && ($link ne ""))
    { &Error ("BarData attribute 'link' not valid in combination with attribute 'barset'.\n") ;
      &GetData ; next BarData ; }

    if ($link ne "")
    {
     if ($text =~ /\[.*\]/)
      {
        &Warning ("BarData contains implicit link(s) in attribute 'text' and explicit attribute 'link'.\n" .
                  "Implicit link(s) ignored.") ;
        $text =~ s/\[+ (?:[^\|]* \|)? ([^\]]*) \]+/$1/gx ;
      }

      if ($hint eq "")
      { $hint = &ExternalLinkToHint ($link) ; }
    }

    if (($bar ne "") && ($bar !~ /[a-zA-Z0-9\_]+/))
    { &Error ("BarData attribute bar:'$bar' invalid.\nUse only characters 'a'-'z', 'A'-'Z', '0'-'9', '_'\n") ;
      &GetData ; next BarData ; }

    if ($bar ne "")
    {
      if (@Axis {"time"} eq "x")
      { push @Bars, $bar ; }
      else
      { unshift @Bars, $bar ; }

      if ($text ne "")
      { @BarLegend {lc ($bar)} = $text ; }
      else
      { @BarLegend {lc ($bar)} = " " ; }

      if ($link ne "")
      { @BarLink {lc ($bar)} = $link ; }
    }
    else
    {
#     for ($b = 1 ; $b <= $barcount ; $b++)
#     {
#       $bar = $barset . "#" . $b ;

        $bar = $barset . "#1" ;
        if (@Axis {"time"} eq "x")
        { push @Bars, $bar ; }
        else
        { unshift @Bars, $bar ; }

        if ($text ne "")
        { @BarLegend {lc ($bar)} = $text . " - " . $b ; }
        else
        { @BarLegend {lc ($bar)} = " " ; }
#     }
    }


    &GetData ;
  }
}

sub ParseColors
{

  &GetData ;
  if ($NoData)
  { &Error ("Data expected for command 'Colors', but line is not indented.\n") ; return ; }

  Colors:
  while ((! $InputParsed) && (! $NoData))
  {
    if (! &ValidAttributes ("Colors"))
    { &GetData ; next ;}

    &CheckPreset ("Colors") ;

    my $addtolegend = $false ;
    my $legendvalue = "" ;
    my $colorvalue  = "" ;

    foreach $attribute (keys %Attributes)
    {
      my $attrvalue = @Attributes {$attribute} ;

      if ($attribute =~ /Id/i)
      {
        $colorname = $attrvalue ;
      }
      elsif ($attribute =~ /Legend/i)
      {
        $addtolegend = $true ;
        $legendvalue = $attrvalue ;
        if ($legendvalue =~ /^[yY]$/)
        { push @LegendData, $colorname ; }
        elsif (! ($attrvalue =~ /^[nN]$/))
        {
          $legendvalue = &ParseText ($legendvalue) ;
          push @LegendData, $legendvalue ;
        }
      }
      elsif ($attribute =~ /Value/i)
      {
        $colorvalue = $attrvalue ;
        if ($colorvalue =~ /^white$/i)
        { $colorvalue = "gray" . $hBrO . "0.999" . $hBrC ; }
      }
    }

    if (&ColorPredefined ($colorvalue))
    {
      &StoreColor ($colorname, $colorvalue, $legendvalue) ;
      &GetData ; next Colors ;
    }

    if ($colorvalue =~ /^[a-z]+$/i)
    {
      if (! ($colorvalue =~ /^(?:gray|rgb|hsb)/i))
      { &Error ("Color value invalid: unknown constant '$colorvalue'.") ;
        &GetData ; next Colors ; }
    }

    if (! ($colorvalue =~ /^(?:gray|rgb|hsb) $hBrO .+? $hBrC/xi))
    { &Error ("Color value invalid. Specify constant or 'gray/rgb/hsb(numeric values)' ") ;
      &GetData ; next Colors ; }

    if ($colorvalue =~ /^gray/i)
    {
      if ($colorvalue =~ /gray $hBrO (?:0|1|0\.\d+) $hBrC/xi)
      { &StoreColor ($colorname, $colorvalue, $legendvalue) ; }
      else
      { &Error ("Color value invalid. Specify 'gray(x) where 0 <= x <= 1' ") ; }

      &GetData ; next Colors ;
    }

    if ($colorvalue =~ /^rgb/i)
    {
      my $colormode = substr ($colorvalue,0,3) ;
      if ($colorvalue =~ /rgb $hBrO
                                 (?:0|1|0\.\d+) \,
                                 (?:0|1|0\.\d+) \,
                                 (?:0|1|0\.\d+)
                              $hBrC/xi)
      { &StoreColor ($colorname, $colorvalue, $legendvalue) ; }
      else
      { &Error ("Color value invalid. Specify 'rgb(r,g,b) where 0 <= r,g,b <= 1' ") ; }

      &GetData ; next Colors ;
    }

    if ($colorvalue =~ /^hsb/i)
    {
      my $colormode = substr ($colorvalue,0,3) ;
      if ($colorvalue =~ /hsb $hBrO
                                 (?:0|1|0\.\d+) \,
                                 (?:0|1|0\.\d+) \,
                                 (?:0|1|0\.\d+)
                              $hBrC/xi)
      { &StoreColor ($colorname, $colorvalue, $legendvalue) ; }
      else
      { &Error ("Color value invalid. Specify 'hsb(h,s,b) where 0 <= h,s,b <= 1' ") ; }

      &GetData ; next Colors ;
    }

    &Error ("Color value invalid.") ;
    &GetData ;
  }
}

sub StoreColor
{
  my $colorname   = shift ;
  my $colorvalue  = shift ;
  my $legendvalue = shift ;
  if (defined (@Colors {lc ($colorname)}))
  { &Warning ("Color '$colorname' redefined.") ; }
  @Colors      {lc ($colorname)} = lc ($colorvalue) ;
  if ((defined ($legendvalue)) && ($legendvalue ne ""))
  { @ColorLabels {lc ($colorname)} = $legendvalue ; }
}

sub ParseDateFormat
{
  &CheckPreset ("DateFormat") ;

  my $datevalue = lc (@Attributes {"single"}) ;
  $datevalue =~ s/\s//g ;
  $datevalue = lc ($datevalue) ;
  if (($datevalue ne "dd/mm/yyyy") && ($datevalue ne "mm/dd/yyyy") && ($datevalue ne "yyyy") && ($datevalue ne "x.y"))
  { &Error ("Invalid DateFormat. Specify as 'dd/mm/yyyy', 'mm/dd/yyyy', 'yyyy' or 'x.y'\n" .
            "  (use first two only for years >= 1800)\n") ;  return ; }

  $DateFormat = $datevalue ;
}

sub ParseDefine
{
  my $command = $Command ;
  my $command2 = $command ;
  $command2 =~ s/^Define\s*//i ;

  my ($name, $value) = split ($hIs, $command2) ;
  $name  =~ s/^\s*(.*?)\s*$/$1/g ;
  $value =~ s/^\s*(.*?)\s*$/$1/g ;

  if (! ($name =~ /^$hDollar/))
  { &Error ("Define '$name' invalid. Name does not start with '\$'.") ;  return ; }
  if (! ($name =~ /^$hDollar[a-zA-Z0-9\_]+$/))
  { &Error ("Define '$name' invalid. Valid characters are 'a'-'z', 'A'-'Z', '0'-'9', '_'.") ;  return ; }

  $value =~ s/($hDollar[a-zA-Z0-9]+)/&GetDefine($command,$1)/ge ;
  @Consts {lc ($name)} = $value ;
}

sub ParseLineData
{
  &GetData ;
  if ($NoData)
  { &Error ("Data expected for command 'LineData', but line is not indented.\n") ; return ; }

  if ((! (defined ($DateFormat))) || (! (defined (@Period {"from"}))))
  {
    if (! (defined ($DateFormat)))
    { &Error ("LineData invalid. No (valid) command 'DateFormat' specified in previous lines.") ; }
    else
    { &Error ("LineData invalid. No (valid) command 'Period' specified in previous lines.") ; }

    while ((! $InputParsed) && (! $NoData))
    { &GetData ; }
    return ;
  }

  my ($at, $from, $till, $atpos, $frompos, $tillpos, $color, $layer, $width, $points, $explanation) ;

  $layer = "front" ;
  $width = 2.0 ;

  my $data2 = $data ;

  LineData:
  while ((! $InputParsed) && (! $NoData))
  {
    $at = "" ; $from = "" ; $till = "" ; $atpos = "" ; $frompos = "" ; $tillpos = "" ; $points = "" ;

    &CheckPreset ("LineData") ;

    if (! &ValidAttributes ("LineData"))
    { &GetData ; next ;}

    if (defined (@LineDefs {"color"}))   { $color   = @LineDefs {"color"} ; }
    if (defined (@LineDefs {"layer"}))   { $layer   = @LineDefs {"layer"} ; }
    if (defined (@LineDefs {"width"}))   { $width   = @LineDefs {"width"} ; }
    if (defined (@LineDefs {"frompos"})) { $frompos = @LineDefs {"frompos"} ; }
    if (defined (@LineDefs {"tillpos"})) { $tillpos = @LineDefs {"tillpos"} ; }
    if (defined (@LineDefs {"atpos"}))   { $atpos   = @LineDefs {"atpos"} ; }

    foreach $attribute (keys %Attributes)
    {
      my $attrvalue = @Attributes {$attribute} ;

      if ($attribute =~ /^(?:At|From|Till)$/i)
      {
        if ($attrvalue =~ /^Start$/i)
        { $attrvalue = @Period {"from"} ; }

        if ($attrvalue =~ /^End$/i)
        { $attrvalue = @Period {"till"} ; }

        if (! &ValidDateFormat ($attrvalue))
        { &Error ("LineData attribute '$attribute' invalid.\n" .
                  "Date does not conform to specified DateFormat '$DateFormat'.") ;
          &GetData ; next LineData ; }

        if (! &ValidDateRange ($attrvalue))
        { &Error ("LineData attribute '$attribute' invalid.\n" .
                  "Date '$attrvalue' not within range as specified by command Period.") ;
          &GetData ; next LineData ; }

#       if (substr ($attrvalue,6,4) < 1800)
#       { &Error ("LineData attribute '$attribute' invalid. Specify year >= 1800.") ;
#         &GetData ; next LineData ; }

        if ($attribute =~ /At/i)
        {
          $at = $attrvalue ; $from = "" ; $till = "" ; }
        elsif ($attribute =~ /From/i)
        { $from = $attrvalue ; $at = "" ; }
        else
        { $till = $attrvalue ; $at = "" ; }
      }
      elsif ($attribute =~ /^(?:atpos|frompos|tillpos)$/i)
      {
        if ($attrvalue =~ /^(?:Start|End)$/i)
        { $attrvalue = lc ($attrvalue) ; }
        elsif (! &ValidAbs ($attrvalue))
        { &Error ("LineData attribute '$attribute' invalid.\n" .
                  "Specify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ;
          &GetData ; next LineData ; }

        if ($attribute =~ /atpos/i)
        { $atpos = &Normalize ($attrvalue) ; }
        elsif ($attribute =~ /frompos/i)
        { $frompos = &Normalize ($attrvalue) ; }
        else
        { $tillpos = &Normalize ($attrvalue) ; }
      }
      elsif ($attribute =~ /Color/i)
      {
        if ((! &ColorPredefined ($attrvalue)) && (! defined (@Colors {lc ($attrvalue)})))
        { &Error ("LineData  attribute '$attribute' invalid. Unknown color '$attrvalue'.\n" .
                  "  Specify command 'Color' before this command.") ;
          &GetData ; next LineData ; }

        if (! &ColorPredefined ($attrvalue))
        { $attrvalue = @Colors {lc ($attrvalue)} ; }

        $color = $attrvalue ;
      }
      elsif ($attribute =~ /Layer/i)
      {
        if (! ($attrvalue =~ /^(?:back|front)$/i))
        { &Error ("LineData attribute '$attrvalue' invalid.\nSpecify back(default) or front") ;
          &GetData ; next LineData ; }

        $layer = $attrvalue ;
      }
      elsif ($attribute =~ /Points/i)
      {
        $attribute =~ s/\s//g ;

        if ($attrvalue !~ /^$hBrO\d+\,\d+$hBrC$hBrO\d+\,\d+$hBrC$/)
        { &Error ("LineData attribute '$attrvalue' invalid.\nSpecify 'points:(x1,y1)(x2,y2)'") ;
          &GetData ; next LineData ; }

        $attrvalue =~ s/^$hBrO(\d+)\,(\d+)$hBrC$hBrO(\d+)\,(\d+)$hBrC$/$1,$2,$3,$4/ ;
        $points = $attrvalue ;
      }
      elsif ($attribute =~ /Width/i)
      {
        if (! &ValidAbs ($attrvalue))
        { &Error ("LineData attribute '$attribute' invalid.\n" .
                  "Specify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ;
          &GetData ; next LineData ; }

        if (($attrvalue < 0.1) || ($attrvalue > 10))
        { &Error ("LineData attribute '$attribute' invalid.\n" .
                  "Specify value as between 0.1 and 10") ;
          &GetData ; next LineData ; }

        $width = $attrvalue ;
      }
    }

    if (($at eq "") && ($from eq "") && ($till eq "") && ($points eq "")) # upd defaults
    {
      if ($color   ne "") { @LineDefs {"color"}   = $color ; }
      if ($layer   ne "") { @LineDefs {"layer"}   = $layer ; }
      if ($width   ne "") { @LineDefs {"width"}   = $width ; }
      if ($atpos   ne "") { @LineDefs {"atpos"}   = $atpos ; }
      if ($frompos ne "") { @LineDefs {"frompos"} = $frompos ; }
      if ($tillpos ne "") { @LineDefs {"tillpos"} = $tillpos ; }
    }

    if ($layer eq "")
    { $layer = "back" ; }

    if ($color eq "")
    { $color = "black" ; }

    $explanation = "\nA line is defined as follows:\n" .
                   "  Perpendicular to the time axis: 'at frompos tillpos'\n" .
                   "  Parralel to the time axis: 'from till atpos'\n" .
                   "  Any direction: points(x1,y1)(x2,y2)\n" .
                   "  at,from,till expect date/time values, just like with command PlotData\n" .
                   "  frompos,tillpos,atpos,x1,x2,y1,y2 expect coordinates (e.g. pixels values)\n" ;

    if (($at ne "") && (($from ne "") || ($till ne "") || ($points ne "")))
    { &Error ("LineData attribute 'at' can not be combined with 'from', 'till' or 'points'\n" . $explanation) ;
      $explanation = "" ;
      &GetData ; next LineData ; }

    if ((($from ne "") && ($till eq "")) || (($from eq "") && ($till ne "")))
    { &Error ("LineData attributes 'from' and 'till' should always be specified together\n" . $explanation) ;
      $explanation = "" ;
      &GetData ; next LineData ; }

    if (($points ne "") && (($from ne "") || ($till ne "") || ($at ne "")))
    { &Error ("LineData attribute 'points' can not be combined with 'at', 'from' or 'till'\n" . $explanation) ;
      $explanation = "" ;
      &GetData ; next LineData ; }

    if ($at ne "")
    { push @DrawLines, sprintf ("1|%s|%s|%s|%s|%s|%s\n", $at, $frompos, $tillpos, lc ($color), $width, lc ($layer)) ; }

    if ($from ne "")
    { push @DrawLines, sprintf ("2|%s|%s|%s|%s|%s|%s\n", $atpos, $from, $till, lc ($color), $width, lc ($layer)) ; }

    if ($points ne "")
    { push @DrawLines, sprintf ("3|%s|%s|%s|%s\n", $points, lc ($color), $width, lc ($layer)) ; }
    &GetData ;
  }
}

sub ParseImageSize
{
  if (! &ValidAttributes ("ImageSize")) { return ; }

  &CheckPreset ("ImageSize") ;

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;

    if ($attribute =~ /Width|Height/i)
    {
      if ($attrvalue !~ /auto/i)
      {
        if (! &ValidAbs ($attrvalue))
        { &Error ("ImageSize attribute '$attribute' invalid.\n" .
                  "Specify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ; return ; }
      }
    }

    elsif ($attribute =~ /BarIncrement/i)
    {
      if (! &ValidAbs ($attrvalue))
      { &Error ("ImageSize attribute '$attribute' invalid.\n" .
                "Specify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ; return ; }

      @Attributes {"barinc"} = $attrvalue ;
    }
#   if ($attribute =~ /Width/i)
#   { @Attributes {"width"} = $attrvalue ; }
#   elsif ($attribute =~ /Height/i)
#   { @Attributes {"height"} = $attrvalue ; }
  }

  if ((@Attributes {"width"} =~ /auto/i) || (@Attributes {"height"} =~ /auto/i))
  {
    if (@Attributes {"barinc"} eq "")
    { &Error ("ImageSize attribute 'barincrement' missing.\n" .
              "Automatic determination of image width or height implies specification of this attribute") ; return ; }
  }

  if ((@Attributes {"width"} !~ /auto/i) && (@Attributes {"height"} !~ /auto/i))
  {
    if (@Attributes {"barinc"} ne "")
    { &Error ("ImageSize attribute 'barincrement' not valid now.\n" .
              "This attribute is only valid (and mandatory) in combination with 'width:auto' or 'height:auto'") ; return ; }
  }

  %Image = %Attributes ;
}

sub ParseLegend
{
  if (! &ValidAttributes ("Legend")) { return ; }

  &CheckPreset ("Legend") ;

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;

    if ($attribute =~ /Columns/i)
    {
      if (($attrvalue < 1) || ($attrvalue > 4))
      { &Error ("Legend attribute 'columns' invalid. Specify 1,2,3 or 4") ; return ; }
    }
    elsif ($attribute =~ /Orientation/i)
    {
      if (! ($attrvalue =~ /^(?:hor|horizontal|ver|vertical)$/i))
      { &Error ("Legend attribute '$attrvalue' invalid. Specify hor[izontal] or ver[tical]") ; return ; }

      @Attributes {"orientation"} = substr ($attrvalue,0,3) ;
    }
    elsif ($attribute =~ /Position/i)
    {
      if (! ($attrvalue =~ /^(?:top|bottom|right)$/i))
      { &Error ("Legend attribute '$attrvalue' invalid.\nSpecify top, bottom or right") ; return ; }
    }
    elsif ($attribute =~ /Left/i)
    {
      if (! &ValidAbsRel ($attrvalue))
      { &Error ("Legend attribute '$attribute' invalid.\nSpecify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ; return ; }    }
    elsif ($attribute =~ /Top/i)
    {
      if (! &ValidAbsRel ($attrvalue))
      { &Error ("Legend attribute '$attribute' invalid.\nSpecify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ; return ; }    }
    elsif ($attribute =~ /ColumnWidth/i)
    {
      if (! &ValidAbsRel ($attrvalue))
      { &Error ("Legend attribute '$attribute' invalid.\nSpecify value as x[.y][px, in, cm] examples: '200', '20px', '1.3in'") ; return ; }
    }
  }

  if (defined (@Attributes {"position"}))
  {
    if (defined (@Attributes {"left"}))
    { &Error ("Legend definition invalid. Attributes 'position' and 'left' are mutually exclusive.") ; return ; }
  }
  else
  {
    if ((! defined (@Attributes {"left"})) && (! defined (@Attributes {"top"})))
    {
      &Info ("Legend definition: none of attributes 'position', 'left' or 'top' have been defined. Position 'bottom' assumed.") ;
      @Attributes {"position"} = "bottom" ;
    }
    elsif ((! defined (@Attributes {"left"})) || (! defined (@Attributes {"top"})))
    { &Error ("Legend definition invalid. Specify 'position', or 'left' & 'top'.") ; return ; }
  }

  if (@Attributes {"position"} =~ /right/i)
  {
    if (defined (@Attributes {"columns"}))
    { &Error ("Legend definition invalid.\nAttribute 'columns' and 'position:right' are mutually exclusive.") ; return ; }
    if (defined (@Attributes {"columnwidth"}))
    { &Error ("Legend definition invalid.\nAttribute 'columnwidth' and 'position:right' are mutually exclusive.") ; return ; }
  }

  if (@Attributes {"orientation"} =~ /hor/i)
  {
    if (@Attributes {"position"} =~ /right/i)
    { &Error ("Legend definition invalid.\n'position:right' and 'orientation:horizontal' are mutually exclusive.") ; return ; }
    if (defined (@Attributes {"columns"}))
    { &Error ("Legend definition invalid.\nAttribute 'columns' and 'orientation:horizontal' are mutually exclusive.") ; return ; }
    if (defined (@Attributes {"columnwidth"}))
    { &Error ("Legend definition invalid.\nAttribute 'columnwidth' and 'orientation:horizontal' are mutually exclusive.") ; return ; }
  }

  if ((@Attributes {"orientation"} =~ /hor/i) && (defined (@Attributes {"columns"})))
  { &Error ("Legend definition invalid.\nDo not specify attribute 'columns' with 'orientation:horizontal'.") ; return ; }

  if (@Attributes {"columns"} > 1)
  {
    if ((defined (@Attributes {"left"})) && (! defined (@Attributes {"columnwidth"})))
    { &Error ("Legend attribute 'columnwidth' not defined.\nThis is needed when attribute 'left' is specified.") ; return ; }
  }

  if (! defined (@Attributes {"orientation"}))
  { @Attributes {"orientation"} = "ver" ; }

  %Legend = %Attributes ;
}

sub ParsePeriod
{
  if (! defined ($DateFormat))
  { &Error ("Period definition ambiguous. No (valid) command 'DateFormat' specified in previous lines.") ;  return ; }

  if (! ValidAttributes ("Period")) { return ; }

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;

    if ($DateFormat eq "yyyy")
    {
       if ($attrvalue !~ /^\-?\d+$/)
       { &Error ("Period definition invalid.\nInvalid year '$attrvalue' specified for attribute '$attribute'.") ;  return ; }
    }
    elsif ($DateFormat eq "x.y")
    {
       if (! ($attrvalue =~ /^\-?\d+(?:\.\d+)?$/))
       { &Error ("Period definition invalid.\nInvalid year '$attrvalue' specified for attribute '$attribute'.") ;  return ; }
    }
    else
    {
      if (($attrvalue =~ /^\d+$/) && ($attrvalue >= 1800) && ($attrvalue <= 2030))
      {
        if ($attribute =~ /^From$/i)
        { $attrvalue = "01/01/" . $attrvalue ; }
        if ($attribute =~ /^Till$/i)
        {
          if ($DateFormat eq "dd/mm/yyyy")
          { $attrvalue = "31/12/" . $attrvalue ; }
          else
          { $attrvalue = "12/31/" . $attrvalue ; }
        }
      }

      $ValidDate = &ValidDateFormat ($attrvalue) ;
      if (! $ValidDate)
      { &Error ("Period attribute '$attribute' invalid.\n" .
                "Date does not conform to specified DateFormat '$DateFormat'.") ;  return ; }
      if (substr ($attrvalue,6,4) < 1800)
      { &Error ("Period attribute '$attribute' invalid. Specify year >= 1800.") ;  return ; }

      @Attributes {$attribute} = $attrvalue ;
    }
  }

  %Period = %Attributes ;
}

sub ParsePlotArea
{
  if (! &ValidAttributes ("PlotArea")) { return ; }

  &CheckPreset ("PlotArea") ;

  foreach $attribute (@Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;
    if (! &ValidAbsRel ($attrvalue))
    { &Error ("PlotArea attribute '$attribute' invalid.\n" .
              "Specify value as x[.y][px, in, cm, %] examples: '200', '20px', '1.3in', '80%'") ; return ; }
  }

  if ((@Attributes {"top"} ne "") && (@Attributes {"height"} ne ""))
  { &Error ("PlotArea attributes 'top' and 'height' are mutually exclusive. Specify only one of them.") ;  return ; }

  if ((@Attributes {"right"} ne "") && (@Attributes {"width"} ne ""))
  { &Error ("PlotArea attributes 'right' and 'width' are mutually exclusive. Specify only one of them.") ;  return ; }

  if ((@Attributes {"top"} eq "") && (@Attributes {"height"} eq ""))
  { &Error ("PlotArea definition incomplete. Either attribute 'top' (advised) or 'height' should be specified") ;  return ; }

  if ((@Attributes {"right"} eq "") && (@Attributes {"width"} eq ""))
  { &Error ("PlotArea definition incomplete. Either attribute 'right' (advised) or 'width' should be specified") ;  return ; }

  %PlotArea = %Attributes ;
}

#                         command Bars found ?
#                  Y                |                   N
#             bar: found ?          |               bar: found ?
#        Y | N                      |              Y | N
# validate | previous bar: found?   | @Bars contains | previous bar: found?
#   bar:.. |                        |        bar: ?  |    Y | N
#          |     Y | N              |                | copy | assume
#          | copy  |  $#Bars ..     | Y  | N         | bar: | bar:---
#          |  bar: |== 0            | -  | assume    |      |
#          |       | assume bar:--- |    |  bar:---  |      |
#          |       |== 1            |
#          |       | assume @Bar[0] |
#          |       |> 1             |
#          |       | err            |
sub ParsePlotData
{
  if (defined (@Bars))
  { $BarsCommandFound = $true ; }
  else
  { $BarsCommandFound = $false ; }
  $prevbar = "" ;

  if ((! (defined ($DateFormat))) || (@Period {"from"} eq "") || (@Axis {"time"} eq ""))
  {
    if (! (defined ($DateFormat)))
    { &Error ("PlotData invalid. No (valid) command 'DateFormat' specified in previous lines.") ; }
    elsif (@Period {"from"} eq "")
    { &Error ("PlotData invalid. No (valid) command 'Period' specified in previous lines.") ; }
    else
    { &Error ("PlotData invalid. No (valid) command 'TimeAxis' specified in previous lines.") ; }

    &GetData ;
    while ((! $InputParsed) && (! $NoData))
    { &GetData ; }
    return ;
  }

  &GetData ;
  if ($NoData)
  { &Error ("Data expected for command 'PlotData', but line is not indented.\n") ; return ; }

  my ($bar, $at, $from, $till, $color, $bgcolor, $textcolor, $fontsize, $width,
      $text, $anchor, $align, $shift, $shiftx, $shifty, $mark, $markcolor, $link, $hint) ;

  @PlotDefs {"anchor"} = "middle" ;

  PlotData:
  while ((! $InputParsed) && (! $NoData))
  {
    if (! &ValidAttributes ("PlotData"))
    { &GetData ; next ;}

    $bar = "" ; # $barset = "" ;
    $at = "" ; $from = "" ; $till = "" ;
    $color = "barcoldefault" ; $bgcolor = "" ; $textcolor = "black" ; $fontsize = "S" ; $width = "0.25" ;
    $text = "" ; $align = "left" ; $shift = "" ; $shiftx = "" ; $shifty = "" ; $anchor = "" ;
    $mark = "" ; $markcolor = "" ;
    $link = "" ; $hint = "" ;

    &CheckPreset ("PlotData") ;

    if (defined (@PlotDefs {"bar"}))       { $bar       = @PlotDefs {"bar"} ; }
 #  if (defined (@PlotDefs {"barset"}))    { $barset    = @PlotDefs {"barset"} ; }
    if (defined (@PlotDefs {"color"}))     { $color     = @PlotDefs {"color"} ; }
    if (defined (@PlotDefs {"bgcolor"}))   { $bgcolor   = @PlotDefs {"bgcolor"} ; }
    if (defined (@PlotDefs {"textcolor"})) { $textcolor = @PlotDefs {"textcolor"} ; }
    if (defined (@PlotDefs {"fontsize"}))  { $fontsize  = @PlotDefs {"fontsize"} ; }
    if (defined (@PlotDefs {"width"}))     { $width     = @PlotDefs {"width"} ; }
    if (defined (@PlotDefs {"anchor"}))    { $anchor    = @PlotDefs {"anchor"} ; }
    if (defined (@PlotDefs {"align"}))     { $align     = @PlotDefs {"align"} ; }
    if (defined (@PlotDefs {"shiftx"}))    { $shiftx    = @PlotDefs {"shiftx"} ; }
    if (defined (@PlotDefs {"shifty"}))    { $shifty    = @PlotDefs {"shifty"} ; }
    if (defined (@PlotDefs {"mark"}))      { $mark      = @PlotDefs {"mark"} ; }
    if (defined (@PlotDefs {"markcolor"})) { $markcolor = @PlotDefs {"markcolor"} ; }
#   if (defined (@PlotDefs {"link"}))      { $link      = @PlotDefs {"link"} ; }
#   if (defined (@PlotDefs {"hint"}))      { $hint      = @PlotDefs {"hint"} ; }

    foreach $attribute (keys %Attributes)
    {
      my $attrvalue = @Attributes {$attribute} ;

      if ($attribute =~ /^Bar$/i)
      {
        if (! ($attrvalue =~ /[a-zA-Z0-9\_]+/))
        { &Error ("PlotData attribute '$attribute' invalid.\n" .
                  "Use only characters 'a'-'z', 'A'-'Z', '0'-'9', '_'\n") ;
          &GetData ; next  PlotData ; }

        $attrvalue2 = $attrvalue ;

        if ($BarsCommandFound)
        {
          if (! &BarDefined ($attrvalue2))
          { &Error ("PlotData invalid. Bar '$attrvalue' not (properly) defined.") ;
            &GetData ; next PlotData ; }
        }
        else
        {
          if (! &BarDefined ($attrvalue2))
          {
            if (@Axis {"time"} eq "x")
            { push @Bars, $attrvalue2 ; }
            else
            { unshift @Bars, $attrvalue2 ; }
          }
        }
        $bar = $attrvalue2 ;
        $prevbar = $bar ;
      }
      elsif ($attribute =~ /^BarSet$/i)
      {
        if (! ($attrvalue =~ /[a-zA-Z0-9\_]+/))
        { &Error ("PlotData attribute '$attribute' invalid.\n" .
                  "Use only characters 'a'-'z', 'A'-'Z', '0'-'9', '_'\n") ;
          &GetData ; next PlotData ; }

        $attrvalue2 = $attrvalue ;

        if ($attrvalue =~ /break/i)
        { $barndx = 0 ; }
        elsif ($attrvalue =~ /skip/i)
        {
          $barndx ++ ;
          &BarDefined ($prevbar . "#" . $barndx) ;
        }
        else
        {
          if ($BarsCommandFound)
          {
            if (! &BarDefined ($attrvalue2 . "#1"))
            { &Error ("PlotData invalid. BarSet '$attrvalue' not (properly) defined with command BarData.") ;
              &GetData ; next PlotData ; }
          }
          $bar = $attrvalue2 ;
          if ($bar ne $prevbar)
          { $barndx = 0 ; }
          $prevbar = $bar ;
        }
      }
      elsif ($attribute =~ /^(?:At|From|Till)$/i)
      {
        if ($attrvalue =~ /^Start$/i)
        { $attrvalue = @Period {"from"} ; }
        if ($attrvalue =~ /^End$/i)
        { $attrvalue = @Period {"till"} ; }

        if (! &ValidDateFormat ($attrvalue))
        {
          &Error ("PlotData attribute '$attribute' invalid.\n" .
                  "Date '$attrvalue' does not conform to specified DateFormat $DateFormat.") ;
          &GetData ; next PlotData ; }

        if (! &ValidDateRange ($attrvalue))
        { &Error ("Plotdata attribute '$attribute' invalid.\n" .
                  "Date '$attrvalue' not within range as specified by command Period.") ;

          &GetData ; next PlotData ; }

        if ($attribute =~ /^At$/i)
        { $at = $attrvalue ; }
        elsif ($attribute =~ /^From$/i)
        { $from = $attrvalue ; }
        else
        { $till = $attrvalue ; }
      }
#      elsif ($attribute =~ /^From$/i)
#      {
#        if ($attrvalue =~ /^Start$/i)
#        { $attrvalue = @Period {"from"} ; }

#        if (! &ValidDateFormat ($attrvalue))
#        { &Error ("PlotData invalid.\nDate '$attrvalue' does not conform to specified DateFormat $DateFormat.") ;
#          &GetData ; next PlotData ; }

#        if (! &ValidDateRange ($attrvalue))
#        { &Error ("Plotdata attribute 'from' invalid.\n" .
#                  "Date '$attrvalue' not within range as specified by command Period.") ;
#          &GetData ; next PlotData ; }

#        $from = $attrvalue ;
#      }
#      elsif ($attribute =~ /^Till$/i)
#      {
#        if ($attrvalue =~ /^End$/i)
#        { $attrvalue = @Period {"till"} ; }

#        if (! &ValidDateFormat ($attrvalue))
#        { &Error ("PlotData invalid. Date '$attrvalue' does not conform to specified DateFormat $DateFormat.") ;
#          &GetData ; next PlotData ; }

#        if (! &ValidDateRange ($attrvalue))
#        { &Error ("Plotdata attribute 'till' invalid.\n" .
#                  "Date '$attrvalue' not within range as specified by command Period.") ;
#          &GetData ; next PlotData ; }

#        $till = $attrvalue ;
#      }
      elsif ($attribute =~ /^Color$/i)
      {
        if (! &ColorPredefined ($attrvalue))
        {
          if (! defined (@Colors {lc ($attrvalue)}))
          { &Error ("PlotData invalid. Attribute '$attribute' has unknown color '$attrvalue'.\n" .
                    "  Specify command 'Color' before this command.") ;
            &GetData ; next PlotData ; }
        }
        if (defined (@Colors {lc ($attrvalue)}))
        { $color = @Colors { lc ($attrvalue) } ; }
        else
        { $color = lc ($attrvalue) ; }

        $color = $attrvalue ;
      }
      elsif ($attribute =~ /^BgColor$/i)
      {
        if (! &ColorPredefined ($attrvalue))
        {
          if (! defined (@Colors {lc ($attrvalue)}))
          { &Error ("PlotData invalid. Attribute '$attribute' has unknown color '$attrvalue'.\n" .
                    "  Specify command 'Color' before this command.") ;
            &GetData ; next PlotData ; }
        }
        if (defined (@Colors {lc ($attrvalue)}))
        { $bgcolor = @Colors { lc ($attrvalue) } ; }
        else
        { $bgcolor = lc ($attrvalue) ; }
      }
      elsif ($attribute =~ /^TextColor$/i)
      {
        if (! &ColorPredefined ($attrvalue))
        {
          if (! defined (@Colors {lc ($attrvalue)}))
          { &Error ("PlotData invalid. Attribute '$attribute' contains unknown color '$attrvalue'.\n" .
                    "  Specify command 'Color' before this command.") ;
            &GetData ; next PlotData ; }
        }
        if (defined (@Colors {lc ($attrvalue)}))
        { $textcolor = @Colors { lc ($attrvalue) } ; }
        else
        { $textcolor = lc ($attrvalue) ; }
      }
      elsif ($attribute =~ /^Width$/i)
      {
        $width = &Normalize ($attrvalue) ;
        if ($width > $MaxBarWidth)
        { $MaxBarWidth = $width ; }
      }
      elsif ($attribute =~ /^FontSize$/i)
      {
        if (($attrvalue !~ /\d+(?:\.\d)?/) && ($attrvalue !~ /xs|s|m|l|xl/i))
        { &Error ("PlotData invalid. Specify for attribute '$attribute' a number of XS,S,M,L,XL.") ;
          &GetData ; next PlotData ; }

        $fontsize = $attrvalue ;
        if ($fontsize =~ /(?:XS|S|M|L|XL)/i)
        {
          if ($fontsize !~ /(?:xs|s|m|l|xl)/i)
          {
            if ($fontsize < 6)
            { &Warning ("TextData attribute 'fontsize' value too low. Font size 6 assumed.\n") ;
              $fontsize = 6 ; }
            if ($fontsize > 30)
            { &Warning ("TextData attribute 'fontsize' value too high. Font size 30 assumed.\n") ;
              $fontsize = 30 ; }
          }
        }
      }
      elsif ($attribute =~ /^Anchor$/i)
      {
        if (! ($attrvalue =~ /^(?:from|till|middle)$/i))
        { &Error ("PlotData value '$attribute' invalid. Specify 'from', 'till' or 'middle'.") ;
          &GetData ; next PlotData ; }

        $anchor = lc ($attrvalue) ;
      }
      elsif ($attribute =~ /^Align$/i)
      {
        if (! ($attrvalue =~ /^(?:left|right|center)$/i))
        { &Error ("PlotData value '$attribute' invalid. Specify 'left', 'right' or 'center'.") ;
          &GetData ; next PlotData ; }

        $align = lc ($attrvalue) ;
      }
      elsif ($attribute =~ /^Shift$/i)
      {
        $shift = $attrvalue ;
        $shift =~ s/$hBrO(.*?)$hBrC/$1/ ;
        $shift =~ s/\s//g ;
        ($shiftx2,$shifty2) = split (",", $shift) ;
        if ($shiftx2 ne "")
        { $shiftx = &Normalize ($shiftx2) ; }
        if ($shifty2 ne "")
        { $shifty = &Normalize ($shifty2) ; }

        if (($shiftx < -10) || ($shiftx > 10) || ($shifty < -10) || ($shifty > 10))
        { &Error ("PlotData invalid. Attribute '$shift', specify value(s) between -1000 and 1000 pixels = -10 and 10 inch.") ;
          &GetData ; next PlotData ; }
      }
      elsif ($attribute =~ /^Text$/i)
      {
        $text = &ParseText ($attrvalue) ;
        $text =~ s/\\n/\n/g ;
        if ($text =~ /\^/)
        { &Warning ("TextData attribute 'text' contains ^ (caret).\n" .
                    "Caret symbol will not be translated into tab character (use TextData when tabs are needed)") ; }

#       $text=~ s/(\[\[ [^\]]* \n [^\]]* \]\])/&NormalizeWikiLink($1)/gxe ;
        $text=~ s/(\[\[? [^\]]* \n [^\]]* \]?\])/&NormalizeWikiLink($1)/gxe ;
      }
      elsif ($attribute =~ /^Link$/i)
      {
        $link = &ParseText ($attrvalue) ;
        $link = &EncodeURL (&NormalizeURL ($link)) ;
      }
#     elsif ($attribute =~ /^Hint$/i)
#     {
#       $hint = &ParseText ($attrvalue) ;
#       $hint =~ s/\\n/\n/g ;
#     }
      elsif ($attribute =~ /^Mark$/i)
      {
        $attrvalue =~ s/$hBrO (.*) $hBrC/$1/x ;
        (@suboptions) = split (",", $attrvalue) ;
        $mark = @suboptions [0] ;
        if (! ($mark =~ /^(?:Line|None)$/i))
        { &Error ("PlotData invalid. Value '$mark' for attribute 'mark' unknown.") ;
          &GetData ; next PlotData ; }

        if (defined (@suboptions [1]))
        {
          $markcolor = @suboptions [1] ;

          if (! &ColorPredefined ($markcolor))
          {
            if (! defined (@Colors {lc ($markcolor)}))
            { &Error ("PlotData invalid. Attribute 'mark': unknown color '$markcolor'.\n" .
                      "  Specify command 'Color' before this command.") ;
              &GetData ; next PlotData ; }
          }
          $markcolor = lc ($markcolor) ;
        }
        else
        { $markcolor = "black" ; }
      }
      else
      { &Error ("PlotData invalid. Unknown attribute '$attribute' found.") ;
        &GetData ; next PlotData ; }
    }

#    if ($text =~ /\[\[.*\[\[/s)
#    { &Error ("PlotData invalid. Text segment '$text' contains more than one wiki link. Only one allowed.") ;
#      &GetData ; next PlotData ; }

#    if (($text ne "") || ($link ne ""))
#    { ($text, $link, $hint) = &ProcessWikiLink ($text, $link, $hint) ; }

    $shift = $shiftx . "," . $shifty ;

    if ($MaxBarWidth eq "")
    { $MaxBarWidth = $width - 0.001 ; }

    if ($bar ne "")
    {
      if (! defined (@BarLegend {lc($bar)}))
      { @BarLegend {lc($bar)} = $bar ; }
      if (! defined (@BarWidths {$bar}))
      { @BarWidths {$bar} = $width ; } # was 0 ??
    }

    if (($at eq "") && ($from eq "") && ($till eq "")) # upd defaults
    {
      if ($bar        ne "") { @PlotDefs {"bar"}       = $bar ; }
#     if ($barset     ne "") { @PlotDefs {"barset"}    = $barset ; }
      if ($color      ne "") { @PlotDefs {"color"}     = $color ; }
      if ($bgcolor    ne "") { @PlotDefs {"bgcolor"}   = $bgcolor ; }
      if ($textcolor  ne "") { @PlotDefs {"textcolor"} = $textcolor ; }
      if ($fontsize   ne "") { @PlotDefs {"fontsize"}  = $fontsize ; }
      if ($width      ne "") { @PlotDefs {"width"}     = $width ; }
      if ($anchor     ne "") { @PlotDefs {"anchor"}    = $anchor ; }
      if ($align      ne "") { @PlotDefs {"align"}     = $align ; }
      if ($shiftx     ne "") { @PlotDefs {"shiftx"}    = $shiftx ; }
      if ($shifty     ne "") { @PlotDefs {"shifty"}    = $shifty ; }
      if ($mark       ne "") { @PlotDefs {"mark"}      = $mark ; }
      if ($markcolor  ne "") { @PlotDefs {"markcolor"} = $markcolor ; }
#     if ($link       ne "") { @PlotDefs {"link"}      = $link ; }
#     if ($hint       ne "") { @PlotDefs {"hint"}      = $hint ; }
      &GetData ; next PlotData ;
    }

    if ($bar eq "")
    {
      if ($prevbar ne "")
      { $bar = $prevbar ; }
      else
      {
#        if ($BarsCommandFound)
#        {
          if ($#Bars > 0)
          { &Error ("PlotData invalid. Specify attribute 'bar'.") ;
            &GetData ; next PlotData ; }
          elsif ($#Bars == 0)
          {
            $bar = @Bars [0] ;
            &Info ($data, "PlotData incomplete. Attribute 'bar' missing, value '" . @Bars [0] . "' assumed.") ;
          }
          else
          { $bar = "1" ; }
#        }
#        else
#        {
#          if ($#Bars > 0)
#          { &Error ("PlotData invalid. Attribute 'bar' missing.") ;
#            &GetData ; next PlotData ; }
#          elsif ($#Bars == 0)
#          {
#            $bar = @Bars [0] ;
#            &Info ($data, "PlotData incomplete. Attribute 'bar' missing, value '" . @Bars [0] . "' assumed.") ;
#          }
#          else { $bar = "1" ; }
#        }
        $prevbar = $bar ;
      }
    }

    if (&BarDefined ($bar . "#1")) # bar is actually a bar set
    {
      if (($from ne "") || ($at ne "") || ($text eq " ")) # data line ?
      {
        $barndx++ ;
        if (! &BarDefined ($bar . "#" . $barndx))
        { $barndx = 1 ; }
        $bar = $bar . "#" . $barndx ;
        # $text = $bar ;
      }
    }

    if (($at ne "") && (($from ne "") || ($till ne "")))
    { &Error ("PlotData invalid. Attributes 'at' and 'from/till' are mutually exclusive.") ;
      &GetData ; next PlotData ; }

    if ((($from eq "") && ($till ne "")) || (($from ne "") && ($till eq "")))
    { &Error ("PlotData invalid. Specify attribute 'at' or 'from' + 'till'.") ;
      &GetData ; next PlotData ; }


    if ($at ne "")
    {
      if ($text ne "")
      {
        if ($align eq "")
        { &Error ("PlotData invalid. Attribute 'align' missing.") ;
          &GetData ; next PlotData ; }
        if ($fontsize eq "")
        { &Error ("PlotData invalid. Attribute '[font]size' missing.") ;
          &GetData ; next PlotData ; }
        if ($text eq "")
        { &Error ("PlotData invalid. Attribute 'text' missing.") ;
          &GetData ; next PlotData ; }
      }
    }
    else
    {
      if (($text ne "") && ($anchor eq ""))
      { &Error ("PlotData invalid. Attribute 'anchor' missing.") ;
        &GetData ; next PlotData ; }
      if ($color eq "")
      { &Error ("PlotData invalid. Attribute 'color' missing.") ;
        &GetData ; next PlotData ; }
      if ($width eq "")
      { &Error ("PlotData invalid. Attribute 'width' missing.") ;
        &GetData ; next PlotData ; }
    }

    if ($from ne "")
    {
      if (($link ne "") && ($hint eq ""))
      { $hint = &ExternalLinkToHint ($link) ; }

      if (($link ne "") || ($hint ne ""))
      { $MapPNG = $true ; }
      if ($link ne "")
      { $MapSVG = $true ; }

      push @PlotBars, sprintf ("%6.3f,%s,%s,%s,%s,%s,%s,\n", $width, $bar, $from, $till, lc ($color),$link,$hint) ;
      if ($width > @BarWidths {$bar})
      { @BarWidths {$bar} = $width ; }

      if ($text ne "")
      {
        if ($anchor eq "from")
        { $at = $from ; }
        elsif ($anchor eq "till")
        { $at = $till ; }
        else
        { $at = &DateMedium ($from, $till) ; }
      }

      if (($mark ne "") && ($mark !~ /none/i))
      {
        push @PlotLines, sprintf ("%s,%s,%s,%s,,,\n", $bar, $from, $from, lc ($markcolor)) ;
        push @PlotLines, sprintf ("%s,%s,%s,%s,,,\n", $bar, $till, $till, lc ($markcolor)) ;
        $mark = "" ;
      }
    }

    if ($at ne "")
    {
      if (($mark ne "") && ($mark !~ /none/i))
      { push @PlotLines, sprintf ("%s,%s,%s,%s,,,\n", $bar, $at, $at, lc ($markcolor)) ; }

      if ($text ne "")
      {
        my $textdetails = "" ;

        if ($link ne "")
        {
          if ($text =~ /\[.*\]/)
          {
            &Warning ("PlotData contains implicit link(s) in attribute 'text' and explicit attribute 'link'. " .
                      "Implicit link(s) ignored.") ;
            $text =~ s/\[+ (?:[^\|]* \|)? ([^\]]*) \]+/$1/gx ;
          }
          if ($hint eq "")
          { $hint = &ExternalLinkToHint ($link) ; }
        }

        if ($anchor eq "")
        { $anchor = "middle" ; }
        if ($align eq "")
        { $align = "center" ; }
        if ($color eq "")
        { $color = "black" ; }
        if ($fontsize eq "")
        { $fontsize = "S" ; }
        if ($adjust eq "")
        { $adjust = "0,0" ; }

#        $textdetails = "  textdetails: align=$align size=$size"  ;
#        if ($textcolor eq "")
#        { $textcolor = "black" ; }
#        if ($color ne "")
#        { $textdetails .= " color=$textcolor" ; }

#        my ($xpos, $ypos) ;
#        my $barcnt = 0 ;
#        for ($b = 0 ; $b <= $#Bars ; $b++)
#        {
#          if (lc(@Bars [$b]) eq lc($bar))
#          { $barcnt = ($b + 1) ; last ; }
#        }

#        if (@Axis {"time"} eq "x")
#        { $xpos = "$at(s)" ; $ypos = "[$barcnt](s)" ; }
#        else
#        { $ypos = "$at(s)" ; $xpos = "[$barcnt](s)" ; }

#        if ($shift ne "")
#        {
#          my ($shiftx, $shifty) = split (",", $shift) ;
#          if ($shiftx > 0)
#          { $xpos .= "+$shiftx" ; }
#          if ($shiftx < 0)
#          { $xpos .= "$shiftx" ; }
#          if ($shifty > 0)
#          { $ypos .= "+$shifty" ; }
#          if ($shifty < 0)
#          { $ypos .= "$shifty" ; }
#        }

        $text      =~ s/\,/\#\%\$/g ;
        $link      =~ s/\,/\#\%\$/g ;
        $hint      =~ s/\,/\#\%\$/g ;
        $shift     =~ s/\,/\#\%\$/g ;
        $textcolor =~ s/\,/\#\%\$/g ;
        push @PlotText, sprintf ("%s,%s,%s,%s,%s,%s,%s,%s,%s", $at, $bar, $text, $textcolor, $fontsize, $align, $shift, $link, $hint) ;
      }
    }

    &GetData ;
  }

  if ((! $BarsCommandFound) && ($#Bars > 1))
  { &Info2 ("PlotBars definition: no (valid) command 'BarData' found in previous lines.\nBars will presented in order of appearance in PlotData.") ; }

  $maxwidth = 0 ;
  foreach $key (keys %BarWidths)
  {
    if (@BarWidths {$key} == 0)
    { &Warning ("PlotData incomplete. No bar width defined for bar '$key', assume width from widest bar (used for line marks).") ; }
    elsif (@BarWidths {$key} > $maxwidth)
    { $maxwidth = @BarWidths {$key} ; }
  }
  foreach $key (keys %BarWidths)
  {
    if (@BarWidths {$key} == 0)
    { @BarWidths {$key} = $maxwidth ; }
  }
}

sub ParsePreset
{
  if (! $firstcmd)
  { &Error ("Specify 'Preset' command before any other commands, if desired at all.\n") ; return ; }

  $preset = @Attributes {"single"} ;
  if ($preset !~ /^(?:TimeVertical_OneBar_UnitYear|TimeHorizontal_AutoPlaceBars_UnitYear)$/i)
  { &Error ("Preset value invalid.\n" .
            "  At the moment two presets are available:\n" .
            "  TimeVertical_OneBar_UnitYear and TimeHorizontal_AutoPlaceBars_UnitYear\n" .
            "  See also meta.wikipedia.org/wiki/EasyTimeline/Presets") ; return ; }

  $Preset = $preset ;

  if ($Preset =~ /^TimeVertical_OneBar_UnitYear/i)
  {
    $DateFormat = "yyyy" ;
    $AlignBars  = "early" ;
    @Axis {"format"} = "yyyy" ;
    @Axis {"time"}   = "y" ;
    @PlotArea {"left"}   = 45 ;
    @PlotArea {"right"}  = 10 ;
    @PlotArea {"top"}    = 10 ;
    @PlotArea {"bottom"} = 10 ;
    push @PresetList, "PlotArea|+|left|"   . @PlotArea {"left"} ;
    push @PresetList, "PlotArea|+|right|"  . @PlotArea {"right"};
    push @PresetList, "PlotArea|+|top|"    . @PlotArea {"top"} ;
    push @PresetList, "PlotArea|+|bottom|" . @PlotArea {"bottom"} ;
    push @PresetList, "PlotArea|-|width" ;
    push @PresetList, "PlotArea|-|height" ;
    push @PresetList, "Dateformat|-||yyyy" ;
    push @PresetList, "TimeAxis|=|format|" . @Axis {"format"} ;
    push @PresetList, "TimeAxis|=|orientation|vertical" ;
    push @PresetList, "ScaleMajor|=|unit|year" ;
    push @PresetList, "ScaleMinor|=|unit|year" ;
    push @PresetList, "AlignBars|=||early" ;
    push @PresetList, "PlotData|+|mark|" . $hBrO . "line,white" . $hBrC ;
    push @PresetList, "PlotData|+|align|left" ;
    push @PresetList, "PlotData|+|fontsize|S" ;
    push @PresetList, "PlotData|+|width|20" ;
    push @PresetList, "PlotData|+|shift|"  . $hBrO . "20,0" . $hBrC ;
  }
  elsif ($Preset =~ /TimeHorizontal_AutoPlaceBars_UnitYear/i)
  {
    $DateFormat = "yyyy" ;
    $AlignBars  = "justify" ;
    @Axis {"format"} = "yyyy" ;
    @Axis {"time"}   = "x" ;
    @PlotArea {"left"}   = 25 ;
    @PlotArea {"right"}  = 25 ;
    @PlotArea {"top"}    = 15 ;
    @PlotArea {"bottom"} = 30 ;
    @Image    {"height"} = "auto" ;
    @Image    {"barinc"} = 20 ;
    @BackgroundColors {"canvas"} = "gray(0.7)" ;
    @Legend   {"orientation"} = "ver" ;
    @Legend   {"left"} = @PlotArea {"left"}+10 ;
    @Legend   {"top"}  = @PlotArea {"bottom"}+100 ;
    &StoreColor ("canvas", &EncodeInput ("gray(0.7)"), "") ;
    &StoreColor ("grid1",  &EncodeInput ("gray(0.4)"), "") ;
    &StoreColor ("grid2",  &EncodeInput ("gray(0.2)"), "") ;
    push @PresetList, "ImageSize|=|height|auto" ;
    push @PresetList, "ImageSize|+|barincrement|20" ;
    push @PresetList, "PlotArea|+|left|"   . @PlotArea {"left"} ;
    push @PresetList, "PlotArea|+|right|"  . @PlotArea {"right"};
    push @PresetList, "PlotArea|+|top|"    . @PlotArea {"top"} ;
    push @PresetList, "PlotArea|+|bottom|" . @PlotArea {"bottom"} ;
    push @PresetList, "PlotArea|-|width" ;
    push @PresetList, "PlotArea|-|height" ;
    push @PresetList, "Dateformat|-||yyyy" ;
    push @PresetList, "TimeAxis|=|format|" . @Axis {"format"} ;
    push @PresetList, "TimeAxis|=|orientation|horizontal" ;
    push @PresetList, "ScaleMajor|=|unit|year" ;
    push @PresetList, "ScaleMajor|+|grid|grid1" ;
    push @PresetList, "ScaleMinor|=|unit|year" ;
    push @PresetList, "AlignBars|=||justify" ;
    push @PresetList, "Legend|+|orientation|" . @Legend   {"orientation"} ;
    push @PresetList, "Legend|+|left|" . @Legend   {"left"} ;
    push @PresetList, "Legend|+|top|" .  @Legend   {"top"} ;
    push @PresetList, "PlotData|+|align|left" ;
    push @PresetList, "PlotData|+|anchor|from" ;
    push @PresetList, "PlotData|+|fontsize|M" ;
    push @PresetList, "PlotData|+|width|15" ;
    push @PresetList, "PlotData|+|textcolor|black" ;
    push @PresetList, "PlotData|+|shift|"  . $hBrO . "4,-6" . $hBrC ;
  }
}

sub ParseScale
{
  my ($scale) ;

  if ($Command =~ /ScaleMajor/i)
  { $scale .= 'Major' ; }
  else
  { $scale .= 'Minor' ; }

  if (! ValidAttributes ("Scale" . $scale)) { return ; }

  &CheckPreset (Scale . $scale) ;

  @Scales {$scale} = $true ;

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;

    if ($attribute =~ /Grid/i) # preferred gridcolor instead of grid, grid allowed for compatability
    {
      if ((! &ColorPredefined ($attrvalue)) && (! defined (@Colors {lc ($attrvalue)})))
      { &Error ("Scale attribute '$attribute' invalid. Unknown color '$attrvalue'.\n" .
                "  Specify command 'Color' before this command.") ;  return ; }
      @Attributes {$scale . " grid"} = $attrvalue ;
      delete (@Attributes {"grid"}) ;
    }
    elsif ($attribute =~ /Text/i)
    {
      $attrvalue =~ s/\~/\\n/g ;
      $attrvalue =~ s/^\"//g ;
      $attrvalue =~ s/\"$//g ;
      @Attributes {$scale . " stubs"} = $attrvalue ;
    }
    elsif ($attribute =~ /Unit/i)
    {
      if ($DateFormat eq "yyyy")
      {
        if (! ($attrvalue =~ /^(?:year|years)$/i))
        { &Error ("Scale attribute '$attribute' invalid. DateFormat 'yyyy' implies 'unit:year'.") ;  return ; }
      }
      else
      {
        if (! ($attrvalue =~ /^(?:year|month|day)s?$/i))
        { &Error ("Scale attribute '$attribute' invalid. Specify year, month or day.") ;  return ; }
      }
      $attrvalue =~ s/s$// ;
      @Attributes {$scale . " unit"} = $attrvalue ;
      delete (@Attributes {"unit"}) ;
    }
    elsif ($attribute =~ /Increment/i)
    {
      if ((! ($attrvalue =~ /^\d+$/i)) || ($attrvalue == 0))
      { &Error ("Scale attribute '$attribute' invalid. Specify positive integer.") ;  return ; }
      @Attributes {$scale . " inc"} = $attrvalue ;
      delete (@Attributes {"increment"}) ;
    }
    elsif ($attribute =~ /Start/i)
    {
      if (! (defined ($DateFormat)))
      { &Error ("Scale attribute '$attribute' invalid.\n" .
                "No (valid) command 'DateFormat' specified in previous lines.") ;  return ; }

      if (($DateFormat eq "dd/mm/yyyy") || ($DateFormat eq "mm/dd/yyyy"))
      {
        if (($attrvalue =~ /^\d+$/) && ($attrvalue >= 1800) && ($attrvalue <= 2030))
        { $attrvalue = "01/01/" . $attrvalue ; }
      }

      if (! &ValidDateFormat ($attrvalue))
      { &Error ("Scale attribute '$attribute' invalid.\n" .
                "Date does not conform to specified DateFormat '$DateFormat'.") ;  return ; }

      if (($DateFormat =~ /\d\d\/\d\d\/\d\d\d\d/) && (substr ($attrvalue,6,4) < 1800))
      { &Error ("Scale attribute '$attribute' invalid.\n" .
                " Specify year >= 1800.") ;  return ; }

      if (! &ValidDateRange ($attrvalue))
      { &Error ("Scale attribute '$attribute' invalid.\n" .
                "Date '$attrvalue' not within range as specified by command Period.") ; return ; }

      @Attributes {$scale . " start"} = $attrvalue ;
      delete (@Attributes {"start"}) ;
    }
    if ($DateFormat eq "yyyy") { @Attributes {$scale . " unit"} = "year" ; }
  }

  foreach $attribute (keys %Attributes)
  { @Scales {$attribute} = @Attributes {$attribute} ; }
}

sub ParseTextData
{
  &GetData ;
  if ($NoData)
  { &Error ("Data expected for command 'TextData', but line is not indented.\n") ; return ; }

  my ($pos, $tabs, $fontsize, $lineheight, $textcolor, $text, $link, $hint) ;

  TextData:
  while ((! $InputParsed) && (! $NoData))
  {
    if (! &ValidAttributes ("TextData"))
    { &GetData ; next ;}

    &CheckPreset ("TextData") ;

    $pos = "" ; $tabs = "" ; $fontsize = "" ; $lineheight = "" ; $textcolor = "" ; $link = "" ; $hint = "" ;

    if (defined (@TextDefs {"tabs"}))       { $tabs       = @TextDefs {"tabs"} ; }
    if (defined (@TextDefs {"fontsize"}))   { $fontsize   = @TextDefs {"fontsize"} ; }
    if (defined (@TextDefs {"lineheight"})) { $lineheight = @TextDefs {"lineheight"} ; }
    if (defined (@TextDefs {"textcolor"}))  { $textcolor  = @TextDefs {"textcolor"} ; }

    my $data2 = $data ;
    ($data2, $text) = &ExtractText ($data2) ;
    @Attributes = split (" ", $data2) ;

    foreach $attribute (keys %Attributes)
    {
      my $attrvalue = @Attributes {$attribute} ;

      if ($attribute =~ /^FontSize$/i)
      {
        if (($attrvalue !~ /\d+(?:\.\d)?/) && ($attrvalue !~ /^(?:xs|s|m|l|xl)$/i))
        { &Error ("TextData invalid. Attribute '$attribute': specify number of XS,S,M,L,XL.") ;
          &GetData ; next TextData ; }

        $fontsize = $attrvalue ;

        if ($fontsize !~ /^(?:xs|s|m|l|xl)$/i)
        {
          if ($fontsize < 6)
          { &Warning ("TextData attribute 'fontsize' value too low. Font size 6 assumed.\n") ;
            $fontsize = 6 ; }
          if ($fontsize > 30)
          { &Warning ("TextData attribute 'fontsize' value too high. Font size 30 assumed.\n") ;
            $fontsize = 30 ; }
        }
      }
      elsif ($attribute =~ /^LineHeight$/i)
      {
        $lineheight = &Normalize ($attrvalue) ;
        if (($lineheight < -0.4) || ($lineheight > 0.4))
        {
          if (! $bypass)
          { &Error ("TextData attribute 'lineheight' invalid.\n" .
                    "Specify value up to 40 pixels = 0.4 inch\n" .
                    "Run with option -b (bypass checks) when this is correct.\n") ; }
        }
      }
      elsif ($attribute =~ /^Pos$/i)
      {
        $attrvalue =~ s/\s*$hBrO (.*) $hBrC\s*/$1/x ;
        ($posx,$posy) = split (",", $attrvalue) ;
        $posx = &Normalize ($posx) ;
        $posy = &Normalize ($posy) ;
        $pos = "$posx,$posy" ;
      }
      elsif ($attribute =~ /^Tabs$/i)
      {
        $tabs = $attrvalue ;
      }
      elsif ($attribute =~ /^(?:Color|TextColor)$/i)
      {
        if (! &ColorPredefined ($attrvalue))
        {
          if (! defined (@Colors {lc ($attrvalue)}))
          { &Error ("TextData invalid. Attribute '$attribute' contains unknown color '$attrvalue'.\n" .
                    "  Specify command 'Color' before this command.") ;
            &GetData ; next TextData ; }
        }
        if (defined (@Colors {lc ($attrvalue)}))
        { $textcolor = @Colors { lc ($attrvalue) } ; }
        else
        { $textcolor = lc ($attrvalue) ; }
      }
      elsif ($attribute =~ /^Text$/i)
      {
        $text = $attrvalue ;
        $text =~ s/\\n/~/gs ;
        if ($text =~ /\~/)
        { &Warning ("TextData attribute 'text' contains ~ (tilde).\n" .
                    "Tilde will not be translated into newline character (only in PlotData)") ; }

      }
      elsif ($attribute =~ /^Link$/i)
      {
        $link = &ParseText ($attrvalue) ;
        $link = &EncodeURL (&NormalizeURL ($link)) ;
      }
    }

    if ($fontsize eq "")
    { $fontsize = "S" ; }

    if ($lineheight eq "")
    {
      if ($fontsize =~ /^(?:XS|S|M|L|XL)$/i)
      {
        if     ($fontsize =~ /XS/i) { $lineheight = 0.11 ; }
        elsif  ($fontsize =~ /S/i)  { $lineheight = 0.13 ; }
        elsif  ($fontsize =~ /M/i)  { $lineheight = 0.155 ; }
        elsif  ($fontsize =~ /XL/i) { $lineheight = 0.24 ; }
        else                        { $lineheight = 0.19 ; }
      }
      else
      {
        $lineheight = sprintf ("%.2f", (($fontsize * 1.2) / 100)) ;
        if ($lineheight < $fontsize/100 + 0.02)
        { $lineheight = $fontsize/100 + 0.02 ; }
      }
    }

    if ($textcolor eq "")
    { $textcolor = "black" ; }

    if ($pos eq "")
    {
      $pos = @TextDefs {"pos"} ;
      ($posx,$posy) = split (",", $pos) ;
      $posy -= $lineheight ;
      if ($posy < 0)
      { $posy = 0 ; }
      $pos = "$posx,$posy" ;
      @TextDefs {"pos"} = $pos ;
    }

#    if ($link ne "")
#    { ($text, $link, $hint) = &ProcessWikiLink ($text, $link, $hint) ; }

    if ($text eq "") # upd defaults
    {
      if ($pos        ne "") { @TextDefs {"pos"}        = $pos ; }
      if ($tabs       ne "") { @TextDefs {"tabs"}       = $tabs ; }
      if ($fontsize   ne "") { @TextDefs {"fontsize"}   = $fontsize ; }
      if ($textcolor  ne "") { @TextDefs {"textcolor"}  = $textcolor ; }
      if ($lineheight ne "") { @TextDefs {"lineheight"} = $lineheight ; }
      &GetData ; next TextData ;
    }

    if ($link ne "")
    {
      if ($text =~ /\[.*\]/)
      {
        &Warning ("TextData contains implicit link(s) in attribute 'text' and explicit attribute 'link'.\n" .
                  "Implicit link(s) ignored.") ;
        $text =~ s/\[+ (?:[^\|]* \|)? ([^\]]*) \]+/$1/gx ;
      }

      if ($hint eq "")
      { $hint = &ExternalLinkToHint ($link) ; }
    }

    if ($text =~ /\[ [^\]]* \^ [^\]]* \]/x)
    {
      &Warning ("TextData attribute 'text' contains tab character (^) inside implicit link ([[..]]). Tab ignored.") ;
      $text =~ s/(\[+ [^\]]* \^ [^\]]* \]+)/($a = $1), ($a =~ s+\^+ +g), $a/gxe ;
    }

    if (defined ($tabs) && ($tabs ne ""))
    {
      $tabs =~ s/^\s*$hBrO (.*) $hBrC\s*$/$1/x ;
      @Tabs = split (",", $tabs) ;
      foreach $tab (@Tabs)
      {
        $tab =~ s/\s* (.*) \s*$/$1/x ;
        if (! ($tab =~ /\d+\-(?:center|left|right)$/))
        { &Error ("Specify attribute 'tabs' as 'n-a,n-a,n-a,.. where n = numeric value, a = left|right|center.") ;
          while ((! $InputParsed) && (! $NoData)) { &GetData ; } return ; }
      }

      @Text = split ('\^', $text) ;
      if ($#Text > $#Tabs + 1)
      { &Error ("TextData invalid. " . $#Text . " tab characters ('^') in text, only " . ($#Tabs+1) . " tab(s) defined.") ;
        &GetData ; next TextData ; }
    }

    &WriteText ("^", "", 0, $posx, $posy, $text, $textcolor, $fontsize, "left", $link, $hint, $tabs) ;

    &GetData ;
  }
}

sub ParseTimeAxis
{
  if (! &ValidAttributes ("TimeAxis")) { return ; }

  &CheckPreset ("TimeAxis") ;

  foreach $attribute (keys %Attributes)
  {
    my $attrvalue = @Attributes {$attribute} ;


    if ($attribute =~ /Format/i)
    {
      if ($attrvalue =~ /^yy$/i)
      { &Error ("TimeAxis attribute '$attribute' valid but not available, waiting for bug fix.\n" .
                "Please specify 'format:yyyy' instead of 'format:yy'.") ;  return ; }

      if ($DateFormat eq "yyyy")
      {
        if (! ($attrvalue =~ /^(?:yy|yyyy)$/i))
        { &Error ("TimeAxis attribute '$attribute' invalid.\n" .
                  "DateFormat 'yyyy' implies 'format:yy' or 'format:yyyy'.") ;  return ; }
      }
    }

    elsif ($attribute =~ /Order/i)
    {
      if ($attrvalue !~ /^(?:normal|reverse)$/i)
      { &Error ("TimeAxis attribute '$attribute' invalid.\n" .
                "  Specify 'order:normal' (default) or 'order:reverse'\n" .
                "  normal =\n" .
                "  vertical axis: highest date on top,\n" .
                "  horizontal axis: highest date at right side\n" ) ; return ; }

      if (($attrvalue =~ /reverse/i) && ($DateFormat ne "yyyy"))
      { &Error ("TimeAxis attribute '$attribute' invalid.\n" .
                "  'order:reverse' is only possible with DateFormat=yyyy (sorry)\n") ; return ; }

      @Attributes {"order"} = lc ($attrvalue) ;
    }

    elsif ($attribute =~ /Orientation/i)
    {
      if ($attrvalue =~ /^hor(?:izontal)?$/i)
      { @Attributes {"time"} = "x" ; }
      elsif ($attrvalue =~ /^ver(?:tical)?$/i)
      { @Attributes {"time"} = "y" ; }
      else
      { &Error ("TimeAxis attribute '$attribute' invalid.\n" .
                "Specify hor[izontal] or ver[tical]") ;  return ; }
      delete (@Attributes {"orientation"}) ;
    }
  }

  if (! defined (@Attributes {"format"}))
  { @Attributes {"format"} = "yyyy" ; }

  %Axis = %Attributes ;
}

sub ParseUnknownCommand
{
  $name = $Command ;
  $name =~ s/[^a-zA-Z].*$// ;
  &Error ("Command '$name' unknown.") ;
}

sub RemoveSpaces
{
  my $text = shift ;
  $text =~ s/\s//g ;
  return ($text) ;
}

sub DetectMissingCommands
{
  if (! defined (%Image))          { &Error2 ("Command ImageSize missing or invalid") ; }
  if (! defined (%PlotArea))       { &Error2 ("Command PlotArea missing or invalid") ; }
  if (! defined ($DateFormat))     { &Error2 ("Command DateFormat missing or invalid") ; }
  if (! defined (@Axis {"time"}))  { &Error2 ("Command TimeAxis missing or invalid") ; }

  if ((@Image {"width"} =~ /auto/i) && (@Axis {"time"} =~ /x/i))
  { &Error2 ("ImageSize value 'width:auto' only allowed with TimeAxis value 'orientation:vertical'") ; }
  if ((@Image {"height"} =~ /auto/i) && (@Axis {"time"} =~ /y/i))
  { &Error2 ("ImageSize value 'height:auto' only allowed with TimeAxis value 'orientation:horizontal'") ; }
}

sub Normalize
{
  my $number    = shift ;
  my $reference = shift ;
  my ($val, $dim) ;

  if (($number eq "") || ($number =~ /auto/i))
  { return ($number) ; }

  $val = $number ; $val =~ s/[^\d\.\-].*$//g ;
  $dim = $number ; $dim =~ s/\d//g ;
  if    ($dim =~ /in/i) { $number = $val ; }
  elsif ($dim =~ /cm/i) { $number = $val / 2.54 ; }
  elsif ($dim =~ /%/)   { $number = $reference * $val / 100 ; }
  else                  { $number = $val / 100 ; }
  return (sprintf ("%.3f", $number)) ;
}

sub ValidateAndNormalizeDimensions
{
  my ($val, $dim) ;

  if (@Image {"width"} =~ /auto/i)
  {
    foreach $attribute ("width","left","right")
    { if (@PlotArea {$attribute} =~ /\%/)
      { &Error2 ("You specified 'ImageSize = width:auto'.\n" .
                 "  This implies absolute values in PlotArea attributes 'left', 'right' and/or 'width' (no \%).\n") ; return ; }
    }

    if ((@PlotArea {"width"} ne "") || (@PlotArea {"left"} eq "") || (@PlotArea {"right"} eq ""))
    { &Error2 ("You specified 'ImageSize = width:auto'.\n" .
               "  This implies  'PlotArea  = width:auto'.\n" .
               "  Instead of 'width' specify plot margins with PlotArea attributes 'left' and 'right'.\n") ; return ; }
  }


  if (@Image {"height"} =~ /auto/i)
  {
    foreach $attribute ("height","top","bottom")
    { if (@PlotArea {$attribute} =~ /\%/)
      { &Error2 ("You specified 'ImageSize = height:auto'.\n" .
                 "  This implies absolute values in PlotArea attributes 'top', 'bottom' and/or 'height' (no \%).\n") ; return ; }
    }

    if ((@PlotArea {"height"} ne "") || (@PlotArea {"top"} eq "") || (@PlotArea {"bottom"} eq ""))
    { &Error2 ("You specified 'ImageSize = height:auto'.\n" .
               "  This implies  'PlotArea  = height:auto'.\n" .
               "  Instead of 'height' specify plot margins with PlotArea attributes 'top' and 'bottom'.\n") ; return ; }
  }

  @Image    {"width"}  = &Normalize (@Image    {"width"}) ;
  @Image    {"height"} = &Normalize (@Image    {"height"}) ;
  @Image    {"barinc"} = &Normalize (@Image    {"barinc"}) ;
  @PlotArea {"width"}  = &Normalize (@PlotArea {"width"},  @Image {"width"}) ;
  @PlotArea {"height"} = &Normalize (@PlotArea {"height"}, @Image {"height"}) ;
  @PlotArea {"left"}   = &Normalize (@PlotArea {"left"},   @Image {"width"}) ;
  @PlotArea {"right"}  = &Normalize (@PlotArea {"right"},  @Image {"width"}) ;
  @PlotArea {"bottom"} = &Normalize (@PlotArea {"bottom"}, @Image {"height"}) ;
  @PlotArea {"top"}    = &Normalize (@PlotArea {"top"},    @Image {"height"}) ;

  if (@Image {"width"} =~ /auto/i)
  {
    @PlotArea {"width"} = $#Bars * @Image {"barinc"} ;
    @Image    {"width"} = @PlotArea {"left"} + @PlotArea {"width"} + @PlotArea {"right"} ;
  }

  elsif (@Image {"height"} =~ /auto/i)
  {
    @PlotArea {"height"} = $#Bars * @Image {"barinc"} ;
    @Image    {"height"} = @PlotArea {"top"} + @PlotArea {"height"} + @PlotArea {"bottom"} ;
  }

  if (@PlotArea {"right"} ne "")
  { @PlotArea {"width"} = @Image {"width"} - @PlotArea {"left"} - @PlotArea {"right"} ; }

  if (@PlotArea {"top"} ne "")
  { @PlotArea {"height"} = @Image {"height"} - @PlotArea {"top"} - @PlotArea {"bottom"} ; }

  if ((@Image {"width"} > 16) || (@Image {"height"} > 20))
  {
    if (! $bypass)
    { &Error2 ("Maximum image size is 1600x2000 pixels = 16x20 inch\n" .
               "  Run with option -b (bypass checks) when this is correct.\n") ; return ; }
  }

  if ((@Image {"width"} < 0.25) || (@Image {"height"} < 0.25))
  {
    &Error2 ("Minimum image size is 25x25 pixels = 0.25x0.25 inch\n") ;
    return ;
  }

  if (@PlotArea {"width"} > @Image {"width"})
  { &Error2 ("Plot width larger than image width. Please adjust.\n") ; return ; }

  if (@PlotArea {"width"} < 0.2)
  { &Error2 ("Plot width less than 20 pixels = 0.2 inch. Please adjust.\n") ; return ; }

  if (@PlotArea {"height"} > @Image {"height"})
  { &Error2 ("Plot height larger than image height. Please adjust.\n") ; return ; }

  if (@PlotArea {"height"} < 0.2)
  { &Error2 ("Plot height less than 20 pixels = 0.2 inch. Please adjust.\n") ; return ; }

  if (@PlotArea {"left"} + @PlotArea {"width"} > @Image {"width"})
  { &Error2 ("Plot width + margins larger than image width. Please adjust.\n") ; return ; }
#   @PlotArea {"left"} = @Image {"width"} - @PlotArea {"width"} ; }

  if (@PlotArea {"left"} < 0)
  { @PlotArea {"left"} = 0 ; }

  if (@PlotArea {"bottom"} + @PlotArea {"height"} > @Image {"height"})
  { &Error2 ("Plot height + margins larger than image height. Please adjust.\n") ; return ; }
#   @PlotArea {"bottom"} = @Image {"height"} - @PlotArea {"height"} ; }

  if (@PlotArea {"bottom"} < 0)
  { @PlotArea {"bottom"} = 0 ; }

  if ((defined (@Scales {"Major"})) ||
      (defined (@Scales {"Minor"})))
  {
    if (defined (@Scales {"Major"}))
    { $margin = 0.2 ; }
    else
    { $margin = 0.05 ; }

    if (@Axis {"time"} eq "x")
    {
      if (@PlotArea {"bottom"} < $margin)
      { &Error2 ("Not enough space below plot area for plotting time axis\n" .
                 " Specify 'PlotArea = bottom:x', where x is at least " . (100 * $margin) . " pixels = $margin inch\n") ; return ; }
    }
    else
    {
      if (@PlotArea {"left"} < $margin)
      { &Error2 ("Not enough space outside plot area for plotting time axis\n" .
                 " Specify 'PlotArea = left:x', where x is at least " . (100 * $margin) . " pixels = $margin inch\n") ; return ; }
    }
  }

  if (defined (@Legend {"orientation"}))
  {
    if (defined (@Legend {"left"}))
    { @Legend {"left"} = &Normalize (@Legend {"left"},        @Image {"width"}) ; }
    if (defined (@Legend {"top"}))
    { @Legend {"top"}  = &Normalize (@Legend {"top"},         @Image {"height"}) ; }
    if (defined (@Legend {"columnwidth"}))
    { @Legend {"columnwidth"} = &Normalize (@Legend {"columnwidth"}, @Image {"width"}) ; }

    if (! defined (@Legend {"columns"}))
    {
      @Legend {"columns"} = 1 ;
      if ((@Legend {"orientation"} =~ /ver/i) &&
          (@Legend {"position"} =~ /^(?:top|bottom)$/i))
      {
        if ($#LegendData > 10)
        {
          @Legend {"columns"} = 3 ;
          &Info2 ("Legend attribute 'columns' not defined. 3 columns assumed.") ;
        }
        elsif ($#LegendData >  5)
        {
          @Legend {"columns"} = 2 ;
          &Info2 ("Legend attribute 'columns' not defined. 2 columns assumed.") ;
        }
      }
    }

    if (@Legend {"position"} =~ /top/i)
    {
      if (! defined (@Legend {"left"}))
      { @Legend {"left"} = @PlotArea {"left"} ; }
      if (! defined (@Legend {"top"}))
      { @Legend {"top"} = (@Image {"height"} - 0.2) ; }
      if ((! defined (@Legend {"columnwidth"}))  && (@Legend {"columns"} > 1))
      { @Legend {"columnwidth"} = sprintf ("%02f", ((@PlotArea {"left"} + @PlotArea {"width"} - 0.2) / @Legend {"columns"})) ; }
    }
    elsif (@Legend {"position"} =~ /bottom/i)
    {
      if (! defined (@Legend {"left"}))
      { @Legend {"left"} = @PlotArea {"left"} ; }
      if (! defined (@Legend {"top"}))
      { @Legend {"top"} = (@PlotArea {"bottom"} - 0.4) ; }
      if ((! defined (@Legend {"columnwidth"}))  && (@Legend {"columns"} > 1))
      { @Legend {"columnwidth"} = sprintf ("%02f", ((@PlotArea {"left"} + @PlotArea {"width"} - 0.2) / @Legend {"columns"})) ; }
    }
    elsif (@Legend {"position"} =~ /right/i)
    {
      if (! defined (@Legend {"left"}))
      { @Legend {"left"} = (@PlotArea {"left"} + @PlotArea {"width"} + 0.2) ; }
      if (! defined (@Legend {"top"}))
      { @Legend {"top"} = (@PlotArea {"bottom"} + @PlotArea {"height"} - 0.2) ; }
    }
  }

  if (! defined (@Axis {"order"}))
  { @Axis {"order"} = "normal" ; }
}

sub WriteProcAnnotate
{
  my $bar         = shift ;
  my $shiftx      = shift ;
  my $xpos        = shift ;
  my $ypos        = shift ;
  my $text        = shift ;
  my $textcolor   = shift ;
  my $fontsize    = shift ;
  my $align       = shift ;
  my $link        = shift ;
  my $hint        = shift ;

  if (length ($text) > 250)
  {  &Error ("Text segments can be up to 250 characters long. This segment is " . length ($text) . " chars.\n" .
             "  You can either shorten the text or\n" .
             "  - PlotData: insert line breaks (~)\n" .
             "  - TextData: insert tabs (~) to produce columns\n") ; return ; }

  if ($textcolor eq "")
  { $textcolor = "black" ; }

  my $textdetails = "  textdetails: align=$align size=$fontsize color=$textcolor"  ;

  push @PlotTextsPng, "#proc annotate\n" ;
  push @PlotTextsSvg, "#proc annotate\n" ;

  push @PlotTextsPng, "  location: $xpos $ypos\n" ;
  push @PlotTextsSvg, "  location: $xpos $ypos\n" ;

  push @PlotTextsPng, $textdetails . "\n" ;
  push @PlotTextsSvg, $textdetails . "\n" ;

  $text2 = $text ;
  $text2 =~ s/\[\[//g ;
  $text2 =~ s/\]\]//g ;
  if ($text2 =~ /^\s/)
  { push @PlotTextsPng, "  text: \n\\$text2\n\n"  ; }
  else
  { push @PlotTextsPng, "  text: $text2\n\n"  ; }

  $text2 = $text ;
  if ($link ne "")
  {
    # put placeholder in Ploticus input file
    # will be replaced by real link after SVG generation
    # this allows adding color info
    push @linksSVG, &DecodeInput ($link) ;
    my $lcnt = $#linksSVG ;
    $text2 =~ s/\[\[ ([^\]]+) \]\]/\[$lcnt\[$1\]$lcnt\]/x ;
    $text2 =~ s/\[\[ ([^\]]+) $/\[$lcnt\[$1\]$lcnt\]/x ;
    $text2 =~ s/^ ([^\[]+) \]\]/\[$lcnt\[$1\]$lcnt\]/x ;
  }

  $text3 = &EncodeHtml ($text2) ;
  if ($text2 ne $text3)
  {
    # put placeholder in Ploticus input file
    # will be replaced by real text after SVG generation
    # Ploticus would autoscale image improperly when text contains &#xxx; tags
    # because this would count as 5 chars
    push @textsSVG, &DecodeInput ($text3) ;
    $text3 = "{{" . $#textsSVG . "}}" ;
    while (length ($text3) < length ($text2)) { $text3 .= "x" ; }
  }

  if ($text3 =~ /^\s/)
  { push @PlotTextsSvg, "  text: \n\\$text3\n\n"  ; }
  else
  { push @PlotTextsSvg, "  text: $text3\n\n"  ; }

  if ($link ne "")
  {
    $MapPNG = $true ;

    push @PlotTextsPng, "#proc annotate\n" ;
    push @PlotTextsPng, "  location: $xpos $ypos\n" ;

#   push @PlotTextsPng, "  boxmargin: 0.01\n" ;

    if ($align ne "right")
    {
      push @PlotTextsPng, "  clickmapurl: $link\n" ;
      if ($hint ne "")
      { push @PlotTextsPng, "  clickmaplabel: $hint\n" ; }
    }
    else
    {
      if ($bar eq "")
      {
        if ($WarnOnRightAlignedText ++ == 0)
        { &Warning2 ("Links on right aligned texts are only supported for svg output,\npending Ploticus bug fix.") ; }
        return ;
      }
      else
      {
        push @PlotTextsPng, "  clickmapurl: $link\&\&$shiftx\n" ;
        if ($hint ne "")
        { push @PlotTextsPng, "  clickmaplabel: $hint\n" ; }
      }
    }

    $textdetails =~ s/color=[^\s]+/color=$LinkColor/ ;
    push @PlotTextsPng, $textdetails . "\n" ;

    $text = &DecodeInput ($text) ;
    if ($text =~ /^[^\[]+\]\]/)
    { $text = "[[" . $text ; }
    if ($text =~ /\[\[[^\]]+$/)
    { $text .= "]]"  ; }
    my $pos1 = index ($text, "[[") ;
    my $pos2 = index ($text, "]]") + 1 ;
    if (($pos1 > -1) && ($pos2 > -1))
    {
      for (my $i = 0 ; $i < length ($text) ; $i++)
      {
        $c = substr ($text, $i, 1) ;
        if ($c ne "\n")
        {
          if (($i < $pos1) || ($i > $pos2))
          { substr ($text, $i, 1) = " " ; }
        }
      }
    }

    $text =~ s/\[\[(.*?)\]\]/$1/s ;

    if ($text =~ /^\s/)
    { push @PlotTextsPng, "  text: \n\\$text\n\n"  ; }
    else
    { push @PlotTextsPng, "  text: $text\n\n"  ; }

#    push @PlotTextsPng, "#proc rect\n" ;
#    push @PlotTextsPng, "  color: green\n" ;
#    push @PlotTextsPng, "  rectangle: 1(s)+0.25 1937.500(s)+0.06 1(s)+0.50 1937.500(s)+0.058\n" ;
#    push @PlotTextsPng, "\n\n" ;
  }
}

sub WriteText
{
  my $mode      = shift ;
  my $bar       = shift ;
  my $shiftx    = shift ;
  my $posx      = shift ;
  my $posy      = shift ;
  my $text      = shift ;
  my $textcolor = shift ;
  my $fontsize  = shift ;
  my $align     = shift ;
  my $link      = shift ;
  my $hint      = shift ;
  my $tabs      = shift ;
  my ($link2, $hint2, $tab) ;
  my $outside = $false ;
  if (@Axis {"order"} =~ /reverse/i)
  {
    if (@Axis {"time"} eq "y")
    { $posy =~ s/(.*)(\(s\))/(-$1).$2/xe ; }
    else
    { $posx =~ s/(.*)(\(s\))/(-$1).$2/xe ; }
  }

  if ($posx !~ /\(s\)/)
  {
     if ($posx < 0)
     { $outside = $true ; }
     if (@Image {"width"}  !~ /auto/i)
     {
       if ($posx > @Image {"width"}/100)
       { $outside = $true ; }
     }
  }
  if ($posy !~ /\(s\)/)
  {
     if ($posy < 0)
     { $outside = $true ; }
     if (@Image {"height"}  !~ /auto/i)
     {
       if ($posy > @Image {"height"}/100)
       { $outside = $true ; }
     }
  }
  if ($outside)
  {
    if ($WarnTextOutsideArea++ < 5)
    { $text =~ s/\n/~/g ;
      &Error ("Text segment '$text' falls outside image area. Text ignored.") ; }
    return ;
  }

  my @Tabs = split (",", $tabs) ;
  foreach $tab (@Tabs)
  { $tab =~ s/\s* (.*) \s*$/$1/x ; }

  $posx0 = $posx ;
  my @Text ;
  my $dy = 0 ;

  if ($text =~ /\[\[.*\]\]/)
  {
    $link = "" ; $hint = "" ;
  }

  my @Text ;
  if ($mode eq "^")
  { @Text = split ('\^', $text) ; }
  elsif ($mode eq "~")
  {
    @Text = split ('\n', $text) ;

    if ($fontsize =~ /^(?:XS|S|M|L|XL)$/i)
    {
      if     ($fontsize =~ /XS/i) { $dy = 0.09 ; }
      elsif  ($fontsize =~ /S/i)  { $dy = 0.11 ; }
      elsif  ($fontsize =~ /M/i)  { $dy = 0.135 ; }
      elsif  ($fontsize =~ /XL/i) { $dy = 0.21 ; }
      else                        { $dy = 0.16 ; }
    }
    else
    {
      $dy = sprintf ("%.2f", (($fontsize * 1.2) / 100)) ;
      if ($dy < $fontsize/100 + 0.02)
      { $dy = $fontsize/100 + 0.02 ; }
    }
  }
  else
  { push @Text, $text ; }


  foreach $text (@Text)
  {
    if ($text !~ /^[\n\s]*$/)
    {
      $link2 = "" ;
      $hint2 = "" ;
      ($text, $link2, $hint2) = &ProcessWikiLink ($text, $link2, $hint2) ;

      if ($link2 eq "")
      {
        $link2 = $link ;
        if (($link ne "") && ($text !~ /\[\[.*\]\]/))
        { $text = "[[" . $text . "]]" ;}
      }
      if ($hint2 eq "")
      { $hint2 = $hint ; }

      &WriteProcAnnotate ($bar, $shiftx, $posx, $posy, $text, $textcolor, $fontsize, $align, $link2, $hint2) ;
    }

    if ($#Tabs >= 0)
    {
      $tab = shift (@Tabs) ;
      ($dx,$align) = split ("\-", $tab) ;
      $posx = $posx0 + &Normalize ($dx) ;
    }
    if ($posy =~ /\+/)
    { ($posy1, $posy2) = split ('\+', $posy) ; }
    elsif ($posy =~ /.+\-/)
    {
      if ($posy =~ /^\-/)
      {
        ($sign, $posy1, $posy2) = split ('\-', $posy) ; $posy2 = -$posy2 ;
        $posy1 = "-" . $posy1 ;
      }
      else
      { ($posy1, $posy2) = split ('\-', $posy) ; $posy2 = -$posy2 ; }
    }
    else
    { $posy1 = $posy ; $posy2 = 0 ; }

    $posy2 -= $dy ;

    if ($posy2 == 0)
    { $posy = $posy1 ; }
    elsif ($posy2 < 0)
    { $posy = $posy1 . "$posy2" ; }
    else
    { $posy = $posy1 . "+" . $posy2 ; }
  }
}

sub WriteProcDrawCommandsOld
{
  my $posx      = shift ;
  my $posy      = shift ;
  my $text      = shift ;
  my $textcolor = shift ;
  my $fontsize  = shift ;
  my $link      = shift ;
  my $hint      = shift ;

  $posx0 = $posx ;
  my @Text = split ('\^', $text) ;
  my $align = "text" ;
  foreach $text (@Text)
  {
    push @TextData, "  mov $posx $posy\n" ;
    push @TextData, "  textsize $fontsize\n" ;
    push @TextData, "  color $textcolor\n" ;
    push @TextData, "  $align $text\n" ;


    $tab = shift (@Tabs) ;
    ($dx,$align) = split ("\-", $tab) ;
    $posx = $posx0 + &Normalize ($dx) ;
    if    ($align =~ /left/i)  { $align = "text" ; }
    elsif ($align =~ /right/i) { $align = "rightjust" ; }
    else                       { $align = "centext" ; }
  }
}

sub WritePlotFile
{
  &WriteTexts ;

  $script = "" ;
  my ($color) ;
  if (@Axis {"time"} eq "x")
  { $AxisBars = "y" ; }
  else
  { $AxisBars = "x" ; }

#  if ((@Axis {"time"} eq "y") && ($#Bars > 0))
#  {
#    undef @BarsTmp ;
#    while ($#Bars >= 0)
#    { push @BarsTmp, pop @Bars ; }
#    @Bars = @BarsTmp ;
#  }

  if ($tmpdir ne "")
  { $file_script = $tmpdir.$pathseparator."EasyTimeline.txt.$$" ; }
  else
  { $file_script = "EasyTimeline.txt" ; }

  print "Ploticus input file = ".$file_script."\n";

  # $fmt = "gif" ;
  open "FILE_OUT", ">", $file_script ;

  #proc settings
#  $script .= "#proc settings\n" ;
#  $script .= "  xml_encoding: utf-8\n" ;
#  $script .= "\n" ;

  # proc page
  $script .= "#proc page\n" ;
  $script .= "  dopagebox: no\n" ;
  $script .= "  pagesize: ". @Image {"width"} . " ". @Image {"height"} . "\n" ;
  if (defined (@BackgroundColors {"canvas"}))
  { $script .= "  backgroundcolor: " . @BackgroundColors {"canvas"} . "\n" ; }
  $script .= "\n" ;

  $barcnt = $#Bars + 1 ;

# if ($AlignBars eq "justify") && ($#Bars > 0)
#
# given P = plotwidth in pixels
# given B = half bar width in pixels
# get   U = plotwidth in units
# get   x = half bar width in units
#
# first bar plotted at unit 1
# last  bar plotted at unit c
# let   C = c - 1 (units between centers of lowest and highest bar) -> x = (U-C) / 2
#
# Justify: calculate range for axis in units:
# axis starts at 1-x and ends at c+x =
# x/B = U/P                    -> x = BU/P         (1)
# U = c+x - (1-x) = (c-1) + 2x -> x = (U-(c-1))/2  (2)
#
# (1) & (2)   ->  BU/P = (U-(c-1))/2
#             -> 2BU/P =  U-(c-1)
#             -> 2BU/P =  U - C
#             -> 2BU   = PU - PC
#             -> U (2B-P) = -PC
#             -> U = -PC/(2B-P)
# P = @PlotArea {$extent}
# C = c - 1 = $#Bars
# 2B = $MaxBarWidth
  if (! defined ($AlignBars))
  {
    &Info2 ("AlignBars not defined. Alignment 'early' assumed.") ;
    $AlignBars = "early" ;
  }

  if (@Axis {"time"} eq "x")
  { $extent = "height" ; }
  else
  { $extent = "width" ; }

  if ($MaxBarWidth > @PlotArea {$extent})
  { &Error2 ("Maximum bar width exceeds plotarea " . $extent . ".") ; return ; }

  if ($MaxBarWidth == @PlotArea {$extent})
  { @PlotArea {$extent} += 0.01 ; }

  if ($MaxBarWidth == @PlotArea {$extent})
  {
    $till = 1 ;
    $from = 1 ;
  }
  else
  {
    if ($AlignBars eq "justify")
    {
      if ($#Bars > 0)
      {
        $U = - (@PlotArea {$extent} * $#Bars)  / ($MaxBarWidth - @PlotArea {$extent}) ;
        $x = ($U - $#Bars) / 2 ;
        $from = 1 - $x ;
        $till = 1 + $#Bars + $x ;
      }
      else # one bar-> "justify" is misnomer here, treat as "center"
      {
        # $x = ($MaxBarWidth /2) / @PlotArea {$extent} ;
        # $from = 0.5 - $x ;
        # $till = $from + 1 ;
        $from = 0.5 ;
        $till = 1.5 ;
      }
    }
    elsif ($AlignBars eq "early")
    {
      $U = $#Bars + 1 ;
      if ($U == 0)
      { $U = 1 ; }
      $x = (($MaxBarWidth /2) * $U) / @PlotArea {$extent} ;
      $from = 1 - $x ;
      $till = $from + $U ;
    }
    elsif ($AlignBars eq "late")
    {
      $U = $#Bars + 1 ;
      $x = (($MaxBarWidth /2) * $U) / @PlotArea {$extent} ;
      $till = $U + $x ;
      $from = $till - $U ;
    }
  }

#  if ($#Bars == 0)
#  {
#    $from = 1 - $MaxBarWidth ;
#    $till = 1 + $MaxBarWidth ;
#  }
  if ($from eq $till)
  { $till = $from + 1 ; }

  #proc areadef
  $script .= "#proc areadef\n" ;
  $script .= "  rectangle: " . @PlotArea {"left"} . " " . @PlotArea {"bottom"} . " " .
                   sprintf ("%.2f", @PlotArea {"left"} + @PlotArea {"width"}). " " . sprintf ("%.2f", @PlotArea {"bottom"} + @PlotArea {"height"}) . "\n" ;
  if (($DateFormat eq "yyyy") || ($DateFormat eq "x.y"))
  { $script .= "  " . @Axis {"time"} . "scaletype: linear\n" ; } # date yyyy
  else
  { $script .= "  " . @Axis {"time"} . "scaletype: date $DateFormat\n" ; }

  if (@Axis {"order"} !~ /reverse/i)
  { $script .= "  " . @Axis {"time"} . "range: " . @Period{"from"} . " " . @Period{"till"} . "\n" ; }
  else
  { $script .= "  " . @Axis {"time"} . "range: " . (-@Period{"till"}) . " " . (-@Period{"from"}) . "\n" ; }

  $script .= "  " . $AxisBars . "scaletype: linear\n" ;
  $script .= "  " . $AxisBars . "range: " . sprintf ("%.3f", $from-0.001) . " " . sprintf ("%.3f", $till) . "\n" ;
  $script .= "  #saveas: A\n" ;
  $script .= "\n" ;

  #proc rect (test)
#  $script .= "#proc rect\n" ;
#  $script .= "  rectangle 1.0 1.0 1.4 1.4\n" ;
#  $script .= "  color gray(0.95)\n" ;
#  $script .= "  clickmaplabel: Vladimir Ilyich Lenin\n" ;
#  $script .= "  clickmapurl: http://www.wikipedia.org/wiki/Vladimir_Lenin\n" ;


  #proc legendentry
  foreach $color (sort keys %Colors)
  {
    $script .= "#proc legendentry\n" ;
    $script .= "  sampletype: color\n" ;

    if ((defined (@ColorLabels {$color})) && (@ColorLabels {$color} ne ""))
    { $script .= "  label: " . @ColorLabels {$color} . "\n" ; }
    $script .= "  details: " . @Colors {$color} . "\n" ;
    $script .= "  tag: $color\n" ;
    $script .= "\n" ;
  }

  if (defined (@BackgroundColors {"bars"}))
  {
    #proc getdata / #proc bars
    $script .= "#proc getdata\n" ;
    $script .= "  delim: comma\n" ;
    $script .= "  data:\n" ;

    $maxwidth = 0 ;
    foreach $entry (@PlotBars)
    {
      ($width) = split (",", $entry) ;
      if ($width > $maxwidth)
      { $maxwidth = $width ; }
    }

    for ($b = 0 ; $b <= $#Bars ; $b++)
    { $script .= ($b+1) . "," . @Period {"from"} . "," . @Period {"till"} . ",".
                 @BackgroundColors {"bars"} . "\n" ; }
    $script .= "\n" ;

    #proc bars
    $script .= "#proc bars\n" ;
    $script .= "  axis: " . @Axis {"time"} . "\n" ;
    $script .= "  barwidth: $maxwidth\n" ;
    $script .= "  outline: no\n" ;
    if (@Axis {"time"} eq "x")
    { $script .= "  horizontalbars: yes\n" ; }
    $script .= "  locfield: 1\n" ;
    $script .= "  segmentfields: 2 3\n" ;
    $script .= "  colorfield: 4\n" ;

#   $script .= "  clickmaplabel: Vladimir Ilyich Lenin\n" ;
#   $script .= "  clickmapurl: http://www.wikipedia.org/wiki/Vladimir_Lenin\n" ;

    $script .= "\n" ;
  }

  #proc axis
  if (defined (@Scales {"Minor grid"}))
  { &PlotScale ("Minor", $true) ; }
  if (defined (@Scales {"Major grid"}))
  { &PlotScale ("Major", $true) ; }

  &PlotLines ("back") ;

  @PlotBarsNow = @PlotBars ;
  &PlotBars ;

  $script .= "\n([inc3])\n\n" ; # will be replace by rects

%x = %BarWidths ;
  foreach $entry (@PlotLines)
  {
   ($bar) = split (",", $entry) ;
   $bar =~ s/\#.*// ;
   $width = @BarWidths {$bar} ;
   $entry  = sprintf ("%6.3f",$width) . "," . $entry ;
  }

  @PlotBarsNow = @PlotLines ;
  &PlotBars ;

  #proc axis
  if ($#Bars > 0)
  {
    $scriptPng2 = "#proc " . $AxisBars . "axis\n" ;
    $scriptSvg2 = "#proc " . $AxisBars . "axis\n" ;
    if ($AxisBars eq "x")
    {
      $scriptPng2 .= "  stubdetails: adjust=0,0.09\n" ;
      $scriptSvg2 .= "  stubdetails: adjust=0,0.09\n" ;
    }
    else
    {
      $scriptPng2 .= "  stubdetails: adjust=0.09,0\n" ;
      $scriptSvg2 .= "  stubdetails: adjust=0.09,0\n" ;
    }
    $scriptPng2 .= "  tics: none\n" ;
    $scriptSvg2 .= "  tics: none\n" ;
    $scriptPng2 .= "  stubrange: 1\n" ;
    $scriptSvg2 .= "  stubrange: 1\n" ;
    if ($AxisBars eq "y")
    {
      $scriptPng2 .= "  stubslide: -" . sprintf ("%.2f", $MaxBarWidth / 2) . "\n" ;
      $scriptSvg2 .= "  stubslide: -" . sprintf ("%.2f", $MaxBarWidth / 2) . "\n" ;
    }
    $scriptPng2 .= "  stubs: text\n" ;
    $scriptSvg2 .= "  stubs: text\n" ;

    my ($text, $link, $hint) ;

    undef (@Bars2) ;
    foreach $bar (@Bars)
    {
      if ($AxisBars eq "y")
      { push @Bars2, $bar ; }
      else
      { unshift @Bars2, $bar ; }
    }

    foreach $bar (@Bars2)
    {
      $hint = "" ;
      $text = @BarLegend {lc ($bar)} ;
      if ($text =~ /^\s*$/)
      { $text = "\\" ; }

      $link = @BarLink {lc ($bar)} ;
      if (! defined ($link))
      {
        if ($text =~ /\[.*\]/)
        { ($text, $link, $hint) = &ProcessWikiLink ($text, $link, $hint) ; }
      }

      $text =~ s/\[+([^\]]*)\]+/$1/ ;
      $scriptPng2 .= "$text\n" ;
      if (defined ($link))
      {
        push @linksSVG, $link ;
        my $lcnt = $#linksSVG ;
        $scriptSvg2 .= "[" . $lcnt . "[" . $text . "]" . $lcnt . "]\n" ;
      }
      else
      { $scriptSvg2 .= "$text\n" ; }
    }
    $scriptPng2 .= "\n" ;
    $scriptSvg2 .= "\n" ;

    $scriptPng2 .= "#proc " . $AxisBars . "axis\n" ;
    if ($AxisBars eq "x")
    { $scriptPng2 .= "  stubdetails: adjust=0,0.09 color=$LinkColor\n" ; }
    else
    { $scriptPng2 .= "  stubdetails: adjust=0.09,0 color=$LinkColor\n" ; }
    $scriptPng2 .= "  tics: none\n" ;
    $scriptPng2 .= "  stubrange: 1\n" ;
    if ($AxisBars eq "y")
    { $scriptPng2 .= "  stubslide: -" . sprintf ("%.2f", $MaxBarWidth / 2) . "\n" ; }
    $scriptPng2 .= "  stubs: text\n" ;

    $barcnt = $#Bars + 1 ;
    foreach $bar (@Bars2)
    {
      $hint = "" ;
      $text = @BarLegend {lc ($bar)} ;
      if ($text =~ /^\s*$/)
      { $text = "\\" ; }

      $link = @BarLink {lc ($bar)} ;
      if (! defined ($link))
      {
        if ($text =~ /\[.*\]/)
        { ($text, $link, $hint) = &ProcessWikiLink ($text, $link, $hint) ; }
      }
      if ((! defined ($link)) || ($link eq ""))
      { $text = "\\" ; }
      else
      {
        $scriptPng3 .= "#proc rect\n" ;
        $scriptPng3 .= "  rectangle: 0 $barcnt(s)+0.05 " . @PlotArea {"left"} . " $barcnt(s)-0.05\n" ;
        $scriptPng3 .= "  color: " . @BackgroundColors {"canvas"} . "\n" ;
        $scriptPng3 .= "  clickmapurl: " . $link . "\n" ;
        if ((defined ($hint)) && ($hint ne ""))
        { $scriptPng3 .= "  clickmaplabel: " . $hint . "\n" ; }

        $text =~ s/\[+([^\]]*)\]+/$1/ ;
      }
      $scriptPng2 .= "$text\n" ;

      $barcnt-- ;
    }
    $scriptPng2 .= "\n" ;
  }

  &PlotLines ("front") ;

  $script .= "\n([inc1])\n\n" ; # will be replaced by annotations
  $script .= "\n([inc2])\n\n" ;


  if ($#PlotTextsPng >= 0)
  {
    foreach $command (@PlotTextsPng)
    {
      if ($command =~ /^\s*location/)
      { $command =~ s/(.*)\[(.*)\](.*)/$1 . ($#Bars - $2 + 2) . $3/xe ; }

      $scriptPng1 .= $command ;
    }
    $scriptPng1 .= "\n" ;
  }

  if ($#PlotTextsSvg >= 0)
  {
    foreach $command (@PlotTextsSvg)
    {
      if ($command =~ /^\s*location/)
      { $command =~ s/(.*)\[(.*)\](.*)/$1 . ($#Bars - $2 + 2) . $3/xe ; }

      $scriptSvg1 .= $command ;
    }
    $scriptSvg1 .= "\n" ;
  }

# $script .= "#proc symbol\n" ;
# $script .= "  location: 01/01/1943(s) Korea \n" ;
# $script .= "  symbol: style=fill shape=downtriangle fillcolor=white radius=0.04\n" ;
# $script .= "\n" ;

  #proc axis
  # repeat without grid to get axis on top of bar
  # needed because axis may overlap bar slightly
  if (defined (@Scales {"Minor"}))
  { &PlotScale ("Minor", $false) ; }
  if (defined (@Scales {"Major"}))
  { &PlotScale ("Major", $false) ; }

  #proc drawcommands
  if ($#TextData >= 0)
  {
    $script .= "#proc drawcommands\n" ;
    $script .= "  commands:\n" ;
    foreach $entry (@TextData)
    { $script .= $entry ; }
    $script .= "\n" ;
  }

  #proc legend
  if (defined (@Legend {"orientation"}))
  {
    if (($#LegendData < 0) && ($Preset eq ""))
    { &Error2 ("Command 'Legend' found, but no entries for the legend were specified.\n" .
               "  Please remove or disable command (disable = put \# before the command)\n" .
               "  or specify entries for the legend with command 'Colors', attribute 'legend'\n") ;
      return ; }

    $perColumn = 999 ;
    if (@Legend {"orientation"}  =~ /ver/i)
    {
      if (@Legend {"columns"} > 1)
      {
        $perColumn   = 0 ;
        while ((@Legend {"columns"} * $perColumn) < $#LegendData + 1)
        { $perColumn ++ ; }
      }
    }

    for ($l = 1 ; $l <= @Legend {"columns"} ; $l++)
    {
      $script .= "#proc legend\n" ;
      $script .= "  noclear: yes\n" ;
      if (@Legend {"orientation"}  =~ /ver/i)
      { $script .= "  format: multiline\n" ; }
      else
      { $script .= "  format: singleline\n" ; }
      $script .= "  seglen: 0.2\n" ;
      $script .= "  swatchsize: 0.12\n" ;
      $script .= "  textdetails: size=S\n" ;
      $script .= "  location: " . (@Legend{"left"}+0.2) . " " . @Legend{"top"} . "\n" ;
      $script .= "  specifyorder:\n" ;
      for ($l2 = 1 ; $l2 <= $perColumn ; $l2++)
      {
        $category = shift (@LegendData) ;
        if (defined ($category))
        { $script .= "$category\n" ; }
      }
      $script .= "\n" ;
      @Legend {"left"} += @Legend {"columnwidth"} ;
    }
  }

  $script .= "#endproc\n" ;

  print "\nGenerating output:\n" ;
  if ( $plcommand ne "" )
  { $pl = $plcommand; }
  else
  {
    $pl = "pl.exe" ;
    if ($env eq "Linux")
    { $pl = "pl" ; }
  }

  print "Using ploticus command \"".$pl."\" (".$plcommand.")\n";

  $script_save = $script ;

  $script =~ s/\(\[inc1\]\)/$scriptSvg1/ ;
  $script =~ s/\(\[inc2\]\)/$scriptSvg2/ ;
  $script =~ s/\(\[inc3\]\)// ;

  $script =~ s/textsize XS/textsize 7/gi ;
  $script =~ s/textsize S/textsize 8.9/gi ;

  $script =~ s/textsize M/textsize 10.5/gi ;
  $script =~ s/textsize L/textsize 13/gi ;
  $script =~ s/textsize XL/textsize 17/gi ;
  $script =~ s/size=XS/size=7/gi ;
  $script =~ s/size=S/size=8.9/gi ;
  $script =~ s/size=M/size=10.5/gi ;
  $script =~ s/size=L/size=13/gi ;
  $script =~ s/size=XL/size=17/gi ;


  $script =~ s/(\n  location:.*)/&ShiftOnePixelForSVG($1)/ge ;

  open "FILE_OUT", ">", $file_script ;
  print FILE_OUT &DecodeInput($script) ;
  close "FILE_OUT" ;

  $map = ($MapSVG) ? "-map" : "";

  print "Running Ploticus to generate svg file\n" ;
# my $cmd = "$pl $map -" . "svg" . " -o $file_vector $file_script -tightcrop -font \"Times\"" ;
# my $cmd = "$pl $map -" . "svg" . " -o $file_vector $file_script -tightcrop" ;
  my $cmd = EscapeShellArg($pl) . " $map -" . "svg" . " -o " .
    EscapeShellArg($file_vector) . " " . EscapeShellArg($file_script) . " -tightcrop" ;
  print "$cmd\n";
  system ($cmd) ;

  $script = $script_save ;
  $script =~ s/dopagebox: no/dopagebox: yes/ ;

  $script =~ s/\(\[inc1\]\)/$scriptPng1/ ;
  $script =~ s/\(\[inc2\]\)/$scriptPng2/ ;
  $script =~ s/\(\[inc3\]\)/$scriptPng3/ ;

  $script =~ s/textsize XS/textsize 6/gi ;
  $script =~ s/textsize S/textsize 8/gi ;
  $script =~ s/textsize M/textsize 10/gi ;
  $script =~ s/textsize L/textsize 14/gi ;
  $script =~ s/textsize XL/textsize 18/gi ;
  $script =~ s/size=XS/size=6/gi ;
  $script =~ s/size=S/size=8/gi ;
  $script =~ s/size=M/size=10/gi ;
  $script =~ s/size=L/size=14/gi ;
  $script =~ s/size=XL/size=18/gi ;

  open "FILE_OUT", ">", $file_script ;
  print FILE_OUT &DecodeInput($script) ;
  close "FILE_OUT" ;

  $map = ($MapPNG && $linkmap) ? "-csmap" : "";
  if ($linkmap && $showmap)
  { $map .= " -csmapdemo" ; }

# $crop = "-crop 0,0," + @ImageSize {"width"} . "," . @ImageSize {"height"} ;
  print "Running Ploticus to generate bitmap\n" ;
# $cmd = "$pl $map -" . $fmt . " -o $file_bitmap $file_script -tightcrop" ; # -v $file_bitmap" ;
# $cmd = "$pl $map -" . $fmt . " -o $file_bitmap $file_script -tightcrop -diagfile $file_pl_info -errfile $file_pl_err" ;
  $cmd = EscapeShellArg($pl) . " $map -" . $fmt . " -o " .
    EscapeShellArg($file_bitmap) . " " . EscapeShellArg($file_script) . " -tightcrop -font FreeSans.ttf" .
    " -mapfile " . EscapeShellArg($file_htmlmap) ;
  print "$cmd\n";
  system ($cmd) ;

  if ((-e $file_bitmap) && (-s $file_bitmap > 500 * 1024))
  {
    &Error2 ("Output image size exceeds 500 K. Image deleted.\n" .
             "Run with option -b (bypass checks) when this is correct.\n") ;
    unlink $file_bitmap ;
  } ;

  # not for Wikipedia, only for offline use:
  if ((-e $file_bitmap) && ($fmt eq "gif"))
  {
    print "Running nconvert to convert gif image to png format\n\n" ;
    print "---------------------------------------------------------------------------\n" ;
    $cmd = "nconvert.exe -out png " . EscapeShellArg($file_bitmap) ;
    system ($cmd) ;
    print "---------------------------------------------------------------------------\n" ;

    if (! (-e $file_png))
    { print "PNG file not created (is nconvert.exe missing?)\n\n" ; }
  }

  if (-e $file_htmlmap) # correct click coordinates of right aligned texts (Ploticus bug)
  {
    open "FILE_IN", "<", $file_htmlmap ;
    @map = <FILE_IN> ;
    close "FILE_IN" ;

    foreach $line (@map)
    {
      chomp $line ;
      if ($line =~ /\&\&/)
      {
        $coords = $line ;
        $shift  = $line ;
        $coords =~ s/^.*coords\=\"([^\"]*)\".*$/$1/ ;
        $shift  =~ s/^.*\&\&([^\"]*)\".*$/$1/ ;
        $line   =~ s/\&\&[^\"]*// ;
        (@updcoords) = split (",", $coords) ;
        $maplength = @updcoords [2] - @updcoords [0] ;
        @updcoords [0] = @updcoords [0] - 2 * ($maplength-25) ;
        @updcoords [2] = @updcoords [0] + $maplength ;
        $coordsnew = join (",", @updcoords) ;
        $line   =~ s/$coords/$coordsnew/ ;
        push @map2, $line . "\n" ;
      }
      else
      { push @map2, $line . "\n" ; }
    }

    open "FILE_OUT", ">", $file_htmlmap ;
    print FILE_OUT @map2 ;
    close "FILE_OUT" ;
  }

  if (-e $file_vector)
  {
    open "FILE_IN", "<", $file_vector ;
    @svg = <FILE_IN> ;
    close "FILE_IN" ;

    foreach $line (@svg)
    {
      $line =~ s/\{\{(\d+)\}\}x+/@textsSVG[$1]/gxe ;
      $line =~ s/\[(\d+)\[ (.*?) \]\d+\]/'<a style="fill:blue;" xlink:href="' . @linksSVG[$1] . '">' . $2 . '<\/a>'/gxe ;
    }

    open "FILE_OUT", ">", $file_vector ;
    print FILE_OUT @svg ;
    close "FILE_OUT" ;
  }

  # not for Wikipedia, for offline use:
  if ($makehtml)
  {
    $map = "" ;
    if ($linkmap)
    {
      open "FILE_IN", "<", $file_htmlmap ;
      while ($line = <FILE_IN>)
      { $map .= $line ; }
      close "FILE_IN" ;
    }
    print "Generating html test file\n" ;
    $width  = sprintf ("%.0f", @Image {"width"}  * 100) ;
    $height = sprintf ("%.0f", @Image {"height"} * 100) ;
    $html = <<__HTML__ ;

<html>
<head>
<title>%FILENAME% - EasyTimeline test file</title>\n
</head>

<body>
<h1><font color="green">EasyTimeline</font> - Test Page</h1>

<b>Fixed size version (PNG): file $file_png</b><p>
<map name="map1">
$map</map>

<!--
If you want a border simplest way is set <img .. border='1'>
Here tables are used to draw similar borders around both images (border='1' seems not to work for embed tag)
-->

<table border='1' cellpadding='0' cellspacing='0'><tr><td>
<img src=$file_png usemap='#map1' border='0'>
</td></tr></table>

<hr>
<b>Scalable version (SVG): file $file_vector</b><p>
<table border='1' cellpadding='0' cellspacing='0'><tr><td>
<noembed>Your browser does not support embedded objects</noembed>
<embed src='$file_vector' name='SVGEmbed' border='1'
width='$width' height='$height' type='image/svg-xml' pluginspage='http://www.adobe.com/svg/viewer/install/'>
</td></tr></table>

<p>As you can see the scalable version renders fonts smoother better than the bitmap version.
<br>Any SVG picture can also be rescaled or zoomed into, without annoying artefacts.

<p>Windows users:<br>
<small>&nbsp;&nbsp;Right mouse click on picture for zoom options or</small>
<p><small>&nbsp;&nbsp;Ctrl+click for zoom in</small>
<br><small>&nbsp;&nbsp;Ctrl+Shift+click for zoom out</small>
<br><small>&nbsp;&nbsp;Alt+drag with mouse to move focus</small>

</body>
</html>

__HTML__

    $html =~ s/\%FILENAME\%/$file_name/ ;

    open "FILE_OUT", ">", $file_html ;
    print FILE_OUT $html ;
    close "FILE_OUT" ;
  }
#  my $cmd = "\"c:\\\\Program Files\\\\XnView\\\\xnview.exe\"" ;
#  system ("\"c:\\\\Program Files\\\\XnView\\\\xnview.exe\"", "d:\\\\Wikipedia\\Perl\\\\Wo2\\\\Test.png") ;
}

sub WriteTexts
{
  my ($line, $xpos, $ypos) ;
  foreach $line (@PlotText)
  {
    my ($at, $bar, $text, $textcolor, $fontsize, $align, $shift, $link, $hint) = split (",", $line) ;
    $text      =~ s/\#\%\$/\,/g ;
    $link      =~ s/\#\%\$/\,/g ;
    $hint      =~ s/\#\%\$/\,/g ;
    $shift     =~ s/\#\%\$/\,/g ;
    $textcolor =~ s/\#\%\$/\,/g ;

    my $barcnt = 0 ;
    for ($b = 0 ; $b <= $#Bars ; $b++)
    {
      if (lc(@Bars [$b]) eq lc($bar))
      { $barcnt = ($b + 1) ; last ; }
    }

    if (@Axis {"time"} eq "x")
    { $xpos = "$at(s)" ; $ypos = "[$barcnt](s)" ; }
    else
    { $ypos = "$at(s)" ; $xpos = "[$barcnt](s)" ; }

    if ($shift ne "")
    {
      my ($shiftx, $shifty) = split (",", $shift) ;
      if ($shiftx > 0)
      { $xpos .= "+$shiftx" ; }
      if ($shiftx < 0)
      { $xpos .= "$shiftx" ; }
      if ($shifty > 0)
      { $ypos .= "+$shifty" ; }
      if ($shifty < 0)
      { $ypos .= "$shifty" ; }
    }

    &WriteText ("~", $bar, $shiftx, $xpos, $ypos, $text, $textcolor, $fontsize, $align, $link, $hint) ;
  }
}

sub PlotBars
{
  #proc getdata / #proc bars
  while ($#PlotBarsNow >= 0)
  {
    undef @PlotBarsLater ;

    $maxwidth = 0 ;
    foreach $entry (@PlotBarsNow)
    {
      ($width) = split (",", $entry) ;
      if ($width > $maxwidth)
      { $maxwidth = $width ; }
    }

    $script .= "#proc getdata\n" ;
    $script .= "  delim: comma\n" ;
    $script .= "  data:\n" ;

    foreach $entry (@PlotBarsNow)
    {
      my ($width, $bar, $from, $till, $color, $link, $hint) = split (",", $entry) ;
      if ($width < $maxwidth)
      {
        push @PlotBarsLater, $entry ;
        next ;
      }
      for ($b = 0 ; $b <= $#Bars ; $b++)
      {
        if (lc(@Bars [$b]) eq lc($bar))
        { $bar = ($#Bars - ($b - 1)) ; last ; }
      }
      if (@Axis {"order"} !~ /reverse/i)
      { $entry = "$bar,$from,$till,$color,$link,$hint,\n" ; }
      else
      { $entry = "$bar," . (-$till) . "," . (-$from) . ",$color,$link,$hint,\n" ; }

      $script .= "$entry" ;
    }
    $script .= "\n" ;

    #proc bars
    $script .= "#proc bars\n" ;
    $script .= "  axis: " . @Axis {"time"} . "\n" ;
    $script .= "  barwidth: $maxwidth\n" ;
    $script .= "  outline: no\n" ;
#   $script .= "  thinbarline: width=5\n" ;
    if (@Axis {"time"} eq "x")
    { $script .= "  horizontalbars: yes\n" ; }
    $script .= "  locfield: 1\n" ;
    $script .= "  segmentfields: 2 3\n" ;
    $script .= "  colorfield: 4\n" ;
#   $script .= "  outline: width=1\n" ;
#   $script .= "  barwidthfield: 5\n" ;
#    if (@fields [4] ne "")
#    { $script .= "  clickmapurl: " . &LinkToUrl ($text) . "\n" ; }
#    if (@fields [5] ne "")
#    { $script .= "  clickmaplabel: $text\n" ; }
    $script .= "  clickmapurl: \@\@5\n" ;
    $script .= "  clickmaplabel: \@\@6\n" ;
    $script .= "\n" ;

    @PlotBarsNow = @PlotBarsLater ;
  }
}

sub PlotScale
{
  my $scale = shift ;
  my $grid  = shift ;
  my ($color, $from, $till, $start) ;

  %x = %Period ;
#  if (($DateFormat =~ /\//) && ($grid))
#  { return ; }

#  if (($DateFormat =~ /\//)
#  {
#  }

#  if (! $grid) # redefine area, scale linear for time axis, showl whole years always, Ploticus bug
#  {
    # $from = @Period {"from"} ;
    # $till = @Period {"till"} ;
    $from = &DateToFloat (@Period {"from"}) ;
    $till = &DateToFloat (@Period {"till"}) ;
    # $from =~ s/.*\///g ; # delete dd mm if present
    # $till =~ s/.*\///g ;
    #proc areadef
    $script .= "#proc areadef\n" ;
    $script .= "  #clone: A\n" ;
    $script .= "  " . @Axis {"time"} . "scaletype: linear\n" ; # date yyyy

    if (@Axis {"order"} !~ /reverse/i)
    { $script .= "  " . @Axis {"time"} . "range: $from $till\n" ; }
    else
    { $script .= "  " . @Axis {"time"} . "range: " . (-$till) . " " . (-$from) . "\n" ; }

    $script .= "\n" ;
#  }

  $script .= "#proc " . @Axis {"time"} . "axis\n" ;

  if (($scale eq "Major") && (! $grid))
  {
#   $script .= "  stubs: incremental " . @Scales {"Major inc"} . " " . @Scales {"Major unit"} . "\n" ;
#   if ($DateFormat =~ /\//)
#   { $script .= "  stubformat: " . @Axis {"format"} . "\n" ; }
# temp always show whole years (Ploticus autorange bug)
    if (@Scales {"Major stubs"} eq "") # ($DateFormat !~ /\//)
    { $script .= "  stubs: incremental " . @Scales {"Major inc"} . "\n" ; }
    else
    { $script .= "  stubs: list " . @Scales {"Major stubs"} . "\n" ; }
  }
  else
  { $script .= "  stubs: none\n" ; }

  if ($DateFormat !~ /\//)
# { $script .= "  ticincrement: " . @Scales {"$scale inc"} . " " . @Scales {"$scale unit"} . "\n" ; }
  { $script .= "  ticincrement: " . @Scales {"$scale inc"} . "\n" ; }
  else
  {
    my $unit = 1 ;
    if (@Scales {"$scale unit"} =~ /month/i)
    { $unit = 1/12 ; }
    if (@Scales {"$scale unit"} =~ /day/i)
    { $unit = 1/365 ; }
    $script .= "  ticincrement: " . @Scales {"$scale inc"} . " $unit\n" ;
  }

  if (defined (@Scales {"$scale start"}))
  {
    $start = @Scales {"$scale start"} ;
    # $start =~ s/.*\///g ; # delete dd mm if present
    $start = &DateToFloat ($start) ;
    if (@Axis {"order"} =~ /reverse/i)
    {
      $loop = 0 ;
      $start = -$start ;
      while ($start - @Scales {"$scale inc"} >= - @Period {"till"})
      {
        $start -= @Scales {"$scale inc"} ;
        if (++$loop > 1000) { last ; } # precaution
      }
    }
    $script .= "  stubrange: $start\n" ;
  }

  if ($scale eq "Major")
  {
    $script .= "  ticlen: 0.05\n" ;
    if (@Axis {"time"} eq "y")
    { $script .= "  stubdetails: adjust=0.05,0\n" ; }
    if (@Axis {"order"} =~ /reverse/i)
    { $script .= "  signreverse: yes\n" ; }
  }
  else
  { $script .= "  ticlen: 0.02\n" ; }
# $script .= "  location: 4\n" ; test

  $color .= @Scales {"$scale grid"} ;

  if (defined (@Colors {$color}))
  { $color = @Colors {$color} ; }

  if ($grid)
  { $script .= "  grid: color=$color\n" ; }

  $script .= "\n" ;

  if ($grid) # restore areadef
  {
    #proc areadef
    $script .= "#proc areadef\n" ;
    $script .= "  #clone: A\n" ;
    $script .= "\n" ;
  }
}

sub PlotLines
{
  my $layer = shift ;

  if ($#DrawLines < 0)
  { return ; }

  undef (@DrawLinesNow) ;

  foreach $line (@DrawLines)
  {
    if ($line =~ /\|$layer\n/)
    { push @DrawLinesNow, $line ; }
  }

  if ($#DrawLinesNow < 0)
  { return ; }

  foreach $entry (@DrawLinesNow)
  {
    chomp ($entry) ;
    $script .= "#proc line\n" ;
#    $script .= "  notation: scaled\n" ;
    if ($entry =~ /^[12]/)
    { ($mode, $at, $from, $till, $color, $width) = split ('\|', $entry) ; }
    else
    { ($mode, $points, $color, $width) = split ('\|', $entry) ; }

    $script .= "  linedetails: width=$width color=$color style=0\n" ;

    if ($mode == 1) # draw perpendicular to time axis
    {
      if (@Axis {"order"} =~ /reverse/i)
      { $at = -$at ; }

      if (@Axis {"time"} eq "x")
      {
        if ($from eq "")
        {  $from = @PlotArea {"bottom"} }
        if ($till eq "")
        {  $till = @PlotArea {"bottom"} + @PlotArea {"height"} }
        $from += ($width/200) ; # compensate for overstrechting of thick lines
        $till -= ($width/200) ;
        if ($from > @Image {"height"})
        { $from = @Image {"height"} ; }
        if ($till > @Image {"height"})
        { $till = @Image {"height"} ; }
        $script .= "  points: $at(s) $from $at(s) $till\n" ;
      }
      else
      {
        if ($from eq "")
        {  $from = @PlotArea {"left"} }
        if ($till eq "")
        {  $till = @PlotArea {"left"} + @PlotArea {"width"} }
        $from += ($width/200) ;
        $till -= ($width/200) ;
        if ($from > @Image {"width"})
        { $from = @Image {"width"} ; }
        if ($till > @Image {"width"})
        { $till = @Image {"width"} ; }
        $script .= "  points: $from $at(s) $till $at(s)\n" ;
      }
    }

    if ($mode == 2) # draw parralel to time axis
    {
      if (@Axis {"order"} =~ /reverse/i)
      {
        $from = -$from ;
        $till = -$till ;
      }

      $from .= "(s)+" .($width/200) ;
      $till .= "(s)-" .($width/200) ;
      if (@Axis {"time"} eq "x")
      {
        if ($at eq "")
        {  $at = @PlotArea {"bottom"} ; }
        if ($at > @Image {"height"})
        { $at = @Image {"height"} ; }
        $script .= "  points: $from $at $till $at\n" ;
      }
      else
      {
        if ($at eq "")
        {  $at = @PlotArea {"left"} ; }
        if ($at > @Image {"width"})
        { $at = @Image {"width"} ; }
        $script .= "  points: $at $from $at $till\n" ;
      }
    }

    if ($mode == 3) # draw free line
    {
      @Points = split (",", $points) ;
      foreach $point (@Points)
      { $point = &Normalize ($point) ; }
      if ((@Points [0] > @Image {"width"}) ||
          (@Points [1] > @Image {"height"}) ||
          (@Points [2] > @Image {"width"})  ||
          (@Points [3] > @Image {"height"}))
      { &Error2 ("Linedata attribute 'points' invalid.\n" .
        sprintf ("(%d,%d)(%d,%d)", @Points[0]*100, @Points[1]*100, @Points[2]*100, @Points[3]*100) . " does not fit in image\n") ;
        return ; }
      $script .= "  points: @Points[0] @Points[1] @Points[2] @Points[3]\n" ;
    }
  }


  $script .= "\n" ;
}

sub ColorPredefined
{
  my $color = shift ;
  if ($color =~ /^(?:black|white|tan1|tan2|red|magenta|claret|coral|pink|orange|
                     redorange|lightorange|yellow|yellow2|dullyellow|yelloworange|
                     brightgreen|green|kelleygreen|teal|drabgreen|yellowgreen|
                     limegreen|brightblue|darkblue|blue|oceanblue|skyblue|
                      purple|lavender|lightpurple|powderblue|powderblue2)$/xi)
  {
    if (! defined (@Colors {lc ($color)}))
    { &StoreColor ($color, $color, "", $command) ; }
    return ($true) ;
  }
  else
  { return ($false) ; }
}

sub ValidAbs
{
  $value = shift ;
  if ($value =~ /^ \d+ \.? \d* (?:px|in|cm)? $/xi)
  { return ($true) ; }
  else
  { return ($false) ; }
}

sub ValidAbsRel
{
  $value = shift ;
  if ($value =~ /^ \d+ \.? \d* (?:px|in|cm|$hPerc)? $/xi)
  { return ($true) ; }
  else
  { return ($false) ; }
}

sub ValidDateFormat
{
  my $date = shift ;
  my ($day, $month, $year) ;

#  if ($date=~ /^\-?\d+$/) # for now full years are always allowed
#  { return ($true) ; }

  if ($DateFormat eq "yyyy")
  {
    if (! ($date=~ /^\-?\d+$/))
    { return ($false) ; }
    return ($true) ;
  }

  if ($DateFormat eq "x.y")
  {
    if (! ($date=~ /^\-?\d+(?:\.\d+)?$/))
    { return ($false) ; }
    return ($true) ;
  }

  if (! ($date=~ /^\d\d\/\d\d\/\d\d\d\d$/))
  { return ($false) ; }

  if ($DateFormat eq "dd/mm/yyyy")
  {
    $day   = substr ($date,0,2) ;
    $month = substr ($date,3,2) ;
    $year  = substr ($date,6,4) ;
  }
  else
  {
    $day   = substr ($date,3,2) ;
    $month = substr ($date,0,2) ;
    $year  = substr ($date,6,4) ;
  }

  if ($month =~ /^(?:01|03|05|07|08|10|12)$/)
  { if ($day > 31) { return ($false) ; }}
  elsif ($month =~ /^(?:04|06|09|11)$/)
  { if ($day > 30) { return ($false) ; }}
  elsif ($month =~ /^02$/)
  {
    if (($year % 4 == 0) && ($year % 100 != 0))
    { if ($day > 29) { return ($false) ; }}
    else
    { if ($day > 28) { return ($false) ; }}
  }
  else { return ($false) ; }
  return ($true) ;
}

sub ValidDateRange
{
  my  $date = shift ;
  my ($day,  $month,  $year,
      $dayf, $monthf, $yearf,
      $dayt, $montht, $yeart) ;

  my $from = @Period {"from"} ;
  my $till = @Period {"till"} ;

  if (($DateFormat eq "yyyy") || ($DateFormat eq "x.y"))
  {
    if (($date < $from) || ($date > $till))
    { return ($false) ; }
    return ($true) ;
  }

  if ($DateFormat eq "dd/mm/yyyy")
  {
    $day    = substr ($date,0,2) ;
    $month  = substr ($date,3,2) ;
    $year   = substr ($date,6,4) ;
    $dayf   = substr ($from,0,2) ;
    $monthf = substr ($from,3,2) ;
    $yearf  = substr ($from,6,4) ;
    $dayt   = substr ($till,0,2) ;
    $montht = substr ($till,3,2) ;
    $yeart  = substr ($till,6,4) ;
  }
  if ($DateFormat eq "mm/dd/yyyy")
  {
    $day    = substr ($date,3,2) ;
    $month  = substr ($date,0,2) ;
    $year   = substr ($date,6,4) ;
    $dayf   = substr ($from,3,2) ;
    $monthf = substr ($from,0,2) ;
    $yearf  = substr ($from,6,4) ;
    $dayt   = substr ($till,3,2) ;
    $montht = substr ($till,0,2) ;
    $yeart  = substr ($till,6,4) ;
  }

  if (($year < $yearf) ||
      (($year == $yearf) &&
       (($month < $monthf) ||
        (($month == $monthf) && ($day < $dayf))
       )))
  { return ($false) }

  if (($year > $yeart) ||
      (($year == $yeart) &&
       (($month > $montht) ||
        (($month == $montht) && ($day > $dayt))
       )))
  { return ($false) }

  return ($true) ;
}

sub DateMedium
{
  my $from = shift ;
  my $till = shift ;

  if (($DateFormat eq "yyyy") || ($DateFormat eq "x.y"))
  { return (sprintf ("%.3f", ($from + $till) / 2)) ; }

  $from2 = &DaysFrom1800 ($from) ;
  $till2 = &DaysFrom1800 ($till) ;
  my $date = &DateFrom1800 (int (($from2 + $till2) / 2)) ;
  return ($date) ;
}

sub DaysFrom1800
{
  @mmm = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) ;
  my $date = shift ;
  if ($DateFormat eq "dd/mm/yyyy")
  {
    $day   = substr ($date,0,2) ;
    $month = substr ($date,3,2) ;
    $year  = substr ($date,6,4) ;
  }
  else
  {
    $day   = substr ($date,3,2) ;
    $month = substr ($date,0,2) ;
    $year  = substr ($date,6,4) ;
  }
  if ($year < 1800)
  { &Error2 ("Function 'DaysFrom1800' expects year >= 1800, not '$year'.") ; return ; }

  $days = ($year - 1800) * 365 ;
  $days += int (($year -1 - 1800) / 4) ;
  $days -= int (($year -1 - 1800) / 100) ;
  if ($month > 1)
  {
    for ($m = $month - 2 ; $m >= 0 ; $m--)
    {
      $days += @mmm [$m] ;
      if ($m == 1)
      {
        if ((($year % 4) == 0) && (($year % 100) != 0))
        { $days ++ ; }
      }
    }
  }
  $days += $day ;

  return ($days) ;
}

sub DateToFloat
{
  my $date = shift ;
  if ($DateFormat !~ /\//)
  { return ($date) ; }
  my $year = $date ;
  $year =~ s/.*\///g ; # delete dd mm/mm dd
  my $fraction = (&DaysFrom1800 ($date) - &DaysFrom1800 ("01/01/" . $year)) / 365.25 ;
  return ($year + $fraction) ;
}

sub DateFrom1800
{
  my $days = shift ;

  @mmm = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31) ;

  $year = 1800 ;
  while ($days > 365 + (($year % 4) == 0))
  {
    if ((($year % 4) == 0) && (($year % 100) != 0))
    { $days -= 366 ; }
    else
    { $days -= 365 ; }
    $year ++ ;
  }

  $month = 0 ;
  while ($days > @mmm [$month])
  {
    $days -= @mmm [$month] ;
    if ($month == 1)
    {
      if ((($year % 4) == 0) && (($year % 100) != 0))
      { $days -- ; } ;
    }
    $month++ ;
  }
  $day = $days ;

  $month ++ ;
  if ($DateFormat eq "dd/mm/yyyy")
  { $date = sprintf ("%02d/%02d/%04d", $day, $month, $year) ; }
  else
  { $date = sprintf ("%02d/%02d/%04d", $month, $day, $year) ; }

  return ($date) ;
}

sub ExtractText
{
  my $data = shift ;
  my $data2 = $data ;
  my $text = "" ;

  # special case: allow embedded spaces when 'text' is last attribute
# $data2 =~ s/\:\:/\@\#\!/g ;
  if ($data2 =~ /text\:[^\:]+$/)
  {
    $text = $data2 ;
    $text =~ s/^.*?text\:// ;
#   $text =~ s/^\s(.*?)\s*$/$1/ ; ?? ->
    $text =~ s/^(.*?)\s*$/$1/ ;
    $text =~ s/\\n/\n/g ;
    $text =~ s/\"\"/\@\#\$/g ;
    $text =~ s/\"//g ;
    $text =~ s/\@\#\$/"/g ;
    $data2 =~ s/text\:.*$// ;
  }

  # extract text between double quotes
  $data2 =~ s/\"\"/\@\#\$/g ;
  if ($data2 =~ /text\:\s*\"/)
  {
    $text = $data2 ;
    $text =~ s/^.*?text\:\s*\"// ;

    if (! ($text =~ /\"/))
    { &Error ("PlotData invalid. Attribute 'text': no closing \" found.") ;
      return ("x", "x") ; }

    $text =~ s/\".*$//;
    $text =~ s/\@\#\$/"/g ;
    $text =~ s/\\n/\n/g ;
  }
  $data2 =~ s/text\:\s*\"[^\"]*\"// ;
  $data2 =~ s/\@\#\$/"/g ;
  return ($data2, $text) ;
}

sub ParseText
{
  my $text = shift ;
  $text =~ s/\_\_/\@\#\$/g ;
  $text =~ s/\_/ /g ;
  $text =~ s/\@\#\$/_/g ;

  $text =~ s/\~\~/\@\#\$/g ;
  $text =~ s/\~/\\n/g ;
  $text =~ s/\@\#\$/~/g ;

  return ($text) ;
}

sub BarDefined
{
  my $bar = shift ;
  foreach $bar2 (@Bars)
  {
    if (lc ($bar2) eq lc ($bar))
    { return ($true) ; }
  }

# not part of barset ? return
  if ($bar != /\#\d+$/)
  { return ($false) ; }

# find previous bar in barset
  my $barcnt = $bar ;
  my $barid  = $bar ;
  $barcnt =~ s/.*\#(\d+$)/$1/ ;
  $barid  =~ s/(.*\#)\d+$/$1/ ;
  $barcnt -- ;
  $a = $#Bars ;
  for (my $b = 0 ; $b <= $#Bars ; $b++)
  {
    if (lc (@Bars [$b]) eq lc ($barid . $barcnt))
    {
      $b++ ;
      for (my $b2 = $#Bars + 1 ; $b2 > $b ; $b2--)
      { @Bars [$b2] = @Bars [$b2-1]; }
      @Bars [$b] = lc ($bar) ;
      @BarLegend {lc ($bar)} = " " ;
      return ($true) ;
    }
  }
  return ($false) ;
}

sub ValidAttributes
{
  my $command = shift ;

  if ($command =~ /^BackgroundColors$/i)
  { return (CheckAttributes ($command, "", "canvas,bars")) ; }

  if ($command =~ /^BarData$/i)
# { return (CheckAttributes ($command, "", "bar,barset,barcount,link,text")) ; }
  { return (CheckAttributes ($command, "", "bar,barset,link,text")) ; }

  if ($command =~ /^Colors$/i)
  { return (CheckAttributes ($command, "id,value", "legend")) ; }

  if ($command =~ /^ImageSize$/i)
  { return (CheckAttributes ($command, "", "width,height,barincrement")) ; }

  if ($command =~ /^Legend$/i)
  { return (CheckAttributes ($command, "", "columns,columnwidth,orientation,position,left,top")) ; }

  if ($command =~ /^LineData$/i)
  { return (CheckAttributes ($command, "", "at,from,till,atpos,frompos,tillpos,points,color,layer,width")) ; }

  if ($command =~ /^Period$/i)
  { return (CheckAttributes ($command, "from,till", "")) ; }

  if ($command =~ /^PlotArea$/i)
  { return (CheckAttributes ($command, "", "left,bottom,width,height,right,top")) ; }

  if ($command =~ /^PlotData$/i)
  { return (CheckAttributes ($command, "", "align,anchor,at,bar,barset,color,fontsize,from,link,mark,shift,text,textcolor,till,width")) ; }

  if ($command =~ /^Scale/i)
  { return (CheckAttributes ($command, "increment,start", "unit,grid,gridcolor,text")) ; }

  if ($command =~ /^TextData$/i)
  { return (CheckAttributes ($command, "", "fontsize,lineheight,link,pos,tabs,text,textcolor")) ; }

  if ($command =~ /^TimeAxis$/i)
  { return (CheckAttributes ($command, "", "orientation,format,order")) ; }

  return ($true) ;
}

sub CheckAttributes
{
  my $name     = shift ;
  my @Required = split (",", shift) ;
  my @Allowed  = split (",", shift) ;

  my $attribute ;
  my %Attributes2 = %Attributes ;

  $hint = "\nSyntax: '$name =" ;
  foreach $attribute (@Required)
  { $hint .= " $attribute:.." ; }
  foreach $attribute (@Allowed)
  { $hint .= " [$attribute:..]" ; }
  $hint .= "'" ;

  foreach $attribute (@Required)
  {
    if ((! defined (@Attributes {$attribute})) || (@Attributes {$attribute} eq ""))
    { &Error ("$name definition incomplete. $hint") ;
      undef (@Attributes) ; return ($false) ; }
    delete (@Attributes2 {$attribute}) ;
  }
  foreach $attribute (@Allowed)
  { delete (@Attributes2 {$attribute}) ; }

  @AttrKeys = keys %Attributes2 ;
  if ($#AttrKeys >= 0)
  {
    if (@AttrKeys [0] eq "single")
    { &Error ("$name definition invalid. Specify all attributes as name:value pairs.") ; }
    else
    { &Error ("$name definition invalid. Invalid attribute '" . @AttrKeys [0] . "' found. $hint") ; }
    undef (@Attributes) ; return ($false) ; }

  return ($true) ;
}

sub CheckPreset
{
  my $command  = shift ;
  my ($preset, $action, $attrname, $attrvalue) ;

  my $newcommand = $true ;
  my $addvalue   = $true ;
  if ($command =~ /^$prevcommand$/i)
  { $newcommand = $false ; }
  if ((! $newcommand) && ($command =~ /^(?:DrawLines|PlotData|TextData)$/i))
  { $addvalue = $false ; }
  $prevcommand = $command ;

  foreach $preset (@PresetList)
  {
    if ($preset =~ /^$command\|/i)
    {
      ($command, $action, $attrname, $attrpreset) = split ('\|', $preset) ;
      if ($attrname eq "")
      { $attrname = "single" ; }

      $attrvalue = @Attributes {$attrname} ;

      if (($action eq "-") && ($attrvalue ne ""))
      {
        if ($attrname eq "single")
        { &Error ("Chosen preset makes this command redundant.\n" .
                  "  Please remove this command.") ; }
        else
        { &Error ("Chosen preset conflicts with '$attrname:...'.\n" .
                  "  Please remove this attribute.") ; }
        @Attributes {$attrname} = "" ;
      }

      if (($action eq "+") && ($attrvalue eq ""))
      {
        if ($addvalue)
        { @Attributes {$attrname} = $attrpreset ; }
      }

      if (($action eq "=") && ($attrvalue eq ""))
      { @Attributes {$attrname} = $attrpreset ; }

      if (($action eq "=") && ($attrvalue ne "") &&
                              ($attrvalue !~ /$attrpreset/i))
      {
        if ($attrname eq "single")
        { &Error ("Conflicting settings.\nPreset defines '$attrpreset'.") ; }
        else
        { &Error ("Conflicting settings.\nPreset defines '$attrname:$attrpreset'.") ; }
        @Attributes {$attrname} = $attrpreset ;
      }
    }
  }
}

sub ShiftOnePixelForSVG
{
  my $line = shift ;
  $line =~ s/location:\s*// ;
  my ($posx, $posy) = split (" ", $line) ;

  if ($posy =~ /\+/)
  { ($posy1, $posy2) = split ('\+', $posy) ; }
  elsif ($posy =~ /.+\-/)
  {
    if ($posy =~ /^\-/)
    {
      ($sign, $posy1, $posy2) = split ('\-', $posy) ; $posy2 = - $posy2 ;
      $posy1 = "-" . $posy1 ;
    }
    else
    { ($posy1, $posy2) = split ('\-', $posy) ; $posy2 = - $posy2 }
  }
  else
  { $posy1 = $posy ; $posy2 = 0 ; }

  if ($posy1 !~ /(s)/)
  { $posy += 0.01 ; }
  else
  {
    $posy2 += 0.01 ;
    if ($posy2 == 0)
    { $posy = $posy1 ; }
    elsif ($posy2 < 0)
    { $posy = $posy1 . "$posy2" ; }
    else
    { $posy = $posy1 . "+" . $posy2 ; }
  }

  $line = "\n  location: $posx $posy" ;
  return ($line) ;
}

sub NormalizeURL
{
  my $url = shift ;
  $url =~ s/(https?)\:?\/?\/?/$1:\/\// ; # add possibly missing special characters
  $url =~ s/ /%20/g ;
  return ($url) ;
}

# wiki style link may include linebreak characters -> split into several wiki links
sub NormalizeWikiLink
{
  my $text = shift ;

  my $brdouble  = $false ;
  if ($text =~ /\[\[.*\]\]/)
  { $brdouble = $true ; }

  $text =~ s/\[\[?// ;
  $text =~ s/\]?\]// ;

  my ($hide,$show) = split ('\|', $text) ;
  if ($show eq "")
  { $show = $hide ; }
  $hide =~ s/\s*\n\s*/ /g ;

  my @Show = split ("\n", $show) ;
  $text = "" ;
  foreach $part (@Show)
  {
    if ($brdouble)
    { $part = "[[" . $hide . "|" . $part . "]]" ; }
    else
    { $part = "["  . $hide . "|" . $part . "]"  ; }
  }
  $text = join ("\n", @Show) ;

  return ($text) ;
}

sub ProcessWikiLink
{
  my $text = shift ;
  my $link = shift ;
  my $hint = shift ;
  my $wikilink = $false ;

  chomp ($text) ;
  chomp ($link) ;
  chomp ($hint) ;

  my ($wiki, $title) ;
  if ($link ne "") # ignore wiki brackets in text when explicit link is specified
  {
    $text =~ s/\[\[ [^\|]+ \| (.*) \]\]/$1/gx ;
    $text =~ s/\[\[ [^\:]+ \: (.*) \]\]/$1/gx ;
#   $text =~ s/\[\[ (.*) \]\]/$1/gx ;
  }
  else
  {
    if ($text =~ /\[.+\]/) # keep first link in text segment, remove others
    {
      $link = $text ;
      $link =~ s/\n//g ;
      $link =~ s/^[^\[\]]*\[/[/x ;

      if ($link =~ /^\[\[/)
      { $wikilink = $true ; }

      $link =~ s/^ [^\[]* \[+ ([^\[\]]*) \].*$/$1/x ;
      $link =~ s/\|.*$// ;
      if ($wikilink)
      { $link = "[[" . $link . "]]" ; }

      $text =~ s/(\[+) [^\|\]]+ \| ([^\]]*) (\]+)/$1$2$3/gx ;
      $text =~ s/(https?)\:/$1colon/gx ;
#     $text =~ s/(\[+) [^\:\]]+ \: ([^\]]*) (\]+)/$1$2$3/gx ;  #???

      # remove interwiki link prefix
      $text =~ s/(\[+) (?:.{2,3}|(?:zh\-.*)|simple|minnan|tokipona) \: ([^\]]*) (\]+)/$1$2$3/gxi ;  #???

      $text =~ s/\[+ ([^\]]+) \]+/{{{$1}}}/x ;
      $text =~ s/\[+ ([^\]]+) \]+/$1/gx ;
      $text =~ s/\{\{\{ ([^\}]*) \}\}\}/[[$1]]/x ;
    }
#    if ($text =~ /\[\[.+\]\]/)
#    {
#      $wikilink = $true ;
#      $link = $text ;
#      $link =~ s/\n//g ;
#      $link =~ s/^.*?\[\[/[[/x ;
#      $link =~ s/\| .*? \]\].*$/]]/x ;
#      $link =~ s/\]\].*$/]]/x ;
#      $text =~ s/\[\[ [^\|\]]+ \| (.*?) \]\]/[[$1]]/x ;
#      $text =~ s/\[\[ [^\:\]]+ \: (.*?) \]\]/[[$1]]/x ;

#      # remove remaining links
#      $text =~ s/\[\[ ([^\]]+) \]\]/^%#$1#%^/x ;
#      $text =~ s/\[+ ([^\]]+) \]+/$1/gx ;
#      $text =~ s/\^$hPerc\# (.*?) \#$hPerc\^/[[$1]]/x ;
#    }
#    elsif ($text =~ /\[.+\]/)
#    {
#      $link = $text ;
#      $link =~ s/\n//g ;
#      $link =~ s/^.*?\[/[/x ;
#      $link =~ s/\| .*? \].*$/]/x ;
#      $link =~ s/\].*$/]/x ;
#      $link =~ s/\[ ([^\]]+) \]/$1/x ;
#      $text =~ s/\[ [^\|\]]+ \| (.*?) \]/[[$1]]/x ;

#      # remove remaining links
#      $text =~ s/\[\[ ([^\]]+) \]\]/^%#$1#%^/x ;
#      $text =~ s/\[+ ([^\]]+) \]+/$1/gx ;
#      $text =~ s/\^$hPerc\# (.*?) \#$hPerc\^/[[$1]]/x ;
##     $text =~ s/\[\[ (.*) \]\]/$1/gx ;
#    }

  }

  if ($wikilink)
  {
#   if ($link =~ /^\[\[.+\:.+\]\]$/) # has a colon in its name
    if ($link =~ /^\[\[ (?:.{2,3}|(?:zh\-.*)|simple|minnan|tokipona) \: .+\]\]$/xi) # has a interwiki link prefix
    {
      # This will fail for all interwiki links other than Wikipedia.
      $wiki  = lc ($link) ;
      $title = $link ;
      $wiki  =~ s/\[\[([^\:]+)\:.*$/$1/x ;
      $title =~ s/^[^\:]+\:(.*)\]\]$/$1/x ;
      $title =~ s/ /_/g ;
      $link = "http://$wiki.wikipedia.org/wiki/$title" ;
	  $link = &EncodeURL ($title) ;
      if (($hint eq "") && ($title ne ""))
      { $hint = "$wiki: $title" ; }
    }
    else
    {
    # $wiki = "en" ;
      $title = $link ;
      $title =~ s/^\[\[(.*)\]\]$/$1/x ;
      $title =~ s/ /_/g ;
      $link = $articlepath ;
	  $urlpart = &EncodeURL ($title) ;
	  $link =~ s/\$1/$urlpart/ ;
      if (($hint eq "") && ($title ne ""))
      { $hint = "$title" ; }
    }
    $hint =~ s/_/ /g ;
  }
  else
  {
    if ($link ne "")
    { $hint = &ExternalLinkToHint ($link) ; }
  }

  if (($link ne "") && ($text !~ /\[\[/) && ($text !~ /\]\]/))
  { $text = "[[" . $text . "]]" ; }

  $hint = &EncodeHtml ($hint) ;
  return ($text, $link, $hint) ;
}

sub ExternalLinkToHint
{
  my $hint = shift ;
  $hint =~ s/^https?\:?\/?\/?// ;
  $hint =~ s/\/.*$// ;
  return (&EncodeHtml ($hint . "/..")) ;
}

sub EncodeInput
{
  my $text = shift ;
  # revert encoding of '<' & '>' by MediaWiki
  $text =~ s/\&lt\;/\</g ;
  $text =~ s/\&gt\;/\>/g ;
  $text =~ s/([\`\{\}\%\&\@\$\(\)\;\=])/"%" . sprintf ("%X", ord($1)) . "%";/ge ;
  return ($text) ;
}

sub DecodeInput
{
  my $text = shift ;
  $text =~ s/\%([0-9A-F]{2})\%/chr(hex($1))/ge ;
  return ($text) ;
}

sub EncodeHtml
{
  my $text = shift ;
  $text =~ s/([\<\>\&\'\"])/"\&\#" . ord($1) . "\;"/ge ;
  $text =~ s/\n/<br>/g ;
  return ($text) ;
}

sub EncodeURL
{
  my $url = shift ;
  # For some reason everything gets run through this weird internal
  # encoding that's similar to URL-encoding. Armor against this as well,
  # or else adjacent encoded bytes will be corrupted.
  $url =~ s/([^0-9a-zA-Z\%\:\/\._])/"%25%".sprintf ("%02X",ord($1))/ge ;
  return ($url) ;
}

sub Error
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines

  $CntErrors++ ;
  if (! $listinput)
  { push @Errors, "Line $LineNo: " . &DecodeInput($Line) . "\n" ; }
  push @Errors, "- $msg\n\n" ;
  if ($CntErrors > 10)
  { &Abort ("More than 10 errors found") ; }
}

sub Error2
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines
  $CntErrors++ ;
  push @Errors, "- $msg\n" ;
}

sub Warning
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines
  if (! $listinput)
  { push @Warnings, "Line $LineNo: " . &DecodeInput ($Line) . "\n" ; }
  push @Warnings, "- $msg\n\n" ;
}

sub Warning2
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines
  push @Warnings, "- $msg\n" ;
}

sub Info
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines
  if (! $listinput)
  { push @Info, "Line $LineNo: " . &DecodeInput ($Line) . "\n" ; }
  push @Info, "- $msg\n\n" ;
}

sub Info2
{
  my $msg = &DecodeInput(shift) ;
  $msg =~ s/\n\s*/\n  /g ; # indent consecutive lines
  push @Info, "- $msg\n" ;
}

sub Abort
{
  my $msg = &DecodeInput(shift) ;

  print "\n\n***** " . $msg . " *****\n\n" ;
  print @Errors ;
  print "Execution aborted.\n" ;

  open "FILE_OUT", ">", $file_errors ;
  print FILE_OUT "<p>EasyTimeline $version</p><p><b>Timeline generation failed: " . &EncodeHtml ($msg) ."</b></p>\n" ;
  foreach $line (@Errors)
  { print FILE_OUT &EncodeHtml ($line) . "\n" ; }
  close "FILE_OUT" ;

  if ($makehtml) # generate html test file, which would normally contain png + svg (+ image map)
  {
    open "FILE_IN",  "<", $file_errors ;
    open "FILE_OUT", ">", $file_html ;
    print FILE_OUT "<html><head>\n<title>Graphical Timelines - HTML test file</title>\n</head>\n" .
                   "<body><h1><font color='green'>EasyTimeline</font> - Test Page</h1>\n\n" .
                   "<code>\n" ;
    print FILE_OUT <FILE_IN> ;
    print FILE_OUT "</code>\n\n</body>\n</html>" ;
    close "FILE_IN" ;
    close "FILE_OUT" ;
  }
  exit ;
}

sub EscapeShellArg
{
  my $arg = shift;
  if ($env eq "Linux") {
    $arg =~ s/'/\\'/;
    $arg = "'$arg'";
  } else {
    $arg =~ s/"/\\"/;
    $arg = "\"$arg\"";
  }
  return $arg;
}

# vim: set sts=2 ts=2 sw=2 et :

sub UnicodeToAscii {
  my $unicode = shift ;
  my $char = substr ($unicode,0,1) ;
  my $ord = ord ($char) ;

  if ($ord < 128)         # plain ascii character
  { return ($unicode) ; } # (will not occur in this script)
  else
  {
    # for completeness sake complete routine, only 2 byte unicodes sent here
    if ($ord >= 252)
    { $value = $ord - 252 ; }
    elsif ($ord >= 248)
    { $value = $ord - 248 ; }
    elsif ($ord >= 240)
    { $value = $ord - 240 ; }
    elsif ($ord >= 224)
    { $value = $ord - 224 ; }
    else
    { $value = $ord - 192 ; }
    for ($c = 1 ; $c < length ($unicode) ; $c++)
    { $value = $value * 64 + ord (substr ($unicode, $c,1)) - 128 ; }

#   $html = "\&\#" . $value . ";" ; any unicode can be specified as html char

    if (($value >= 128) && ($value <= 255))
    { return (chr ($value)) ; }
    else
    { return "?" ; }
  }
}

