#!/usr/bin/perl

=head1 NAME

mpc_sub - Submit MPC 80-column data via email

=head1 SYNOPSIS

mpc_sub [options] [TRACKLET_IDS]

  TRACKLET_IDS : list of MOPS tracklet IDs to submit
  --neo : indicate objects are NEO candidates, mail singly
  --neobatch : indicate objects are a batch of low-scoring NEO candidates
  --force_seq_num : force a specific sequence number, usually for cleaning up the DB
  --dets IDS : specify pipe-separated list of detection IDs to submit; e.g. '1387|2378|9244'

  --known_nn NIGHT_NUMBER : batch-send all known pair tracklets from night NN (just pairs for now)
  --help : show this manpage

Examples.  Note:  all examples assume "--instance=psmops_ps1_nomagic --submitter=larry"

  mpc_sub --neo T23487
  mpc_sub --followup=1999XA88 T09378
  mpc_sub --followup=1999XA88 --dets='234|236|238' T09378
  mpc_sub T9238 T9234 T2394 ...
  mpc_sub --neobatch T9238 T9234 T2394 ...
  mpc_sub --force_seq_num=916247405 --followup=1999XA88 --dets='234|236|238' T09378

  mpc_sub --known_nn NIGHT_NUMBER

=head1 DESCRIPTION

Submit via email to mpc@cfa.harvard.edu.

The rules: for NEO submissions, a single email is sent with multiple header
sections so that COM headers can be written for each NEO candidate.

For standard submissions (follow-up, MBOs, etc.), a single email with no
COM header is sent, and all observations follow.

=head1 REVISED ASTROMETRY

mpc_sub supports a hack that allows a submitter to override database
astrometry and photometry when submitting a tracklet.  Typically new
positions and mags will be computed using IDL trail fitting software via
the MOPS web interface, and we want to submit these revisions instead
of what's in the database.

The right way to do this is beyond the scope of this program.  Ideally
we would write new rows in the detections table and update tracklets.
But that is a design modification we are unwilling to tackle for now.
So we are hacking it in this way.

Note that only the MPC_SUB table will contain the revised positions
and mags.  We also assume that the revised positions will remain
close enough so that positional searches return the correct tracklet.

The revision string must contain a packed data structure that rewrites
the RA, DEC and MAG for each detection in a submitted tracklet.  We will 
support

revision_data='DET_ID1:ra=RA1&dec=DEC2&mag=MAG1|DET_ID2:ra=RA2...'

=head1 CAVEATS

This program started out simple and now does too many things, resulting
in too many invocation options.  It's hard to test.

=cut


use strict;
use warnings;
use Getopt::Long;
use Pod::Usage;
use Mail::Send;
use List::Util qw(sum);
use Astro::Time;
use Astro::SLA;

# Just a stub for quick debugging.
use PS::MOPS::Lib qw(:all);
use PS::MOPS::MPC;
use PS::MOPS::DC::Instance;
use PS::MOPS::DC::Field;
use PS::MOPS::DC::Tracklet;
use PS::MOPS::DC::DerivedObject;


# Detection statuses.
our $SUBMISSION_PENDING_NEO = 'P';
our $SUBMISSION_PENDING_STD = 'Q';
our $SUBMISSION_NEO = 'S';
our $SUBMISSION_STD = 'T';

# Other globals.
#our $B62_TEMPLATE = 'Q000000';      # testing
our $B62_TEMPLATE = 'P000000';
our $NEW_THRESHOLD_DAYS = 5;        # MPC 'fresh NEO' window, anything older is incidental astrometry
our $ISS_Q_THRESHOLD = 5.5;         # ISS/OSS boundary; do not automatically submit objects with q > 5.5AU  

# Start program here.
my $instance_name;
my $submitter = 'MOPS';
my $neo;
my $incidental;
my $neobatch;
my $followup;
my $to = 'mpc@cfa.harvard.edu';
#my $to = 'denneau@ifa.hawaii.edu';
#my $new_cc = 'Robert McMillan <bob@lpl.arizona.edu>, "Ryan, Eileen" <eryan@admin.nmt.edu>, ari@astro-research.org, "J. D. Armstrong" <jd@maile.ifa.hawaii.edu>, tlister@lcogt.net, tholen@ifa.hawaii.edu, micheli@ifa.hawaii.edu, chambers@ifa.hawaii.edu, shinsuke.avell@gmail.com, mopsczar@ifa.hawaii.edu';
my $new_cc = 'mopsczar@ifa.hawaii.edu, chambers@ifa.hawaii.edu';
#my $new_cc = 'psmops@gmail.com';
my $batch_cc = 'psmops@gmail.com';
my $user_cc;     # user-override cc:
my $force_seq_num;
my $sub_dets;
my %sub_dets;
my $send_pending;       # send leftover pending records
my $revision_data;      # hack for revised astrometry
my $known_nn;
my $debug;
my $nosend;
my $help;
GetOptions(
    'instance=s' => \$instance_name,
    'submitter=s' => \$submitter,
    'to=s' => \$to,             # allow override of To: header
    'cc=s' => \$user_cc,        # allow override of Cc: header
    neo => \$neo,
    incidental => \$incidental,
    neobatch => \$neobatch,
    'followup=s' => \$followup,
    'dets=s' => \$sub_dets,
    'force_seq_num=s' => \$force_seq_num,   # for DB cleanup
    send_pending => \$send_pending,
    'known_nn=i' => \$known_nn,
    'revision_data=s' => \$revision_data,
    debug => \$debug,
    nosend => \$nosend,
    help => \$help,
) or pod2usage(2);
pod2usage(-verbose => 3) if $help;

if ($sub_dets) {
    %sub_dets = map { ($_, 1) } split /\|/, $sub_dets;
}


my $inst = PS::MOPS::DC::Instance->new(DBNAME => $instance_name);
my $config = $inst->getConfig();
my $logger = $inst->getLogger();
my $v2filt = $config->{site}->{v2filt} or die "can't get v2filt from config";
my $dbh = $inst->dbh;
my $lookup_sth = $dbh->prepare(<<"SQL") or die $dbh->errstr;
/* args are P1.dec, P2.dec, P1.dec, P2.dec, P1.ra, P2.ra */
select desig 
from export.mpc_sub 
where fpa_id=?
and abs(
    degrees(
        /* spherical distance from tracklet to target field center */
        acos(least(1.0, /* handle slight roundoff error causing arg > 1.0 */
            sin(radians(export.mpc_sub.dec_deg)) * sin(radians(?))
            + cos(radians(export.mpc_sub.dec_deg)) * cos(radians(?)) * cos(radians(export.mpc_sub.ra_deg - ?))
        ))
    )
) < 0.000555 /* 2 arcsec */
SQL


my $insert_sth = $dbh->prepare(<<"SQL") or die $dbh->errstr;
insert into export.mpc_sub
values (
    ?, /* seq_num */
    null, /* batch_num; will write later */
    ?, /* fpa_id */
    ?, /* epoch_mjd */
    ?, /* ra_deg */
    ?, /* dec_deg */
    ?, /* filter_id */
    ?, /* mag */
    ?, /* obscode */
    ?, /* desig */
    ?, /* digest */
    ?, /* spatial_idx */
    ?, /* survey_mode */
    ?, /* dbname */
    ?, /* det_id */
    ?, /* tracklet_id */
    ?, /* derivedobject_id */
    ?, /* submitter */
    current_timestamp(),
    ?, /* disposition */
    ?, /* discovery */
    ? /* mpc_desig */,
    ? /* revised */
)
SQL


# Select submission data for a particular designation.
my $desig_sth = $dbh->prepare(<<"SQL") or die $dbh->errstr;
select desig, epoch_mjd, ra_deg, dec_deg, filter_id, mag, obscode, dbname, tracklet_id, derivedobject_id, digest
from export.mpc_sub
where desig=?
SQL


# Select unsubmitted designations.
my $pending_sth = $dbh->prepare(<<"SQL") or die $dbh->errstr;
select distinct(desig)
from export.mpc_sub
where disposition=?
SQL

# Update submitted observations.
my $update_sth = $dbh->prepare(<<"SQL") or die $dbh->errstr;
update export.mpc_sub
set batch_num=?, disposition=?
where desig=?
SQL


my @submit_ids = @ARGV;
# If we're batch-sending known tracklets, get our list here.
if ($known_nn) {
    $neo = 0;       # disable NEO mode
    $submitter ||= 'mops';

    @submit_ids = @{$dbh->selectcol_arrayref(<<"SQL")};
select concat("T", t.tracklet_id)
from known k 
join tracklets t using(known_id)
join tracklet_attrib ta using(tracklet_id)
join fields f using(field_id)
where k.q < $ISS_Q_THRESHOLD
and f.nn = $known_nn
group by tracklet_id
having count(*) = 2
SQL
}


# Unpack revision data if present.
my $revision_href;
if ($revision_data) {
    use Data::Dumper;
    my @dets = split /\|/, $revision_data;
    foreach my $line (@dets) {
        my ($det_id, $stuff) = split /:/, $line;
        my @kv = split /\&|=/, $stuff;
        $revision_href->{$det_id} = { @kv };
    }

#    print Dumper($revision_href);
#    exit;
}

# Plan: 
# for each tracklet, see if any of its detections have been submitted.
#   if submitted, put in @already_inserted list
#   if other problem, put in @problem list
#   otherwise put in @to_submit

# for each @to_submit
#   get a new ID from mpc_seq
#   create B62 internal desig
#   write records
#   add to $content

# mail @submitted to mpc@cfa.
# mail @problems and @already_inserted to user.


my @already_inserted;
my @problem;
my @trk_to_insert;
my @dobj_to_insert;
my @to_submit;
my %seen_detids;

my $last_epoch_mjd;
my $newest_mjd = -999999;


# Collect unsubmitted records first.
my @pending = get_pending();
if (@pending > 0) {
    printf "Found %d pending %sdesignation(s).\n", scalar @pending, ($neo ? 'NEO ' : '');
}


foreach my $id (@submit_ids) {
    # 1. Check that none of the tracklet's detections have been submitted.
    # 2. Set up insert data.
    # 3. Insert into table & retrieve designation.
    # 4. Submit via email.

    if ($id =~ /^T/) {
        # This is a tracklet; fetch it and add the detections.
        my $tid = $id;
        $tid =~ s/^T//;     # strip leading T if present
        my $trk = modct_retrieve($inst, trackletId => $tid);
        unless ($trk) {
            emit("No tracklet $tid found.\n");
            next;
        }

        my @detections = @{$trk->detections};
        my $can_submit = 1;
        foreach my $det (@detections) {
            # Look up det in DB.
            $last_epoch_mjd = $det->epoch;
            my $found_desig;
            if ($found_desig = lookup_det($det)) {
                emit(sprintf "%s already submitted as %s: %s/D%d.\n", $id, $found_desig, $inst->dbname, $det->detId);
                $can_submit = 0;
                last;
            }
            elsif (exists($seen_detids{$det->detId})) {
                emit(sprintf "%s: already seen det ID %d in this batch.\n", $id, $det->detId);
                $can_submit = 0;
                last;
            }
            $seen_detids{$det->detId} = 1;
        }

        if ($can_submit) {
            push @trk_to_insert, $trk;
            $newest_mjd = $last_epoch_mjd if $last_epoch_mjd > $newest_mjd;
        }
        else {
            push @already_inserted, $trk;
        }
    }
    elsif ($id =~ /^L/) {
        # This is a derived object; fetch its tracklets and add the detections.
        my $dobj = modcdo_retrieve($inst, objectName => $id);
        unless ($dobj) {
            emit("No derived object $dobj found.\n");
            next;
        }

        my $can_submit = 1;
        foreach my $trk ($dobj->fetchTracklets()) {
            my @detections = @{$trk->detections};
            foreach my $det (@detections) {
                # Look up det in DB.
                $last_epoch_mjd = $det->epoch;
                my $found_desig;
                if ($found_desig = lookup_det($det)) {
                    emit(sprintf "%s already submitted as %s/%s.\n", $id, $inst->dbname, $found_desig);
                    $can_submit = 0;
                    last;
                }
            }
            last if $can_submit == 0;    # bail from outer loop
        }

        if ($can_submit) {
            push @dobj_to_insert, $dobj;
            $newest_mjd = $last_epoch_mjd if $last_epoch_mjd > $newest_mjd;
        }
    }
    else {
        die "ID must be a tracklet ID (Txxxx) or derived object name (Lxxxx).\n";
    }
}

# Standard eval for DB transaction.
$dbh->begin_work() or die $dbh->errstr;
eval {
    foreach my $trk (@trk_to_insert) {
        my $seq_num = $force_seq_num || get_seq_num($dbh);
        my $desig = insert_tracklet($trk, $seq_num, $revision_href);
        push @to_submit, $desig;
    }

    foreach my $dobj (@dobj_to_insert) {
        my $seq_num = get_seq_num($dbh);
        my $desig = insert_dobj($dobj, $seq_num);
        push @to_submit, $desig;
    }
};
if ($@) {
    $dbh->rollback();
    die $@;
}
$dbh->commit() or die $dbh->errstr;



if ($send_pending) {
    push @to_submit, @pending;
}
if (!@to_submit) {
    emit("Nothing to submit.\n");
    exit;
}


# Now submit the designations by reading records out of the submit table.
# After submission, record the submission status (disposition).
my $batch_num;
my $batch_id;
my @lines;
my $ack;
my $com;
my $subject;

if ($neo) {
    # This is a single new NEO submission, with notification to PS1 follow-up partners.
    my $cc = ($user_cc || $new_cc);
    printf "Sending %d NEO tracklet(s) to $to\n", scalar @to_submit;
    printf "Cc: $cc\n" if $cc;
    foreach my $desig (@to_submit) {
        $batch_num = get_batch_num($dbh);
        $batch_id = sprintf "%07d", $batch_num;

        my @data = fetch_desig_data($desig) or die "no data for $desig";
        my ($vtot, $ext_mag) = extra_from_data(@data);

        $ack = sprintf "$desig (%s-%s/T%d)", $submitter, ($data[0]->{dbname} || 'NODB'), ($data[0]->{tracklet_id} || 'NOTR');
        $com = sprintf "Digest %.2f, batch ID %07d", $data[0]->{digest}, $batch_id;
        $com .= ' (REVISED)' if $revision_data;
        push @lines, make_header($ack, $com);
        push @lines, make_mpc_records(@data);
        printf "  %s: %s/T%d\n", $desig, $data[0]->{dbname}, $data[0]->{tracklet_id};

        # MPC requests 'NEO Candidate' subject only for recent data (5 days or less).
        if (now2mjd() - $newest_mjd < $NEW_THRESHOLD_DAYS) {
            $subject = sprintf "PS1 NEO Candidate $desig [%.1f;%.2f deg/day;V=%.1f]\n", $data[0]->{digest}, $vtot, $ext_mag;
        }
        else {
            $subject = "PS1 Incidental Astrometry $desig";
        }
        printf "Subject: $subject\n";

        mailit($submitter, $to, $cc, $subject, @lines) unless $nosend;
        eval {
            $dbh->begin_work();
            $update_sth->execute($batch_num, $SUBMISSION_NEO, $_) foreach @to_submit;
        };
        if ($@) {
            $dbh->rollback();
            die $@;
        }
        $dbh->commit();
    }
}
elsif ($incidental) {
    # This is a single incidental submission, with notification to PS1 follow-up partners.
    my $cc = ($user_cc || $new_cc);
    printf "Sending %d incidental astrometry tracklet(s) to $to\n", scalar @to_submit;
    printf "Cc: $cc\n" if $cc;
    foreach my $desig (@to_submit) {
        $batch_num = get_batch_num($dbh);
        $batch_id = sprintf "%07d", $batch_num;

        my @data = fetch_desig_data($desig) or die "no data for $desig";
        $ack = sprintf "$desig (%s-%s/T%d)", $submitter, ($data[0]->{dbname} || 'NODB'), ($data[0]->{tracklet_id} || 'NOTR');
        $com = sprintf "Digest %.2f, batch ID %07d", $data[0]->{digest}, $batch_id;
        $com .= ' (REVISED)' if $revision_data;
        push @lines, make_header($ack, $com);
        push @lines, make_mpc_records(@data);
        printf "  %s: %s/T%d\n", $desig, $data[0]->{dbname}, $data[0]->{tracklet_id};

        $subject = "PS1 Incidental Astrometry $desig";
        print "Subject: $subject\n";

        mailit($submitter, $to, $cc, $subject, @lines) unless $nosend;
        eval {
            $dbh->begin_work();
            $update_sth->execute($batch_num, $SUBMISSION_STD, $_) foreach @to_submit;
        };
        if ($@) {
            $dbh->rollback();
            die $@;
        }
        $dbh->commit();
    }
}
else {
    $batch_num = get_batch_num($dbh);
    $batch_id = sprintf "%07d", $batch_num;
    $ack = sprintf "%s-%07d", $submitter, $batch_id;
    if ($followup) {
        die sprintf("Follow-up is allowed only for single designations (%d found).\n", scalar @to_submit)
            if (@to_submit > 1);
        $followup =~ s/\s+//g;       # strip everything but 
        my $desig = $to_submit[0]; # assume one object
        if ($followup eq 'N/A') {
            $subject = "PS1 Follow-up $desig";
            $com = "$desig batch ID $batch_id";
        }
        else {
            $subject = "PS1 Follow-up $desig=$followup";
            $com = "$desig=$followup batch ID $batch_id";
        }
        my @data = fetch_desig_data($desig) or die "no data for $desig";
        $ack .= "/$desig " . sprintf("(%s-%s/T%d)", $submitter, ($data[0]->{dbname} || 'NODB'), ($data[0]->{tracklet_id} || 'NOTR'));
    }
    else {
        if ($neobatch) {
            if (now2mjd() - $newest_mjd < $NEW_THRESHOLD_DAYS) {
                $subject = "PS1 NEO Candidates Batch $batch_id";
            }
            else {
                $subject = "PS1 Incidental Astrometry Batch $batch_id";
            }
        }
        else {
            $subject = "PS1 Incidental Astrometry Batch $batch_id";
        }
        $com = sprintf "%d tracklet(s) from %s", scalar @to_submit, $inst->dbname;
    }

    my $cc = ($user_cc || $batch_cc);
    printf "Sending %d batch tracklet(s) to $to.\n\n", scalar @to_submit;
    print "To: $to\n";
    print "Subject: $subject\n";
    print "Cc: $cc\n" if $cc;

    push @lines, make_header($ack, $com);
    push @lines, make_mpc_records(map { fetch_desig_data($_) } @to_submit);
    mailit($submitter, $to, $cc, $subject, @lines) unless $nosend;

    eval {
        $dbh->begin_work();
        $update_sth->execute($batch_num, $SUBMISSION_STD, $_) foreach @to_submit;
    };
    if ($@) {
        $dbh->rollback();
        die $@;
    }
    $dbh->commit();
}
exit;


sub lookup_det {
    my ($det) = @_;
    my $field = lookup_field($det);
    $lookup_sth->execute(
        $field->fpaId, $det->dec, $det->dec, $det->ra
    ) or die $lookup_sth->errstr;
    my ($near) = $lookup_sth->fetchrow_array();
    return $near;
}


sub lookup_field {
    my ($det) = @_;
    my $field = modcf_retrieve($inst, fieldId => $det->fieldId);
    return $field;
}


sub get_seq_num {
    # Return a new seq_num for a provisional designation.
    my ($dbh) = @_;

    $dbh->do('insert into export.desig_seq values(null)');
    my $id = $dbh->{'mysql_insertid'};
    die "Got bogus insert ID trying to get submission seq num" unless $id;
}


sub get_batch_num {
    # Return a new batch number for a submission. This is written to observation
    # records after email is sent.
    my ($dbh) = @_;

    $dbh->do('insert into export.batch_seq values(null)');
    my $id = $dbh->{'mysql_insertid'};
    die "Got bogus insert ID trying to get batch num" unless $id;
}


sub insert_tracklet {
    # Insert a tracklet into the submission table.  We will convert the 
    # sequence number to a provisional designation.
    my ($trk, $seq_num) = @_;
    my $desig = seq2desig($seq_num);

    # Filter the list of detections if the caller specifed a list.  This may be the case
    # if the user unchecked a bad detection in a single-object submisstion.
    foreach my $det (grep { !%sub_dets || exists($sub_dets{$_->detId}) } @{$trk->detections}) {
        my ($ra, $dec, $mag);
        my $revised;

        if ($revision_href && exists(${$revision_href}{$det->detId})) {
            $ra = $revision_href->{$det->detId}->{ra};
            $dec = $revision_href->{$det->detId}->{dec};
            $mag = $revision_href->{$det->detId}->{mag};
            $revised = 'Y';
        }
        else {
            $ra = $det->ra;
            $dec = $det->dec;
            $mag = $det->mag;
        }

        my $field = lookup_field($det);
        $insert_sth->execute(
            $seq_num,
            $field->fpaId,
            $det->epoch,
            $ra,
            $dec,
            $det->filter,
            $mag,
            $det->obscode,
            $desig,
            $trk->digest,
            0,  # spatial idx
            $field->surveyMode,
            $inst->dbname,
            $det->detId,
            $trk->trackletId,
            undef,      # derivedobject ID
            $submitter,
            ($neo ? $SUBMISSION_PENDING_NEO : $SUBMISSION_PENDING_STD),
            undef,
            undef,
            $revised,          # revised
        ) or die $insert_sth->errstr;
    }

    return $desig;
}


sub insert_dobj {
    # Insert a derived object into the submission table.  We will convert the 
    # sequence number to a provisional designation.
    my ($dobj, $seq_num) = @_;
    my $desig = seq2desig($seq_num);

    foreach my $trk ($dobj->fetchTracklets()) {
        foreach my $det (@{$trk->detections}) {
            my $field = lookup_field($det);
            $insert_sth->execute(
                $seq_num,
                $field->fpaId,
                $det->epoch,
                $det->ra,
                $det->dec,
                $det->filter,
                $det->mag,
                $det->obscode,
                $desig,
                $trk->digest,
                0,  # spatial idx
                $field->surveyMode,
                $inst->dbname,
                $det->detId,
                $trk->trackletId,
                $dobj->derivedobjectId,
                $submitter,
                ($neo ? $SUBMISSION_PENDING_NEO : $SUBMISSION_PENDING_STD),
                undef,
            ) or die $insert_sth->errstr;
        }
    }

    return $desig;
}


sub seq2desig {
    my ($seq_num) = @_;
    my $desig = mopslib_toB62($seq_num, $B62_TEMPLATE);
    return $desig;
}


sub emit {
    print @_;
}


sub make_det {
    # Make a PS::MOPS::DC::Detection-like object.
    my ($href) = @_;
    my $foo = bless {
        objectName => $href->{desig},
        epoch => $href->{epoch_mjd},
        ra => $href->{ra_deg},
        dec => $href->{dec_deg},
        filter => $href->{filter_id},
        mag => $href->{mag},
        obscode => $href->{obscode},
        %{$href},       # pass through everything else
    };
    $foo;
}


sub fetch_desig_data {
    # Given a PS1 provisional designation, return a hash containing
    # values to submit to MPC.
    my ($desig) = @_;
    $desig_sth->execute($desig);
    my $href;
    my @stuff;
    while ($href = $desig_sth->fetchrow_hashref()) {
        push @stuff, make_det($href);
    }

    return @stuff;
}


sub make_mpc_records {
    # Convert hash containing MPC_SUB data into MPC 80-column record for PS1.
    my @dets = @_;
    return map { PS::MOPS::MPC::det2mpc($_) } @dets;
}


sub get_pending {
    my @pending;
    my $desig;
    $pending_sth->execute($neo ? $SUBMISSION_PENDING_NEO : $SUBMISSION_PENDING_STD);
    while (($desig) = $pending_sth->fetchrow_array()) {
        push @pending, $desig;
    }

    return @pending;
}


sub make_header {
    my ($ack, $com) = @_;
    my $com_str = $com ? "COM $com\n" : '';
    return <<"HEADER";

COD F51
CON Pan-STARRS MOPS
OBS N. Primak, A. Schultz, S. Watters, T. Goggia
MEA Pan-STARRS 1 Science Consortium
TEL 1.8-m Ritchey-Chretien + CCD
NET 2MASS
${com_str}AC2 psmops\@gmail.com
ACK $ack

HEADER
}


sub mailit {
    my ($submitter, $to, $cc, $subject, @lines) = @_;
    my $mail = new Mail::Send;

#    if ($revision_data) {
#        $to = 'denneau@ifa.hawaii.edu';
#    }
    $mail->to($to);
    $mail->set('From', "MOPS via $submitter <mops\@ifa.hawaii.edu>");
    $mail->set('Reply-To', 'MOPS Czar <mopsczar@ifa.hawaii.edu>');
    $mail->cc($cc) if $cc;
    $mail->subject($subject);
    my $fh = $mail->open();
    print $fh join("\n", @lines), "\n";
    $fh->close();

    # Copy to STDOUT.
    print join("\n", @lines), "\n";
}


sub extra_from_data {
    # Return velocity in deg/day of this tracklet.
    my @d = sort { $a->{epoch_mjd} <=> $b->{epoch_mjd} } @_;
    my $ra1_rad = deg2rad($d[0]->{ra_deg});
    my $dec1_rad = deg2rad($d[0]->{dec_deg});
    my $ra2_rad = deg2rad($d[-1]->{ra_deg});
    my $dec2_rad = deg2rad($d[-1]->{dec_deg});
    my $deltat = $d[-1]->{epoch_mjd} - $d[0]->{epoch_mjd};
    my $vel_dd;
    my $ext_mag;

    $vel_dd = rad2deg(slaDsep($ra1_rad, $dec1_rad, $ra2_rad, $dec2_rad)) / $deltat;
    $ext_mag = sum(map { mopslib_filt2V($_->{mag}, $_->{filter}, $v2filt) } @d) / @d;

    return ($vel_dd, $ext_mag);
}
