3 # Melchior FRANZ <mfranz#aon:at> Public Domain
5 # Usage: $ freq [IACO:ksfo [RANGE:15]]
11 # The RANGE is in km and defines which NDB, VOR, VORTAC, ... to
12 # display. Default is 15 km.
14 # Note that the directions given for NDB, VOR, VORTAC, ... are
15 # always the heading from this radio facility to the airport!
18 use POSIX qw(ceil floor);
20 my $ID = shift || "KSFO";
21 my $RANGE = shift || 15; # for NDB/VOR [km]
23 my $FG_ROOT = $ENV{'FG_ROOT'} || "/usr/local/share/FlightGear";
24 my $APTFILE = "$FG_ROOT/Airports/apt.dat.gz" || die "airport file not found";
25 my $NAVFILE = "$FG_ROOT/Navaids/nav.dat.gz" || die "nav file not found";
28 my $PI = 3.1415926535897932384626433832795029;
31 my $ERAD = 6378138.12;
35 'ILS' => "\033[33;1m",
36 'TWR' => "\033[31;1m",
37 'ATIS' => "\033[32;1m",
38 'NDB' => "\033[36;1m",
39 'VOR' => "\033[35;1m",
49 open(F, "gzip -d -c $APTFILE|") or die "can't open airport file $APTFILE";
51 if (/^1\s+\S+\s+\S+\s+\S+\s+$ID\s+(.+)\s+/) {
52 my $title = "$ID - $1";
54 print "=" x length($title) . "\n";
60 if (/^1.\s+(\S+)\s+(\S+)\s+/) {
61 my ($lat, $lon) = ($1, $2);
62 map { s/^(-?)0+/$1/ } ($lat, $lon);
66 } elsif (/^(5\d+)\s+(\d+)\s+(.*)\s*/) {
67 my ($id, $freq, $desc) = ($1, $2, $3);
68 $freq =~ s/(..)$/.$1/;
69 &addfreq($freq, $desc);
75 close F or die "can't close airport file $APTFILE";
77 die "no data for $ID" unless $aptdatacnt;
79 # calculate mean location from all structures on the airport
80 $aptlat /= $aptdatacnt;
81 $aptlon /= $aptdatacnt;
82 my ($aptx, $apty, $aptz) = &ll2xyz($aptlat, $aptlon);
93 open(F, "gzip -d -c $NAVFILE|") or die "can't open airport file $NAVFILE";
97 my @l = split /\s+/, $_, 9;
98 map { s/^(-?)0+/$1/ } @l[1,2];
99 my $dist = &coord_dist_sq(&ll2xyz($l[1], $l[2]), $aptx, $apty, $aptz);
100 push @NDB, [$dist, @l];
102 } elsif (/^3\s/) { # VOR/VOR-DME/DME/VORTAC/TACAN
103 my @l = split /\s+/, $_, 9;
104 map { s/^(-?)0+/$1/ } @l[1,2];
105 my $dist = &coord_dist_sq(&ll2xyz($l[1], $l[2]), $aptx, $apty, $aptz);
106 if ($l[8] =~ /\b(VOR|VOR-DME)$/) {
107 push @VOR, [$dist, @l];
108 } elsif ($l[8] =~ /\bDME\b/) {
109 push @DME, [$dist, @l];
111 push @OTHERS, [$dist, @l];
114 } elsif (/^(4|5)\s/) { # LLZ
115 my @l = split /\s+/, $_, 11;
116 next unless $l[8] eq $ID;
117 $l[4] =~ s/(..)$/.$1/;
118 &addfreq($l[4], "LLZ " . $l[9]);
120 } elsif (/^6\s/) { # GS
121 my @l = split /\s+/, $_, 11;
122 next unless $l[8] eq $ID;
123 $l[4] =~ s/(..)$/.$1/;
124 &addfreq($l[4], "GS " . $l[9]);
126 } elsif (/^7\s/) { # OM
127 my @l = split /\s+/, $_, 11;
128 next unless $l[8] eq $ID;
131 } elsif (/^8\s/) { # MM
132 my @l = split /\s+/, $_, 11;
133 next unless $l[8] eq $ID;
136 } elsif (/^9\s/) { # IM
137 my @l = split /\s+/, $_, 11;
138 next unless $l[8] eq $ID;
141 } elsif (/^12\s/) { # DME (ILS)
142 my @l = split /\s+/, $_, 11;
143 next unless $l[8] eq $ID;
144 $l[4] =~ s/(..)$/.$1/;
145 &addfreq($l[4], "DME " . $l[9]);
149 close F or die "can't close airport file $NAVFILE";
152 foreach my $freq (sort { $a <=> $b } keys %FREQ) {
154 map { $h{$_} = 1 } @{$FREQ{$freq}};
159 foreach my $d (@uniq) {
160 if ($d =~ /(\S*)\s*(\d\d[LRC]?)\s*(\S*)/) {
161 push @{$rwy{$2}}, ($1 . $3);
166 foreach my $r (keys %rwy) {
167 push @desc, ((join "/", sort @{$rwy{$r}}) . " $r");
171 my $k = join ", ", @desc;
172 if ($k =~ /\bTWR\b/) {
174 } elsif ($k =~ /\bATIS\b/) {
176 } elsif ($k =~ /\b(GZ|LLZ)\b/) {
179 $s .= sprintf "%-7s %s\033[m\n", $freq, join ", ", $k;
184 &printfreq(0, $COLOR{'NDB'}, @NDB);
185 &printfreq(1, $COLOR{'VOR'}, @VOR);
186 &printfreq(1, $COLOR{'DME'}, @DME);
187 &printfreq(1, $COLOR{'NONE'}, @OTHERS);
189 print " OM " . (join ", ", sort @OM) . "\n" if @OM;
190 print " MM " . (join ", ", sort @MM) . "\n" if @MM;
191 print " IM " . (join ", ", sort @IM) . "\n" if @IM;
199 my $divfreq = shift; # divide frequency by 100?
201 foreach (sort { @{$a}[0] <=> @{$b}[0] } @_) {
203 my $dist = &distance($l[0]);
204 my $dir = &llll2dir($l[2], $l[3], $aptlat, $aptlon);
206 $freq =~ s/(..)$/.$1/ if $divfreq;
207 printf "$color%-7s %s (\"%s\")\t-->\t%s km/%s nm @ %s (%s)$COLOR{'NONE'}\n",
209 &round($dist, 0.1), # km
210 &round($dist / 1.852, 0.1), # nm
211 int $dir, &symdir($dir);
212 next if $l[9] =~ /\b$ID\b/;
213 last if $dist > $RANGE;
220 my ($freq, $desc) = @_;
221 push @{$FREQ{$freq}}, $desc;
228 return $ERAD * sqrt($t) / 1000;
235 my $m = (shift or 1);
237 $i = $i - &floor($i) >= 0.5 ? &ceil($i) : &floor($i);
245 my $latA = (shift) * $D2R;
246 my $lonA = (shift) * $D2R;
247 my $latB = (shift) * $D2R;
248 my $lonB = (shift) * $D2R;
249 my $xdist = sin($lonB - $lonA) * $ERAD * cos(($latA + $latB) / 2);
250 my $ydist = sin($latB - $latA) * $ERAD;
251 my $dir = atan2($xdist, $ydist) * $R2D;
252 $dir += 360 if $dir < 0;
259 my $lat = (shift) * $D2R;
260 my $lon = (shift) * $D2R;
261 my $cosphi = cos $lat;
262 my $di = $cosphi * cos $lon;
263 my $dj = $cosphi * sin $lon;
265 return ($di, $dj, $dk);
271 my ($di, $dj, $dk) = @_;
272 my $aux = $di * $di + $dj * $dj;
273 my $lat = atan2($dk, sqrt $aux) * $R2D;
274 my $lon = atan2($dj, $di) * $R2D;
279 sub coord_dist_sq($$$$$$)
281 my ($xa, $ya, $za, $xb, $yb, $zb) = @_;
285 return $x * $x + $y * $y + $z * $z;
292 my @names = ("N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
293 "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW");
294 my $nnames = scalar @names;
295 my $idx = int($nnames * (($dir / 360) + (0.5 / $nnames)));
296 if ($idx >= $nnames) {