#! /usr/bin/perl -w use Tk; use Tk::widgets qw/Dialog ErrorDialog ROText/; use Time::Local qw/timegm_nocheck timelocal/; use strict; my $mw = MainWindow->new; $mw->CmdLine; $mw->title("IGC log tool"); $mw->configure(-menu => my $menubar = $mw->Menu); my $file = $menubar->cascade(qw/-label File -underline 0 -menuitems/ => [ [command => 'Open IGC', -command => [\&load_igc]], [command => 'Open DAT', -command => [\&load_dat]], [command => 'Exit', -command => [\&exit]], ]); my $igc = $menubar->cascade(qw/-label Log -underline 0 -menuitems/ => [ [command => 'Open IGC', -command => [\&load_igc]], [command => 'Barograph', -command => [\&baro]], [command => 'Track map', -command => [\&map]], [command => 'Generate Mapfile', -command => [\&generate_tab]], [command => 'Split log', -command => [\&logsplit]], ]); my $turnpoints = $menubar->cascade(qw/-label Turnpoints -underline 0 -menuitems/ => [ [command => 'Open DAT', -command => [\&load_dat]], [command => 'View list', -command => [\&tpdump]], [command => 'View task', -command => [\&taskdump]], [command => 'Turn/Task', -command => [\&tplist]], ]); my $help = $menubar->cascade(qw/-label Help -underline 0 -menuitems/ => [ [command => 'About'], [command => 'Usage'], [command => 'Barograph'], [command => 'Track map'], [command => 'Split log'], ]); my $DIALOG_ABOUT = $mw->Dialog( -title => 'About IGC tool', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "IGC log tool\nWindows version\nPhilip Plane\n" . "6 March 2006\n\nPerl Version $]" . "\nTk Version $Tk::VERSION", ); $help->cget(-menu)->entryconfigure('About', -command => [$DIALOG_ABOUT => 'Show'], ); my $DIALOG_USAGE = $mw->Dialog( -title => 'Using IGC tool', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "To use the IGC tool, first open an IGC log file " . "from the File menu. The text of the log will " . "be displayed in the main window. " . "When a file has been loaded the actions in the Log " . "menu can be applied to the log.", ); $help->cget(-menu)->entryconfigure('Usage', -command => [$DIALOG_USAGE => 'Show'], ); my $DIALOG_BARO = $mw->Dialog( -title => 'Barograph', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "The barograph trace shows height in feet, " . "and various information from the log. " , ); $help->cget(-menu)->entryconfigure('Barograph', -command => [$DIALOG_BARO => 'Show'], ); my $DIALOG_MAP = $mw->Dialog( -title => 'Track map', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "The track map shows the track from the log file, " . "any turnpoints in the log file, " . "and any task in the log file. " . "Projection is straight xy." . "Turnpoints are shown as 500 meter circles", ); $help->cget(-menu)->entryconfigure('Track map', -command => [$DIALOG_MAP => 'Show'], ); my $DIALOG_SPLIT = $mw->Dialog( -title => 'Split log', -bitmap => 'info', -default_button => 'OK', -buttons => ['OK'], -text => "Some IGC loggers and their software produce logs " . "that have more than one flight in the log. " . "The log splitter scans through the log and attempts " . "to seperate the flights into individual logs. " . "If the number of logs it finds sounds reasonable, " . "save the new logs to disk. The log files are named " . "using the original file name with the eighth character " . "replaced with a sequence number. ", ); $help->cget(-menu)->entryconfigure('Split log', -command => [$DIALOG_SPLIT => 'Show'], ); my $text = $mw->Scrolled("ROText")->pack(); # Globals from IGC file my $maxalt; my $minalt; my $maxpres; my $minpres; my $minlat; my $maxlat; my $minlon; my $maxlon; my $mday; my $month; my $mon; my $year; my $time; my $glider; my $cid; my $pilot; my $recorder; my $igcfile; my @log; my @tp; my @task; my @track; my $mindiff; my $maxdiff; my $lbtask; my $lbturn; my @landings; my $initialdir = "/"; sub load_igc { my $types = [ ['IGC Files','.igc'], ['IGC Files','.IGC'], ]; my $igc = $mw->getOpenFile(-initialdir=>$initialdir, -filetypes=>$types); if ( defined $igc ){ my @igc = split /\//,$igc; $igcfile = pop @igc; open IGC, $igc or die "Can't open $igc : $!\n"; my @log = ; close IGC; $text->delete("1.0",'end'); @tp = (); @task = (); @track = (); @landings = (); $maxalt = 0; $minalt = 99999; $maxpres = 0; $minpres = 99999; $minlat = 90; $maxlat = -90; $minlon = 180; $maxlon = 0; $mindiff = 9999; $maxdiff = 0; my $oh = 0; while ( $_ = shift @log ){ s/\r//; SWITCH: { if ( /^HFDTE/ ){ my $date = substr($_,5); $mday = substr($date,0,2); $month = substr($date,2,2); $year = substr($date,-2,2); $mon = $month - 1; last SWITCH; } if ( /^H.GID/ ){ $glider = substr($_,5); last SWITCH; } if ( /^H.CID/ ){ $cid = substr($_,5); last SWITCH; } if ( /^H.PLT/ ){ $pilot = substr($_,5); last SWITCH; } if ( /^H.FTY/ ){ $recorder = substr($_,5); chop $recorder; last SWITCH; } if ( /^L\D{4}?(\d{5}?)(\d{7}?[NS])(\d{8}?[WE])(\d{6})(.{8}?)(.*)/ ){ # [ lat, lon, id, name, type ] # turnpoints my $lat = latitude($2); my $lon = longitude($3); push @tp, [ $lat, $lon, $1, $6, $5 ]; last SWITCH; } if ( /^C(\d{7}?[NS])(\d{8}?[WE])(.*)/ ){ # [ lat, lon, name ] # task my $lat = latitude($1); my $lon = longitude($2); if ( $lat == 0 ){ last SWITCH; } push @task, [ $lat, $lon, $3 ]; last SWITCH; } if ( /^B(\d{6})(\d{7}[NS])(\d{8}?[WE])A(\d{5})(\d{5})/ ){ $time = $1; my $lat = latitude($2); my $lon = longitude($3); my $alt = $4; my $pres = $5; # keep track of the difference between GPS altitude # and pressure altitude my $diff = $pres - $alt; if ( $diff < 0 ){ $diff = 0 - $diff; } if ( $diff > $maxdiff ){ $maxdiff = $diff; } if ( $diff < $mindiff ){ $mindiff = $diff; } # $time is HHMMSS $time =~ /(\d{2})(\d{2})(\d{2})/; my $hour = $1; my $min = $2; my $sec = $3; if ( $hour < $oh ) { $mday++; } $oh = $hour; my $gt = timegm_nocheck($sec,$min,$hour,$mday,$mon,$year); if ( $lat > $maxlat ) { $maxlat = $lat; } if ( $lat < $minlat ) { $minlat = $lat; } if ( $lon > $maxlon ) { $maxlon = $lon; } if ( $lon < $minlon ) { $minlon = $lon; } if ( $alt < $minalt ){ $minalt = $alt; } if ( $alt > $maxalt ){ $maxalt = $alt; } if ( $pres < $minpres ){ $minpres = $pres; } if ( $pres > $maxpres ){ $maxpres = $pres; } push @track, [ $gt, $lat, $lon, $alt, $pres ]; last SWITCH; } } $text->insert('end',$_); } } } sub baro { my $maxrec = scalar @track; if ( $maxrec > 0 ) { my $bwin = $mw->Toplevel(); $bwin->title($igcfile . " Barograph"); my $bg = $bwin->Scrolled('Canvas', -width => 850, -height => 500, -scrollregion => [-10,-10,1000,1000])->pack(); my $id; my $date = sprintf("%02d/%02d/20%02d",$mday,$month,$year); if ( not defined $glider ) { $glider = $cid; } #clean up pilot name, remove extra whitespace if ( defined $pilot ){ chop $pilot; while ( $pilot =~ m/ / ){ $pilot =~ s/ / /; } } if ( defined $glider ){ chop $glider; } my $gps = sprintf("GPS Low point: %5d ft High point: %5d ft", feet($minalt),feet($maxalt)); my $pres = sprintf("Alt Low point: %5d ft High point: %5d ft", feet($minpres),feet($maxpres)); my @t = localtime($track[0][0]); my $start = sprintf("Log started at %02d:%02d:%02d",$t[2],$t[1],$t[0]); my @f = localtime($track[$maxrec - 1][0]); my $finish = sprintf("Log ended at %02d:%02d:%02d",$f[2],$f[1],$f[0]); my $duration = sprintf("Log duration %s", sec2time($track[$maxrec - 1][0] - $track[0][0])); my $diff = sprintf("GPS/Pressure difference: max %4d ft / min %4d ft", feet($maxdiff),feet($mindiff)); $id = $bg->createText(10,10, -text => $pilot, -anchor => "w"); $id = $bg->createText(410,10, -text => $recorder, -anchor => "w"); $id = $bg->createText(10,25, -text => $glider, -anchor => "w"); $id = $bg->createText(10,40, -text => $pres,-fill => "red", -anchor => "w"); $id = $bg->createText(10,55, -text => $gps, -fill => "blue", -anchor => "w"); $id = $bg->createText(10,70, -text => $date, -anchor => "w"); $id = $bg->createText(410,70, -text => $diff, -anchor => "w"); $id = $bg->createText(10,85, -text => $start, -anchor => "w"); $id = $bg->createText(10,100, -text => $finish, -anchor => "w"); $id = $bg->createText(10,115, -text => $duration, -anchor => "w"); my $width = 800; my $height = 450; my $hscale = ($track[$maxrec-1][0] - $track[0][0])/($width - 50); my $vscale = 25; if ($maxalt < 2000){ $vscale = 10; } elsif ($maxalt < 4000){ $vscale = 15; } my $m = $maxalt / 305; # Draw lines at 1000ft (305m) for(my $mil = 0;$mil < $m;$mil++){ my $ml = 400 - (($mil * 305)/$vscale); $id = $bg->createLine(40,$ml,$width ,$ml, -dash => [6,4]); $id = $bg->createText(35,$ml, -text => sprintf("%5d", 1000 * $mil), -anchor => "e"); } my $or = 400 - ( $track[0][4] / $vscale); my $ob = 400 - ( $track[0][3] / $vscale); my $ox = 30; # munch through the track array [ $gt, $lat, $lon, $alt, $pres ] my $osec = $track[0][0]; my $olat = $track[0][1]; my $olon = $track[0][2]; my $oalt = $track[0][3]; my $opres = $track[0][4]; my $launch = 0; my $land = 0; for (my $i = 0; $i < $maxrec; $i++) { my $t_sec = $track[$i][0]; $t_sec = $t_sec - $track[0][0]; my $x = 50 + int($t_sec / $hscale); my $pres = $track[$i][4]; my $alt = $track[$i][3]; # figure out the takeoff/landing my $dlat = abs($track[$i][1] - $olat); my $dlng = abs($track[$i][2] - $olon); my $dist = sqrt(($dlat*$dlat) + ($dlng*$dlng)); my $speed = 0; my $dsec = $t_sec - $osec; $osec = $t_sec; if ( ($dsec > 0) and ($dist > 0)){ $speed = $dist / $dsec; } if ( ($speed > 0.000100) and ($oalt < $alt)){ $launch++; $land = 0; } if ( $launch == 3 ) { $id = $bg->createLine($x,400,$x,150, -fill => "black"); $id = $bg->createText($x,140, -text => "takeoff"); $launch++; } if ( ($speed < 0.0000001) and ($oalt == $alt) and ($launch > 0)){ $land++; } if ( $land == 3 ){ $land = 0; $launch = 0; $id = $bg->createLine($x,400,$x,150,-fill => "black"); $id = $bg->createText($x,140, -text => "land"); push @landings,$track[$i][0]; } my $y = 400 - ( $pres/$vscale); $id = $bg->createLine($x,$y,$ox,$or, -fill => "red"); $or = $y; $y = 400 - ($alt/$vscale); $id = $bg->createLine($x,$y,$ox,$ob, -fill => "blue"); $ob = $y; $ox = $x; $olat = $track[$i][1]; $olon = $track[$i][2]; $opres = $pres; $oalt = $alt; } # add the timeticks my @start = localtime($track[0][0]); my @finish = localtime($track[$maxrec-1][0]); for ( my $h = $start[2]+1; $h <= $finish[2]; $h++ ){ @t = @start; $t[2] = $h; $t[0] = 0; $t[1] = 0; my $t = timelocal(@t); my $x = 50 + int(($t - $track[0][0]) / $hscale); $id = $bg->createText($x,410, -text => sprintf("%02d:00",$h)); $id = $bg->createLine($x,400,$x,350, -dash => [3,2], -fill => "black"); } } } sub map { my $maxrec = scalar @track; if ( $maxrec > 0 ) { my $zone; my $ox; my $oy; #use max vertical to set scale as most tracks are taller than wider. my $scale = $maxlat - $minlat; my $bwin = $mw->Toplevel(); $bwin->title($igcfile . " Flight Track"); my $bg = $bwin->Scrolled('Canvas', -width => 850, -height => 600, -scrollregion => [-500,-500,1500,1500])->pack(); my $width = 1000; my $height = 1000; $scale = $height / $scale; # 500 meter radius circle my $r = .004 * $scale; #show the turnpoints my $maxtp = scalar @tp; for (my $t = 0; $t < $maxtp; $t++) { my $y = $height - (($tp[$t][0] - $minlat) * $scale); my $x = ($tp[$t][1] - $minlon) * $scale; my $id = $bg->createText($x, $y + 10 + $r, -text => $tp[$t][3]); $id = $bg->createOval($x - $r, $y - $r, $x + $r, $y + $r); } #show the task my $maxtask = scalar @task; if ( $maxtask > 0 ){ $oy = $height - (($task[0][0] - $minlat) * $scale); $ox = ($task[0][1] - $minlon) * $scale; } for (my $t = 0; $t < $maxtask; $t++) { my $y = $height - (($task[$t][0] - $minlat) * $scale); my $x = ($task[$t][1] - $minlon) * $scale; my $id = $bg->createText($x, $y + 10 + $r, -text => $task[$t][2]); $id = $bg->createOval($x - $r, $y - $r, $x + $r, $y + $r, -outline => "green"); $id = $bg->createLine($x,$y,$ox,$oy, -dash => [6,4], -fill => "green"); $ox = $x; $oy = $y; } #prime the pump $oy = $height - (($track[0][1] - $minlat) * $scale); $ox = ($track[0][2] - $minlon) * $scale; #show the track for (my $i = 0; $i < $maxrec; $i++) { my $y = $height - (($track[$i][1] - $minlat) * $scale); my $x = ($track[$i][2] - $minlon) * $scale; my $id = $bg->createLine($ox,$oy,$x,$y, -fill => "blue"); $ox = $x; $oy = $y; } } } sub generate_tab { my $header = "sTrackDescr\tiTrkID\tiColor\tsTimestamp\tfLat\tfLong\tfAlt\tfEasting\tfNorthing\n"; #IGClog 123 3:21 44.500000 170.200000 123.0 my $tab = $mw->getSaveFile(-initialdir=>$initialdir, -initialfile=>'tracklog.txt'); if ( defined $tab ){ open TXT, "> " . $tab or die "Couldn't open file $tab: $!\n"; print TXT $header; my $maxrec = scalar @track; for (my $i = 0; $i < $maxrec; $i++) { my @now = localtime($track[$i][0]); print TXT "IGCLog\t001\t\t"; printf TXT ("%02d:%02d:%02d\t",$now[2], $now[1], $now[0]); printf TXT ("%6f\t%6f\n",$track[$i][1], $track[$i][2]); } close TXT; } } sub logsplit { my $maxrec = scalar @log; my $osec = 0; my $igccore; my $igccount = 0; my @logs =(); for (my $i = 0; $i < $maxrec; $i++) { if ( $log[$i] =~ /^B(\d{6})(\d{7}[NS])(\d{8}?[WE]).{1}(\d{5})(\d{5})/ ){ my $alt = $4; my $time = $1; $time =~ /(\d{2})(\d{2})(\d{2})/; my $sec = ($1 *3600) + ($2 * 60) + $3; # if the current second is less than the previous, time # has gone backwards. This indicates you've crossed the # boundry of a new day. if ( $sec < $osec ) { $osec -= 86400; } # if the gap in the log is more than sixty seconds assume that # it's a new flight if (( $sec - $osec ) > 60){ $logs[$igccount++] = $igccore; $igccore = ""; } $osec = $sec; } $igccore .= $log[$i]; } $logs[$igccount] = $igccore; my $split = $mw->Dialog( -title => 'Split IGC log file', -bitmap => 'question', -default_button => 'OK', -buttons => ['OK','Cancel'], -text => "Found $igccount logs.\nWant to create seperate log files for them?\n", )->Show(); if ($split eq 'OK'){ for (my $i = 1; $i <= $igccount; $i++) { substr($igcfile,7,1) = $i; my $frag = $mw->getSaveFile(-initialdir=>$initialdir, -initialfile=>$igcfile); if ( defined $frag ){ open TEMP, ">" . $frag or die "Couldn't open $frag: $!\n"; print TEMP $logs[0], $logs[$i]; close TEMP; } } } } sub load_dat { # @tp # [ lat, lon, id, name, type ] # Cambridge turnpoints file #1,44:29.080S,169:58.660E,1380F,TAH,01-Omarama ,*EastEnd Rwa my $types = [ ['DAT Files','.dat'], ['DAT Files','.DAT'], ]; my $dat = $mw->getOpenFile(-initialdir=>$initialdir, -filetypes=>$types); if ( defined $dat ){ my $count = 0; @tp = (); my @dat = split /\//,$dat; my $datfile = pop @dat; open DAT, $dat or die "Can't open $dat : $!\n"; my @log = ; close DAT; while ( $_ = shift @log ){ if ( $_ =~ /^\d/ ){ @dat = split(/\,/,$_); if ( $dat[4] =~ /T|S|F/ ){ if ( $dat[5] eq "" ) { $dat[5] = $dat[0]; } $tp[$count][0] = datlat($dat[1]); $tp[$count][1] = datlng($dat[2]); $tp[$count][2] = $dat[0]; $tp[$count][3] = $dat[5]; $tp[$count][4] = $dat[4]; $count++; } } } } } sub datlat{ #44:29.080S my $lat = shift @_; $lat =~ /(\d{2})\:(\d{2}\.\d{3})([SN])/; my $degrees = $1; my $min = $2; my $hemi = $3; $min = $min * 0.0166666667; $degrees = $degrees + $min; if ( $hemi eq 'S' ){ $degrees = 0 - $degrees; } return $degrees; } sub datlng{ #169:58.660E my $lng = shift @_; $lng =~ /(\d{3})\:(\d{2}\.\d{3})([WE])/; my $degrees = $1; my $min = $2; my $hemi = $3; $min = $min * 0.0166666667; $degrees = $degrees + $min; if ( $hemi eq 'W' ){ $degrees = 0 - $degrees; } return $degrees; } sub tpdump { #show the turnpoints my $maxtp = scalar @tp; $text->delete("1.0",'end'); for (my $t = 0; $t < $maxtp; $t++) { my $tmp = sprintf("%6.2f %6.2f %s\n",$tp[$t][0], $tp[$t][1], $tp[$t][3]); $text->insert('end',$tmp); } } sub taskdump { #show task my $maxrec = scalar @task; $text->delete("1.0".'end'); for (my $t = 0; $t < $maxrec; $t++) { my $tmp = sprintf("%6.2f %6.2f %s\n",$task[$t][0], $task[$t][1], $task[$t][2]); $text->insert('end',$tmp); } } sub tplist { my $maxrec = scalar @tp; if ( $maxrec > 0 ) { my $bwin = $mw->Toplevel(); $bwin->title("Turnpoints"); $bwin->minsize(175,50); $bwin->Label(-text=>"Turnpoints")->grid( "x", $bwin->Label(-text=>"Task")); $lbturn = $bwin->Scrolled("Listbox", -scrollbars => "e", -selectmode => "single")->grid(-row=> 0, -column=> 0, -rowspan=> 2, -sticky=> "nsew"); $bwin->Button(-text=>"Add ->", -command=> \&add_to_task )->grid(-row=> 0, -column=> 1, -sticky=> "sew"); $lbtask = $bwin->Scrolled("Listbox", -scrollbars => "e", -selectmode => "single")->grid(-row=> 0, -column=> 2, -rowspan=> 2, -sticky=> "nsew"); $bwin->Button(-text=>"Del <-", -command=> \&del_from_task )->grid(-row=> 1, -column=> 1, -sticky=> "new"); #populate the turnpoint list $lbturn->delete("1.0",'end'); for (my $t = 0; $t < $maxrec; $t++) { $lbturn->insert('end',$tp[$t][3]); } # populate the task $maxrec = scalar @task; $lbtask->delete("1.0",'end'); for (my $t = 0; $t < $maxrec; $t++) { $lbtask->insert('end',$task[$t][2]); } } } sub add_to_task { my @selection = $lbturn->curselection(); foreach ( @selection ){ my $turnpoint = $lbturn->get($_); $lbtask->insert('end',$turnpoint); } } sub del_from_task { my @selection = $lbtask->curselection(); foreach ( @selection ){ $lbtask->delete($_); } } sub feet{ my $meters = shift @_; my $feet = $meters * 3.2808399; return int($feet); } sub duration{ # expects 2 parameters, start time and finish time my $start = shift @_; my $end = shift @_; my $ss = seconds($start); my $se = seconds($end); # if we finished before we started, we crossed the day boundry if ( $se < $ss ) { $se += 60 * 60 * 24; } return $se - $ss; } sub seconds{ my $time = shift @_; my $hours = substr($time,0,2); my $minutes = substr($time,2,2); my $seconds = substr($time,4,2); return $seconds + ($minutes * 60) + ($hours * 60 * 60); } sub sec2time{ my $sec = shift @_; my $hours = int($sec / 3600); $sec = $sec % 3600; my $minutes = int($sec / 60); $sec = $sec % 60; return sprintf("%02d:%02d:%02d",$hours,$minutes,$sec); } sub latitude{ my $lat = shift @_; my $degrees = substr($lat,0,2); my $min = '0.' . substr($lat,2,5); $min = $min * 1.66666667; $degrees = $degrees + $min; my $hemi = substr($lat,-1); if ( $hemi eq 'S' ){ $degrees = 0 - $degrees; } return $degrees; } sub longitude{ my $lon = shift @_; my $degrees = substr($lon,0,3); my $min = '0.' . substr($lon,3,5); $min = $min * 1.66666667; $degrees = $degrees + $min; my $hemi = substr($lon,-1); if ( $hemi eq 'W' ){ $degrees = 0 - $degrees; } return $degrees; } MainLoop;