Index: /trunk/BNC/scripts/Bnc.pm
===================================================================
--- /trunk/BNC/scripts/Bnc.pm	(revision 9597)
+++ /trunk/BNC/scripts/Bnc.pm	(revision 9597)
@@ -0,0 +1,853 @@
+package Bnc;
+
+# Perl utility functions for BNC
+#
+# Revision: $Header$
+
+use strict;
+use warnings;
+use File::Basename;
+use File::Spec::Functions qw(catfile);
+use File::Temp qw(tempfile);
+use Exporter;
+use Time::Piece 1.30;
+use PDL::Lite;    # to avoid namespace pollution
+use PDL::Primitive;
+use Log::Log4perl qw(:easy);
+
+# use List::MoreUtils qw(uniq); # Prototype mismatch with PDL (uniq)
+
+# =============================================================================
+# callBnc ($bnc_ini, %$opts_ref)
+# =============================================================================
+# Call BNC
+#
+# Param  : $bnc_ini  [optional] The BNC config file that should be used. If not set, the default config is ised.
+#          $opts_ref [required] Hash with BNC config options as key. They will overwrite the settings from the config file.
+# Return : BNC exit status
+# =============================================================================
+sub callBnc {
+    my ( $bnc, $bnc_ini, $opts ) = @_;
+
+    my $config_file = "";
+    if ( $bnc_ini && -s $bnc_ini ) {
+        $config_file = "--conf $bnc_ini";
+    }
+    else {
+        DEBUG("callBnc: Use default bnc-ini file");
+    }
+
+    my $opts_str = "";
+    if ($opts) { $opts_str = options2string($opts) }
+
+    #    my @cmd = (
+    #                'xvfb-run',
+    #                "--server-args='-screen 0, 1280x1024x8'",    # 1024x768x24
+    #                "$bnc",
+    #                "--nw",
+    #                $opts_str,
+    #    );
+
+#my $rc = call_system("xvfb-run -a -e /home/user/xvfb.err --server-args='-screen 0 1280x1024x8' $bnc --nw $config_file $opts_str");
+    return Common::runCmd("$bnc --nw $config_file $opts_str");
+}
+
+# Converts options map to string used for calling BNC.
+sub options2string {
+    my ($opts) = shift;
+
+    my $opts_str = "";
+    if ($opts) {
+        foreach my $key ( keys %{$opts} ) {
+            $opts_str .= "--key $key" . ' "' . $opts->{$key} . '" ';
+        }
+    }
+    return $opts_str;
+}
+
+# =============================================================================
+# parseMessageTypesFromFile
+# =============================================================================
+# Parse Message-Types with repetition rate from a BNC/scanRTCM logfile
+#
+# Param  : $logfile     [required] Path to the BNC-logfile.
+#          $caAbbr      [optional] caster name or abbreviation
+#
+# Return : Message-types for each mountpoint (as Hash-Ref)
+#          Hash with mp as key and the list of messTypes as value
+#          Example: $VAR1 = { 'RIO10' => ['1004(1)','1006(15)','1008(15)',...],
+#                             'CLIB0' => ['1004(1)','1006(10)','1008(10)',...],
+#                           };
+# =============================================================================
+sub parseMessageTypesFromFile {
+    my ( $logfile, $caAbbr, $mytmpPath ) = @_;
+
+    unless ( -s $logfile ) {
+        ERROR "File [$logfile] is empty or does not exist";
+        return;
+    }
+
+    my $tmp                = File::Temp->new( UNLINK => 1, SUFFIX => '.messtyps' );
+    my $casterMessTypeFile = $tmp->filename;
+
+    #my $caster = $logfilename =~ s/\.log\.messtyps//r;
+    #INFO "Process caster '$caster'";
+
+    INFO "Process $logfile";
+
+    # scan beginns with that line: 16-02-18 23:58:02 WTZ37: Get data in RTCM 3.x format
+
+    # First grep for message type lines in bnc-logfile and write them to local temp. directory
+    # NOTE: 'sort -u' because Ephemeries message-types 1019 (GPS), 1020 (GLONASS), 1045 (Galileo) come
+    #       for every sat. at the same time/second. This is done for getting the repetition rate
+    system ("grep \"Received message type\" $logfile | sort -u > $casterMessTypeFile") == 0
+      or ERROR "Fehler: $!";
+
+    # --------------------------------------------
+    # Get message types foreach station/mountpoint
+    # --------------------------------------------
+    my $cmd = "cat $casterMessTypeFile | awk '{print \$3, \$7}' | sort -u";
+    my @messTypes = `$cmd`;    # ['DARX1: 1004\n','DARX1: 1006\n',... ]
+    if ( scalar @messTypes < 1 ) {
+        ERROR "Could not retrieve message types from file $casterMessTypeFile";
+        return;
+    }
+
+    # Note: Skip first 1500 lines because BNC is weird here, all lines with same timestamp, buffer problem?
+
+    # ------------------------------------------------------
+    # Guess repetition rate for each mountpoint/message-type
+    # ------------------------------------------------------
+    # For that get the first $minSamples appearances of mountp with same message type
+    # and build the differences between them
+    my $minSamples = 4;
+    foreach my $mpType (@messTypes) {
+        chomp $mpType;    # 'AUBG3: 1004'
+        my ( $mp, $mt ) = split ( /:\s*/, $mpType );
+        my $cmd  = "grep \"$mp: Received message type $mt\" $logfile";
+        my @rows = `$cmd`;
+        if ( scalar @rows < 1 ) {
+            ERROR "Could not retrieve message types for $mpType from file $casterMessTypeFile";
+            next;
+        }
+
+        if ( scalar @rows <= $minSamples ) {
+            WARN "Could not guess repetition rate for message type $mpType: only " . scalar @rows . " matches found";
+            next;
+        }
+
+        my $repRate = _computeMessTypRepetitonRate( \@rows, $caAbbr );
+        if ($repRate) {
+            $mpType .= '(' . $repRate . ')';
+        }
+    }    # -----  end foreach messageType -----
+
+    #if ( unlink $casterMessTypeFile ) { TRACE "Removed file [$casterMessTypeFile]" }
+
+    # Return an HASH OF ARRAYS with mp as key and the list of messTypes as value
+    my %messTypesHash;
+    foreach (@messTypes) {
+        my @ele = split ( ': ', $_ );
+        my $mp  = shift @ele;
+        push ( @{ $messTypesHash{$mp} }, shift @ele );
+    }
+
+    return \%messTypesHash;
+}
+
+# =============================================================================
+# _computeMessTypRepetitonRate
+# =============================================================================
+# Guess repetition rate of message types
+#
+# Param  : $firstMatches [required] Array-Ref with first appearances of station
+#          and mess type. Complete logfile lines, e.g.
+#          '13-07-04 17:38:26 BRUX0: Received message type 1004 '
+#          $caAbbr       [optional] caster abbreviation
+# Return : repetition rate in secs, (median value)
+# =============================================================================
+sub _computeMessTypRepetitonRate {
+    my ( $rows, $caAbbr ) = @_;
+
+    my $maxGap = 600;    # if gap > 10min then we guess it is a new scan
+
+    $rows->[0] =~ /.{18}([a-x0-9]+): Received message type (\d+)/i;
+    my $stat  = $1;
+    my $mestp = $2;
+
+    # Create list of unix timestamps
+    my @scans;
+    my $scan = 0;
+    my ( $prevTime, $deltaT ) = ( 0, 0 );
+    foreach (@$rows) {
+        my $uxtime = date2unix( substr ( $_, 0, 17 ) );
+        if ( !$uxtime ) {
+            ERROR "Could not parse date from line $_";
+            next;
+        }
+        $deltaT = $uxtime - $prevTime;
+        next if ( $deltaT == 0 );    # e.g. eph 1019,1020 one message for each sat.
+        next if ( $deltaT <= 1 && $mestp =~ /10(19|20|42|43|44|45|46)|63/ );
+        if ( $prevTime && $deltaT > $maxGap ) {
+            $scan++;
+        }
+        push ( @{ $scans[$scan] }, $uxtime );
+        $prevTime = $uxtime;
+    }
+
+    my @repRates;
+    my $highest_nof_diffs = 0;
+    foreach (@scans) {
+        my @timestamps = @{$_};
+
+        # Compute the differences
+        my @diffs;
+        for ( my $i = 1; $i <= $#timestamps; $i++ ) {
+            push ( @diffs, $timestamps[$i] - $timestamps[ $i - 1 ] );
+        }
+        my $nof_diffs = scalar @diffs;
+
+        if ( $nof_diffs < 2 ) {
+            WARN("$stat: $mestp: only $nof_diffs diffs");
+            next;
+        }
+
+        my ( $mean, $prms, $median, $min, $max, $adev, $rms_n ) = stats( pdl \@diffs );
+        $mean  = sprintf ( "%.0f",  $mean );
+        $rms_n = sprintf ( "%.02f", $rms_n );
+        print $stat, ": ", $mestp, ": ", join ( ' ', @diffs ), "[Sig: $mean, $rms_n]\n";
+
+        if ( $rms_n > 10 ) {
+            WARN("$stat: $mestp: RMS too high: $rms_n");
+            next;
+        }
+
+        # get the most frequent value
+        my %counti = ();
+        $counti{$_}++ foreach (@diffs);
+        my ( $ni, $mfv ) = ( 0, 0 );
+        while ( my ( $k, $v ) = each %counti ) {
+            if ( $v > $ni ) {
+                $ni  = $v;
+                $mfv = $k;
+            }
+        }
+
+        my $rounded_val = $mfv;                                  # init
+        foreach ( ( 1, 5, 10, 15, 30, 60, 120, 150, 300 ) ) {    # most likely values
+            my $mdiff = abs ( $mfv - $_ );
+            if ( $mdiff <= 2 ) {
+                $rounded_val = $_;
+            }
+        }
+        push ( @repRates, [ $rounded_val, $nof_diffs ] );
+
+        if ( $nof_diffs > $highest_nof_diffs ) {
+            $highest_nof_diffs = $nof_diffs;
+        }
+    }    # -----  end foreach scan -----
+
+    my @mostLikelyRates = grep { $_->[1] == $highest_nof_diffs } @repRates;
+    my $mostLikelyRate  = $mostLikelyRates[0]->[0];
+    foreach (@repRates) {
+        if ( abs ( $_->[0] - $mostLikelyRate ) > 2 ) {
+            ERROR "$stat: $caAbbr: $mestp: repetition rates from different scans differ: $mostLikelyRate $_->[0]";
+            if ( scalar @repRates == 2 ) {
+                return;
+            }
+        }
+    }
+
+    return $mostLikelyRate;
+}
+
+# =============================================================================
+# parseConfig ($confFile)
+# =============================================================================
+# Parse the BNC config file.
+#
+# Param  : $confFile [required] BNC config file
+# Return : Hash with configuration on success, otherwise undef
+# Usage  : $bncConf = parseConf($bncConfFile);
+#          $corrMount = $bncConf->{'PPP'}->{'corrMount'};
+# =============================================================================
+sub parseConfig {
+    my ($confFile) = @_;
+
+    -s $confFile || LOGDIE "BNC config file \"$confFile\" does not exist\n";
+    TRACE "Parse BNC config file $confFile";
+    open ( my $INP, '<', $confFile ) || die "Could not open file '$confFile': $!";
+    my @confLines = <$INP>;
+    close ($INP);
+
+    my %conf;
+    my $section;    # [General], [PPP]
+    foreach (@confLines) {
+        chomp;
+        s/#.*//;     # entfernt Kommentare
+        s/^\s*//;    # whitespace am Anfang entfernen
+        s/\s+$//;    # entfernt alle whitespaces am Ende
+        next unless length;
+        if ( $_ =~ /\[(\S+)\]/ ) { $section = $1 }
+        next if ( !$section );
+        my ( $key, $val ) = split ( /\s*=\s*/, $_, 2 );
+        if ( !defined $val ) { $val = "" }
+
+        if ( $key eq "mountPoints" ) {
+
+            # Simple parsing
+            $val =~ s/^\/\///;
+            my @mpts = split ( /,\s?\/{2}/, $val );
+            $conf{$section}->{$key} = \@mpts;
+
+            # Extended parsing
+            my @mpts_def = ();
+            foreach (@mpts) {
+
+                #  user:passwd@igs-ip.net:2101/ASPA0 RTCM_3.0 ASM -14.33 189.28 no 1
+                if ( $_ =~
+                    /^([\w-]+):(.+[^@])@([\w\.-]+):(\d{3,5})\/([-\w]+) ([\w\.]+) ?(\w{3})? ([\+\-\d\.]+) ([\+\-\d\.]+) no (\w+)/i
+                  )
+                {
+                    push ( @mpts_def, { caster => $3, port => $4, mp => $5, ntripVers => $10 } );
+                }
+                else { ERROR "$confFile: Could not parse mountPoints string $_" }
+            }
+            $conf{$section}->{'mountPoints_parsed'} = \@mpts_def;
+        }
+        elsif ( $key eq "cmbStreams" ) {
+            my @cmbStrs = split ( /\s*,\s*/, $val );
+            foreach (@cmbStrs) {
+                s/"//g;
+                s/\s+$//;    # entfernt alle whitespaces am Ende
+            }
+            $conf{$section}->{$key} = \@cmbStrs;
+        }
+        else { $conf{$section}->{$key} = $val }
+    }
+
+    my @nofPar = keys %conf;
+    if ( scalar @nofPar < 1 ) {
+        ERROR "No parameter found in BNC conf \"$confFile\"";
+        return;
+    }
+    return \%conf;
+}
+
+# =============================================================================
+# parseLogfile ($file, $sampling, $goBackSecs, $logMode )
+# =============================================================================
+# Parse BNCs' logfile
+#
+# Param  : $file       [required] BNC logfile
+#          $sampling   [optional] sampling rate for logfile
+#          $logMode    [optional] Flag. If set, remember the position of the file-read
+#                      for the next read. Default: off
+# Return : \%data
+# =============================================================================
+sub parseLogfile {
+    my $file     = shift;
+    my $sampling = shift // 1;
+    my $logMode  = shift // 0;
+
+    open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";
+
+    # Goto last position from last read
+    #my $fPos = filePosition($file);
+    #TRACE "Current file pos: $fPos";
+    $logMode && seek ( $fh, filePosition($file), 0 );
+
+    #$logMode && seek ( $fh, $fPos, 0 );
+    my $ln = "";
+    my ( @hlp, @epochs, @latencies, @restarts );
+    my $rec = {};
+    while (<$fh>) {
+        chomp ( $ln = $_ );
+        $rec = {};
+
+        if ( $ln =~ /\bNEU/ ) {    # NEU displacements
+            @hlp = split ( /\s+/, $ln );
+            my $tp = Time::Piece->strptime( substr ( $hlp[2], 0, 19 ), '%Y-%m-%d_%H:%M:%S' );
+
+            if ( $hlp[14] eq '-nan' || $hlp[15] eq '-nan' || $hlp[16] eq '-nan' ) {
+                WARN("$hlp[2] $hlp[3]: NEU displacements are NAN");
+                next;
+            }
+
+            #DEBUG ($tp->epoch, $hlp[3]);
+            push (
+                   @epochs,
+                   {
+                     time => $tp->epoch,
+                     site => $hlp[3],
+                     dN   => $hlp[14],
+                     dE   => $hlp[15],
+                     dU   => $hlp[16],
+                     TRP  => $hlp[18] + $hlp[19],
+                   }
+            );
+        }
+        elsif ( index ( $ln, "latency", 25 ) > 1 ) {
+
+# DEBUG ($ln);
+# altes format:      15-10-06 15:29:02 POTS0: Mean latency 2.34 sec, min 1.58, max 3, rms 0.48, 43 epochs, 17 gaps
+# neu in BNC 2.12.4: 17-06-06 15:35:02 OHI37 Observations: Mean latency 1.51 sec, min 0.57, max 2.7, rms 0.5, 203 epochs, 73 gaps
+            @hlp = split ( /\s+/, $ln );
+
+            # old latency log format
+            if ( $hlp[2] =~ /:$/ ) {
+                splice @hlp, 3, 0, 'Placeholder:';
+                $hlp[2] =~ s/:$//;
+            }
+            $hlp[3] =~ s/:$//;
+
+            my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
+            $rec = {
+                     time    => $tp->epoch,
+                     mp      => $hlp[2],
+                     meanLat => $hlp[6] + 0.0,
+                     epochs  => int ( $hlp[14] ),
+                     type    => $hlp[3]
+            };
+
+            # Unter bestimmten Bedingungen werden die gaps nicht rausgeschrieben!
+            if ( $ln =~ /gaps/ ) {
+                $rec->{'gaps'} = int ( $hlp[16] );
+            }
+
+            push ( @latencies, $rec );
+        }
+        elsif ( index ( $ln, "Start BNC" ) > 1 ) {
+
+            # 17-06-13 07:06:58 ========== Start BNC v2.12.3 (LINUX) ==========
+            @hlp = split ( /\s+/, $ln );
+            my $tp = Time::Piece->strptime( "$hlp[0] $hlp[1]", '%y-%m-%d %H:%M:%S' );
+            push (
+                   @restarts,
+                   {
+                     time    => $tp->epoch,
+                     bncvers => $hlp[5]
+                   }
+            );
+        }
+
+    }    # -----  next line  -----
+
+    $logMode && filePosition( $file, tell ($fh) );    # Remember pos for next read
+    close $fh;
+
+    # Sampling must be done afterwords, for each station separated!
+    my @epochs_sampled;
+    my @sites = map { $_->{'site'} } @epochs;
+
+    #@sites = uniq @sites;
+    my %hlp1 = ();
+    @sites = grep { !$hlp1{$_}++ } @sites;
+    foreach my $s (@sites) {
+        my $epoch_selected = 0;
+        foreach my $rec (@epochs) {
+            next if ( $rec->{'site'} ne $s );
+            if ( $rec->{'time'} - $epoch_selected >= $sampling ) {
+                push ( @epochs_sampled, $rec );
+                $epoch_selected = $rec->{'time'};
+            }
+        }
+    }
+
+    my %data = (
+                 EPOCHS    => \@epochs_sampled,
+                 LATENCIES => \@latencies,
+                 RESTARTS  => \@restarts
+    );
+
+    return \%data;
+}
+
+# =============================================================================
+# parsePPPLogfile ($file, $sampling, $goBackSecs, $logMode )
+# =============================================================================
+# Parse BNCs' PPP station logfile
+#
+# Param  : $file       [required] BNC PPP station
+#          $sampling   [optional] sampling rate for logfile
+#          $goBackSecs [optional] go back that seconds from now in logfile
+#          $logMode    [optional] Flag. If set, remember the position of the file-read
+#                      for the next read. Default: off
+# Return : $station, \%data
+# =============================================================================
+sub parsePPPLogfile {
+    my $file       = shift;
+    my $sampling   = shift // 1;
+    my $goBackSecs = shift // 0;
+    my $logMode    = shift // 0;
+
+    if ($logMode) { $goBackSecs = 0 }
+
+    my $startSec;
+    if ($goBackSecs) {
+        $startSec = time () - $goBackSecs;
+    }
+    my $isFreePPP = 1;    # Free or closed PPP version
+    my $epo;
+    my $old_epochSec = 0;
+    my $epochSec     = 0;
+    my $epochDiff    = 0;
+    my (
+         @hlp,    @EPOCHs, @N,      @E,      @U,    %SATNUM, @TRPs, @CLKs,
+         @G_CLKs, @R_CLKs, @E_CLKs, @C_CLKs, @OGRs, @OGEs,   @OGCs, @OFFGLOs
+    );
+    my ( @EPOCHs_OGE,   @EPOCHs_OGR,   @EPOCHs_OGC );
+    my ( @EPOCHs_G_CLK, @EPOCHs_R_CLK, @EPOCHs_E_CLK, @EPOCHs_C_CLK );
+    my ( %AMB,          %RES,          %ELE, %ION, %BIA );
+    my ( $station,      $lki,          $sys, $sat, $amb );
+    open ( my $fh, "<", $file ) || LOGDIE "Could not open file $file: $!\n";
+
+    # Goto last position from last read
+    #my $fPos = filePosition($file);
+    #TRACE "Current file pos: $fPos";
+    $logMode && seek ( $fh, filePosition($file), 0 );
+
+    #$logMode && seek ( $fh, $fPos, 0 );
+    my $ln = "";
+    while (<$fh>) {
+        chomp ( $ln = $_ );
+
+        if ( $ln =~ /\bof Epoch\b/ ) {
+
+            # PPP of Epoch 2015-08-27_14:00:15.000
+            if ( $ln =~ /PPP of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {    # closed PPP
+                $isFreePPP = 0;
+                $epo       = $1;                                                         #print "$epo\n";
+            }
+            elsif ( $ln =~ /Point Positioning of Epoch (\d{4}-\d{2}-\d{2}_\d{2}:\d{2}:\d{2})\.\d+/ ) {    # free PPP
+                $isFreePPP = 1;
+                $epo       = $1;
+            }
+            else { ERROR "strange line: \"$ln\""; next }
+
+            #my $date = sprintf ( "%s %s", split ( /_/, $epo ) );
+            my $tp = Time::Piece->strptime( $epo, '%Y-%m-%d_%H:%M:%S' );
+            $epochSec  = $tp->epoch();
+            $epochDiff = $epochSec - $old_epochSec;
+            next;
+        }
+
+        next if ( !$epo );
+        next if ( defined $startSec && $epochSec < $startSec );
+        next if ( $epochDiff && $epochDiff < $sampling );
+
+        @hlp = split ( /\s+/, $ln );
+
+        if ( $ln =~ /\bOFFGLO\b/ ) {    # ... OFFGLO       8.417 +- 28.479
+            push ( @OFFGLOs, $hlp[2] );
+        }
+
+        elsif ( $ln =~ /\bdN\b/ ) {
+            push ( @EPOCHs, $epochSec );    # besser $epo ?
+            $old_epochSec = $epochSec;
+
+#2015-08-27_13:59:50.000 DIEP1 X = 3842152.9054 +- 0.0242 Y = 563402.0331 +- 0.0176 Z = 5042888.5182 +- 0.0319 dN = 0.0130 +- 0.0193 dE = -0.0032 +- 0.0178 dU = -0.0248 +- 0.0349
+            $station = $hlp[1];
+
+            if ( $hlp[19] eq '-nan' || $hlp[24] eq '-nan' || $hlp[29] eq '-nan' ) {
+                WARN("$hlp[0] $station: NEU displacements are NAN");
+            }
+
+            push @N, $hlp[19];
+            push @E, $hlp[24];
+            push @U, $hlp[29];
+        }
+        elsif ( ( $ln =~ /\bAMB\b/ ) && ( $ln !~ /RESET/ ) ) {
+            if ($isFreePPP) {    # 2015-10... AMB G05  -6.754 +-  0.086  nEpo = 633
+                $sat = $hlp[2];
+                $sys = substr ( $sat, 0, 1 );
+                $amb = $hlp[4];
+                push @{ $AMB{$sys}{$sat}{EPOCH} },  $epochSec;
+                push @{ $AMB{$sys}{$sat}{DATA} },   $amb;
+                push @{ $AMB{$sys}{$sat}{NUMEPO} }, $hlp[9];
+            }
+            else {               # 2015-08... AMB lIF G04 253.0000 -8.9924 +- 1.7825 el = 22.03 epo = 86
+                $lki = $hlp[2];
+                $sat = $hlp[3];
+                $sys = substr ( $sat, 0, 1 );
+                $amb = $hlp[4] + $hlp[5];
+                push @{ $AMB{$lki}{$sys}{$sat}{EPOCH} },  $epochSec;
+                push @{ $AMB{$lki}{$sys}{$sat}{DATA} },   $amb;
+                push @{ $AMB{$lki}{$sys}{$sat}{NUMEPO} }, $hlp[13];
+                push @{ $ELE{$sys}{$sat}{EPOCH} }, $epochSec;
+                push @{ $ELE{$sys}{$sat}{DATA} },  $hlp[10];
+            }
+        }
+        elsif ( $ln =~ /\bRES\b/ && $ln !~ /Neglected/ ) {
+            if ($isFreePPP) {    # 2015-10... RES R08   L3   -0.0069
+                $sat = $hlp[2];
+                $lki = $hlp[3];
+            }
+            else {               # 2015-08... RES lIF G30  -0.0076
+                $sat = $hlp[3];
+                $lki = $hlp[2];
+            }
+            $sys = substr ( $sat, 0, 1 );
+
+            #print "$epo $lki $sys $sat $res\n";
+            push @{ $RES{$lki}{$sys}{$sat}{EPOCH} }, $epochSec;
+            push @{ $RES{$lki}{$sys}{$sat}{DATA} },  $hlp[4];
+        }
+        elsif ( ( $ln =~ /\bION\b/ ) && ( $ln !~ /RESET/ ) ) {
+
+            # 2018-12-01_20:37:58.000 ION      G02     0.0000    -0.3277 +-   2.4663
+            $sat = $hlp[2];
+            $sys = substr ( $sat, 0, 1 );
+            push @{ $ION{$sys}{$sat}{EPOCH} }, $epochSec;
+            push @{ $ION{$sys}{$sat}{DATA} },  $hlp[4];
+        }
+        elsif ( ( $ln =~ /\bBIA\b/ ) && ( $ln !~ /RESET/ ) ) {
+
+            # 2020-12-09_00:55:19.000 BIA  c1  G       0.0000    +2.5149 +-   9.6543
+            $lki = $hlp[2];
+            $sys = $hlp[3];
+            push @{ $BIA{$lki}{$sys}{EPOCH} }, $epochSec;
+            push @{ $BIA{$lki}{$sys}{DATA} },  $hlp[4] + $hlp[5];
+        }
+        elsif ( $ln =~ /\bCLK\b/ ) {
+            if   ($isFreePPP) { push ( @CLKs, $hlp[2] ) }
+            else              { push ( @CLKs, $hlp[2] + $hlp[3] ) }
+        }
+
+        # REC_CLK in BNC 2.13
+        #elsif ( $ln =~ /\bREC_CLK\b/ ) {
+        elsif ( $ln =~ /\bREC_CLK\s{8}/ ) {
+            push ( @CLKs, $hlp[2] + $hlp[3] );
+        }
+        elsif ( $ln =~ /\bREC_CLK  G\b/ ) {
+            push ( @EPOCHs_G_CLK, $epochSec );
+            push ( @G_CLKs,       $hlp[3] + $hlp[4] );
+        }
+        elsif ( $ln =~ /\bREC_CLK  R\b/ ) {
+            push ( @EPOCHs_R_CLK, $epochSec );
+            push ( @R_CLKs,       $hlp[3] + $hlp[4] );
+        }
+        elsif ( $ln =~ /\bREC_CLK  E\b/ ) {
+            push ( @EPOCHs_E_CLK, $epochSec );
+            push ( @E_CLKs,       $hlp[3] + $hlp[4] );
+        }
+        elsif ( $ln =~ /\bREC_CLK  C\b/ ) {
+            push ( @EPOCHs_C_CLK, $epochSec );
+            push ( @C_CLKs,       $hlp[3] + $hlp[4] );
+        }
+        elsif ( $ln =~ /\bOGR\b/ ) {    # 2015-08... OGR 52.6806 -3.8042 +- 9.0077
+            push ( @EPOCHs_OGR, $epochSec );
+            push ( @OGRs,       $hlp[2] + $hlp[3] );    # only https so far
+        }
+        elsif ( $ln =~ /\bOGE\b/ ) {                    # 2015-08... OGE 52.6806 -3.8042 +- 9.0077
+            push ( @EPOCHs_OGE, $epochSec );
+            push ( @OGEs,       $hlp[2] + $hlp[3] );    # only https so far
+        }
+        elsif ( $ln =~ /\bOGC\b/ ) {                    # 2015-08... OGC 52.6806 -3.8042 +- 9.0077
+            push ( @EPOCHs_OGC, $epochSec );
+            push ( @OGCs,       $hlp[2] + $hlp[3] );    # only https so far
+        }
+        elsif ( $ln =~ /\bSATNUM\b/ ) {                 # 2015-09... SATNUM G  8
+            push ( @{ $SATNUM{ $hlp[2] } }, $hlp[3] );
+        }
+        elsif ( $ln =~ /\bTRP\b/ ) {                    # 2015-08... TRP  2.3803 +0.1009 +- 0.0324
+            push ( @TRPs, $hlp[2] + $hlp[3] );
+        }
+
+    }    # -----  next line  -----
+
+    $logMode && filePosition( $file, tell ($fh) );    # Remember pos for next read
+    close $fh;
+
+    my $nof_epochs = scalar @EPOCHs;
+    DEBUG(   "epochs:$nof_epochs, North displac.: "
+           . scalar @N
+           . ", East displac.: "
+           . scalar @E
+           . ", Up displac.: "
+           . scalar @U
+           . ", TRPs:"
+           . scalar @TRPs
+           . ", CLKs:"
+           . scalar @CLKs
+           . ", OFFGLOs:"
+           . scalar @OFFGLOs );
+    if ( $nof_epochs != scalar @N )                          { LOGDIE "number of epochs and residuals not equal\n" }
+    if ( $nof_epochs != scalar @TRPs )                       { LOGDIE "number of epochs and TRPs not equal\n" }
+    if ( @CLKs && $nof_epochs != scalar @CLKs )              { LOGDIE "number of epochs and CLKs not equal\n" }
+    if ( @G_CLKs && scalar @EPOCHs_G_CLK != scalar @G_CLKs ) { LOGDIE "number of epochs and G_CLKs not equal\n" }
+    if ( @R_CLKs && scalar @EPOCHs_R_CLK != scalar @R_CLKs ) { LOGDIE "number of epochs and R_CLKs not equal\n" }
+    if ( @E_CLKs && scalar @EPOCHs_E_CLK != scalar @E_CLKs ) { LOGDIE "number of epochs and E_CLKs not equal\n" }
+    if ( @C_CLKs && scalar @EPOCHs_C_CLK != scalar @C_CLKs ) { LOGDIE "number of epochs and C_CLKs not equal\n" }
+    if ( @OGRs && scalar @EPOCHs_OGR != scalar @OGRs )       { LOGDIE "number of epochs and OGRs not equal\n" }
+    if ( @OGEs && scalar @EPOCHs_OGE != scalar @OGEs )       { LOGDIE "number of epochs and OGEs not equal\n" }
+    if ( @OGCs && scalar @EPOCHs_OGC != scalar @OGCs )       { LOGDIE "number of epochs and OGCs not equal\n" }
+
+    my %data = (
+                 EPOCHS  => \@EPOCHs,
+                 N       => \@N,
+                 E       => \@E,
+                 U       => \@U,
+                 SATNUM  => \%SATNUM,
+                 TRPs    => \@TRPs,
+                 CLKs    => \@CLKs,
+                 G_CLKs  => \@G_CLKs,
+                 R_CLKs  => \@R_CLKs,
+                 E_CLKs  => \@E_CLKs,
+                 C_CLKs  => \@C_CLKs,
+                 OGRs    => \@OGRs,
+                 OGEs    => \@OGEs,
+                 OGCs    => \@OGCs,
+                 OFFGLOs => \@OFFGLOs,
+                 RES     => \%RES,
+                 AMB     => \%AMB,
+                 ELE     => \%ELE,
+                 ION     => \%ION,
+                 BIA     => \%BIA,
+    );
+
+    return ( $station, \%data, $isFreePPP );
+}
+
+# =============================================================================
+# BncStillWorks ($bncConfFile)
+# =============================================================================
+# Checks if BNC is still working.
+#
+# BNC Jobs can still be alive (in processlist) but are not producing any more.
+# This function checks if a BNC process is proper working.
+#
+# Param  : $bncConfFile [required] path of BNC config file
+# Return : true if BNC is still working otherwise false.
+# =============================================================================
+sub BncStillWorks {
+    my ($bncConfFile) = @_;
+
+    my $timep = Time::Piece->new;
+
+    # for safety if it is exatly at 00:00, add 30 sec
+    my $min_tmp = $timep->strftime("%M");
+    if ( $min_tmp =~ /00|15|30|45/ && $timep->strftime("%S") < 15 ) {
+        $timep += 30;
+        sleep 30;
+    }
+    my $yyyy = $timep->year;
+    my $yy   = $timep->yy;
+    my $doy  = sprintf "%03d", $timep->yday + 1;
+    my $hh   = $timep->strftime("%H");
+    my $h    = uc ( chr ( 65 + $hh ) );
+    my $min  = $timep->min;
+    my $startmin;
+    if    ( $min < 15 )  { $startmin = "00" }
+    elsif ( $min < 30 )  { $startmin = "15" }
+    elsif ( $min < 45 )  { $startmin = "30" }
+    elsif ( $min <= 59 ) { $startmin = "45" }
+    my $bncConf        = parseConf($bncConfFile);
+    my $bncLogFileStub = $bncConf->{'General'}->{'logFile'};
+
+    # BNC log file
+    # ------------
+    my $bncLogFile = "${bncLogFileStub}_" . $timep->strftime("%y%m%d");    # -> bnc.log_160425
+    unless ( -s $bncLogFile ) {
+        WARN("BNC logfile \"$bncLogFile\" is empty or does not exist");
+        return 0;
+    }
+
+    # RINEX Obs Generation
+    # --------------------
+    if ( $bncConf->{'General'}->{'rnxPath'} ) {
+        my $rnxPath = $bncConf->{'General'}->{'rnxPath'};
+        $rnxPath =~ s/\/$//;
+
+        # Write Rnx3 files (i.e. long Rnx3 filenames) 2: on ('rnxV3filenames' is deprecated since 2.12.8!!!)
+        my $writeRnxV3 = $bncConf->{'General'}->{'rnxV3'};
+        my $rnxIntr    = $bncConf->{'General'}->{'rnxIntr'};
+        my $fileMask;
+
+        if ($writeRnxV3) {
+            if ( $rnxIntr eq "1 hour" ) {
+                $fileMask = "*_S_${yyyy}${doy}${hh}??_01H_30S_?O.rnx";
+            }
+            elsif ( $rnxIntr eq "15 min" ) {
+                $fileMask = "*_S_${yyyy}${doy}${hh}${startmin}_15M_01S_?O.rnx";
+            }
+            else {    # daily?
+                $fileMask = "*_S_${yyyy}${doy}????_01D_30S_?O.rnx";    # HRAG00ZAF_S_20191220000_01D_30S_MO.rnx
+            }
+        }
+        else {                                                         # Rnx2
+            if ( $rnxIntr eq "1 hour" ) {
+                $fileMask = "????${doy}${h}.${yy}O";
+            }
+            elsif ( $rnxIntr eq "15 min" ) {
+                $fileMask = "????${doy}${h}${startmin}.${yy}O";
+            }
+            else {                                                     # daily?
+                $fileMask = "????${doy}*.${yy}O";
+            }
+        }
+
+        my @rnxFiles = glob "$rnxPath/$fileMask";
+        if ( scalar @rnxFiles < 1 ) {
+            ERROR("BNC does not create RINEX Obs files. (Filemask: \"$fileMask\" Path: $rnxPath)");
+
+            #return 0;
+        }
+    }
+
+    # RINEX Ephemerides Generation
+    # ----------------------------
+    if ( $bncConf->{'General'}->{'ephPath'} ) {
+        my $rnxPath = $bncConf->{'General'}->{'ephPath'};
+        $rnxPath =~ s/\/$//;
+        my $writeRnxV3 = $bncConf->{'General'}->{'ephV3'};
+        my $rnxIntr    = $bncConf->{'General'}->{'ephIntr'};
+        my $fileMask;
+
+        if ($writeRnxV3) {
+            if ( $rnxIntr eq "1 hour" ) {
+                $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}00_01H_?N.rnx";
+            }
+            elsif ( $rnxIntr eq "15 min" ) {
+                $fileMask = "BRD?00WRD_S_${yyyy}${doy}${hh}${startmin}_15M_?N.rnx"; # BRDC00WRD_S_20191220900_15M_MN.rnx
+            }
+            else {                                                                  # daily?
+                $fileMask = $fileMask = "BRD?00WRD_S_${yyyy}${doy}0000_01D_?N.rnx";
+            }
+        }
+        else {                                                                      # Rnx2
+            $fileMask = "BRD?${doy}*.${yy}N";
+        }
+
+        my @rnxFiles = glob "$rnxPath/$fileMask";
+        if ( scalar @rnxFiles < 1 ) {
+            ERROR("BNC does not create RINEX Nav files. (Filemask: \"$fileMask\" Path: $rnxPath)");
+
+            #return 0;
+        }
+    }
+
+    # Check jobs making PPP
+    # ---------------------
+    if ( $bncConf->{'PPP'}->{'corrMount'} && $bncConf->{'PPP'}->{'staTable'} ) {
+        my $timeOfLastCoo = `grep "NEU:" $bncLogFile | tail -1 | cut -d ' ' -f1,2`;
+        chomp $timeOfLastCoo;
+        if ( !$timeOfLastCoo ) {
+            ERROR "BNC does not compute coordinates";
+            return 0;
+        }
+
+        my $tp    = Time::Piece->strptime( $timeOfLastCoo, '%y-%m-%d %H:%M:%S' );
+        my $now   = Time::Piece->new;
+        my $tdiff = $now - $tp;
+        if ( $tdiff > 1200 ) {
+            ERROR( "Last computed coordinates are " . $tdiff / 60 . " min old" );
+            return 0;
+        }
+    }
+
+    # BNC works
+    return 1;
+}
+
+1;    # End of Bnc
Index: /trunk/BNC/scripts/Common.pm
===================================================================
--- /trunk/BNC/scripts/Common.pm	(revision 9597)
+++ /trunk/BNC/scripts/Common.pm	(revision 9597)
@@ -0,0 +1,279 @@
+package Common;
+
+#
+# Some common functions
+#
+# Revision: $Header$
+#
+
+use strict;
+use warnings;
+use Exporter;
+use Data::Dumper;
+use Log::Log4perl qw(:easy);
+use File::Temp;
+use File::Basename;
+use File::Spec::Functions;
+
+#$| = 1; # Buffer off (print)
+
+sub trim {
+    my ($text) = @_;
+    $text =~ s/^[\s]*//g;
+    $text =~ s/[\s]*$//g;
+    return $text;
+}
+
+# run a command in the shell.
+sub runCmd {
+    my ($cmd) = shift;
+
+    DEBUG("run cmd: \"$cmd\"");
+
+    my $tmpfh   = File::Temp->new( UNLINK => 1, SUFFIX => '.dat' );
+    my $tmpfile = $tmpfh->filename();
+
+    $cmd = "$cmd 2>$tmpfile |";
+    my $PIPE;
+    unless ( open $PIPE, $cmd ) {
+        ERROR("open $cmd | failed ($!)");
+        return ( -1, "", "" );
+    }
+    my $stdout = join '', <$PIPE>;
+    close $PIPE;
+    my $exit_code = $?;
+
+    my $stderr = getContent($tmpfile);
+    return ( $exit_code, $stdout, $stderr );
+}
+
+#** @function amInteractiv ()
+# @brief  Checks if a program runs interactivly, or e.g. was invoked by cron.
+# @see    Perl Cookbook Kap. 15.2
+# @return true if interactive, else false.
+#*
+sub amInteractiv {
+    use POSIX qw(getpgrp tcgetpgrp);
+    my $tty;
+    open ( $tty, "<", "/dev/tty" ) || LOGDIE "Could not open /dev/tty: $!";
+    my $tpgrp = tcgetpgrp( fileno ($tty) );    # get terminal foreground process group
+    my $pgrp  = getpgrp ();                    # get the current process group
+    close $tty;
+    return ( $tpgrp == $pgrp );
+}
+
+#** @function printData ($data)
+# @brief  Prints perl data structures. Uses Data::Dumper.
+# @param  $data [required] Daten
+# @return -
+#*
+sub printData {
+    my $data = shift;
+    my $d    = Data::Dumper->new($data);
+    $d->Indent(3);    # pretty print with array indices
+    print $d->Dump;
+}
+
+sub getContent {
+    my ($fileName) = @_;
+    my $errmsg = "";
+    if ( -f $fileName ) {
+        my $readMode = $/;
+        undef $/;
+        open ( FILE, "<", $fileName )
+          or ERROR("Kann Datei nicht zum lesen oeffnen");
+        $errmsg = <FILE>;
+        close (FILE);
+        $/ = $readMode;
+    }
+    return $errmsg;
+}
+
+#** @function filePosition ($file, $filePos)
+# @brief Save position of a file in an idxfile.
+#
+# Especially for logfiles. For performance reasons, logfiles should not parsed always from the
+# beginning, the next read access should start at the pos of the last read.
+# Saves the position of the last read in a help-file ".filePos" in
+# the directory of the logfile. If the param $filePos is set, then this position will be saved (Write).
+# Otherwise the current position will be returned (Read).
+# @param  $file    [required] File of interest
+# @param  $filePos [optional] if given, this position ()in Bytes) will be saved, else return current position
+# @return file position in Bytes from beginning of the file
+#*
+sub filePosition {
+    my ( $file, $filePos ) = @_;
+
+    my ( $filName, $filDir ) = fileparse($file);
+    my $idxFil = catfile( $filDir, ".filePos" );
+
+    if ($filePos) {
+
+        # Save this position in idxfile
+        my $newPos = "$filName $filePos";
+        if ( -e $idxFil ) {
+            unless ( replace( $idxFil, "^${filName} \\d+", $newPos ) ) {
+
+                # could not replace -> append
+                blurt( "$newPos\n", $idxFil, { append => 1 } );
+#                my $INP;
+#                if ( !open ( $INP, ">>", $idxFil ) ) {
+#                    ERROR("Could not append file [$idxFil]: $!");
+#                    return 0;
+#                }
+#                print $INP "$newPos\n";
+#                close ($INP);
+            }
+        }
+        else {
+            # initial write
+            blurt( "$newPos\n", $idxFil );
+            #writeTextInFile( $idxFil, "$newPos\n" );
+        }
+    }
+    else {
+
+        # Read current position from idxfile
+        # ----------------------------------
+        if ( !-e $idxFil ) {
+            return 0;
+        }
+        my $line = "";
+        my $idx_fh;
+        if ( !open ( $idx_fh, "<", $idxFil ) ) {
+            ERROR("Could not open for read $idxFil: $!");
+            return 0;
+        }
+        while (<$idx_fh>) {
+            if ( $_ =~ /^$filName (\d+)/ ) {    # RegEx problematic with getContentFiltered()
+                $line .= $_;
+                $filePos = $1;
+                last;
+            }
+        }
+        close ($idx_fh);
+
+        if ( $line && !$filePos ) {
+            ERROR("Could not read file position in idxfile $idxFil. Weired line \"$line\"");
+            $filePos = 0;
+        }
+
+        if ($filePos) {
+
+            # If the file was meanwhile newly created, e.g. after restart from BNC
+            my $fileSize = -s $file;
+            if ( $filePos > $fileSize ) {
+                WARN "file $file seams to be newly created";
+                DEBUG("size:$fileSize pos:$filePos");
+                $filePos = 0;
+            }
+        }
+        else {
+            WARN("No position entry found in idxfile for \"$filName\"");
+            $filePos = 0;
+        }
+    }
+
+    return $filePos;
+}
+
+#** @function isAlreadyRunning ()
+# @brief   Check if this program is already running.
+# @details Usage: isAlreadyRunning() && die "Same job is already running!\n";@n
+#          Purpose: e.g. to avoid a program running twice
+# @see     bernese gpsutil::check_job()
+# @return  pid of already running job else false
+#*
+sub isAlreadyRunning {
+    my ($scriptName) = fileparse($0);
+
+    use Proc::ProcessTable;
+    my $pt        = new Proc::ProcessTable;
+    my $pid_found = 0;                        # from already running job
+    foreach my $p ( @{ $pt->table } ) {
+        my $cmd = $p->cmndline;
+        if ( $cmd =~ /$scriptName/ && $p->pid != $$ ) {
+            next if ( $p->pid == getppid () );    # when started from cron!
+            DEBUG( "Found: " . $p->pid . ", myself: $$" );
+            $pid_found = $p->pid;
+            DEBUG "Process $cmd is running";
+            last;
+        }
+    }
+
+    return $pid_found;
+}
+
+#** @function rms ($numbers)
+# @brief  Compute the Root mean square.
+# @param  $numbers [required] List with your numbers
+# @return RMS value
+# @note   use PDL stats() if installed!
+# @see    http://rosettacode.org/wiki/Averages/Root_mean_square#Perl
+#*
+sub rms {
+    my $r = 0;
+    $r += $_**2 for @_;
+    return sqrt ( $r / @_ );
+}
+
+#** @function avg ($numbers)
+# @brief  Compute the arithmetic mean value.
+# @param  $numbers [required] List with your numbers
+# @note   use PDL stats() if installed!
+# @return mean value
+# @see    http://rosettacode.org/wiki/Average/Arithmetic_mean#Perl
+#*
+sub avg {
+    @_ or return 0;
+    my $sum = 0;
+    $sum += $_ foreach @_;
+    return $sum / @_;
+}
+
+#** @function sigma ($numbers)
+# @brief  Compute the STDDEV of the arithmetic mean.
+# @param  $numbers [required] List with your numbers
+# @return Sigma value
+# @note   use use PDL::Primitive qw(stats) if installed!
+#*
+sub sigma {
+    my $n = scalar (@_);
+
+    if ( $n == 1 ) {
+        WARN "Could not compute sigma for only one sample";
+        return;
+    }
+
+    my $e = 0;    # arithm. Mittel
+    $e += $_ for @_;
+    $e = $e / $n;
+
+    my @v  = map { $e - $_ } @_;
+    my $vv = 0;
+    $vv += $_**2 for @v;
+    return sqrt ( $vv / ( $n * ( $n - 1 ) ) );
+}
+
+#** @function median (@numbers)
+# @brief  Compute the median of a list of numbers.
+# @param  @numbers [required] List with your numbers
+# @see    https://en.wikipedia.org/wiki/Median
+# @note   use PDL stats() if installed!
+# @return median
+#*
+sub median {
+    my $mid    = int ( @_ / 2 );
+    my @sorted = sort { $a <=> $b } (@_);
+    if ( @sorted % 2 ) {
+        return $sorted[$mid];
+    }
+    else {
+        return ( $sorted[ $mid - 1 ] + $sorted[$mid] ) / 2;
+    }
+
+    return;    # undef
+}
+
+1;             # End of Common
+
Index: /trunk/BNC/scripts/pppPlot.pl
===================================================================
--- /trunk/BNC/scripts/pppPlot.pl	(revision 9597)
+++ /trunk/BNC/scripts/pppPlot.pl	(revision 9597)
@@ -0,0 +1,827 @@
+#!/usr/bin/env perl
+
+# ========================================================================
+# pppPlot.pl - plot BNC's PPP results using gnuplot
+# ========================================================================
+#
+# Plot metrics:
+#    - code and phase residuals in [m],
+#    - receiver clock errors in [m],
+#    - a-priori and correction values of tropospheric zenith delay in [m],
+#    - time offset between GPS time and Galileo/GLONASS/BDS time in [m],
+#    - ambiguities, given per satellite
+#    - elevations,  given per satellite
+#
+# Author  : Andrea Stuerze
+# Revision: $Header$
+# Changes :
+# ========================================================================
+
+# Uses
+use strict;
+use warnings;
+
+BEGIN {
+    use FindBin qw($Bin);
+    use lib "$Bin";
+}
+
+use diagnostics;
+use PDL;
+use FindBin qw($Bin);
+use Getopt::Long;
+use Chart::Gnuplot;
+use Data::Dumper qw(Dumper);
+use File::Basename;
+use Date::Manip;
+use Log::Log4perl qw(:easy);
+use PDF::API2;
+use constant {
+               mm   => 25.4 / 72,
+               inch => 1 / 72,
+               pt   => 1,
+};    # There are 72 postscript points in an inch and there are 25.4 millimeters in an inch.
+
+# BKG Perl libs
+use Bnc;
+use Common;
+
+
+# Logging
+Log::Log4perl->easy_init(
+    {
+
+        #file   => "plotPPP.log",
+        layout => '%d [%c l%L] %p: %m%n',
+        level  => $TRACE
+    }
+);
+
+# Options
+my ($prog)    = fileparse($0);
+my $help      = 0;
+my @plotTypes = ();
+my @logFiles  = ();
+my $sampling  = 1;
+
+GetOptions(
+            'help'        => \$help,
+            'plotTypes=s' => \@plotTypes,
+            'logFiles=s'  => \@logFiles,
+            'sampling=s'  => \$sampling,
+);
+
+HELP_MESSAGE() if $help;
+@plotTypes = qw(NEU) unless (@plotTypes);
+@plotTypes = map {uc} split ( /[, ]/, join ( ',', @plotTypes ) );
+@logFiles  = split ( /[, ]/, join ( ',', @logFiles ) );
+unless (@logFiles) { ERROR "logfiles missing"; HELP_MESSAGE() }
+DEBUG("\n       plotTpes: @plotTypes\n       logfiles: @logFiles\n       sampling:  $sampling");
+
+# -----------------------------------------------------------------------------
+# Generate data sets for gnuplot
+# -----------------------------------------------------------------------------
+# for pdf gerneration
+my ( $png, $page, $headline, $headline_text );
+my $y0 = 180 / mm;
+my ( $x, $y, $width, $height ) = ( 40 / mm, $y0, 130 / mm, 80 / mm );
+my $dy = $height + 10 / mm;
+
+# Loop over logfiles
+foreach my $file (@logFiles) {
+    DEBUG "Parse logfile $file";
+
+    # -----------------------------------------------------------------------------
+    # Create pdf for plot results
+    # -----------------------------------------------------------------------------
+    my ( $inputFilename, $inputDir, $inputSuffix ) = fileparse( $file, '\..*' );
+    my $pdf_name = sprintf ( "%s.pdf", $inputFilename );
+    my $pdf      = PDF::API2->new( -file => "$inputDir$pdf_name" );
+    my $font1    = $pdf->corefont('Helvetica-Bold');
+
+    # -----------------------------------------------------------------------------
+    # Read logfile
+    # -----------------------------------------------------------------------------
+    my ( $station, $file ) = Bnc::parsePPPLogfile( $file, $sampling );
+    my $EPOCHS       = $file->{'EPOCHS'};
+    my $EPOCHS_OGR   = $file->{'EPOCHS_OGR'};
+    my $EPOCHS_OGE   = $file->{'EPOCHS_OGE'};
+    my $EPOCHS_OGC   = $file->{'EPOCHS_OGC'};
+    my $EPOCHS_G_CLK = $file->{'EPOCHS_G_CLK'};
+    my $EPOCHS_R_CLK = $file->{'EPOCHS_R_CLK'};
+    my $EPOCHS_E_CLK = $file->{'EPOCHS_E_CLK'};
+    my $EPOCHS_C_CLK = $file->{'EPOCHS_C_CLK'};
+    my %AMB          = %{ $file->{'AMB'} };
+    my %RES          = %{ $file->{'RES'} };
+    my %ELE          = %{ $file->{'ELE'} };
+    my %ION          = %{ $file->{'ION'} };
+    my %BIA          = %{ $file->{'BIA'} };
+
+    # -----------------------------------------------------------------------------
+    # RMS computation
+    # -----------------------------------------------------------------------------
+    my ( $mean, $prms, $median, $min, $max, $adev, $rms_n, $rms_e, $rms_u, $rms_trp );
+    my ( $n, $e, $u, $trp, $str_rms_n, $str_rms_e, $str_rms_u, $str_rms_trp );
+    $n = pdl( $file->{'N'} );
+    ( $mean, $prms, $median, $min, $max, $adev, $rms_n ) = stats($n);
+    $e = pdl( $file->{'E'} );
+    ( $mean, $prms, $median, $min, $max, $adev, $rms_e ) = stats($e);
+    $u = pdl( $file->{'U'} );
+    ( $mean, $prms, $median, $min, $max, $adev, $rms_u ) = stats($u);
+    $trp = pdl( $file->{'TRPs'} );
+    ( $mean, $prms, $median, $min, $max, $adev, $rms_trp ) = stats($trp);
+    $str_rms_n   = sprintf ( " %.2f ", $rms_n );
+    $str_rms_e   = sprintf ( " %.2f ", $rms_e );
+    $str_rms_u   = sprintf ( " %.2f ", $rms_u );
+    $str_rms_trp = sprintf ( " %.2f ", $rms_trp );
+    DEBUG("RMS: North: $str_rms_n, East: $str_rms_e, Up: $str_rms_u, TRP: $str_rms_trp");
+
+    # -----------------------------------------------------------------------------
+    # Plot several data sets
+    # -----------------------------------------------------------------------------
+    my $dataset;
+    ######### NEU #####################
+    DEBUG "Plot NEU";
+    $page = $pdf->page();
+    $page->mediabox('A4');
+    $headline      = sprintf ( "PPP results for station %s", $station );
+    $headline_text = $page->text;
+    $headline_text->font( $font1, 11 / pt );
+    $headline_text->translate( 15 / mm, 280 / mm );
+    $headline_text->text($headline);
+    $y = $y0;
+    my $pngName  = sprintf ( "%s_NEU.png", $station );
+    my $chartNEU = newChart($station);
+    $chartNEU->set( output => $pngName );
+    $chartNEU->set( ylabel => "Displacements [m]", yrange => [ " -0.5 ", " 0.5 " ] );
+
+    #y2label => "Number of Satellites [-]", y2range => [" 0 ", " 20 "], y2tics => 'on',
+
+    my $dataN = Chart::Gnuplot::DataSet->new(
+                                              xdata   => $EPOCHS,
+                                              ydata   => $file->{'N'},
+                                              title   => "Displacements N, RMS + -$str_rms_n m",
+                                              timefmt => '%s',
+                                              style   => "dots",
+    );
+    my $dataE = Chart::Gnuplot::DataSet->new(
+                                              xdata   => $EPOCHS,
+                                              ydata   => $file->{'E'},
+                                              title   => "Displacements E, RMS + -$str_rms_e m",
+                                              timefmt => '%s',
+                                              style   => "dots",
+    );
+    my $dataU = Chart::Gnuplot::DataSet->new(
+                                              xdata   => $EPOCHS,
+                                              ydata   => $file->{'U'},
+                                              title   => "Displacements U, RMS + -$str_rms_u m",
+                                              timefmt => '%s',
+                                              style   => "dots",
+    );
+    my @datasets = ( $dataN, $dataE, $dataU );
+    $chartNEU->plot2d(@datasets);
+
+    $png = $page->gfx();
+    LOGDIE("could not find image file: $!\n") unless -e $pngName;
+    $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );    #print "y= : $y \n"
+
+    ######### TRP #####################
+    if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+        DEBUG "Plot TRPs";
+        my $pngName  = sprintf ( "%s_TRP.png", $station );
+        my $chartTRP = newChart($station);
+        $chartTRP->set( output => $pngName );
+        $chartTRP->set( ylabel => "Tropospheric Delay[m]", yrange => [ " 2.0 ", " 2.6 " ] );
+
+        my $dataTRP = Chart::Gnuplot::DataSet->new(
+                                                    xdata   => $EPOCHS,
+                                                    ydata   => $file->{'TRPs'},
+                                                    title   => "Tropospheric Delay, RMS + -$str_rms_trp m",
+                                                    timefmt => '%s',
+                                                    style   => "dots",
+        );
+        $chartTRP->plot2d($dataTRP);
+        $y = $y - $dy;
+
+        if ( $y < 30 / mm ) {
+            $page = $pdf->page();
+            $page->mediabox('A4');
+            $y = $y0;
+        }
+        $png = $page->gfx();
+        LOGDIE("could not find image file: $!\n") unless -e $pngName;
+        $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+
+        ######### CLK #####################
+        if ( scalar @{ $file->{'CLKs'} } < 1 ) {
+            DEBUG "No CLKs found";
+        }
+        else {
+            DEBUG "Plot CLKs";
+            $pngName = sprintf ( "%s_CLK.png", $station );
+            my $chartCLK = newChart($station);
+            $chartCLK->set( output => $pngName );
+            $chartCLK->set( ylabel => "CLK [m]" );
+
+            $dataset = Chart::Gnuplot::DataSet->new(
+                                                     xdata   => $EPOCHS,
+                                                     ydata   => $file->{'CLKs'},
+                                                     title   => "Receiver clock",
+                                                     timefmt => '%s',
+                                                     style   => "dots",
+            );
+            $chartCLK->plot2d($dataset);
+            $y = $y - $dy;
+
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            LOGDIE("could not find image file: $!\n") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+        ######### GPS CLK #####################
+        if ( scalar @{ $file->{'G_CLKs'} } < 1 ) {
+            DEBUG "No GPS CLKs found";
+        }
+        else {
+            DEBUG "Plot GPS CLKs";
+            $pngName = sprintf ( "%s_CLK_G.png", $station );
+            my $chartCLK = newChart($station);
+            $chartCLK->set( output => $pngName );
+            $chartCLK->set( ylabel => "CLK [m]" );
+
+            $dataset = Chart::Gnuplot::DataSet->new(
+                                                     xdata   => $EPOCHS_G_CLK,
+                                                     ydata   => $file->{'G_CLKs'},
+                                                     title   => "GPS Receiver clock",
+                                                     timefmt => '%s',
+                                                     style   => "dots",
+            );
+            $chartCLK->plot2d($dataset);
+            $y = $y - $dy;
+
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            LOGDIE("could not find image file: $!\n") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+        ######### GLONASS CLK #####################
+        if ( scalar @{ $file->{'R_CLKs'} } < 1 ) {
+            DEBUG "No GLONASS CLKs found";
+        }
+        else {
+            DEBUG "Plot GLONASS CLKs";
+            $pngName = sprintf ( "%s_CLK_R.png", $station );
+            my $chartCLK = newChart($station);
+            $chartCLK->set( output => $pngName );
+            $chartCLK->set( ylabel => "CLK [m]" );
+
+            $dataset = Chart::Gnuplot::DataSet->new(
+                                                     xdata   => $EPOCHS_R_CLK,
+                                                     ydata   => $file->{'R_CLKs'},
+                                                     title   => "GLONASS Receiver clock",
+                                                     timefmt => '%s',
+                                                     style   => "dots",
+            );
+            $chartCLK->plot2d($dataset);
+            $y = $y - $dy;
+
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            LOGDIE("could not find image file: $!\n") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+        ######### Galileo CLK #####################
+        if ( scalar @{ $file->{'E_CLKs'} } < 1 ) {
+            DEBUG "No Galileo CLKs found";
+        }
+        else {
+            DEBUG "Plot Galileo CLKs";
+            $pngName = sprintf ( "%s_CLK_E.png", $station );
+            my $chartCLK = newChart($station);
+            $chartCLK->set( output => $pngName );
+            $chartCLK->set( ylabel => "CLK [m]" );
+
+            $dataset = Chart::Gnuplot::DataSet->new(
+                                                     xdata   => $EPOCHS_E_CLK,
+                                                     ydata   => $file->{'E_CLKs'},
+                                                     title   => "Galileo Receiver clock",
+                                                     timefmt => '%s',
+                                                     style   => "dots",
+            );
+            $chartCLK->plot2d($dataset);
+            $y = $y - $dy;
+
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            LOGDIE("could not find image file: $!\n") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+
+        ######### Beidou CLK #####################
+        if ( scalar @{ $file->{'C_CLKs'} } < 1 ) {
+            DEBUG "No BDS CLKs found";
+        }
+        else {
+            DEBUG "Plot BDS CLKs";
+            $pngName = sprintf ( "%s_CLK_C.png", $station );
+            my $chartCLK = newChart($station);
+            $chartCLK->set( output => $pngName );
+            $chartCLK->set( ylabel => "CLK [m]" );
+
+            $dataset = Chart::Gnuplot::DataSet->new(
+                                                     xdata   => $EPOCHS_C_CLK,
+                                                     ydata   => $file->{'C_CLKs'},
+                                                     title   => "BDS Receiver clock",
+                                                     timefmt => '%s',
+                                                     style   => "dots",
+            );
+            $chartCLK->plot2d($dataset);
+            $y = $y - $dy;
+
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            LOGDIE("could not find image file: $!\n") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+
+        ######### OGR #####################
+        if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+            if ( scalar @{ $file->{'OGRs'} } < 1 ) {
+                DEBUG "No OGRs found";
+            }
+            else {
+                DEBUG "Plot OGRs";
+                my $pngName  = sprintf ( "%s_OGR.png", $station );
+                my $chartOGR = newChart($station);
+                $chartOGR->set( output => $pngName );
+                $chartOGR->set( ylabel => "OGR [m]" );
+
+                $dataset = Chart::Gnuplot::DataSet->new(
+                                                         xdata   => $EPOCHS_OGR,
+                                                         ydata   => $file->{'OGRs'},
+                                                         title   => "Offset GPS - GLONASS",
+                                                         timefmt => '%s',
+                                                         style   => "dots",
+                );
+                $chartOGR->plot2d($dataset);    #system ("display $pngName&");
+                $y = $y - $dy;
+
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file: $!\n") unless -e $pngName;
+                $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+            }
+        }
+
+        ######### OGE #####################
+        if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+            if ( scalar @{ $file->{'OGEs'} } < 1 ) {
+                DEBUG "No OGEs found";
+            }
+            else {
+                DEBUG "Plot OGEs";
+                my $pngName  = sprintf ( "%s_OGE.png", $station );
+                my $chartOGE = newChart($station);
+                $chartOGE->set( output => $pngName );
+                $chartOGE->set( ylabel => "OGE [m]" );
+
+                $dataset = Chart::Gnuplot::DataSet->new(
+                                                         xdata   => $EPOCHS_OGE,
+                                                         ydata   => $file->{'OGEs'},
+                                                         title   => "Offset GPS - Galileo",
+                                                         timefmt => '%s',
+                                                         style   => "dots",
+                );
+                $chartOGE->plot2d($dataset);    #system ("display $pngName&");
+                $y = $y - $dy;
+
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file: $!\n") unless -e $pngName;
+                $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+            }
+        }
+
+        ######### OGC #####################
+        if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+            if ( scalar @{ $file->{'OGCs'} } < 1 ) {
+                DEBUG "No OGCs found";
+            }
+            else {
+                DEBUG "Plot OGCs";
+                my $pngName  = sprintf ( "%s_OGC.png", $station );
+                my $chartOGC = newChart($station);
+                $chartOGC->set( output => $pngName );
+                $chartOGC->set( ylabel => "OGC [m]" );
+
+                $dataset = Chart::Gnuplot::DataSet->new(
+                                                         xdata   => $EPOCHS_OGC,
+                                                         ydata   => $file->{'OGCs'},
+                                                         title   => "Offset GPS - BDS",
+                                                         timefmt => '%s',
+                                                         style   => "dots",
+                );
+                $chartOGC->plot2d($dataset);    #system ("display $pngName&");
+                $y = $y - $dy;
+
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file: $!\n") unless -e $pngName;
+                $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+            }
+        }
+
+        ######### ELE #####################
+        DEBUG "Plot Elevations";
+        $page = $pdf->page();
+        $page->mediabox('A4');
+        $y             = $y0 + $dy;
+        $headline      = sprintf ( "Satellite Elevations for station %s", $station );
+        $headline_text = $page->text;
+        $headline_text->font( $font1, 11 / pt );
+        $headline_text->translate( 15 / mm, 280 / mm );
+        $headline_text->text($headline);
+
+        my $chartELE = newChart($station);
+        $chartELE->set( legend => { position => "outside right" } );
+
+        # SYSTEM #print Dumper \%ELE;
+        foreach my $key_sys ( sort keys %ELE ) {
+
+            # print "$key_sys \n"; #print Dumper $ELE{$key_sys};
+            my @datasets = ();                                                # init datasets
+            my $pngName  = sprintf ( "%s_ELE_%s.png", $station, $key_sys );
+            $chartELE->set( output => $pngName );
+            $chartELE->set( ylabel => "Elevation [°]", yrange => [ " 0.0 ", " 90.0 " ] );
+
+            # SATELLITE
+            foreach my $key_sat ( sort keys %{ $ELE{$key_sys} } ) {
+
+                # print "$key_sat = $ELE{$key_sys}{$key_sat} \n";
+                $dataset = Chart::Gnuplot::DataSet->new(
+                    xdata   => \@{ $ELE{$key_sys}{$key_sat}{EPOCH} },         # array of epochs
+                    ydata   => \@{ $ELE{$key_sys}{$key_sat}{DATA} },          # array of elevations of one satellite
+                    title   => "$key_sat",
+                    timefmt => '%s',
+
+                    #style   => "dots",
+                    style => "dots",
+                );
+                push ( @datasets, $dataset );
+            }
+            $chartELE->plot2d(@datasets);
+
+            # system ("display $pngName&");
+            $y = $y - $dy;
+            if ( $y < 30 / mm ) {
+                $page = $pdf->page();
+                $page->mediabox('A4');
+                $y = $y0;
+            }
+            $png = $page->gfx();
+            die ("could not find image file: $!") unless -e $pngName;
+            $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+        }
+
+        ######### AMB #####################
+        DEBUG "Plot Ambiguities";
+        $page = $pdf->page();
+        $page->mediabox('A4');
+        $y             = $y0 + $dy;
+        $headline      = sprintf ( "Ambiguities for station %s", $station );
+        $headline_text = $page->text;
+        $headline_text->font( $font1, 11 / pt );
+        $headline_text->translate( 15 / mm, 280 / mm );
+        $headline_text->text($headline);
+
+        # AMBIGUITY_TYPE #print Dumper \%AMB;
+        foreach my $key_ambType (%AMB) {    #print "$key_ambType \n";  ??????
+            foreach my $key_sys ( sort keys %{ $AMB{$key_ambType} } ) {
+
+                #print "$key_sys \n"; print Dumper $AMB{$key_ambType};
+                my ( @datasets_amb, @datasets_epo );    # init datasets
+                my $pngName_amb = sprintf ( "%s_AMB_%s_%s.png", $station, $key_ambType, $key_sys );
+                my $pngName_epo = sprintf ( "%s_EPO_%s_%s.png", $station, $key_ambType, $key_sys );
+                my $chartAMB = Chart::Gnuplot->new(
+                    output   => $pngName_amb,
+                    terminal => 'png',
+                    title    => $station,
+                    ylabel   => "Ambiguities $key_ambType [m]",
+
+                    # yrange => [" 0.0 ", " 90.0 "],
+                    xlabel   => "Time [h]",
+                    timeaxis => 'x',
+                    xtics    => { labelfmt => '%H:%M', rotate => '-270', },
+                    legend   => { position => "outside right", },
+                    grid     => 'on',
+                );
+                my $chartEPO = Chart::Gnuplot->new(
+                    output   => $pngName_epo,
+                    terminal => 'png',
+                    title    => $station,
+                    ylabel   => "Number of Epochs $key_ambType [-]",
+
+                    # yrange => [" 0.0 ", " 90.0 "],
+                    xlabel   => "Time [h]",
+                    timeaxis => 'x',
+                    xtics    => { labelfmt => '%H:%M', rotate => '-270', },
+                    legend   => { position => "outside right", },
+                    grid     => 'on',
+                );
+
+                # SATELLITE
+                foreach my $key_sat ( sort keys %{ $AMB{$key_ambType}{$key_sys} } ) {
+
+                    #print "$key_sat = $AMB{$key_ambType}{$key_sys}{$key_sat} \n";
+                    # ambiguities
+                    my $dataset_amb = Chart::Gnuplot::DataSet->new(
+                        xdata => $AMB{$key_ambType}{$key_sys}{$key_sat}{EPOCH},  # array of epochs
+                        ydata => $AMB{$key_ambType}{$key_sys}{$key_sat}{DATA},   # array of ambiguities of one satellite
+                        title => "$key_sat",
+                        timefmt => '%s',
+                        style   => "dots",
+
+                        #style   => "dots",
+                    );
+                    push ( @datasets_amb, $dataset_amb );
+
+                    # number of epochs used for ambiguity
+                    my $dataset_epo = Chart::Gnuplot::DataSet->new(
+                        xdata => $AMB{$key_ambType}{$key_sys}{$key_sat}{EPOCH},  # array of epochs
+                        ydata => $AMB{$key_ambType}{$key_sys}{$key_sat}{NUMEPO}, # array of ambiguities of one satellite
+                        title => "$key_sat",
+                        timefmt => '%s',
+
+                        #style   => "dots",
+                        style => "dots",
+                    );
+                    push ( @datasets_epo, $dataset_epo );
+                }
+
+                # ambiguities
+                $chartAMB->plot2d(@datasets_amb);
+
+                # system ("display $pngName_amb&");
+                $y = $y - $dy;
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file: $!\n") unless -e $pngName_amb;
+                $png->image( $pdf->image_png($pngName_amb), $x, $y, $width, $height );
+
+                # number of epochs used for ambiguity
+                $chartEPO->plot2d(@datasets_epo);
+
+                # system ("display $pngName_epo&");
+                $y = $y - $dy;
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file $pngName_epo: $!\n") unless -e $pngName_epo;
+                $png->image( $pdf->image_png($pngName_epo), $x, $y, $width, $height );
+            }
+        }
+
+        ######### RES #####################
+        DEBUG "Plot Residuals";
+        $page = $pdf->page();
+        $page->mediabox('A4');
+        $y             = $y0 + $dy;
+        $headline      = sprintf ( "Residuals for station %s", $station );
+        $headline_text = $page->text;
+        $headline_text->font( $font1, 11 / pt );
+        $headline_text->translate( 15 / mm, 280 / mm );
+        $headline_text->text($headline);
+
+        my $chartRES = newChart($station);
+        $chartRES->set( legend => { position => "outside right" } );
+
+        # RESIDUAL_TYPE   #print Dumper \%RES;
+        foreach my $key_resType ( sort keys %RES ) {    #print "key_resType: $key_resType \n";
+                                                        #SYSTEM
+            foreach my $key_sys ( sort keys %{ $RES{$key_resType} } ) {
+
+                #print "key_sys: $key_sys \n"; #print Dumper $RES{$key_resType};
+                my @datasets;
+                my $pngName = sprintf ( "%s_RES_%s_%s.png", $station, $key_resType, $key_sys );
+                $chartRES->set( output => $pngName );
+                $chartRES->set( ylabel => "Residuals $key_resType [m]" );
+
+                if ( $key_resType =~ /^c/ ) {
+                    $chartRES->set( yrange => [ " -6.0 ", " 6.0 " ] );
+                }
+                elsif ( $key_resType =~ /^l/ ) {
+                    $chartRES->set( yrange => [ " -0.06 ", " 0.06 " ] );
+                }
+
+                elsif ( $key_resType =~ /^GIM/ ) {
+                    $chartRES->set( yrange => [ " -6.0 ", " 6.0 " ] );
+                }
+
+                # SATELLITE
+                foreach my $key_sat ( sort keys %{ $RES{$key_resType}{$key_sys} } ) {
+
+                    #print "$key_sat = $RES{$key_resType}{$key_sys}{$key_sat} \n";
+                    $dataset = Chart::Gnuplot::DataSet->new(
+                        xdata   => $RES{$key_resType}{$key_sys}{$key_sat}{EPOCH},  # array of epochs
+                        ydata   => $RES{$key_resType}{$key_sys}{$key_sat}{DATA},   # array of residuals of one satellite
+                        title   => "$key_sat",
+                        timefmt => '%s',
+
+                        #style   => "dots",
+                        style => "dots",
+                    );
+                    push ( @datasets, $dataset );
+                }
+                $chartRES->plot2d(@datasets);
+                $y = $y - $dy;
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                LOGDIE("could not find image file: $!\n") unless -e $pngName;
+                $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+            }
+        }
+
+        ######### ION #####################
+        if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+            DEBUG "Plot ION";
+            $page = $pdf->page();
+            $page->mediabox('A4');
+            $y             = $y0 + $dy;
+            $headline      = sprintf ( "Ionosphere Delay for station %s", $station );
+            $headline_text = $page->text;
+            $headline_text->font( $font1, 11 / pt );
+            $headline_text->translate( 15 / mm, 280 / mm );
+            $headline_text->text($headline);
+
+            my $chartION = newChart($station);
+            $chartION->set( ylabel => "Ionophere Delay [m]" );
+            $chartION->set( legend => { position => "outside right" } );
+
+            # SYSTEM
+            foreach my $ksys ( sort keys %ION ) {    #print "$key_sys \n"; #print Dumper $ION{$key_sys};
+                my @datasets;                        # init datasets
+                my $pngName = sprintf ( "%s_ION_%s.png", $station, $ksys );
+                $chartION->set( output => $pngName );
+
+                # SATELLITE
+                foreach my $sat ( sort keys %{ $ION{$ksys} } ) {    #print "$key_sat = $ION{$key_sys}{$key_sat} \n";
+                    my $dataset = Chart::Gnuplot::DataSet->new(
+                        xdata   => $ION{$ksys}{$sat}{EPOCH},        # array of epochs
+                        ydata   => $ION{$ksys}{$sat}{DATA},         # array of ionvations of one satellite
+                        title   => "$sat",
+                        timefmt => '%s',
+
+                        #style   => " dots ",
+                        style => "dots",
+                    );
+                    push ( @datasets, $dataset );
+                }
+
+                $chartION->plot2d(@datasets);                       #system ("display $pngName&");
+                $y = $y - $dy;
+                if ( $y < 30 / mm ) {
+                    $page = $pdf->page();
+                    $page->mediabox('A4');
+                    $y = $y0;
+                }
+                $png = $page->gfx();
+                die ("could not find image file: $!") unless -e $pngName;
+                $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+            }
+        }
+
+        ######### BIAS #####################
+        if ( grep ( $_ eq "ALL", @plotTypes ) ) {
+            DEBUG "Plot BIAS";
+            $page = $pdf->page();
+            $page->mediabox('A4');
+            $y             = $y0 + $dy;
+            $headline      = sprintf ( "Receiver Biases for station %s", $station );
+            $headline_text = $page->text;
+            $headline_text->font( $font1, 11 / pt );
+            $headline_text->translate( 15 / mm, 280 / mm );
+            $headline_text->text($headline);
+
+            my $chartBIAS = newChart($station);
+            $chartBIAS->set( legend => { position => "outside right" } );
+
+            # BIAS_TYPE   #print Dumper \%BIA;
+            foreach my $key_biasType ( sort keys %BIA ) {    #print "key_biasType: $key_biasType \n";
+                foreach my $key_sys ( sort keys %{ $BIA{$key_biasType} } ) {
+
+                    #print "key_sys: $key_sys \n"; #print Dumper $BIA{$key_biasType};
+                    my @datasets;                            # init datasets
+                    my $pngName = sprintf ( "%s_BIAS_%s_%s.png", $station, $key_biasType, $key_sys );
+                    $chartBIAS->set( output => $pngName );
+                    $chartBIAS->set( ylabel => "Receiver Bias $key_biasType [m]" );
+
+                    my $dataset =
+                      Chart::Gnuplot::DataSet->new(
+                                                    xdata   => $BIA{$key_biasType}{$key_sys}{EPOCH},
+                                                    ydata   => $BIA{$key_biasType}{$key_sys}{DATA},
+                                                    title   => "$key_sys",
+                                                    timefmt => '%s',
+                                                    style   => "dots",
+                      );
+                    push ( @datasets, $dataset );
+
+                    $chartBIAS->plot2d(@datasets);
+                    $y = $y - $dy;
+                    if ( $y < 30 / mm ) {
+                        $page = $pdf->page();
+                        $page->mediabox('A4');
+                        $y = $y0;
+                    }
+                    $png = $page->gfx();
+                    die ("could not find image file: $!") unless -e $pngName;
+                    $png->image( $pdf->image_png($pngName), $x, $y, $width, $height );
+                }
+            }
+        }
+    }    # end if ALL @plotTypes
+
+    $pdf->save();
+    $pdf->end();
+
+    #system ("rm *.png");
+    if (Common::amInteractiv ) {
+    	system ("evince $inputDir/$pdf_name");
+    }
+}    # -----  next logfile  -----
+
+# newChart returns a default chart.
+sub newChart {
+    my $title = shift;
+    return Chart::Gnuplot->new(
+                                terminal => 'png',
+                                title    => $title,
+                                xlabel   => "Time [h]",
+                                timeaxis => 'x',
+                                xtics    => { labelfmt => '%H:%M', rotate => '-270' },
+                                grid     => 'on',
+    );
+}
+
+sub HELP_MESSAGE {
+    print <<EOI_HILFE;
+$prog - plot BNC's PPP results using gnuplot
+
+USAGE:
+  $prog [OPTIONS]
+
+OPTIONS:
+  -p,--plotTypes    ALL or NEU (default)
+  -l,--logFiles     comma separated list of BNC's PPP logfiles
+  -s,--sampling     sampling interval in seconds for the logfile data (default: 1); for a daily logfile <30> seconds should be usefull
+  -h,--help         show help contents
+
+EXAMPLES:
+  $prog --plotTypes ALL -s 30 -l /path/to/FFMJ186730.ppp
+
+2021 andrea.stuerze\@bkg.bund.de
+EOI_HILFE
+    exit;
+}
