The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
Changes 027
META.json 1111
META.yml 1111
bin/satpass 812
eg/almanac 11
eg/azimuth 11
eg/closest 11
eg/convert_tle 11
eg/iridium 11
eg/passes 11
eg/visual 27
eg/xml 11
inc/Astro/Coord/ECI/Test.pm 11
lib/Astro/Coord/ECI/Mixin.pm 11
lib/Astro/Coord/ECI/Moon.pm 22
lib/Astro/Coord/ECI/Star.pm 11
lib/Astro/Coord/ECI/Sun.pm 22
lib/Astro/Coord/ECI/TLE/Iridium.pm 11
lib/Astro/Coord/ECI/TLE/Set.pm 11
lib/Astro/Coord/ECI/TLE.pm 104195
lib/Astro/Coord/ECI/Utils.pm 962
lib/Astro/Coord/ECI.pm 44
t/basic.t 210
t/tle_pass.t 249
xt/author/magnitude_status.t 22
25 files changed (This is a version diff) 171406
@@ -1,3 +1,30 @@
+0.065		2014-07-27	T. R. Wyant
+  No changes since 0.064_03.
+
+0.064_03	2014-07-20	T. R. Wyant
+  Another cut at preventing TLE manufacture for oddball objects --
+    hopefully one with fewer false negatives.
+
+0.064_02	2014-07-18	T. R. Wyant
+  Prevent TLE manufacture for oddball objects -- or at least try to.
+
+0.064_01	2014-06-30	T. R. Wyant
+  Add TLE pass variant PASS_VARIANT_TRUNCATE, which truncates passes to
+    the beginning and end times given to pass(), rather then computing
+    complete passes (and therefore requiring the satellite to rise and
+    set).
+  Expose Space Track time formatting and decoding routines for possible
+    use by subclasses. The key routines __to_json() and __from_json()
+    are still undocumented and considered highly experimental, though.
+    Sorry.
+  Add OID 00694 to Astro::Coord::ECI::TLE canned magnitudes.
+
+0.064		2014-05-29	T. R. Wyant
+  No changes from 0.063_01.
+
+0.063_01	2014-05-21	T. R. Wyant
+  Add Astro::Coord::ECI::TLE method illuminated()
+
 0.063		2014-05-02	T. R. Wyant
   Allow PASS_VARIANT_BRIGHTEST event even if the 'visible' attribute is
     false.
@@ -4,7 +4,7 @@
       "Tom Wyant (wyant at cpan dot org)"
    ],
    "dynamic_config" : 1,
-   "generated_by" : "Module::Build version 0.4205",
+   "generated_by" : "Module::Build version 0.4206",
    "license" : [
       "perl_5"
    ],
@@ -45,39 +45,39 @@
    "provides" : {
       "Astro::Coord::ECI" : {
          "file" : "lib/Astro/Coord/ECI.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::Mixin" : {
          "file" : "lib/Astro/Coord/ECI/Mixin.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::Moon" : {
          "file" : "lib/Astro/Coord/ECI/Moon.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::Star" : {
          "file" : "lib/Astro/Coord/ECI/Star.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::Sun" : {
          "file" : "lib/Astro/Coord/ECI/Sun.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::TLE" : {
          "file" : "lib/Astro/Coord/ECI/TLE.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::TLE::Iridium" : {
          "file" : "lib/Astro/Coord/ECI/TLE/Iridium.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::TLE::Set" : {
          "file" : "lib/Astro/Coord/ECI/TLE/Set.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       },
       "Astro::Coord::ECI::Utils" : {
          "file" : "lib/Astro/Coord/ECI/Utils.pm",
-         "version" : "0.063"
+         "version" : "0.065"
       }
    },
    "release_status" : "stable",
@@ -89,5 +89,5 @@
          "http://dev.perl.org/licenses/"
       ]
    },
-   "version" : "0.063"
+   "version" : "0.065"
 }
@@ -5,7 +5,7 @@ author:
 build_requires:
   Test::More: '0.88'
 dynamic_config: 1
-generated_by: 'Module::Build version 0.4205, CPAN::Meta::Converter version 2.140640'
+generated_by: 'Module::Build version 0.4206, CPAN::Meta::Converter version 2.141520'
 license: perl
 meta-spec:
   url: http://module-build.sourceforge.net/META-spec-v1.4.html
@@ -19,31 +19,31 @@ no_index:
 provides:
   Astro::Coord::ECI:
     file: lib/Astro/Coord/ECI.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::Mixin:
     file: lib/Astro/Coord/ECI/Mixin.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::Moon:
     file: lib/Astro/Coord/ECI/Moon.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::Star:
     file: lib/Astro/Coord/ECI/Star.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::Sun:
     file: lib/Astro/Coord/ECI/Sun.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::TLE:
     file: lib/Astro/Coord/ECI/TLE.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::TLE::Iridium:
     file: lib/Astro/Coord/ECI/TLE/Iridium.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::TLE::Set:
     file: lib/Astro/Coord/ECI/TLE/Set.pm
-    version: '0.063'
+    version: '0.065'
   Astro::Coord::ECI::Utils:
     file: lib/Astro/Coord/ECI/Utils.pm
-    version: '0.063'
+    version: '0.065'
 requires:
   Carp: '0'
   Data::Dumper: '0'
@@ -59,4 +59,4 @@ requires:
 resources:
   bugtracker: https://rt.cpan.org/Public/Dist/Display.html?Name=Astro-satpass
   license: http://dev.perl.org/licenses/
-version: '0.063'
+version: '0.065'
@@ -37,7 +37,7 @@ $clipboard_unavailable = $IO::Clipboard::clipboard_unavailable;
 #	Initialization
 #
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use constant LINFMT => <<eod;
 %s %s %8.4f %9.4f %7.1f %-4s %s
@@ -1518,7 +1518,8 @@ sub magnitude_table {
     BEGIN {
 	foreach my $code (PASS_EVENT_NONE, PASS_EVENT_SHADOWED,
 	    PASS_EVENT_LIT, PASS_EVENT_DAY, PASS_EVENT_RISE,
-	    PASS_EVENT_MAX, PASS_EVENT_SET, PASS_EVENT_BRIGHTEST ) {
+	    PASS_EVENT_MAX, PASS_EVENT_SET, PASS_EVENT_START,
+	    PASS_EVENT_END, PASS_EVENT_BRIGHTEST ) {
 	    my $val = ucfirst $code;
 	    $event_name[$code] = sub {$val};
 	}
@@ -1528,7 +1529,7 @@ sub magnitude_table {
 		$_[0]{appulse}{body}->get ('id')};
 
 	$cmdlgl{pass} = [ qw{ choose=s@ dump flatten
-	    magnitude|brightest quiet } ];
+	    magnitude|brightest quiet truncate! } ];
     }
 
     sub pass {
@@ -1571,7 +1572,9 @@ eod
     my $appulse = deg2rad ($parm{appulse});
     my $pass_threshold = deg2rad( $parm{pass_threshold} );
     my $pass_variant = $cmdopt{magnitude} ? PASS_VARIANT_BRIGHTEST :
-    PASS_VARIANT_NONE;
+	PASS_VARIANT_NONE;
+    $cmdopt{truncate}
+	and $pass_variant |= PASS_VARIANT_TRUNCATE;
     my $line_format = $cmdopt{magnitude} ? LINFMT_MAG : LINFMT;
 
 #	Print the header
@@ -1790,10 +1793,11 @@ while ($time <= $endtm) {
 	my $name = $body->get ('name') || $body->get ('id') || 'body';
 	$name = substr $name, 0, 16;
 	if ($body->represents ('Astro::Coord::ECI::TLE')) {
-	    my $illum = ($sta->azel ($body))[1] < 0 ? 10 :
-		($sta->azel ($sun->universal ($time)))[1] > $twilight ? 2 :
-##		($body->azel ($sun))[1] > $body->dip() ? 1 : 0;
-		$body->__sun_elev_from_sat( $time ) >= 0 ? 1 : 0;
+	    my $illum = $body->illuminated() ? 1 : 0;
+	    $illum
+		and ( $sta->azel( $body ) )[1] >= 0
+		and ( $sta->azel($sun->universal( $time ) ) )[1] > $twilight
+		and $illum = 2;
 	    my $line;
 	    if ( $cmdopt{magnitude} ) {
 		my $mag = $body->magnitude( $sta );
@@ -17,7 +17,7 @@ use Pod::Usage;
 use POSIX qw{strftime};
 use Time::Local;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 Getopt::Long::Configure( 'pass_through' );	# Numbers may be negative.
 
@@ -12,7 +12,7 @@ use Astro::Coord::ECI;
 use Astro::Coord::ECI::Sun;
 use Astro::Coord::ECI::Utils qw{ deg2rad rad2deg };
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 my $time = time;
 
@@ -23,7 +23,7 @@ use Getopt::Long 2.33;
 use Pod::Usage;
 use POSIX qw{strftime};
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 my %opt;
 
@@ -9,7 +9,7 @@ use Astro::Coord::ECI::TLE;
 use Getopt::Long 2.33;
 use Pod::Usage;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 my %opt;
 
@@ -18,7 +18,7 @@ use Astro::Coord::ECI::Utils qw(deg2rad rad2deg SECSPERDAY);
 use Astro::SpaceTrack;
 use POSIX qw{strftime};
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use constant TIMFMT => '%d %b %H:%M:%S';
 
@@ -15,7 +15,7 @@ use Pod::Usage;
 use POSIX qw{ strftime };
 use Time::Local;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 # Map -want-events values to TLE event codes.
 my %wanted_event_map = (
@@ -12,7 +12,7 @@ use HTTP::Date;
 use LWP::UserAgent;
 use Pod::Usage;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 my %opt;
 
@@ -79,6 +79,11 @@ if ( $opt{merge} ) {
     $output =~ s/ [}] \s* \z /);\n/smx;
     $output =~ s/ ' ( -? \d+ [.] \d+ ) ' /$1/smxg;
     $output =~ s/ (?<= \d ) (?: (?= \n ) | \z ) /,/smxg;
+    print <<"EOD";
+# Last-Modified: @{[ time2str( $date ) ]}
+
+EOD
+
     print $output;
 } else {
     foreach my $oid ( sort { $a <=> $b } keys %found ) {
@@ -86,8 +91,8 @@ if ( $opt{merge} ) {
 	    and next;
 	say "$oid $sources[$found{$oid}]";
     }
+    say 'Last-Modified: ', time2str( $date );
 }
-say 'Last-Modified: ', time2str( $date );
 
 sub last_modified {
     my ( $resp ) = @_;
@@ -14,7 +14,7 @@ use Getopt::Long 2.33;
 use Pod::Usage;
 use XML::Writer;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 my %opt = (
     latitude => 52.069858,	# Degrees north of Equator
@@ -5,7 +5,7 @@ use 5.006002;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use base qw{ Exporter };
 
@@ -11,7 +11,7 @@ use Astro::Coord::ECI::Utils qw{ __default_station PIOVER2 SECSPERDAY };
 use Exporter ();
 use POSIX qw{ floor };
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 our @EXPORT_OK = qw{
     almanac almanac_hash
@@ -43,7 +43,7 @@ package Astro::Coord::ECI::Moon;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use base qw{Astro::Coord::ECI};
 
@@ -483,7 +483,7 @@ eod
 
 # The moon is normally positioned in inertial coordinates.
 
-sub _initial_inertial { return 1 }
+sub __initial_inertial { return 1 }
 
 1;
 
@@ -50,7 +50,7 @@ package Astro::Coord::ECI::Star;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use base qw{Astro::Coord::ECI};
 
@@ -50,7 +50,7 @@ package Astro::Coord::ECI::Sun;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use base qw{Astro::Coord::ECI};
 
@@ -457,7 +457,7 @@ eod
 
 # The Sun is normally positioned in inertial coordinates.
 
-sub _initial_inertial { return 1 }
+sub __initial_inertial { return 1 }
 
 1;
 
@@ -121,7 +121,7 @@ use warnings;
 
 use base qw{Astro::Coord::ECI::TLE};
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use Astro::Coord::ECI::Sun;
 use Astro::Coord::ECI::Utils qw{:all};
@@ -140,7 +140,7 @@ our @CARP_NOT = qw{
     Astro::Coord::ECI
 };
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use constant ERR_NOCURRENT => <<eod;
 Error - Can not call %s because there is no current member. Be
@@ -229,14 +229,15 @@ package Astro::Coord::ECI::TLE;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use base qw{ Astro::Coord::ECI Exporter };
 
 use Astro::Coord::ECI::Utils qw{ :params :time deg2rad distsq
-    dynamical_delta embodies find_first_true fold_case load_module
-    looks_like_number max min mod2pi PI PIOVER2 rad2deg
-    SECSPERDAY TWOPI thetag __default_station };
+    dynamical_delta embodies find_first_true fold_case
+    format_space_track_json_time load_module looks_like_number max min
+    mod2pi PI PIOVER2 rad2deg SECSPERDAY TWOPI thetag __default_station
+    };
 
 use Carp qw{carp croak confess};
 use Data::Dumper;
@@ -268,6 +269,7 @@ BEGIN {
 	PASS_VARIANT_NO_ILLUMINATION
 	PASS_VARIANT_START_END
 	PASS_VARIANT_BRIGHTEST
+	PASS_VARIANT_TRUNCATE
 	PASS_VARIANT_NONE
 	BODY_TYPE_UNKNOWN
 	BODY_TYPE_DEBRIS
@@ -769,6 +771,18 @@ L</Attributes> section for a description of the attributes.
     }
 }
 
+=item $illuminated = $tle->illuminated();
+
+This method returns a true value if the body is illuminated, and a false
+value if it is not.
+
+=cut
+
+sub illuminated {
+    my ( $self, $time ) = @_;
+    return $self->__sun_elev_from_sat( $time ) >= 0;
+}
+
 =item @events = $tle->intrinsic_events( $start, $end );
 
 This method returns any events that are intrinsic to the C<$tle> object.
@@ -893,8 +907,8 @@ sub magnitude {
     my $time = $self->universal();
 
     # If the illuminating body is below the horizon, we return undef.
-    $self->__sun_elev_from_sat( $time ) < 0
-	and return undef;	## no critic (ProhibitExplicitReturnUndef)
+    $self->illuminated()
+	or return undef;	## no critic (ProhibitExplicitReturnUndef)
 
     # Compute the range amd the elevation.
     my ( undef, $elev, $range ) = $sta->universal( $time )->azel( $self );
@@ -1385,7 +1399,8 @@ This method returns passes of the body over the given station between
 the given start end end times. The \@sky argument is background bodies
 to compute appulses with (see note 3).
 
-A pass is detected by this method when the body sets. This means that
+A pass is detected by this method when the body sets. Unless
+C<PASS_VARIANT_TRUNCATE> (see below) is in effect, this means that
 passes are not usefully detected for geosynchronous or
 near-geosynchronous bodies, and that passes where the body sets after
 the C<$end> time will not be detected.
@@ -1545,13 +1560,15 @@ use constant PASS_VARIANT_FAKE_MAX	=> 0x02;
 use constant PASS_VARIANT_START_END	=> 0x04;
 use constant PASS_VARIANT_NO_ILLUMINATION => 0x08;
 use constant PASS_VARIANT_BRIGHTEST	=> 0x10;
+use constant PASS_VARIANT_TRUNCATE	=> 0x20;
 use constant PASS_VARIANT_NONE => 0x00;		# Must be 0.
 
 my @pass_variant_mask = (
     PASS_VARIANT_NO_ILLUMINATION | PASS_VARIANT_START_END |
-	PASS_VARIANT_BRIGHTEST,
+	PASS_VARIANT_BRIGHTEST | PASS_VARIANT_TRUNCATE,
     PASS_VARIANT_VISIBLE_EVENTS | PASS_VARIANT_FAKE_MAX |
-	PASS_VARIANT_START_END | PASS_VARIANT_BRIGHTEST,
+	PASS_VARIANT_START_END | PASS_VARIANT_BRIGHTEST |
+	PASS_VARIANT_TRUNCATE,
 );
 
 use constant SCREENING_HORIZON_OFFSET => deg2rad( -3 );
@@ -1563,14 +1580,35 @@ use constant SCREENING_HORIZON_OFFSET => deg2rad( -3 );
 
 *_nodelegate_pass = \&pass;
 
+# The following method is not supported, and may be changed or retracted
+# at any time without notice. Its purpose in life is to provide a handle
+# by which the experimental and unreleased Astro::Coord::ECI::Points
+# objects can manipulate the the start and end times of the pass
+# calculation.
+sub __default_pass_times {
+    my ( $self, $start, $end ) = @_;
+    defined $start
+	or $start = time;
+    defined $end
+	or $end = $start + 7 * SECSPERDAY;
+    return ( $start, $end );
+}
+
 sub pass {
     my @args = __default_station( @_ );
     my @sky;
     ref $args[-1] eq 'ARRAY' and @sky = @{pop @args};
     my $tle = shift @args;
     my $sta = shift @args;
-    my $pass_start = shift @args || time ();
-    my $pass_end = shift @args || $pass_start + 7 * SECSPERDAY;
+
+    # We give subclasses a way of specifying their own default times. If
+    # an undefined end time is returned, the subclass is stating that
+    # there are no passes in the given range, and we simply return.
+    my ( $pass_start, $pass_end ) = $tle->__default_pass_times(
+	splice @args, 0, 2 );
+    defined $pass_start
+	or return;
+
     $pass_end >= $pass_start or croak <<eod;
 Error - End time must be after start time.
 eod
@@ -1596,10 +1634,10 @@ eod
 	$pass_variant_mask[ $want_visible ? 1 : 0 ];
     defined $tle->get( 'intrinsic_magnitude' )
 	or $pass_variant &= ~ PASS_VARIANT_BRIGHTEST;
+    my $truncate = $pass_variant & PASS_VARIANT_TRUNCATE;
     defined $pass_threshold
 	and $pass_threshold > $horizon
 	or $pass_threshold = $horizon;
-    my $pass_backup_earliest = $tle->__pass_backup_earliest();
 
     # We need the number of radians the satellite travels in a minute so
     # we can be slightly conservative determining whether the satellite
@@ -1644,6 +1682,8 @@ eod
     my $bigstep = 5 * $step;
     my $littlestep = $step;
     my $end = $pass_end;
+    $truncate
+	and $end += $littlestep;
     my @info;	# Information on an individual pass.
     my @passes;	# Accumulated informtion on all passes.
     my $visible;
@@ -1693,27 +1733,29 @@ eod
 #	but not the screening horizon before the end of the prediction
 #	period. Sigh.
 
-	if ( $elev < $screening_horizon ||
-	    @info && $elev < $effective_horizon &&
-	    $info[-1]{elevation} >= $effective_horizon
+	if ( $elev < $screening_horizon
+	    || @info && $elev < $effective_horizon &&
+		$info[-1]{elevation} >= $effective_horizon
+	    || $truncate && $time >= $pass_end
 	) {
 	    @info = () unless $visible;
 	    next unless @info;
 
-
 #	    We may have skipped part of the pass because it began in
 #	    daylight or before the official beginning of the prediction
 #	    period. Pick up that part now.
 
 	    {	# Single-iteration loop.
-		( my $time = $info[0]{time} - $step ) < $pass_backup_earliest
+		my $time = $info[0]{time} - $step;
+		$truncate
+		    and $time < $pass_start
 		    and last;
 		my ( $try_azm, $try_elev, $try_rng ) = $sta->azel (
 		    $tle->universal( $time ) );
 		last if $try_elev < $effective_horizon;
 		my $litup = $time < $suntim ? 2 - $dawn : 1 + $dawn;
 		1 == $litup
-		    and $tle->__sun_elev_from_sat( $time ) < 0
+		    and not $tle->illuminated( $time )
 		    and $litup = 0;
 		unshift @info, {
 		    azimuth => $try_azm,
@@ -1772,6 +1814,11 @@ eod
 		( $info[0]{time} - $pass_step,
 		    $info[-1]{time} + $pass_step
 		);
+	    $truncate
+		and ( $trial_start, $trial_finish ) = (
+		    max( $trial_start, $pass_start ),
+		    min( $trial_finish, $pass_end )
+		);
 	    my $culmination = find_first_true( $trial_start,
 		$trial_finish,
 		    sub { ( $sta->azel( $tle->universal( $_[0] ) ) )[1] >
@@ -1782,14 +1829,17 @@ eod
 
 	    # Compute exact rise and set.
 
-	    my $sat_rise = find_first_true( $info[0]{time} - $step,
+	    $truncate
+		or ( $trial_start, $trial_finish ) = (
+		$info[0]{time} - $step, $info[-1]{time} + $step );
+	    my $sat_rise = find_first_true( $trial_start,
 		$culmination,
 		sub { ( $sta->azel( $tle->universal( $_[0] ) ) )[1] >=
 		    $effective_horizon
 		},
 	    );
 	    my $sat_set = find_first_true ( $culmination,
-		$info[-1]{time} + $step,
+		$trial_finish,
 		sub { ( $sta->azel( $tle->universal( $_[0] ) ) )[1] <
 		    $effective_horizon
 		},
@@ -1842,7 +1892,7 @@ eod
 			if !$suntim || $time >= $suntim;
 		    my $litup = $time < $suntim ? 2 - $dawn : 1 + $dawn;
 		    1 == $litup
-			and $tle->__sun_elev_from_sat( $time ) < 0
+			and not $tle->illuminated( $time )
 			and $litup = 0;
 		    push @illumination, illumination => $lighting[$litup];
 		}
@@ -1877,7 +1927,7 @@ eod
 			    my $litup = $_[0] < $suntim ?
 				2 - $dawn : 1 + $dawn;
 			    1 == $litup
-				and $tle->__sun_elev_from_sat( $_[0] ) < 0
+				and not $tle->illuminated( $_[0] )
 				and $litup = 0;
 			    $lighting[$litup] == $evt->{illumination}
 			    });
@@ -1974,6 +2024,26 @@ eod
 		$info[-1]{event} = PASS_EVENT_END;
 	    }
 
+	    # If PASS_VARIANT_TRUNCATE is in effect, the first and last
+	    # events should be 'start' and 'end' IF AND ONLY IF the
+	    # satellite is above the horizon at that point AND the time
+	    # is at the start or end of the interval. Because the first
+	    # event is AFTER its exact time, we need to back up a bit
+	    # and recalculate.
+
+	    if ( $truncate ) {
+		my $prior = $info[0]{time} - 1;
+		if ( $prior <= $pass_start ) {
+		    my $elevation = ( $sta->azel(
+			    $tle->universal( $prior ) ) )[1];
+		    $elevation > $effective_horizon
+			and $info[0]{event} = PASS_EVENT_START;
+		}
+		$info[-1]{elevation} > $effective_horizon
+		    and $info[-1]{time} >= $pass_end
+		    and $info[-1]{event} = PASS_EVENT_END;
+	    }
+
 	    # Pick up the first and last event times, to use to bracket
 	    # future calculations.
 
@@ -2190,14 +2260,6 @@ eod
     }
 }
 
-# Unpublished, and subject to retraction. The sole purpose of this is to
-# give the experimental Astro::Coord::ECI::Point class a way to prevent
-# the Astro::Coord::ECI::TLE pass() method from backing up ad infinitum
-# trying to find the time when the body rises.
-sub __pass_backup_earliest {
-    return 0;
-}
-
 # Compute the position of the satellite at its brightest. We expect to
 # be called only if the computation makes sense -- that is, if the
 # intrinsic_magnitude attribute is set and we are calculating
@@ -7199,6 +7261,11 @@ sub _r_dump {
 # given time.
 sub __sun_elev_from_sat {
     my ( $self, $time ) = @_;
+    if ( defined $time ) {
+	$self->universal( $time );
+    } else {
+	$time = $self->universal();
+    }
     return ( $self->azel_offset(
 	    $self->get( 'illum' )->universal( $time ),
 	    $self->get( 'edge_of_earths_shadow' ),
@@ -7296,14 +7363,14 @@ encoded with a four-digit year.
 	    return 'Generated by ' . __PACKAGE__ . ' v' . $VERSION;
 	},
 	CREATION_DATE	=> sub {
-	    return _format_json_time( time );
+	    return format_space_track_json_time( time );
 	},
 	ECCENTRICITY	=> 'eccentricity',
 	ELEMENT_SET_NO	=> 'elementnumber',
 	EPHEMERIS_TYPE	=> 'ephemeristype',
 	EPOCH		=> sub {
 	    my ( $self ) = @_;
-	    return _format_json_time( floor(  $self->get( 'epoch' ) ) );
+	    return format_space_track_json_time( floor(  $self->get( 'epoch' ) ) );
 	},
 	EPOCH_MICROSECONDS	=> sub {
 	    my ( $self ) = @_;
@@ -7380,23 +7447,11 @@ encoded with a four-digit year.
 	# TLE_LINE2 is handled programmatically
 	effective_date	=> sub {
 	    my ( $self ) = @_;
-	    return _format_json_time( $self->get( 'effective' ) );
+	    return format_space_track_json_time( $self->get( 'effective' ) );
 	},
 	intrinsic_magnitude	=> 'intrinsic_magnitude',
     );
 
-    sub _format_json_time {
-	my ( $time ) = @_;
-	defined $time
-	    and $time =~ m/ \S /smx
-	    or return;
-	my @parts = gmtime floor( $time + .5 );
-	$parts[4] += 1;
-	$parts[5] += 1900;
-	return sprintf '%04d-%02d-%02d %02d:%02d:%02d', reverse
-	    @parts[ 0 .. 5 ];
-    };
-
     # This guy is to be used by subclasses so they don't have to
     # implement their own converter. The arguments (after the invocant)
     # are a reference to the mapping hash and an optional reference to
@@ -7477,7 +7532,7 @@ encoded with a four-digit year.
 #	    return 'Generated by ' . __PACKAGE__ . ' v' . $VERSION;
 #	},
 #	CREATION_DATE	=> sub {
-#	    return _format_json_time( time );
+#	    return format_space_track_json_time( time );
 #	},
 	EPOCH		=> 'epoch',
 	FILE		=> 'file',
@@ -7540,65 +7595,74 @@ encoded with a four-digit year.
 	foreach my $arg ( @args ) {
 	    my $decode = $json->decode( $arg );
 
-BODY_LOOP:
 	    foreach my $hash ( 'ARRAY' eq ref $decode ? @{ $decode } :
 		$decode ) {
 
-		if ( exists $hash->{SATNAME} ) {	# TODO Deprecated
-		    warnings::enabled( 'deprecated' )
-			and carp 'The SATNAME JSON key is deprecated ',
-			    'in favor of the OBJECT_NAME key';
-		    exists $hash->{OBJECT_NAME}
-			or $hash->{OBJECT_NAME} = $hash->{SATNAME};
-		    delete $hash->{SATNAME};
-		}
+		my $class = $hash->{astro_coord_eci_class} || __PACKAGE__;
+		load_module( $class );
+		push @rslt, $class->__from_json( $hash );
 
-		foreach my $key ( @required ) {
-		    defined $hash->{$key} and next;
-		    next BODY_LOOP;
-		}
+	    }
+	}
 
-		defined $hash->{INTLDES}
-		    and $hash->{INTLDES} =~
-			s/ \A \d{2} ( \d{2} ) - /$1/smx;
+	return @rslt;
+    }
 
-		foreach my $key ( qw{ EPOCH effective_date } ) {
-		    defined $hash->{$key}
-			and $hash->{$key} = _decode_json_time( $hash->{$key} );
-		}
-		defined $hash->{EPOCH_MICROSECONDS}
-		    and $hash->{EPOCH} += $hash->{EPOCH_MICROSECONDS} /
-			1_000_000;
-
-		foreach my $key ( qw{
-			ARG_OF_PERICENTER INCLINATION MEAN_ANOMALY
-			RA_OF_ASC_NODE
-		    } ) {
-		    $hash->{$key} *= SGP_DE2RA;
-		}
+    sub __from_json {
+	my ( $class, $hash ) = @_;
 
-		{
-		    my $temp = SGP_TWOPI;
-		    foreach my $key ( qw{
-			    MEAN_MOTION MEAN_MOTION_DOT MEAN_MOTION_DDOT
-			} ) {
-			$temp /= SGP_XMNPDA;
-			$hash->{$key} *= $temp;
-		    }
-		}
+	if ( exists $hash->{SATNAME} ) {	# TODO Deprecated
+	    warnings::enabled( 'deprecated' )
+		and carp 'The SATNAME JSON key is deprecated ',
+		    'in favor of the OBJECT_NAME key';
+	    exists $hash->{OBJECT_NAME}
+		or $hash->{OBJECT_NAME} = $hash->{SATNAME};
+	    delete $hash->{SATNAME};
+	}
 
-		my %tle;
-		foreach my $key ( keys %{ $hash } ) {
-		    my $value = $hash->{$key};
-		    my $attr = $json_map{$key}
-			or next;
-		    $tle{$attr} = $value;
-		}
-		push @rslt, __PACKAGE__->new( %tle );
+	foreach my $key ( @required ) {
+	    defined $hash->{$key}
+		or return;
+	}
+
+	defined $hash->{INTLDES}
+	    and $hash->{INTLDES} =~
+		s/ \A \d{2} ( \d{2} ) - /$1/smx;
+
+	foreach my $key ( qw{ EPOCH effective_date } ) {
+	    defined $hash->{$key}
+		and $hash->{$key} = _decode_json_time( $hash->{$key} );
+	}
+	defined $hash->{EPOCH_MICROSECONDS}
+	    and $hash->{EPOCH} += $hash->{EPOCH_MICROSECONDS} /
+		1_000_000;
+
+	foreach my $key ( qw{
+		ARG_OF_PERICENTER INCLINATION MEAN_ANOMALY
+		RA_OF_ASC_NODE
+	    } ) {
+	    $hash->{$key} *= SGP_DE2RA;
+	}
+
+	{
+	    my $temp = SGP_TWOPI;
+	    foreach my $key ( qw{
+		    MEAN_MOTION MEAN_MOTION_DOT MEAN_MOTION_DDOT
+		} ) {
+		$temp /= SGP_XMNPDA;
+		$hash->{$key} *= $temp;
 	    }
 	}
 
-	return @rslt;
+	my %tle;
+	foreach my $key ( keys %{ $hash } ) {
+	    my $value = $hash->{$key};
+	    my $attr = $json_map{$key}
+		or next;
+	    $tle{$attr} = $value;
+	}
+
+	return $class->new( %tle );
     }
 }
 
@@ -7720,7 +7784,7 @@ sub _find_position {
 # Initial value of the 'inertial' attribute. TLEs are assumed to be
 # inertial until set otherwise.
 
-sub _initial_inertial{ return 1 };
+sub __initial_inertial{ return 1 };
 
 # Unsupported, experimental, and subject to change or retraction without
 # notice. The intent is to provide a way for the Astro::App::Satpass2
@@ -7780,14 +7844,21 @@ sub _looks_like_real {
 	},
     );
 
+    my @required_fields = qw{
+	firstderivative secondderivative bstardrag inclination
+	ascendingnode eccentricity argumentofperigee meananomaly
+	meanmotion revolutionsatepoch
+    };
+
     sub _make_tle {
 	my $self = shift;
 	my $output;
 
 	my $oid = $self->get('id');
+	my $name = $self->get( 'name' );
 	my @line0;
 
-	if ( defined ( my $name = $self->get( 'name' ) ) ) {
+	if ( defined $name ) {
 	    $name =~ s/ \s+ \z //smx;
 	    $name ne ''
 		and push @line0, substr $name, 0, 24;
@@ -7805,13 +7876,23 @@ sub _looks_like_real {
 
 	my %ele;
 	{
-	    foreach (qw{firstderivative secondderivative bstardrag
-		inclination ascendingnode eccentricity
-		argumentofperigee meananomaly meanmotion
-		revolutionsatepoch}) {
-		defined ($ele{$_} = $self->get($_))
-		    or croak "OID $oid ", ucfirst $_,
-			"undefined; can not generate TLE";
+	    my @missing_fields;
+	    foreach ( @required_fields ) {
+		defined( $ele{$_} = $self->get( $_ ) )
+		    and next;
+		push @missing_fields, $_;
+	    }
+
+	    if ( @missing_fields ) {
+		# If all required fields are missing we presume it is
+		# deliberate, and return nothing.
+		@required_fields == @missing_fields
+		    and return undef;	## no critic (ProhibitExplicitReturnUndef)
+		# Otherwise we croak with an error
+		croak 'Can not generate TLE for ',
+		    defined $oid ? $oid : $name,
+		    '; undefined attribute(s) ',
+		    join ', ', @missing_fields;
 	    }
 	    my $temp = SGP_TWOPI;
 	    foreach (qw{meanmotion firstderivative secondderivative}) {
@@ -8864,7 +8945,7 @@ sub _next_elevation_screen {
 #
 #   $ eg/visual -merge
 #
-# Last-Modified: Tue, 07 Jan 2014 03:07:16 GMT
+# Last-Modified: Sat, 14 Jun 2014 22:30:35 GMT
 
 %magnitude_table = (
   '00694' => 3.5,
@@ -9419,6 +9500,16 @@ that setting this (with C<< visible => 0 >>) saves about 20% in wall
 clock time versus not setting it. Your mileage may, of course, vary.
 This has no effect unless the C<visible> attribute is false.
 
+* C<PASS_VARIANT_TRUNCATE> - Specifies that any pass in progress at the
+beginning or end of the calculation interval is to be truncated to the
+interval. If the calculation interval starts with the body already above
+the horizon, the initial event of that pass will be C<PASS_EVENT_START>
+rather than C<PASS_EVENT_RISE>. Similarly, if the calculation interval
+ends with the body still above the horizon, the final event of that pass
+will be C<PASS_EVENT_END>. With this variant in effect, 'passes' will be
+computed for synchronous satellites. This variant is to be considered
+B<experimental.>
+
 * C<PASS_VARIANT_BRIGHTEST> - Specifies that the the moment the
 satellite is brightest be computed as part of the pass statistics. The
 computation is to the nearest second, and is not done if
@@ -104,7 +104,7 @@ package Astro::Coord::ECI::Utils;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 our @ISA = qw{Exporter};
 
 use Carp;
@@ -140,15 +140,16 @@ our @EXPORT_OK = ( qw{
 	AU $DATETIMEFORMAT $JD_GREGORIAN JD_OF_EPOCH LIGHTYEAR PARSEC
 	PERL2000 PI PIOVER2 SECSPERDAY SECS_PER_SIDERIAL_DAY
 	SPEED_OF_LIGHT TWOPI acos add_magnitudes asin
-	atmospheric_extinction date2epoch date2jd deg2rad distsq
-	dynamical_delta embodies epoch2datetime equation_of_time
-	find_first_true fold_case intensity_to_magnitude jcent2000
-	jd2date jd2datetime jday2000 julianday keplers_equation
-	load_module looks_like_number max min mod2pi
+	atmospheric_extinction date2epoch date2jd
+	decode_space_track_json_time deg2rad distsq dynamical_delta
+	embodies epoch2datetime equation_of_time find_first_true
+	fold_case format_space_track_json_time intensity_to_magnitude
+	jcent2000 jd2date jd2datetime jday2000 julianday
+	keplers_equation load_module looks_like_number max min mod2pi
 	nutation_in_longitude nutation_in_obliquity obliquity omega
 	rad2deg tan theta0 thetag vector_cross_product
-	vector_dot_product vector_magnitude vector_unitize
-       	__classisa __default_station __instance },
+	vector_dot_product vector_magnitude vector_unitize __classisa
+	__default_station __instance },
 	@time_routines );
 
 our %EXPORT_TAGS = (
@@ -337,6 +338,39 @@ sub date2epoch {
     (($hr || 0) * 60 + ($min || 0)) * 60 + ($sec || 0);
 }
 
+=item $time = decode_space_track_json_time( $string )
+
+This subroutine decodes a time in the format Space Track uses in their
+JSON code. This is ISO-8601-ish, but with a possible fractional part and
+no zone.
+
+=cut
+
+sub decode_space_track_json_time {
+    my ( $string ) = @_;
+    $string =~ m{ \A \s*
+	( \d+ ) \D+ ( \d+ ) \D+ ( \d+ ) \D+
+	( \d+ ) \D+ ( \d+ ) \D+ ( \d+ ) (?: ( [.] \d* ) )? \s* \z }smx
+	or return;
+    my @time = ( $1, $2, $3, $4, $5, $6 );
+    my $frac = $7;
+    if ( $time[0] < 100 ) {
+	$time[0] < 57
+	    and $time[0] += 100;
+    } elsif ( $time[0] < 1900 ) {
+	return;
+    } else {
+	$time[0] -= 1900;
+    }
+    $time[1] -= 1;
+    my $rslt = timegm( reverse @time );
+    defined $frac
+	and $frac ne '.'
+	and $rslt += $frac;
+    return $rslt;
+}
+
+
 # my ( $self, $station, @args ) = __default_station( @_ )
 #
 # This exportable subroutine checks whether the second argument embodies
@@ -589,10 +623,29 @@ C<CORE::fc> if that is available, otherwise it maps to C<CORE::lc>.
 
 *fold_case = CORE->can( 'fc' ) || sub ($) { return lc $_[0] };
 
+=item $fmtd = format_space_track_json_time( time() )
+
+This function takes as input a Perl time, and returns that time
+in a format consistent with the Space Track JSON data. This is
+ISO-8601-ish, in Universal time, but without the zone indicated.
+
+=cut
+
+sub format_space_track_json_time {
+    my ( $time ) = @_;
+    defined $time
+	and $time =~ m/ \S /smx
+	or return;
+    my @parts = gmtime floor( $time + .5 );
+    $parts[4] += 1;
+    $parts[5] += 1900;
+    return sprintf '%04d-%02d-%02d %02d:%02d:%02d', reverse
+	@parts[ 0 .. 5 ];
+}
 
 =item $difference = intensity_to_magnitude ($ratio)
 
-This method converts a ratio of light intensities to a difference in
+This function converts a ratio of light intensities to a difference in
 stellar magnitudes. The algorithm comes from Jean Meeus' "Astronomical
 Algorithms", Second Edition, Chapter 56, Page 395.
 
@@ -143,7 +143,7 @@ package Astro::Coord::ECI;
 use strict;
 use warnings;
 
-our $VERSION = '0.063';
+our $VERSION = '0.065';
 
 use Astro::Coord::ECI::Utils qw{:all};
 use Carp;
@@ -186,7 +186,7 @@ to the set() method once the object has been instantiated.
 sub new {
     my ( $class, @args ) = @_;
     my $self = bless { %static }, ref $class || $class;
-    $self->{inertial} = $self->_initial_inertial();
+    $self->{inertial} = $self->__initial_inertial();
     @args and $self->set( @args );
     exists $self->{almanac_horizon}
 	or $self->set( almanac_horizon => 0 );
@@ -3374,7 +3374,7 @@ sub _expand_args_default_station {
     return @args;
 }
 
-#	$value = $self->_initial_inertial
+#	$value = $self->__initial_inertial
 #
 #	Return the initial setting of the inertial attribute. At this
 #	level we are assumed not inertial until we acquire a position.
@@ -3383,7 +3383,7 @@ sub _expand_args_default_station {
 #	Setting the coordinates explicitly will still set the {inertial}
 #	attribute appropriately.
 
-sub _initial_inertial{ return };
+sub __initial_inertial{ return };
 
 #	$value = _local_mean_delta ($coord)
 
@@ -290,6 +290,9 @@ u_cmp_eql find_first_true => [
     0, 1, sub{ sin( $_[0] ) >= sin( .5 ) }, .0001 ], .5, '%.4f',
     'find_first_true looking for sin( $x ) >= sin( .5 )';
 
+u_cmp_eql format_space_track_json_time => timegm( 0, 0, 0, 1, 3, 114 ),
+    '2014-04-01 00:00:00', '%s', 'Format Space Tracj JSON time';
+
 done_testing;
 
 sub instantiate ($) {
@@ -324,8 +327,13 @@ sub u_cmp_eql (@) {
 	} else {
 	    defined $tplt
 		and ( $want, $got ) = map { sprintf $tplt, $_ } ( $want, $got );
-	    @_ = ( $got, '==', $want, $title );
-	    goto &cmp_ok;
+	    if ( defined $tplt && '%s' eq $tplt ) {
+		@_ = ( $got, $want, $title );
+		goto &is;
+	    } else {
+		@_ = ( $got, '==', $want, $title );
+		goto &cmp_ok;
+	    }
 	}
     } else {
 	@_ = "Astro::Coord::ECI::Utils does not have subroutine $sub()";
@@ -416,6 +416,53 @@ EOD
                     -41.0  61.9    49.8 Moon
 EOD
 
+    $tle->set( pass_variant => PASS_VARIANT_TRUNCATE );
+    @pass = ();
+    if (
+	eval {
+	    @pass = $tle->pass(
+		timegm( 0, 0, 0, 13, 9, 80 ),
+		timegm( 0, 0, 0, 14, 9, 80 ),
+	    );
+	    1;
+	}
+    ) {
+	ok @pass == 1, 'Found 1 passes over Greenwich'
+	    or diag "Found @{[ scalar @pass ]} passes over Greenwich";
+    } else {
+	fail "Error in pass() method: $@";
+    }
+
+    is format_pass( $pass[0] ), <<'EOD', 'Pass 1';
+1980/10/13 05:39:02   0.0 199.0  1687.8 lit   rise
+1980/10/13 05:42:43  55.9 115.6   255.5 lit   max
+1980/10/13 05:46:37   0.0  29.7  1778.5 lit   set
+EOD
+
+    @pass = ();
+    if (
+	eval {
+	    @pass = $tle->pass(
+		timegm( 0, 40, 5, 13, 9, 80 ),
+		timegm( 0, 45, 5, 13, 9, 80 ),
+	    );
+	    1;
+	}
+    ) {
+	ok @pass == 1, 'Found 1 passes over Greenwich'
+	    or diag "Found @{[ scalar @pass ]} passes over Greenwich";
+    } else {
+	fail "Error in pass() method: $@";
+    }
+
+    is format_pass( $pass[0] ), <<'EOD', 'Pass 1';
+1980/10/13 05:40:01   4.2 197.4  1251.1 lit   start
+1980/10/13 05:42:43  55.9 115.6   255.5 lit   max
+1980/10/13 05:45:00   7.4  32.6  1063.4 lit   end
+EOD
+
+    $tle->set( pass_variant => PASS_VARIANT_NONE );
+
     my ( $tle2 ) = Astro::Coord::ECI::TLE->parse(
 	{ station => $sta },  <<'EOD' );
 11801
@@ -440,11 +487,11 @@ EOD
     }
 
     is format_pass( $pass[0] ), <<'EOD', 'Pass 1';
-1980/10/13 05:39:02   0.0 199.0  1687.8 lit   start
+1980/10/13 05:39:02   0.0 199.0  1687.8 lit   rise
 1980/10/13 05:39:02   0.0 199.0  1687.4 lit   apls
                     -17.8 203.7    17.8 11801
 1980/10/13 05:42:43  55.9 115.6   255.5 lit   max
-1980/10/13 05:46:37   0.0  29.7  1778.5 lit   end
+1980/10/13 05:46:37   0.0  29.7  1778.5 lit   set
 EOD
 
 }
@@ -20,7 +20,7 @@ EOD
 
 is last_modified(
     'http://celestrak.com/SpaceTrack/query/visual.txt' ),
-'Tue, 07 Jan 2014 03:07:16 GMT',
+'Sat, 14 Jun 2014 22:30:35 GMT',
 'Celestrak visual.txt Last-Modified';
 
 is last_modified( mccants => 'vsnames' ),
@@ -28,7 +28,7 @@ is last_modified( mccants => 'vsnames' ),
     'McCants vsnames.mag Last-Modified';
 
 is last_modified( mccants => 'mcnames' ),
-    'Mon, 06 Jan 2014 10:21:29 GMT',
+    'Tue, 20 May 2014 23:28:07 GMT',
     'McCants mcnames.mag Last-Modified';
 
 done_testing;